diff --git a/Engine.Systems/Tween/Easings.cs b/Engine.Systems/Tween/Easings.cs new file mode 100644 index 0000000..6e879ee --- /dev/null +++ b/Engine.Systems/Tween/Easings.cs @@ -0,0 +1,72 @@ +// Reference: https://easings.net + +namespace Syntriax.Engine.Systems.Tween; + +internal static class EaseConstants +{ + internal const float c1 = 1.70158f; + internal const float c2 = c1 * 1.525f; + internal const float c3 = c1 + 1f; + internal const float c4 = 2f * Core.Math.PI / 3; + internal const float c5 = 2f * Core.Math.PI / 4.5f; +} + +public readonly struct EaseLinear : IEasing { public readonly float Evaluate(float value) => value; } + +public readonly struct EaseInQuad : IEasing { public readonly float Evaluate(float value) => value * value; } +public readonly struct EaseOutQuad : IEasing { public readonly float Evaluate(float value) => 1f - (1f - value) * (1f - value); } +public readonly struct EaseInOutQuad : IEasing { public readonly float Evaluate(float value) => value < 0.5f ? 2f * value * value : 1f - Core.Math.Pow(-2f * value + 2f, 2f) * .5f; } + +public readonly struct EaseInCubic : IEasing { public readonly float Evaluate(float value) => value * value * value; } +public readonly struct EaseOutCubic : IEasing { public readonly float Evaluate(float value) => 1f - Core.Math.Pow(1f - value, 3); } +public readonly struct EaseInOutCubic : IEasing { public readonly float Evaluate(float value) => value < 0.5f ? 4f * value * value * value : 1f - Core.Math.Pow(-2f * value + 2f, 3) * .5f; } + +public readonly struct EaseInQuart : IEasing { public readonly float Evaluate(float value) => value * value * value * value; } +public readonly struct EaseOutQuart : IEasing { public readonly float Evaluate(float value) => 1f - Core.Math.Pow(1f - value, 4f); } +public readonly struct EaseInOutQuart : IEasing { public readonly float Evaluate(float value) => value < 0.5f ? 8 * value * value * value * value : 1f - Core.Math.Pow(-2f * value + 2f, 4f) * .5f; } + +public readonly struct EaseInQuint : IEasing { public readonly float Evaluate(float value) => value * value * value * value * value; } +public readonly struct EaseOutQuint : IEasing { public readonly float Evaluate(float value) => 1f - Core.Math.Pow(1f - value, 5); } +public readonly struct EaseInOutQuint : IEasing { public readonly float Evaluate(float value) => value < 0.5f ? 16 * value * value * value * value * value : 1f - Core.Math.Pow(-2f * value + 2f, 5) * .5f; } + +public readonly struct EaseInSine : IEasing { public readonly float Evaluate(float value) => 1f - Core.Math.Cos(value * Core.Math.PI * .5f); } +public readonly struct EaseOutSine : IEasing { public readonly float Evaluate(float value) => Core.Math.Sin(value * Core.Math.PI * .5f); } +public readonly struct EaseInOutSine : IEasing { public readonly float Evaluate(float value) => -(Core.Math.Cos(Core.Math.PI * value) - 1f) * .5f; } + +public readonly struct EaseInExpo : IEasing { public readonly float Evaluate(float value) => value == 0 ? 0 : Core.Math.Pow(2f, 10f * value - 10f); } +public readonly struct EaseOutExpo : IEasing { public readonly float Evaluate(float value) => value == 1f ? 1f : 1f - Core.Math.Pow(2f, -10f * value); } +public readonly struct EaseInOutExpo : IEasing { public readonly float Evaluate(float value) => value == 0 ? 0 : value == 1f ? 1f : value < 0.5f ? Core.Math.Pow(2f, 20 * value - 10f) * .5f : (2f - Core.Math.Pow(2f, -20 * value + 10f)) * .5f; } + +public readonly struct EaseInCirc : IEasing { public readonly float Evaluate(float value) => 1f - Core.Math.Sqrt(1f - Core.Math.Pow(value, 2f)); } +public readonly struct EaseOutCirc : IEasing { public readonly float Evaluate(float value) => Core.Math.Sqrt(1f - Core.Math.Pow(value - 1f, 2f)); } +public readonly struct EaseInOutCirc : IEasing { public readonly float Evaluate(float value) => value < 0.5f ? (1f - Core.Math.Sqrt(1f - Core.Math.Pow(2f * value, 2f))) * .5f : (Core.Math.Sqrt(1f - Core.Math.Pow(-2f * value + 2f, 2f)) + 1f) * .5f; } + +public readonly struct EaseInBack : IEasing { public readonly float Evaluate(float value) => EaseConstants.c3 * value * value * value - EaseConstants.c1 * value * value; } +public readonly struct EaseOutBack : IEasing { public readonly float Evaluate(float value) => 1f + EaseConstants.c3 * Core.Math.Pow(value - 1f, 3) + EaseConstants.c1 * Core.Math.Pow(value - 1f, 2f); } +public readonly struct EaseInOutBack : IEasing { public readonly float Evaluate(float value) => value < 0.5f ? Core.Math.Pow(2f * value, 2f) * ((EaseConstants.c2 + 1f) * 2f * value - EaseConstants.c2) * .5f : (Core.Math.Pow(2f * value - 2f, 2f) * ((EaseConstants.c2 + 1f) * (value * 2f - 2f) + EaseConstants.c2) + 2f) * .5f; } + +public readonly struct EaseInElastic : IEasing { public readonly float Evaluate(float value) => value == 0 ? 0 : value == 1f ? 1f : -Core.Math.Pow(2f, 10f * value - 10f) * Core.Math.Sin((value * 10f - 10.75f) * EaseConstants.c4); } +public readonly struct EaseOutElastic : IEasing { public readonly float Evaluate(float value) => value == 0 ? 0 : value == 1f ? 1f : Core.Math.Pow(2f, -10f * value) * Core.Math.Sin((value * 10f - 0.75f) * EaseConstants.c4) + 1f; } +public readonly struct EaseInOutElastic : IEasing { public readonly float Evaluate(float value) => value == 0 ? 0 : value == 1f ? 1f : value < 0.5f ? -(Core.Math.Pow(2f, 20 * value - 10f) * Core.Math.Sin((20 * value - 11.125f) * EaseConstants.c5)) * .5f : Core.Math.Pow(2f, -20 * value + 10f) * Core.Math.Sin((20 * value - 11.125f) * EaseConstants.c5) * .5f + 1f; } + +public readonly struct EaseInBounce : IEasing { public readonly float Evaluate(float value) => 1f - new EaseOutBounce().Evaluate(1f - value); } +public readonly struct EaseOutBounce : IEasing +{ + public readonly float Evaluate(float value) + { + const float n1 = 7.5625f; + const float d1 = 2.75f; + + if (value < 1 / d1) + return n1 * value * value; + + if (value < 2 / d1) + return n1 * (value -= 1.5f / d1) * value + 0.75f; + + if (value < 2.5 / d1) + return n1 * (value -= 2.25f / d1) * value + 0.9375f; + + return n1 * (value -= 2.625f / d1) * value + 0.984375f; + } +} +public readonly struct EaseInOutBounce : IEasing { public readonly float Evaluate(float value) => value < 0.5f ? (1f - new EaseOutBounce().Evaluate(1f - 2f * value)) * .5f : (1f + new EaseOutBounce().Evaluate(2f * value - 1f)) * .5f; } diff --git a/Engine.Systems/Tween/IEasing.cs b/Engine.Systems/Tween/IEasing.cs new file mode 100644 index 0000000..92ed50a --- /dev/null +++ b/Engine.Systems/Tween/IEasing.cs @@ -0,0 +1,7 @@ +namespace Syntriax.Engine.Systems.Tween; + +public interface IEasing +{ + float Evaluate(float value); +} + diff --git a/Engine.Systems/Tween/ITween.cs b/Engine.Systems/Tween/ITween.cs new file mode 100644 index 0000000..6f90137 --- /dev/null +++ b/Engine.Systems/Tween/ITween.cs @@ -0,0 +1,25 @@ +namespace Syntriax.Engine.Systems.Tween; + +public interface ITween +{ + event TweenEventHandler? OnStarted; + event TweenEventHandler? OnPaused; + event TweenEventHandler? OnResumed; + event TweenEventHandler? OnCancelled; + event TweenEventHandler? OnCompleted; + event TweenEventHandler? OnEnded; + + event TweenEventHandler? OnUpdated; + event TweenDeltaEventHandler? OnDeltaUpdated; + + TweenState State { get; set; } + + float Counter { get; } + float Duration { get; } + float Progress { get; } + float Value { get; } + + delegate void TweenEventHandler(ITween sender); + delegate void TweenDeltaEventHandler(ITween sender, float delta); +} + diff --git a/Engine.Systems/Tween/Tween.cs b/Engine.Systems/Tween/Tween.cs new file mode 100644 index 0000000..a9fb89d --- /dev/null +++ b/Engine.Systems/Tween/Tween.cs @@ -0,0 +1,82 @@ +using Syntriax.Engine.Core; + +namespace Syntriax.Engine.Systems.Tween; + +internal class Tween : ITween +{ + public event ITween.TweenEventHandler? OnStarted = null; + public event ITween.TweenEventHandler? OnPaused = null; + public event ITween.TweenEventHandler? OnResumed = null; + public event ITween.TweenEventHandler? OnCancelled = null; + public event ITween.TweenEventHandler? OnCompleted = null; + public event ITween.TweenEventHandler? OnEnded = null; + public event ITween.TweenEventHandler? OnUpdated = null; + public event ITween.TweenDeltaEventHandler? OnDeltaUpdated = null; + + private TweenState _state = TweenState.Idle; + public TweenState State + { + get => _state; + set + { + if (value == _state) + return; + + TweenState previousState = _state; + _state = value; + switch (value) + { + case TweenState.Completed: OnCompleted?.Invoke(this); OnEnded?.Invoke(this); break; + case TweenState.Cancelled: OnCancelled?.Invoke(this); OnEnded?.Invoke(this); break; + case TweenState.Paused: OnPaused?.Invoke(this); break; + case TweenState.Playing: + if (previousState == TweenState.Idle) + OnStarted?.Invoke(this); + else + OnResumed?.Invoke(this); + break; + } + } + } + + public float Duration { get; internal set; } = 1f; + public float Progress { get; internal set; } = 0f; + private float _counter = 0f; + + public IEasing Easing { get; set; } = new EaseLinear(); + public float Value => Easing.Evaluate(Progress); + + public float Counter + { + get => _counter; + set + { + if (value < _counter) + return; + + float previousProgress = Progress; + + _counter = value.Min(Duration).Max(0f); + Progress = Counter / Duration; + OnUpdated?.Invoke(this); + + OnDeltaUpdated?.Invoke(this, Easing.Evaluate(Progress) - Easing.Evaluate(previousProgress)); + + if (_counter >= Duration) + State = TweenState.Completed; + } + } + + internal void Reset() + { + _counter = 0f; + Progress = 0f; + State = TweenState.Idle; + } + + public Tween() { } + public Tween(float duration) + { + Duration = duration; + } +} diff --git a/Engine.Systems/Tween/TweenExtensions.cs b/Engine.Systems/Tween/TweenExtensions.cs new file mode 100644 index 0000000..77e02ac --- /dev/null +++ b/Engine.Systems/Tween/TweenExtensions.cs @@ -0,0 +1,90 @@ +namespace Syntriax.Engine.Systems.Tween; + +public static class TweenExtensions +{ + public static ITween Loop(this ITween tween, int count) + { + Tween tweenConcrete = (Tween)tween; + int counter = count; + + tweenConcrete.OnCompleted += _ => + { + if (counter-- <= 0) + return; + + tweenConcrete.Reset(); + tweenConcrete.State = TweenState.Playing; + }; + + return tween; + } + + public static ITween LoopInfinitely(this ITween tween) + { + Tween tweenConcrete = (Tween)tween; + tweenConcrete.OnCompleted += _ => + { + tweenConcrete.Reset(); + tweenConcrete.State = TweenState.Playing; + }; + + return tween; + } + + public static ITween Ease(this ITween tween, IEasing easing) + { + Tween tweenConcrete = (Tween)tween; + tweenConcrete.Easing = easing; + + return tween; + } + + public static ITween OnStart(this ITween tween, Action callback) + { + Tween tweenConcrete = (Tween)tween; + tweenConcrete.OnStarted += _ => callback.Invoke(); + return tween; + } + public static ITween OnPause(this ITween tween, Action callback) + { + Tween tweenConcrete = (Tween)tween; + tweenConcrete.OnPaused += _ => callback.Invoke(); + return tween; + } + public static ITween OnResume(this ITween tween, Action callback) + { + Tween tweenConcrete = (Tween)tween; + tweenConcrete.OnResumed += _ => callback.Invoke(); + return tween; + } + public static ITween OnCancel(this ITween tween, Action callback) + { + Tween tweenConcrete = (Tween)tween; + tweenConcrete.OnCancelled += _ => callback.Invoke(); + return tween; + } + public static ITween OnComplete(this ITween tween, Action callback) + { + Tween tweenConcrete = (Tween)tween; + tweenConcrete.OnCompleted += _ => callback.Invoke(); + return tween; + } + public static ITween OnEnd(this ITween tween, Action callback) + { + Tween tweenConcrete = (Tween)tween; + tweenConcrete.OnEnded += _ => callback.Invoke(); + return tween; + } + public static ITween OnUpdate(this ITween tween, Action callback) + { + Tween tweenConcrete = (Tween)tween; + tweenConcrete.OnUpdated += _ => callback.Invoke(); + return tween; + } + public static ITween OnDeltaUpdate(this ITween tween, Action callback) + { + Tween tweenConcrete = (Tween)tween; + tweenConcrete.OnDeltaUpdated += (_, delta) => callback.Invoke(delta); + return tween; + } +} diff --git a/Engine.Systems/Tween/TweenManager.cs b/Engine.Systems/Tween/TweenManager.cs new file mode 100644 index 0000000..d045cef --- /dev/null +++ b/Engine.Systems/Tween/TweenManager.cs @@ -0,0 +1,66 @@ +using System.Collections; + +using Syntriax.Engine.Core; +using Syntriax.Engine.Core.Abstract; + +namespace Syntriax.Engine.Systems.Tween; + +public class TweenManager : HierarchyObject +{ + private CoroutineManager coroutineManager = null!; + + private readonly Dictionary runningCoroutines = []; + + public ITween StartTween(float duration, TweenSetCallback? setCallback = null) + { + Tween tween = new(duration); + tween.OnUpdated += tween => setCallback?.Invoke(tween.Value); + runningCoroutines.Add(tween, coroutineManager.StartCoroutine(RunTween(tween))); + return tween; + } + + private IEnumerator RunTween(Tween tween) + { + tween.State = TweenState.Playing; + yield return null; + + while (true) + { + if (tween.State.CheckFlag(TweenState.Cancelled | TweenState.Completed)) + break; + + if (tween.State.CheckFlag(TweenState.Paused)) + { + yield return null; + continue; + } + + tween.Counter += GameManager.Time.DeltaTime; + yield return null; + } + + runningCoroutines.Remove(tween); + } + + public void CancelTween(ITween tween) + { + if (!runningCoroutines.TryGetValue(tween, out IEnumerator? runningCoroutine)) + return; + + tween.State = TweenState.Cancelled; + coroutineManager.StopCoroutine(runningCoroutine); + runningCoroutines.Remove(tween); + } + + protected override void OnEnteringHierarchy(IGameManager gameManager) + { + coroutineManager = gameManager.FindHierarchyObject() ?? throw new($"No {nameof(CoroutineManager)} was found in the game manager"); + } + + protected override void OnExitingHierarchy(IGameManager gameManager) + { + coroutineManager = null!; + } + + public delegate void TweenSetCallback(float t); +} diff --git a/Engine.Systems/Tween/TweenState.cs b/Engine.Systems/Tween/TweenState.cs new file mode 100644 index 0000000..166e925 --- /dev/null +++ b/Engine.Systems/Tween/TweenState.cs @@ -0,0 +1,10 @@ +namespace Syntriax.Engine.Systems.Tween; + +public enum TweenState +{ + Idle = 0b00001, + Playing = 0b00010, + Paused = 0b00100, + Cancelled = 0b01000, + Completed = 0b10000, +} diff --git a/Engine.Systems/Tween/Yields/WaitForTweenCompleteCoroutineYield.cs b/Engine.Systems/Tween/Yields/WaitForTweenCompleteCoroutineYield.cs new file mode 100644 index 0000000..0a8c290 --- /dev/null +++ b/Engine.Systems/Tween/Yields/WaitForTweenCompleteCoroutineYield.cs @@ -0,0 +1,5 @@ +using Syntriax.Engine.Core; + +namespace Syntriax.Engine.Systems.Tween; + +public class WaitForTweenCompleteCoroutineYield(ITween tween) : CoroutineYield(() => tween.State == TweenState.Completed); diff --git a/Engine.Systems/Tween/Yields/WaitForTweenDoneCoroutineYield.cs b/Engine.Systems/Tween/Yields/WaitForTweenDoneCoroutineYield.cs new file mode 100644 index 0000000..b34330b --- /dev/null +++ b/Engine.Systems/Tween/Yields/WaitForTweenDoneCoroutineYield.cs @@ -0,0 +1,5 @@ +using Syntriax.Engine.Core; + +namespace Syntriax.Engine.Systems.Tween; + +public class WaitWhileTweenActiveCoroutineYield(ITween tween) : CoroutineYield(() => tween.State.CheckFlag(TweenState.Completed | TweenState.Cancelled));