ui effects working

This commit is contained in:
Rowan 2025-07-18 20:38:44 -04:00
parent 816a779678
commit 4ea0f58bc7
28 changed files with 327 additions and 137 deletions

View file

@ -10,13 +10,16 @@ MonoBehaviour:
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 94cc50e7b5f20a817aef3384d0889279, type: 3}
m_Name: New Transition Effect
m_Name: Fade In
m_EditorClassIdentifier:
propertyName:
addTransition: 1
propertyName: opacity
duration:
duration: 0
unit: 0
easing: 0
duration: 0.3
unit: 2
easing: 3
delay:
duration: 0
unit: 0
unit: 2
from: 0
to: 1

View file

@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: 630c186ef2ab4b37495b5001d208878f
guid: 0902dc21238be2bb3ae758e1d5218922
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000

View file

@ -9,9 +9,12 @@ MonoBehaviour:
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 0}
m_Script: {fileID: 11500000, guid: 0b01085379e9a10319622dd3dd23f4bd, type: 3}
m_Name: Modal
m_EditorClassIdentifier: Assembly-CSharp:KitsuneCafe.UI:ModalElementSO
m_EditorClassIdentifier:
visualTreeAsset: {fileID: 9197481963319205126, guid: bc70bdaaf37609283aac14435c5441a2, type: 3}
effects:
- timing: 0
effect: {fileID: 11400000, guid: 0902dc21238be2bb3ae758e1d5218922, type: 2}
titleId: title
contentId: content

View file

@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: 1a47b12328bfde496ab2f3847d762a79
guid: 71a6f29fae4392a4f97870a52b6adf1e
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000

View file

@ -1,17 +0,0 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 0b01085379e9a10319622dd3dd23f4bd, type: 3}
m_Name: New Modal Element
m_EditorClassIdentifier:
visualTreeAsset: {fileID: 9197481963319205126, guid: bc70bdaaf37609283aac14435c5441a2, type: 3}
titleId: title
contentId: content

View file

@ -1,8 +0,0 @@
fileFormatVersion: 2
guid: 71a6f29fae4392a4f97870a52b6adf1e
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,6 @@
namespace KitsuneCafe.Extension
{
public static class CollectionExtension
{
}
}

View file

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 4134cfdaf5630d9e49cdf518b31bc662

View file

@ -4,6 +4,7 @@ using System;
using KitsuneCafe.System;
using System.Collections.Generic;
using System.Threading;
using UnityEngine.Rendering.Universal;
namespace KitsuneCafe.Extension
{
@ -177,6 +178,29 @@ namespace KitsuneCafe.Extension
{
return source.Where(Func.Identity);
}
public static Observable<T> ObserveBool<T>(this T component, Func<T, bool> predicate) where T : MonoBehaviour
{
if (predicate(component))
{
return Observable.Return(component);
}
return Observable.EveryValueChanged(component, predicate)
.Where(Func.Identity)
.Take(1)
.Select(_ => component);
}
public static Observable<T> ObserveAwake<T>(this T component) where T : MonoBehaviour
{
return component.ObserveBool(c => c.didAwake);
}
public static Observable<T> ObserveStart<T>(this T component) where T : MonoBehaviour
{
return component.ObserveBool(c => c.didStart);
}
}
}

View file

@ -0,0 +1,40 @@
using System.Collections.Generic;
using System.Threading;
using R3;
using UnityEngine.UIElements;
namespace KitsuneCafe.UI
{
public readonly struct AddTransitionEffect : IUiEffect
{
public readonly StylePropertyName PropertyName;
public readonly TimeValue Duration;
public readonly EasingFunction Easing;
public readonly TimeValue Delay;
public readonly IUiEffect Effect;
public AddTransitionEffect(StylePropertyName name, TimeValue duration, EasingFunction easing, TimeValue delay, IUiEffect effect)
{
PropertyName = name;
Duration = duration;
Easing = easing;
Delay = delay;
Effect = effect;
}
private static StyleList<T> ToStyleList<T>(T value)
{
return new StyleList<T>(new List<T> { value });
}
public Observable<R3.Unit> Execute(VisualElement target, CancellationToken token)
{
target.style.transitionProperty = ToStyleList(PropertyName);
target.style.transitionDuration = ToStyleList(Duration);
target.style.transitionTimingFunction = ToStyleList(Easing);
target.style.transitionDelay = ToStyleList(Delay);
return Effect.Execute(target, token);
}
}
}

View file

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: a0412a2b6dff88311aec68b3fc4dee47

View file

@ -1,4 +1,6 @@
using System.Threading;
using KitsuneCafe.System;
using KitsuneCafe.System.Attributes;
using R3;
using UnityEngine;
using UnityEngine.UIElements;
@ -6,44 +8,72 @@ using UnityEngine.UIElements;
namespace KitsuneCafe.UI
{
[CreateAssetMenu(menuName = KitsuneCafeMenu.UiEffect + "Fade")]
public class TransitionEffect : BaseUIEffect
public class FadeEffect : BaseUiEffect
{
[SerializeField]
private string propertyName;
private bool addTransition = false;
[SerializeField]
[SerializeField, ShowIf("addTransition")]
private string propertyName = "opacity";
[SerializeField, ShowIf("addTransition")]
private SerializableDuration duration;
[SerializeField]
[SerializeField, ShowIf("addTransition")]
private EasingMode easing;
[SerializeField]
[SerializeField, ShowIf("addTransition")]
private SerializableDuration delay;
public override IUIEffect Instantiate()
[SerializeField, Range(0f, 1f)]
private float from;
[SerializeField, Range(0f, 1f)]
private float to;
public override IUiEffect Instantiate()
{
return new TransitionEffectInstance(propertyName, duration, easing, delay);
var fade = new FadeEffectInstance(from, to);
if (addTransition)
{
return new AddTransitionEffect(propertyName, duration, easing, delay, fade);
}
return fade;
}
}
public readonly struct TransitionEffectInstance : IUIEffect
public readonly struct FadeEffectInstance : IUiEffect
{
public readonly StylePropertyName PropertyName;
public readonly TimeValue Duration;
public readonly EasingFunction Easing;
public readonly TimeValue Delay;
public readonly float From;
public readonly float To;
public TransitionEffectInstance(StylePropertyName name, TimeValue duration, EasingFunction easing, TimeValue delay)
public FadeEffectInstance(float from, float to)
{
PropertyName = name;
Duration = duration;
Easing = easing;
Delay = delay;
From = from;
To = to;
}
public Observable<R3.Unit> Execute(VisualElement target)
private void DoTransition(VisualElement target)
{
return Observable.Empty<R3.Unit>();
}
target.style.opacity = From;
target.style.opacity = To;
}
public Observable<R3.Unit> Execute(VisualElement target, CancellationToken token)
{
target.style.opacity = From;
var to = To;
return Observable.NextFrame(cancellationToken: token)
.Do(_ =>
{
target.style.opacity = to;
})
.AsUnitObservable();
}
}
}

View file

@ -1,16 +0,0 @@
using R3;
using UnityEngine;
using UnityEngine.UIElements;
namespace KitsuneCafe.UI
{
public interface IUIEffect
{
Observable<Unit> Execute(VisualElement target);
}
public abstract class BaseUIEffect : ScriptableObject
{
public abstract IUIEffect Instantiate();
}
}

View file

@ -0,0 +1,18 @@
using System.Collections;
using System.Threading;
using R3;
using UnityEngine;
using UnityEngine.UIElements;
namespace KitsuneCafe.UI
{
public interface IUiEffect
{
Observable<Unit> Execute(VisualElement target, CancellationToken token);
}
public abstract class BaseUiEffect : ScriptableObject
{
public abstract IUiEffect Instantiate();
}
}

View file

@ -1,10 +1,11 @@
using System.Collections.Generic;
using KitsuneCafe.System;
using UnityEngine;
using UnityEngine.UIElements;
namespace KitsuneCafe.UI
{
public readonly struct ModalElementInstance : IUIElement
public readonly struct ModalElementInstance : IUiElement
{
public readonly string Title;
public readonly string Content;
@ -31,10 +32,15 @@ namespace KitsuneCafe.UI
ve.Q<Label>(so.TitleId).text = Title;
ve.Q<Label>(so.ContentId).text = Content;
}
public IEnumerable<BaseUiEffect> GetEffectsFor(UiEvent timing)
{
return so.GetEffectsFor(timing);
}
}
[CreateAssetMenu(menuName = KitsuneCafeMenu.UiElement + "Modal")]
public class ModalElement : UIElementSO
public class ModalElement : UiElementSo
{
[SerializeField]
private string titleId = "title";

View file

@ -5,7 +5,7 @@ namespace KitsuneCafe.UI
public class Notification : MonoBehaviour
{
[SerializeField]
private UIOrchestrator orchestrator;
private UiOrchestrator orchestrator;
[SerializeField]
private ModalElement modal;

View file

@ -6,17 +6,17 @@ namespace KitsuneCafe.UI
public readonly struct SpawnElementRequest : IEquatable<SpawnElementRequest>
{
public readonly ElementId Id;
public readonly IUIElement Element;
public readonly IUiElement Element;
public readonly Vector3 Position;
public SpawnElementRequest(ElementId id, IUIElement element, Vector3 position)
public SpawnElementRequest(ElementId id, IUiElement element, Vector3 position)
{
Id = id;
Element = element;
Position = position;
}
public static SpawnElementRequest Create(ElementId id, IUIElement element, Vector3 position) => new(id, element, position);
public static SpawnElementRequest Create(ElementId id, IUiElement element, Vector3 position) => new(id, element, position);
public override bool Equals(object obj)
{

View file

@ -5,13 +5,13 @@ using UnityEngine;
namespace KitsuneCafe.UI
{
[CreateAssetMenu(menuName = KitsuneCafeMenu.Ui + "Orchestrator")]
public class UIOrchestrator : ScriptableObject
public class UiOrchestrator : ScriptableObject
{
public event EventHandler<SpawnElementRequest> SpawnRequested = default;
public event EventHandler<DespawnElementRequest> DespawnRequested = default;
public ElementId SpawnElement(IUIElement element, Vector3 worldPosition)
public ElementId SpawnElement(IUiElement element, Vector3 worldPosition)
{
var id = ElementId.Create();
SpawnRequested.Invoke(this, SpawnElementRequest.Create(id, element, worldPosition));

View file

@ -5,13 +5,13 @@ using System;
using UnityEngine.UIElements;
using UnityEngine.Pool;
using KitsuneCafe.System;
using System.Threading;
using KitsuneCafe.Extension;
namespace KitsuneCafe.UI
{
public class UISceneManager : MonoBehaviour
{
[SerializeField] private UIOrchestrator orchestrator;
[SerializeField] private UiOrchestrator orchestrator;
[SerializeField] private Transform root;
[SerializeField] private PanelSettings panelSettings;
@ -46,7 +46,7 @@ namespace KitsuneCafe.UI
d.RegisterTo(destroyCancellationToken);
}
private IObjectPool<GameObject> GetOrCreatePool(IUIElement element)
private IObjectPool<GameObject> GetOrCreatePool(IUiElement element)
{
if (!pools.TryGetValue(element.VisualTreeAsset, out var pool))
{
@ -77,7 +77,7 @@ namespace KitsuneCafe.UI
obj = new GameObject("Pooled Object", typeof(UIDocument));
}
obj.transform.SetParent(root);
obj.AddComponent<UiElementInstance>();
var doc = obj.GetComponent<UIDocument>();
doc.panelSettings = panelSettings;
@ -85,6 +85,8 @@ namespace KitsuneCafe.UI
var poolObj = obj.AddComponent<PooledObject>();
poolObj.objectPool = pool;
obj.transform.SetParent(root);
return obj;
}
@ -103,12 +105,21 @@ namespace KitsuneCafe.UI
Destroy(obj);
}
private GameObject GetUiDocument(IUIElement element)
private GameObject GetUiDocument(IUiElement element)
{
return GetOrCreatePool(element).Get();
}
private void SpawnElement(ElementId id, IUIElement element, Vector3 worldPosition)
private Observable<T> ObserveEvent<T>(VisualElement target) where T : EventBase<T>, new()
{
return Observable.FromEvent<EventCallback<T>, T>(
h => e => h(e),
e => target.RegisterCallback(e),
e => target.UnregisterCallback(e)
);
}
private void SpawnElement(ElementId id, IUiElement element, Vector3 worldPosition)
{
var obj = GetUiDocument(element);
@ -117,33 +128,21 @@ namespace KitsuneCafe.UI
trans.SetPositionAndRotation(worldPosition, Quaternion.identity);
trans.localScale = Vector3.one;
if (obj.TryGetComponent<UIDocument>(out var doc))
if (obj.TryGetComponent(out UIDocument doc) && obj.TryGetComponent(out UiElementInstance instance))
{
doc.visualTreeAsset = element.VisualTreeAsset;
doc.panelSettings = panelSettings;
ConfigureWhenReady(doc, element);
Observable.EveryValueChanged(doc, d => d.rootVisualElement)
.WhereNotNull()
.Select(_ => doc)
.Subscribe(doc =>
{
instance.Configure(element, doc.rootVisualElement);
})
.AddTo(doc.gameObject);
}
activeElements[id] = obj;
//Observable.Timer(TimeSpan.FromSeconds(3))
// .Subscribe(_ => orchestrator.DespawnElement(id))
// .AddTo(obj);
}
private void ConfigureWhenReady(UIDocument document, IUIElement element)
{
var cts = new CancellationTokenSource();
Observable.EveryValueChanged(document, d => d.didStart)
.Where(started => started)
.Take(1)
.DoCancelOnCompleted(cts)
.Subscribe(f =>
{
element.Configure(document.rootVisualElement);
})
.AddTo(document.gameObject);
}
private void DespawnElement(ElementId id)

View file

@ -5,7 +5,7 @@ namespace KitsuneCafe.UI
public class TestThing : MonoBehaviour
{
[SerializeField]
private UIOrchestrator orchestrator;
private UiOrchestrator orchestrator;
[SerializeField]
private ModalElement modal;

View file

@ -1,31 +0,0 @@
using KitsuneCafe.System;
using UnityEngine;
using UnityEngine.UIElements;
namespace KitsuneCafe.UI
{
public interface IUIElement
{
VisualTreeAsset VisualTreeAsset { get; }
VisualElement Instantiate();
void Configure(VisualElement element);
}
[CreateAssetMenu(menuName = KitsuneCafeMenu.UiElement + "Element")]
public abstract class UIElementSO : ScriptableObject, IUIElement
{
[SerializeField]
private VisualTreeAsset visualTreeAsset;
public VisualTreeAsset VisualTreeAsset => visualTreeAsset;
public virtual VisualElement Instantiate()
{
var instance = visualTreeAsset.CloneTree();
Configure(instance);
return instance;
}
public virtual void Configure(VisualElement ve) { }
}
}

View file

@ -0,0 +1,56 @@
using System;
using System.Collections.Generic;
using System.Linq;
using KitsuneCafe.System;
using UnityEngine;
using UnityEngine.UIElements;
namespace KitsuneCafe.UI
{
public enum UiEvent
{
OnCreate,
OnDestroy
}
public interface IUiElement
{
VisualTreeAsset VisualTreeAsset { get; }
void Configure(VisualElement element);
IEnumerable<BaseUiEffect> GetEffectsFor(UiEvent timing);
}
[Serializable]
public struct EventEffectPair
{
[SerializeField]
private UiEvent uiEvent;
public readonly UiEvent Event => uiEvent;
[SerializeField]
private BaseUiEffect effect;
public readonly BaseUiEffect Effect => effect;
}
[CreateAssetMenu(menuName = KitsuneCafeMenu.UiElement + "Element")]
public abstract class UiElementSo : ScriptableObject, IUiElement
{
[SerializeField]
private VisualTreeAsset visualTreeAsset;
public VisualTreeAsset VisualTreeAsset => visualTreeAsset;
[SerializeField]
private List<EventEffectPair> effects;
private ILookup<UiEvent, BaseUiEffect> effectLookup;
public ILookup<UiEvent, BaseUiEffect> Effects => effectLookup ??= effects.ToLookup(p => p.Event, p => p.Effect);
public virtual void Configure(VisualElement ve) { }
public IEnumerable<BaseUiEffect> GetEffectsFor(UiEvent timing)
{
return Effects[timing];
}
}
}

View file

@ -0,0 +1,71 @@
using System;
using R3;
using UnityEngine;
using UnityEngine.UIElements;
namespace KitsuneCafe.UI
{
public class UiElementInstance : MonoBehaviour, IDisposable
{
private IUiElement uiElement;
private VisualElement visualElement;
private readonly CompositeDisposable activeEffects = new();
private bool configured = false;
public void Configure(IUiElement uiElement, VisualElement visualElement)
{
configured = true;
this.uiElement = uiElement;
this.visualElement = visualElement;
activeEffects.Clear();
uiElement.Configure(visualElement);
Raise(UiEvent.OnCreate);
}
private void Raise(UiEvent evt)
{
foreach (var effect in uiElement.GetEffectsFor(evt))
{
// using explicit null comparison because of the way
// GameObjects handle being null
if (effect != null)
{
effect.Instantiate()
.Execute(visualElement, destroyCancellationToken)
.Subscribe()
.AddTo(activeEffects);
}
}
}
public void Dispose()
{
if (!configured) { return; }
Raise(UiEvent.OnDestroy);
activeEffects.Dispose();
activeEffects.Clear();
uiElement = default;
visualElement = default;
configured = false;
}
private void OnDisable()
{
Dispose();
}
private void OnDestroy()
{
Dispose();
}
}
}

View file

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 8f7e084f3ed7b4e5babd039bbd888b29