diff --git a/Assets/Cryville/Common/Tweener.cs b/Assets/Cryville/Common/Tweener.cs new file mode 100644 index 0000000..3a75c82 --- /dev/null +++ b/Assets/Cryville/Common/Tweener.cs @@ -0,0 +1,95 @@ +using System; + +namespace Cryville.Common { + public class PropertyTweener { + readonly Func _getter; + readonly Action _setter; + readonly Tweener _tweener; + public PropertyTweener(Func getter, Action setter, Tweener tweener) { + _getter = getter; + _setter = setter; + _tweener = tweener; + var initialValue = getter(); + _tweener.Start(initialValue, initialValue, float.Epsilon); + } + public PropertyTweener Start(T endValue, float duration) { + _tweener.Start(_getter(), endValue, duration); + return this; + } + public void Advance(float deltaTime) { + if (!_tweener.Advance(deltaTime, out var value)) return; + _setter(value); + } + } + public delegate T Addition(T a, T b); + public delegate T Multiplication(float k, T b); + public delegate float EasingFunction(float t); + public class Tweener { + readonly Addition _addition; + readonly Multiplication _multiplication; + public Tweener(Addition addition, Multiplication multiplication) { + _addition = addition; + _multiplication = multiplication; + } + public EasingFunction EasingFunction { get; set; } = EasingFunctions.Linear; + public Tweener With(EasingFunction easing) { + EasingFunction = easing; + return this; + } + + T _startValue = default; + T _endValue = default; + float _duration = float.Epsilon; + float _time; + bool _endOfTween; + public Tweener Start(T startValue, T endValue, float duration) { + _startValue = startValue; + _endValue = endValue; + _duration = duration; + _time = 0; + _endOfTween = false; + return this; + } + public bool Advance(float deltaTime, out T value) { + if (_endOfTween) { + value = _endValue; + return false; + } + if (_time >= _duration) { + value = _endValue; + _endOfTween = true; + return true; + } + _time += deltaTime; + var ratio = EasingFunction(System.Math.Clamp(_time / _duration, 0, 1)); + value = _addition(_multiplication(1 - ratio, _startValue), _multiplication(ratio, _endValue)); + return true; + } + public Tweener Boxed() { + return new Tweener((a, b) => _addition((T)a, (T)b), (k, v) => _multiplication(k, (T)v)); + } + } + public static class Tweeners { + public static Tweener Byte => new((a, b) => (byte)(a + b), (k, v) => (byte)(k * v)); + public static Tweener SByte => new((a, b) => (sbyte)(a + b), (k, v) => (sbyte)(k * v)); + public static Tweener Int16 => new((a, b) => (short)(a + b), (k, v) => (short)(k * v)); + public static Tweener UInt16 => new((a, b) => (ushort)(a + b), (k, v) => (ushort)(k * v)); + public static Tweener Int32 => new((a, b) => a + b, (k, v) => (int)(k * v)); + public static Tweener UInt32 => new((a, b) => a + b, (k, v) => (uint)(k * v)); + public static Tweener Int64 => new((a, b) => a + b, (k, v) => (long)(k * v)); + public static Tweener UInt64 => new((a, b) => a + b, (k, v) => (ulong)(k * v)); + public static Tweener IntPtr => new((a, b) => new IntPtr((long)a + (long)b), (k, v) => new IntPtr((long)(k * (long)v))); + public static Tweener UIntPtr => new((a, b) => new UIntPtr((ulong)a + (ulong)b), (k, v) => new UIntPtr((ulong)(k * (ulong)v))); + public static Tweener Float => new((a, b) => a + b, (k, v) => k * v); + public static Tweener Double => new((a, b) => a + b, (k, v) => k * v); + } + public static class EasingFunctions { + public static float Linear(float x) => x; + public static float InQuad(float x) => x * x; + public static float InCubic(float x) => x * x * x; + public static float InSine(float x) => 1 - OutSine(1 - x); + public static float OutQuad(float x) => 1 - InQuad(1 - x); + public static float OutCubic(float x) => 1 - InCubic(1 - x); + public static float OutSine(float x) => MathF.Sin(x * MathF.PI / 2); + } +} diff --git a/Assets/Cryville/Common/Tweener.cs.meta b/Assets/Cryville/Common/Tweener.cs.meta new file mode 100644 index 0000000..c6db865 --- /dev/null +++ b/Assets/Cryville/Common/Tweener.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 0b4037ba4138aae47b8da984f30b4db9 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Cryville/Common/Unity/StateTweener.cs b/Assets/Cryville/Common/Unity/StateTweener.cs new file mode 100644 index 0000000..c75af96 --- /dev/null +++ b/Assets/Cryville/Common/Unity/StateTweener.cs @@ -0,0 +1,181 @@ +using Cryville.Common.Reflection; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using UnityEngine; + +namespace Cryville.Common.Unity { + public class StateTweener : MonoBehaviour { + [SerializeField] + State[] m_states; + [Serializable] + struct State { + [SerializeField] + public string Name; + [SerializeField] + public AttributeState[] Attributes; + } + [Serializable] + struct AttributeState { + [SerializeField] + public AttributeBinding Attribute; + [SerializeField] + public string Value; + } + [Serializable] + struct AttributeBinding : IEquatable { + [SerializeField] + public Component Component; + [SerializeField] + public string Attribute; + + public bool Equals(AttributeBinding other) { + return Component.Equals(other.Component) && Attribute.Equals(other.Attribute); + } + public override bool Equals(object obj) { + return obj is AttributeBinding && Equals((AttributeBinding)obj); + } + public override int GetHashCode() { + return Component.GetHashCode() ^ Attribute.GetHashCode(); + } + } + + [SerializeField] + StateTweener[] m_children; + + readonly List _statePriority = new List(); + readonly Dictionary _defaults = new Dictionary(); + readonly Dictionary> _tweeners = new Dictionary>(); + readonly Dictionary> _runtimeStates = new Dictionary>(); + + void Awake() { + var types = new Dictionary(); + foreach (var state in m_states) { + Dictionary attrs; + _statePriority.Add(state.Name); + _runtimeStates.Add(state.Name, attrs = new Dictionary()); + foreach (var attr in state.Attributes) { + var binding = attr.Attribute; + if (!types.TryGetValue(binding, out var type)) { + var comp = binding.Component; + var ctype = comp.GetType(); + var path = binding.Attribute.Split('.'); + + var propName = path[0]; + var prop = ctype.GetMember(propName, MemberTypes.Field | MemberTypes.Property, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static).Single(); + var gvd = FieldLikeHelper.GetGetValueDelegate(prop); + var svd = FieldLikeHelper.GetSetValueDelegate(prop); + + Func getter; + Action setter; + if (path.Length == 1) { + type = FieldLikeHelper.GetMemberType(prop); + getter = () => gvd(comp); + setter = v => svd(comp, v); + } + else if (path.Length == 2) { + var propType = FieldLikeHelper.GetMemberType(prop); + var subPropName = path[1]; + var subProp = propType.GetMember(subPropName, MemberTypes.Field | MemberTypes.Property, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static).Single(); + + type = FieldLikeHelper.GetMemberType(subProp); + var gsvd = FieldLikeHelper.GetGetValueDelegate(subProp); + getter = () => gsvd(gvd(comp)); + var ssvd = FieldLikeHelper.GetSetValueDelegate(subProp); + setter = v => { + object obj = gvd(comp); + ssvd(obj, v); + svd(comp, obj); + }; + } + else { + throw new FormatException("Invalid attribute path."); + } + _tweeners.Add(binding, CreateTweener(type, getter, setter)); + _defaults.Add(binding, getter()); + types.Add(binding, type); + } + + attrs.Add(binding, Convert.ChangeType(attr.Value, type)); + } + } + foreach (var def in _defaults) { + foreach (var state in _runtimeStates) { + if (!state.Value.ContainsKey(def.Key)) { + state.Value.Add(def.Key, def.Value); + } + } + } + _spcmp = new StatePriorityComparer(this); + } + + PropertyTweener CreateTweener(Type type, Func getter, Action setter) { + if (type == typeof(byte)) return new PropertyTweener(getter, setter, Tweeners.Byte.Boxed()); + else if (type == typeof(sbyte)) return new PropertyTweener(getter, setter, Tweeners.SByte.Boxed()); + else if (type == typeof(short)) return new PropertyTweener(getter, setter, Tweeners.Int16.Boxed()); + else if (type == typeof(ushort)) return new PropertyTweener(getter, setter, Tweeners.UInt16.Boxed()); + else if (type == typeof(int)) return new PropertyTweener(getter, setter, Tweeners.Int32.Boxed()); + else if (type == typeof(uint)) return new PropertyTweener(getter, setter, Tweeners.UInt32.Boxed()); + else if (type == typeof(long)) return new PropertyTweener(getter, setter, Tweeners.Int64.Boxed()); + else if (type == typeof(ulong)) return new PropertyTweener(getter, setter, Tweeners.UInt64.Boxed()); + else if (type == typeof(IntPtr)) return new PropertyTweener(getter, setter, Tweeners.IntPtr.Boxed()); + else if (type == typeof(UIntPtr)) return new PropertyTweener(getter, setter, Tweeners.UIntPtr.Boxed()); + else if (type == typeof(float)) return new PropertyTweener(getter, setter, Tweeners.Float.Boxed()); + else if (type == typeof(double)) return new PropertyTweener(getter, setter, Tweeners.Double.Boxed()); + else throw new NotSupportedException("Property type not supported."); + } + + void Update() { + foreach (var tweener in _tweeners) tweener.Value.Advance(Time.deltaTime); + } + + readonly List m_cState = new List(); + public IReadOnlyList CurrentState => m_cState; + public void ClearState(float transitionDuration = float.Epsilon) { + foreach (var child in m_children) child.ClearState(transitionDuration); + m_cState.Clear(); + foreach (var tweener in _tweeners) { + tweener.Value.Start(_defaults[tweener.Key], transitionDuration); + } + } + public void EnterState(string state, float transitionDuration = float.Epsilon) { + if (_runtimeStates.TryGetValue(state, out var rs)) { + int index = m_cState.BinarySearch(state, _spcmp); + if (index < 0) { + index = ~index; + m_cState.Insert(index, state); + } + if (index == m_cState.Count - 1) { + foreach (var s in rs) { + _tweeners[s.Key].Start(s.Value, transitionDuration); + } + } + } + foreach (var child in m_children) child.EnterState(state, transitionDuration); + } + public void ExitState(string state, float transitionDuration = float.Epsilon) { + foreach (var child in m_children) child.ExitState(state, transitionDuration); + if (!_runtimeStates.ContainsKey(state)) return; + int index = m_cState.BinarySearch(state, _spcmp); + if (index < 0) return; + m_cState.RemoveAt(index); + if (index < m_cState.Count) return; + var attrs = m_cState.Count == 0 ? _defaults : _runtimeStates[m_cState[m_cState.Count - 1]]; + foreach (var tweener in _tweeners) { + tweener.Value.Start(attrs[tweener.Key], transitionDuration); + } + } + + StatePriorityComparer _spcmp; + class StatePriorityComparer : IComparer { + readonly StateTweener _self; + public StatePriorityComparer(StateTweener self) { + _self = self; + } + public int Compare(string x, string y) { + return _self._statePriority.IndexOf(x).CompareTo(_self._statePriority.IndexOf(y)); + } + } + } +} diff --git a/Assets/Cryville/Common/Unity/StateTweener.cs.meta b/Assets/Cryville/Common/Unity/StateTweener.cs.meta new file mode 100644 index 0000000..167abb9 --- /dev/null +++ b/Assets/Cryville/Common/Unity/StateTweener.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d605791cb18d88e4fb0ab6f794361eee +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: