riversong code showcase

This commit is contained in:
Daniele Marotta
2026-05-21 15:52:18 +02:00
commit 4c9eea1c02
462 changed files with 23406 additions and 0 deletions

View File

@@ -0,0 +1,10 @@
using System;
using UnityEngine.UIElements;
namespace DanieleMarotta.RiversongCodeShowcase
{
public interface IUIModel : INotifyBindablePropertyChanged
{
event Action Changed;
}
}

View 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;
}
}

View 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);
}
}
}

View 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);
}
}
}

View 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();
}
}
}

View 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;
}
}
}

View 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);
}
}
}
}

View File

@@ -0,0 +1,8 @@
namespace DanieleMarotta.RiversongCodeShowcase
{
[InitializeAfter(typeof(LateGameSystemGroup))]
public class UISystemGroup : GameSystemGroup
{
}
}

View 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;
}
}
}

View 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);
}
}
}

View 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; }
}
}

View 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));
}
}
}