using OctoberStudio.Extensions; using System; using System.Collections; using System.Collections.Generic; using System.Runtime.CompilerServices; using Unity.Burst; using Unity.Collections; using Unity.Jobs; using Unity.Mathematics; using UnityEngine; using UnityEngine.Events; namespace OctoberStudio.Easing { public class EasingManager : MonoBehaviour { private static EasingManager instance; protected EasingPositionJobRunner positionJobRunner; public static EasingPositionJobRunner PositionJobRunner => instance.positionJobRunner; public virtual void Awake() { instance = this; positionJobRunner = new EasingPositionJobRunner(); } protected virtual void OnDestroy() { positionJobRunner.Clear(); } protected virtual void Update() { positionJobRunner.Update(); } protected virtual void LateUpdate() { positionJobRunner.LateUpdate(); } public static IEasingCoroutine DoFloat(float from, float to, float duration, UnityAction action, float delay = 0) { return new FloatEasingCoroutine(from, to, duration, delay, action); } public static IEasingCoroutine DoAfter(float seconds, UnityAction action, bool unscaledTime = false) { return new WaitCoroutine(seconds, unscaledTime).SetOnFinish(action); } public static IEasingCoroutine DoAfter(Func condition) { return new WaitForConditionCoroutine(condition); } public static IEasingCoroutine DoNextFrame() { return new NextFrameCoroutine(); } public static IEasingCoroutine DoNextFrame(UnityAction action) { return new NextFrameCoroutine().SetOnFinish(action); } public static IEasingCoroutine DoNextFixedFrame() { return new NextFixedFrameCoroutine(); } public static Coroutine StartCustomCoroutine(IEnumerator coroutine) { return instance.StartCoroutine(coroutine); } public static void StopCustomCoroutine(Coroutine coroutine) { if (instance != null) instance.StopCoroutine(coroutine); } } public interface IEasingCoroutine { bool IsActive { get; } IEasingCoroutine SetEasing(EasingType easingType); IEasingCoroutine SetEasingCurve(AnimationCurve easingCurve); IEasingCoroutine SetOnFinish(UnityAction callback); IEasingCoroutine SetUnscaledTime(bool unscaledTime); IEasingCoroutine SetDelay(float delay); void Stop(); } public abstract class EmptyCoroutine : IEasingCoroutine { protected Coroutine coroutine; public bool IsActive { get; protected set; } protected UnityAction finishCallback; protected EasingType easingType = EasingType.Linear; protected float delay = -1; protected bool unscaledTime; protected bool useCurve; protected AnimationCurve easingCurve; public IEasingCoroutine SetEasing(EasingType easingType) { this.easingType = easingType; useCurve = false; return this; } public IEasingCoroutine SetOnFinish(UnityAction callback) { finishCallback = callback; return this; } public IEasingCoroutine SetUnscaledTime(bool unscaledTime) { this.unscaledTime = unscaledTime; return this; } public IEasingCoroutine SetEasingCurve(AnimationCurve curve) { easingCurve = curve; useCurve = true; return this; } public IEasingCoroutine SetDelay(float delay) { this.delay = delay; return this; } public void Stop() { EasingManager.StopCustomCoroutine(coroutine); IsActive = false; } } public class NextFrameCoroutine : EmptyCoroutine { public NextFrameCoroutine() { coroutine = EasingManager.StartCustomCoroutine(Coroutine()); } private IEnumerator Coroutine() { IsActive = true; yield return null; finishCallback?.Invoke(); IsActive = false; } } public class NextFixedFrameCoroutine : EmptyCoroutine { public NextFixedFrameCoroutine() { coroutine = EasingManager.StartCustomCoroutine(Coroutine()); } private IEnumerator Coroutine() { IsActive = true; yield return new WaitForFixedUpdate(); finishCallback?.Invoke(); IsActive = false; } } public class WaitCoroutine : EmptyCoroutine { protected float duration; public WaitCoroutine(float duration, bool unscaledTime = false) { this.duration = duration; this.unscaledTime = unscaledTime; coroutine = EasingManager.StartCustomCoroutine(Coroutine()); } private IEnumerator Coroutine() { IsActive = true; while (delay > 0) { yield return null; delay -= unscaledTime ? Time.unscaledDeltaTime : Time.deltaTime; } if (unscaledTime) { yield return new WaitForSecondsRealtime(duration); } else { yield return new WaitForSeconds(duration); } finishCallback?.Invoke(); IsActive = false; } } public class WaitForConditionCoroutine : EmptyCoroutine { private Func condition; public WaitForConditionCoroutine(Func condition) { this.condition = condition; coroutine = EasingManager.StartCustomCoroutine(Coroutine()); } private IEnumerator Coroutine() { IsActive = true; while (delay > 0) { yield return null; delay -= unscaledTime ? Time.unscaledDeltaTime : Time.deltaTime; } do { yield return null; } while (!condition()); finishCallback?.Invoke(); IsActive = false; } } public abstract class EasingCoroutine : EmptyCoroutine { protected T from; protected T to; protected float duration; protected UnityAction callback; [MethodImpl(MethodImplOptions.AggressiveInlining)] public abstract T Lerp(T a, T b, float t); public EasingCoroutine(T from, T to, float duration, float delay, UnityAction callback) { this.from = from; this.to = to; this.duration = duration; this.callback = callback; this.delay = delay; coroutine = EasingManager.StartCustomCoroutine(Coroutine()); } private IEnumerator Coroutine() { IsActive = true; float time = 0; while (time < duration) { yield return null; if (delay > 0) { delay -= unscaledTime ? Time.unscaledDeltaTime : Time.deltaTime; if (delay > 0) continue; } time += unscaledTime ? Time.unscaledDeltaTime : Time.deltaTime; float t; if (useCurve) { t = easingCurve.Evaluate(time / duration); } else { t = EasingFunctions.ApplyEasing(time / duration, easingType); } T value = Lerp(from, to, t); callback?.Invoke(value); } callback.Invoke(to); finishCallback?.Invoke(); IsActive = false; } } public class FloatEasingCoroutine : EasingCoroutine { public FloatEasingCoroutine(float from, float to, float duration, float delay, UnityAction callback) : base(from, to, duration, delay, callback) { } [MethodImpl(MethodImplOptions.AggressiveInlining)] public override float Lerp(float a, float b, float t) { return Mathf.LerpUnclamped(a, b, t); } } public class VectorEasingCoroutine3 : EasingCoroutine { public VectorEasingCoroutine3(Vector3 from, Vector3 to, float duration, float delay, UnityAction callback) : base(from, to, duration, delay, callback) { } [MethodImpl(MethodImplOptions.AggressiveInlining)] public override Vector3 Lerp(Vector3 a, Vector3 b, float t) { return Vector3.LerpUnclamped(a, b, t); } } public class VectorEasingCoroutine2 : EasingCoroutine { public VectorEasingCoroutine2(Vector2 from, Vector2 to, float duration, float delay, UnityAction callback) : base(from, to, duration, delay, callback) { } [MethodImpl(MethodImplOptions.AggressiveInlining)] public override Vector2 Lerp(Vector2 a, Vector2 b, float t) { return Vector2.LerpUnclamped(a, b, t); } } public class ColorEasingCoroutine : EasingCoroutine { public ColorEasingCoroutine(Color from, Color to, float duration, float delay, UnityAction callback) : base(from, to, duration, delay, callback) { } [MethodImpl(MethodImplOptions.AggressiveInlining)] public override Color Lerp(Color a, Color b, float t) { return Color.LerpUnclamped(a, b, t); } } public abstract class EasingJobAnimation { protected float startTime; public float StartTime => startTime; protected float endTime; public float EndTime => endTime; protected bool useUnscaledTime; public bool UseUnscaledTime => useUnscaledTime; protected EasingType easingType; public EasingType EasingType => easingType; protected UnityAction finishCallback; protected EasingJobAnimation(float duration, float delay, bool useUnscaledTime, EasingType easingType) { this.useUnscaledTime = useUnscaledTime; var time = useUnscaledTime ? Time.unscaledTime : Time.time; startTime = time + delay; endTime = startTime + duration; this.easingType = easingType; } public bool IsActive => useUnscaledTime ? Time.unscaledTime < endTime : Time.time < endTime; public bool IsStarted => useUnscaledTime ? startTime <= Time.unscaledTime : startTime <= Time.time; public virtual void SetOnFinish(UnityAction finishCallback) { this.finishCallback = finishCallback; } public virtual void Finish() { finishCallback?.Invoke(); } } public class PositionEasingJobAnimation : EasingJobAnimation { protected Transform transform; protected Transform targetTransform; public float2 Position { get => transform.position.XY(); set => transform.position = (Vector2)value; } public float2 Target => targetTransform.position.XY(); public PositionEasingJobAnimation(Transform transform, Transform targetTransform, float duration, float delay, bool useUnscaledTime, EasingType easingType) : base(duration, delay, useUnscaledTime, easingType) { this.transform = transform; this.targetTransform = targetTransform; IsValid = true; EasingManager.PositionJobRunner.AddJobAnimaiton(this); } public bool IsValid { get; protected set; } } public class EasingPositionJobRunner { protected List waitingAnimations; protected List activeAnimations; [ReadOnly] public NativeList timeData; [ReadOnly] public NativeList useUnscaledTime; [ReadOnly] public NativeList> easingFunctions; [ReadOnly] public NativeList startPositions; [ReadOnly] public NativeList targets; [WriteOnly] public NativeList positions; public bool isJobRunning = false; protected DoPosition2DJob doPosition2DJob; protected JobHandle doPosition2DJobHandle; protected int capacityCache; public EasingPositionJobRunner() { waitingAnimations = new List(10); activeAnimations = new List(50); timeData = new NativeList(50, Allocator.Persistent); useUnscaledTime = new NativeList(50, Allocator.Persistent); easingFunctions = new NativeList>(50, Allocator.Persistent); startPositions = new NativeList(50, Allocator.Persistent); targets = new NativeList(50, Allocator.Persistent); positions = new NativeList(50, Allocator.Persistent); doPosition2DJob = new DoPosition2DJob(); capacityCache = timeData.Capacity; ReinitializeJob(); } public virtual void AddJobAnimaiton(PositionEasingJobAnimation jobAnimation) { if (jobAnimation.IsStarted && !isJobRunning) { AddActiveAnimation(jobAnimation); } else { waitingAnimations.Add(jobAnimation); } } public virtual void Update() { if (waitingAnimations.Count > 0) { for (int i = 0; i < waitingAnimations.Count; i++) { if (waitingAnimations[i].IsStarted) { AddActiveAnimation(waitingAnimations[i]); waitingAnimations.RemoveAt(i); i--; } } } if (activeAnimations.Count == 0) return; for (int i = 0; i < activeAnimations.Count; i++) { var animation = activeAnimations[i]; if (animation.IsValid) { targets[i] = activeAnimations[i].Target; } else { RemoveActiveAnimation(i); i--; } } doPosition2DJob.scaledTime = Time.time; doPosition2DJob.unscaledTime = Time.unscaledTime; doPosition2DJobHandle = doPosition2DJob.Schedule(activeAnimations.Count, 16); JobHandle.ScheduleBatchedJobs(); isJobRunning = true; } protected virtual void ReinitializeJob() { doPosition2DJob.timeData = timeData.AsDeferredJobArray(); doPosition2DJob.useUnscaledTime = useUnscaledTime.AsDeferredJobArray(); doPosition2DJob.easingFunctions = easingFunctions.AsDeferredJobArray(); doPosition2DJob.startPositions = startPositions.AsDeferredJobArray(); doPosition2DJob.targets = targets.AsDeferredJobArray(); doPosition2DJob.positions = positions.AsDeferredJobArray(); } protected virtual void AddActiveAnimation(PositionEasingJobAnimation jobAnimation) { activeAnimations.Add(jobAnimation); timeData.Add(new float2(jobAnimation.StartTime, jobAnimation.EndTime)); useUnscaledTime.Add(jobAnimation.UseUnscaledTime ? 1f : 0f); easingFunctions.Add(EasingFunctions.Functions[(int)jobAnimation.EasingType]); startPositions.Add(jobAnimation.Position); targets.Add(jobAnimation.Target); positions.Add(float2.zero); if(timeData.Capacity != capacityCache) { capacityCache = timeData.Capacity; ReinitializeJob(); } } public virtual void LateUpdate() { if (!isJobRunning) return; isJobRunning = false; doPosition2DJobHandle.Complete(); for (int i = 0; i < activeAnimations.Count; i++) { var animation = activeAnimations[i]; var remove = false; if (animation.IsValid) { activeAnimations[i].Position = positions[i]; if (!animation.IsActive) { animation.Finish(); remove = true; } } else { remove = true; } if (remove) { RemoveActiveAnimation(i); i--; } } } protected virtual void RemoveActiveAnimation(int index) { activeAnimations.RemoveAt(index); timeData.RemoveAt(index); useUnscaledTime.RemoveAt(index); easingFunctions.RemoveAt(index); startPositions.RemoveAt(index); targets.RemoveAt(index); positions.RemoveAt(index); } public virtual void Clear() { if(isJobRunning) doPosition2DJobHandle.Complete(); timeData.Dispose(); useUnscaledTime.Dispose(); easingFunctions.Dispose(); startPositions.Dispose(); targets.Dispose(); positions.Dispose(); } [BurstCompile(OptimizeFor = OptimizeFor.Performance)] public struct DoPosition2DJob : IJobParallelFor { // x - startTime, y = endTime [ReadOnly] public NativeArray timeData; [ReadOnly] public NativeArray useUnscaledTime; [ReadOnly] public NativeArray> easingFunctions; [ReadOnly] public NativeArray startPositions; [ReadOnly] public NativeArray targets; [WriteOnly] public NativeArray positions; [ReadOnly] public float scaledTime; [ReadOnly] public float unscaledTime; public void Execute(int i) { var time = math.select(unscaledTime, scaledTime, useUnscaledTime[i] == 0f); var t = math.unlerp(timeData[i].x, timeData[i].y, time); t = easingFunctions[i].Invoke(math.saturate(t)); positions[i] = startPositions[i] + (targets[i] - startPositions[i]) * t; } } } }