660 lines
19 KiB
C#

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<float> 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<bool> 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<bool> condition;
public WaitForConditionCoroutine(Func<bool> 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<T> : EmptyCoroutine
{
protected T from;
protected T to;
protected float duration;
protected UnityAction<T> 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<T> 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<float>
{
public FloatEasingCoroutine(float from, float to, float duration, float delay, UnityAction<float> 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<Vector3>
{
public VectorEasingCoroutine3(Vector3 from, Vector3 to, float duration, float delay, UnityAction<Vector3> 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<Vector2>
{
public VectorEasingCoroutine2(Vector2 from, Vector2 to, float duration, float delay, UnityAction<Vector2> 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<Color>
{
public ColorEasingCoroutine(Color from, Color to, float duration, float delay, UnityAction<Color> 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<PositionEasingJobAnimation> waitingAnimations;
protected List<PositionEasingJobAnimation> activeAnimations;
[ReadOnly] public NativeList<float2> timeData;
[ReadOnly] public NativeList<float> useUnscaledTime;
[ReadOnly] public NativeList<FunctionPointer<EasingFunctions.EasingFunction>> easingFunctions;
[ReadOnly] public NativeList<float2> startPositions;
[ReadOnly] public NativeList<float2> targets;
[WriteOnly] public NativeList<float2> positions;
public bool isJobRunning = false;
protected DoPosition2DJob doPosition2DJob;
protected JobHandle doPosition2DJobHandle;
protected int capacityCache;
public EasingPositionJobRunner()
{
waitingAnimations = new List<PositionEasingJobAnimation>(10);
activeAnimations = new List<PositionEasingJobAnimation>(50);
timeData = new NativeList<float2>(50, Allocator.Persistent);
useUnscaledTime = new NativeList<float>(50, Allocator.Persistent);
easingFunctions = new NativeList<FunctionPointer<EasingFunctions.EasingFunction>>(50, Allocator.Persistent);
startPositions = new NativeList<float2>(50, Allocator.Persistent);
targets = new NativeList<float2>(50, Allocator.Persistent);
positions = new NativeList<float2>(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<float2> timeData;
[ReadOnly] public NativeArray<float> useUnscaledTime;
[ReadOnly] public NativeArray<FunctionPointer<EasingFunctions.EasingFunction>> easingFunctions;
[ReadOnly] public NativeArray<float2> startPositions;
[ReadOnly] public NativeArray<float2> targets;
[WriteOnly] public NativeArray<float2> 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;
}
}
}
}