riversong code showcase
This commit is contained in:
10
Source/Riversong/Game/UI/Framework/IUIModel.cs
Normal file
10
Source/Riversong/Game/UI/Framework/IUIModel.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
using System;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
namespace DanieleMarotta.RiversongCodeShowcase
|
||||
{
|
||||
public interface IUIModel : INotifyBindablePropertyChanged
|
||||
{
|
||||
event Action Changed;
|
||||
}
|
||||
}
|
||||
19
Source/Riversong/Game/UI/Framework/IUIRoot.cs
Normal file
19
Source/Riversong/Game/UI/Framework/IUIRoot.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
using System;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
namespace DanieleMarotta.RiversongCodeShowcase
|
||||
{
|
||||
public interface IUIRoot : IDisposable
|
||||
{
|
||||
VisualElement RootVisualElement { get; }
|
||||
|
||||
event Action<ClickEvent> ElementClicked;
|
||||
|
||||
UniTask Initialize(UIService uiService);
|
||||
|
||||
void MakeDraggable(VisualElement target);
|
||||
|
||||
T GetView<T>() where T : UIView;
|
||||
}
|
||||
}
|
||||
54
Source/Riversong/Game/UI/Framework/UIControllerSystem.cs
Normal file
54
Source/Riversong/Game/UI/Framework/UIControllerSystem.cs
Normal file
@@ -0,0 +1,54 @@
|
||||
using Cysharp.Threading.Tasks;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
namespace DanieleMarotta.RiversongCodeShowcase
|
||||
{
|
||||
[GameSystemGroup(typeof(UISystemGroup))]
|
||||
[InitializeAfter(typeof(UIInitializationSystem))]
|
||||
public abstract class UIControllerSystem : GameSystem, IInitializable
|
||||
{
|
||||
protected UIControllerSystem(IServiceLocator serviceLocator) : base(serviceLocator)
|
||||
{
|
||||
}
|
||||
|
||||
[field: InjectService] protected UIService UIService { get; }
|
||||
|
||||
protected IUIRoot UIRoot => UIService.UIRoot;
|
||||
|
||||
public virtual UniTask InitializeAsync()
|
||||
{
|
||||
return UniTask.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
[GameSystemGroup(typeof(UISystemGroup))]
|
||||
[InitializeAfter(typeof(UIInitializationSystem))]
|
||||
public abstract class UIControllerSystem<T> : UIControllerSystem where T : UIView
|
||||
{
|
||||
private Button _closeButton;
|
||||
|
||||
protected UIControllerSystem(IServiceLocator serviceLocator) : base(serviceLocator)
|
||||
{
|
||||
}
|
||||
|
||||
protected abstract T View { get; }
|
||||
|
||||
public override async UniTask InitializeAsync()
|
||||
{
|
||||
await base.InitializeAsync();
|
||||
|
||||
_closeButton = View.RootElement.Q<Button>(className: "close-button");
|
||||
_closeButton?.RegisterCallback<ClickEvent>(OnCloseButtonClick);
|
||||
}
|
||||
|
||||
private void OnCloseButtonClick(ClickEvent evt)
|
||||
{
|
||||
CloseView();
|
||||
}
|
||||
|
||||
protected virtual void CloseView(bool animate = true)
|
||||
{
|
||||
View.Show(false, animate);
|
||||
}
|
||||
}
|
||||
}
|
||||
66
Source/Riversong/Game/UI/Framework/UIInitializationSystem.cs
Normal file
66
Source/Riversong/Game/UI/Framework/UIInitializationSystem.cs
Normal file
@@ -0,0 +1,66 @@
|
||||
using System;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using UnityEngine.UIElements;
|
||||
using IServiceProvider = DanieleMarotta.RiversongCodeShowcase.IServiceProvider;
|
||||
|
||||
namespace DanieleMarotta.RiversongCodeShowcase
|
||||
{
|
||||
[GameSystemGroup(typeof(UISystemGroup))]
|
||||
public class UIInitializationSystem : GameSystem, IServiceProvider, IInitializable, IDisposable
|
||||
{
|
||||
[InjectService]
|
||||
private GameConfig _config;
|
||||
|
||||
[InjectService]
|
||||
private ISoundPlayer _soundPlayer;
|
||||
|
||||
private IUIRoot _uiRoot;
|
||||
|
||||
private UIService _uiService;
|
||||
|
||||
private TextFormatHelper _textFormatHelper;
|
||||
|
||||
public UIInitializationSystem(IServiceLocator serviceLocator) : base(serviceLocator)
|
||||
{
|
||||
}
|
||||
|
||||
public void RegisterServices(IServiceLocator serviceLocator)
|
||||
{
|
||||
serviceLocator.RegisterService(new UIState());
|
||||
|
||||
_uiService = new UIService();
|
||||
serviceLocator.RegisterService(_uiService);
|
||||
|
||||
_textFormatHelper = new TextFormatHelper();
|
||||
serviceLocator.RegisterService(_textFormatHelper);
|
||||
}
|
||||
|
||||
public async UniTask InitializeAsync()
|
||||
{
|
||||
var uiRootGameObject = await _config.UI.RootPrefab.InstantiateAsync();
|
||||
|
||||
_uiRoot = uiRootGameObject.GetComponent<IUIRoot>();
|
||||
await _uiRoot.Initialize(_uiService);
|
||||
|
||||
_uiRoot.ElementClicked += OnElementClicked;
|
||||
|
||||
_uiService.UIRoot = _uiRoot;
|
||||
_uiService.TemplateLibrary = await _config.UI.TemplateLibrary.LoadAssetAsync<UITemplateLibrary>();
|
||||
_uiService.TextFormatHelper = _textFormatHelper;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_uiRoot.ElementClicked -= OnElementClicked;
|
||||
_uiRoot.Dispose();
|
||||
}
|
||||
|
||||
private void OnElementClicked(ClickEvent e)
|
||||
{
|
||||
if (e.target is not VisualElement element) return;
|
||||
if (element.GetFirstAncestorOrSelf(static candidate => candidate is Button || candidate.ClassListContains("ui-click")) == null) return;
|
||||
|
||||
_soundPlayer.Play(SystemSoundId.UIClick);
|
||||
}
|
||||
}
|
||||
}
|
||||
33
Source/Riversong/Game/UI/Framework/UIModel.cs
Normal file
33
Source/Riversong/Game/UI/Framework/UIModel.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
namespace DanieleMarotta.RiversongCodeShowcase
|
||||
{
|
||||
public class UIModel : IUIModel
|
||||
{
|
||||
public event EventHandler<BindablePropertyChangedEventArgs> propertyChanged;
|
||||
|
||||
public event Action Changed;
|
||||
|
||||
protected void SetProperty<T>(ref T field, T value, [CallerMemberName] string property = null)
|
||||
{
|
||||
if (EqualityComparer<T>.Default.Equals(field, value)) return;
|
||||
|
||||
field = value;
|
||||
|
||||
NotifyPropertyChanged(property);
|
||||
}
|
||||
|
||||
protected void NotifyPropertyChanged(string property = null)
|
||||
{
|
||||
propertyChanged?.Invoke(this, new BindablePropertyChangedEventArgs(property));
|
||||
}
|
||||
|
||||
public void NotifyChanged()
|
||||
{
|
||||
Changed?.Invoke();
|
||||
}
|
||||
}
|
||||
}
|
||||
83
Source/Riversong/Game/UI/Framework/UIRoot.cs
Normal file
83
Source/Riversong/Game/UI/Framework/UIRoot.cs
Normal file
@@ -0,0 +1,83 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
namespace DanieleMarotta.RiversongCodeShowcase
|
||||
{
|
||||
[RequireComponent(typeof(UIDocument))]
|
||||
public class UIRoot : MonoBehaviour, IUIRoot
|
||||
{
|
||||
private UIDocument _document;
|
||||
|
||||
private Dictionary<Type, UIView> _views = new();
|
||||
|
||||
public VisualElement RootVisualElement { get; private set; }
|
||||
|
||||
public event Action<ClickEvent> ElementClicked;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
_document = GetComponent<UIDocument>();
|
||||
}
|
||||
|
||||
public async UniTask Initialize(UIService uiService)
|
||||
{
|
||||
RootVisualElement = _document.rootVisualElement;
|
||||
|
||||
await DiscoverViewsAsync(uiService);
|
||||
|
||||
InitializeDragging();
|
||||
|
||||
RootVisualElement.RegisterCallback<ClickEvent>(OnClick);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
foreach (var view in _views.Values) view.Dispose();
|
||||
|
||||
RootVisualElement.UnregisterCallback<ClickEvent>(OnClick);
|
||||
}
|
||||
|
||||
private void OnClick(ClickEvent evt)
|
||||
{
|
||||
ElementClicked?.Invoke(evt);
|
||||
}
|
||||
|
||||
private async UniTask DiscoverViewsAsync(UIService uiService)
|
||||
{
|
||||
var assemblies = AppDomain.CurrentDomain.GetAssemblies();
|
||||
|
||||
foreach (var assembly in assemblies)
|
||||
foreach (var type in assembly.GetTypes())
|
||||
{
|
||||
var viewAttribute = type.GetCustomAttribute<UIViewAttribute>();
|
||||
if (viewAttribute == null) continue;
|
||||
|
||||
var element = RootVisualElement.Q(className: viewAttribute.UssClassName);
|
||||
var view = await uiService.CreateView(type, element);
|
||||
|
||||
_views.Add(type, view);
|
||||
}
|
||||
}
|
||||
|
||||
private void InitializeDragging()
|
||||
{
|
||||
var targets = RootVisualElement.Query<VisualElement>(className: "drag-target").ToList();
|
||||
foreach (var target in targets) MakeDraggable(target);
|
||||
}
|
||||
|
||||
public void MakeDraggable(VisualElement target)
|
||||
{
|
||||
var handle = target.Q<VisualElement>(className: "drag-handle") ?? target;
|
||||
handle.AddManipulator(new DraggableManipulator(target));
|
||||
}
|
||||
|
||||
public T GetView<T>() where T : UIView
|
||||
{
|
||||
return _views.TryGetValue(typeof(T), out var view) ? (T)view : null;
|
||||
}
|
||||
}
|
||||
}
|
||||
41
Source/Riversong/Game/UI/Framework/UIService.cs
Normal file
41
Source/Riversong/Game/UI/Framework/UIService.cs
Normal file
@@ -0,0 +1,41 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
namespace DanieleMarotta.RiversongCodeShowcase
|
||||
{
|
||||
public class UIService
|
||||
{
|
||||
public IUIRoot UIRoot { get; set; }
|
||||
|
||||
public UITemplateLibrary TemplateLibrary { get; set; }
|
||||
|
||||
public TextFormatHelper TextFormatHelper { get; set; }
|
||||
|
||||
public async UniTask<UIView> CreateView(Type type, VisualElement rootElement)
|
||||
{
|
||||
var view = (UIView)Activator.CreateInstance(type);
|
||||
await view.InitializeAsync(this, rootElement);
|
||||
return view;
|
||||
}
|
||||
}
|
||||
|
||||
public static class UIServiceExtensions
|
||||
{
|
||||
public static async UniTask CreateViews<TModel, TView>(this UIService uiService, List<TModel> models, VisualElement container, VisualTreeAsset template)
|
||||
where TModel : UIModel where TView : UIView<TModel>
|
||||
{
|
||||
container.Clear();
|
||||
|
||||
foreach (var model in models)
|
||||
{
|
||||
var element = template.CloneTree();
|
||||
container.Add(element);
|
||||
|
||||
var view = (TView)await uiService.CreateView(typeof(TView), element);
|
||||
view.SetModel(model);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
8
Source/Riversong/Game/UI/Framework/UISystemGroup.cs
Normal file
8
Source/Riversong/Game/UI/Framework/UISystemGroup.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
|
||||
namespace DanieleMarotta.RiversongCodeShowcase
|
||||
{
|
||||
[InitializeAfter(typeof(LateGameSystemGroup))]
|
||||
public class UISystemGroup : GameSystemGroup
|
||||
{
|
||||
}
|
||||
}
|
||||
60
Source/Riversong/Game/UI/Framework/UITemplateLibrary.cs
Normal file
60
Source/Riversong/Game/UI/Framework/UITemplateLibrary.cs
Normal file
@@ -0,0 +1,60 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
namespace DanieleMarotta.RiversongCodeShowcase
|
||||
{
|
||||
[CreateAssetMenu(fileName = "UITemplateLibrary", menuName = "Riversong Code Showcase/UI Template Library")]
|
||||
public class UITemplateLibrary : ScriptableObject
|
||||
{
|
||||
public CommonTemplateLibrary Common;
|
||||
|
||||
public WorldUITemplateLibrary WorldUI;
|
||||
|
||||
public BuildMenuTemplateLibrary BuildMenu;
|
||||
|
||||
public BuildingPanelTemplateLibrary BuildingPanel;
|
||||
|
||||
public OnboardingPanelTemplateLibrary OnboardingPanel;
|
||||
|
||||
[Serializable]
|
||||
public class CommonTemplateLibrary
|
||||
{
|
||||
public VisualTreeAsset ProductAmount;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class WorldUITemplateLibrary
|
||||
{
|
||||
public VisualTreeAsset Badge;
|
||||
|
||||
public Vector2 BadgeYOffset = new(32, 64);
|
||||
|
||||
public Vector2 BadgeScale = new(4, 1);
|
||||
|
||||
public float BadgeZoomCullThreshold = 90;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class BuildMenuTemplateLibrary
|
||||
{
|
||||
public VisualTreeAsset Button;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class BuildingPanelTemplateLibrary
|
||||
{
|
||||
public VisualTreeAsset Need;
|
||||
|
||||
public VisualTreeAsset UpgradeMaterial;
|
||||
|
||||
public VisualTreeAsset StorageProduct;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class OnboardingPanelTemplateLibrary
|
||||
{
|
||||
public VisualTreeAsset Message;
|
||||
}
|
||||
}
|
||||
}
|
||||
85
Source/Riversong/Game/UI/Framework/UIView.cs
Normal file
85
Source/Riversong/Game/UI/Framework/UIView.cs
Normal file
@@ -0,0 +1,85 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
namespace DanieleMarotta.RiversongCodeShowcase
|
||||
{
|
||||
public abstract class UIView : IDisposable
|
||||
{
|
||||
private UIVisibilityAnimation _visibilityAnimation;
|
||||
|
||||
public UIService UIService { get; private set; }
|
||||
|
||||
public VisualElement RootElement { get; private set; }
|
||||
|
||||
public event Action<bool> OpenedOrClosed;
|
||||
|
||||
public virtual UniTask InitializeAsync(UIService uiService, VisualElement rootElement)
|
||||
{
|
||||
UIService = uiService;
|
||||
RootElement = rootElement;
|
||||
|
||||
_visibilityAnimation = new UIVisibilityAnimation(rootElement, isOpen => OpenedOrClosed?.Invoke(isOpen));
|
||||
|
||||
return UniTask.CompletedTask;
|
||||
}
|
||||
|
||||
public bool IsOpen()
|
||||
{
|
||||
return _visibilityAnimation.IsOpen;
|
||||
}
|
||||
|
||||
public void Show(bool show, bool animate = false)
|
||||
{
|
||||
_visibilityAnimation.Show(show, animate);
|
||||
}
|
||||
|
||||
public virtual void Dispose()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public abstract class UIView<T> : UIView where T : class
|
||||
{
|
||||
public T Model { get; private set; }
|
||||
|
||||
public void SetModel(T model)
|
||||
{
|
||||
if (EqualityComparer<T>.Default.Equals(Model, model)) return;
|
||||
|
||||
var oldModel = Model;
|
||||
Model = model;
|
||||
|
||||
if (oldModel is IUIModel oldUiModel) oldUiModel.Changed -= OnModelChanged;
|
||||
if (oldModel is INotifyBindablePropertyChanged oldNotifyingModel) oldNotifyingModel.propertyChanged -= OnModelPropertyChanged;
|
||||
|
||||
if (model is IUIModel uiModel) uiModel.Changed += OnModelChanged;
|
||||
if (model is INotifyBindablePropertyChanged notifyingModel) notifyingModel.propertyChanged += OnModelPropertyChanged;
|
||||
|
||||
RootElement.dataSource = model;
|
||||
|
||||
OnNewModel(model);
|
||||
}
|
||||
|
||||
protected virtual void OnNewModel(T model)
|
||||
{
|
||||
}
|
||||
|
||||
protected virtual void OnModelChanged()
|
||||
{
|
||||
}
|
||||
|
||||
protected virtual void OnModelPropertyChanged(object sender, BindablePropertyChangedEventArgs e)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public static class UIViewExtensions
|
||||
{
|
||||
public static void Toggle(this UIView view, bool animate = false)
|
||||
{
|
||||
view.Show(!view.IsOpen(), animate);
|
||||
}
|
||||
}
|
||||
}
|
||||
15
Source/Riversong/Game/UI/Framework/UIViewAttribute.cs
Normal file
15
Source/Riversong/Game/UI/Framework/UIViewAttribute.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
using System;
|
||||
|
||||
namespace DanieleMarotta.RiversongCodeShowcase
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Class)]
|
||||
public class UIViewAttribute : Attribute
|
||||
{
|
||||
public UIViewAttribute(string ussClassName = null)
|
||||
{
|
||||
UssClassName = ussClassName;
|
||||
}
|
||||
|
||||
public string UssClassName { get; set; }
|
||||
}
|
||||
}
|
||||
112
Source/Riversong/Game/UI/Framework/UIVisibilityAnimation.cs
Normal file
112
Source/Riversong/Game/UI/Framework/UIVisibilityAnimation.cs
Normal file
@@ -0,0 +1,112 @@
|
||||
using System;
|
||||
using PrimeTween;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
namespace DanieleMarotta.RiversongCodeShowcase
|
||||
{
|
||||
public class UIVisibilityAnimation
|
||||
{
|
||||
private VisualElement _element;
|
||||
|
||||
private Action<bool> _onCompleted;
|
||||
|
||||
private Sequence _visibilityAnimation;
|
||||
|
||||
public UIVisibilityAnimation(VisualElement element, Action<bool> onCompleted)
|
||||
{
|
||||
_element = element;
|
||||
_onCompleted = onCompleted;
|
||||
|
||||
IsOpen = element.resolvedStyle.display != DisplayStyle.None;
|
||||
TargetIsOpen = IsOpen;
|
||||
|
||||
ApplyCommittedState(IsOpen);
|
||||
}
|
||||
|
||||
public bool IsOpen { get; private set; }
|
||||
|
||||
public bool TargetIsOpen { get; private set; }
|
||||
|
||||
public bool IsAnimating => _visibilityAnimation.isAlive;
|
||||
|
||||
public void Show(bool show, bool animate = false)
|
||||
{
|
||||
TargetIsOpen = show;
|
||||
|
||||
if (IsAnimating) return;
|
||||
if (IsOpen == TargetIsOpen) return;
|
||||
|
||||
if (!animate)
|
||||
{
|
||||
ApplyCommittedState(TargetIsOpen);
|
||||
IsOpen = TargetIsOpen;
|
||||
_onCompleted?.Invoke(IsOpen);
|
||||
return;
|
||||
}
|
||||
|
||||
TryAdvance();
|
||||
}
|
||||
|
||||
private void TryAdvance()
|
||||
{
|
||||
if (IsAnimating) return;
|
||||
if (IsOpen == TargetIsOpen) return;
|
||||
|
||||
if (TargetIsOpen)
|
||||
PlayOpenAnimation();
|
||||
else
|
||||
PlayCloseAnimation();
|
||||
}
|
||||
|
||||
private void PlayOpenAnimation()
|
||||
{
|
||||
_element.style.display = DisplayStyle.Flex;
|
||||
_visibilityAnimation = PlayShowTween(_element).OnComplete(() => CompleteTransition(true));
|
||||
}
|
||||
|
||||
private void PlayCloseAnimation()
|
||||
{
|
||||
_element.style.display = DisplayStyle.Flex;
|
||||
_visibilityAnimation = PlayHideTween(_element).OnComplete(() => CompleteTransition(false));
|
||||
}
|
||||
|
||||
private void CompleteTransition(bool isOpen)
|
||||
{
|
||||
_visibilityAnimation = default;
|
||||
ApplyCommittedState(isOpen);
|
||||
IsOpen = isOpen;
|
||||
_onCompleted?.Invoke(IsOpen);
|
||||
TryAdvance();
|
||||
}
|
||||
|
||||
private void ApplyCommittedState(bool isOpen)
|
||||
{
|
||||
_element.style.display = isOpen ? DisplayStyle.Flex : DisplayStyle.None;
|
||||
_element.style.opacity = isOpen ? 1 : 0;
|
||||
if (isOpen)
|
||||
_element.style.scale = StyleKeyword.Null;
|
||||
else
|
||||
_element.SetScale(0.8f);
|
||||
}
|
||||
|
||||
public static Sequence PlayShowTween(VisualElement element, float duration = 0.3f)
|
||||
{
|
||||
element.style.opacity = 0;
|
||||
element.SetScale(0.8f);
|
||||
|
||||
return Sequence.Create(useUnscaledTime: true)
|
||||
.Group(Tween.Custom(0.8f, 1, duration, element.SetScale, Ease.OutQuad))
|
||||
.Group(Tween.Custom(0, 1, duration, value => element.style.opacity = value, Ease.OutQuad));
|
||||
}
|
||||
|
||||
public static Sequence PlayHideTween(VisualElement element, float duration = 0.2f)
|
||||
{
|
||||
element.style.opacity = 1;
|
||||
element.SetScale(1);
|
||||
|
||||
return Sequence.Create(useUnscaledTime: true)
|
||||
.Group(Tween.Custom(1, 0.8f, duration, value => element.SetScale(value), Ease.OutQuad))
|
||||
.Group(Tween.Custom(1, 0, duration, value => element.style.opacity = value, Ease.OutQuad));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user