using System; using System.Runtime.CompilerServices; using Unity.Collections; using UnityEngine; using LitMotion.Collections; namespace LitMotion { internal class MotionBuilderBuffer where TValue : unmanaged where TOptions : unmanaged, IMotionOptions { static MotionBuilderBuffer PoolRoot = new(); public static MotionBuilderBuffer Rent() { MotionBuilderBuffer result; if (PoolRoot == null) { result = new(); } else { result = PoolRoot; PoolRoot = PoolRoot.NextNode; result.NextNode = null; } return result; } public static void Return(MotionBuilderBuffer buffer) { buffer.Version++; buffer.Data = MotionData.Default; buffer.CallbackData = MotionCallbackData.Default; buffer.AnimationCurve = default; buffer.Scheduler = default; buffer.IsPreserved = default; buffer.BindOnSchedule = default; if (buffer.Version != ushort.MaxValue) { buffer.NextNode = PoolRoot; PoolRoot = buffer; } } public ushort Version; public MotionBuilderBuffer NextNode; public bool IsPreserved; public bool BindOnSchedule; public MotionData Data = MotionData.Default; public MotionCallbackData CallbackData = MotionCallbackData.Default; public AnimationCurve AnimationCurve; public IMotionScheduler Scheduler; } /// /// Supports construction, scheduling, and binding of motion entities. /// /// The type of value to animate /// The type of special parameters given to the motion data /// The type of adapter that support value animation public struct MotionBuilder : IDisposable where TValue : unmanaged where TOptions : unmanaged, IMotionOptions where TAdapter : unmanaged, IMotionAdapter { internal MotionBuilder(MotionBuilderBuffer buffer) { this.buffer = buffer; this.version = buffer.Version; } internal ushort version; internal MotionBuilderBuffer buffer; /// /// Specify easing for motion. /// /// The type of easing /// This builder to allow chaining multiple method calls. [MethodImpl(MethodImplOptions.AggressiveInlining)] public readonly MotionBuilder WithEase(Ease ease) { CheckEaseType(ease); CheckBuffer(); buffer.Data.Core.Ease = ease; return this; } /// /// Specify easing for motion. /// /// Animation curve /// This builder to allow chaining multiple method calls. [MethodImpl(MethodImplOptions.AggressiveInlining)] public readonly MotionBuilder WithEase(AnimationCurve animationCurve) { CheckBuffer(); buffer.AnimationCurve = animationCurve; buffer.Data.Core.Ease = Ease.CustomAnimationCurve; return this; } /// /// Specify the delay time when the motion starts. /// /// Delay time (seconds) /// Delay type /// Whether to skip updating values during the delay time /// This builder to allow chaining multiple method calls. [MethodImpl(MethodImplOptions.AggressiveInlining)] public readonly MotionBuilder WithDelay(float delay, DelayType delayType = DelayType.FirstLoop, bool skipValuesDuringDelay = true) { CheckBuffer(); buffer.Data.Core.Delay = delay; buffer.Data.Core.DelayType = delayType; buffer.CallbackData.SkipValuesDuringDelay = skipValuesDuringDelay; return this; } /// /// Specify the number of times the motion is repeated. If specified as less than 0, the motion will continue to play until manually completed or canceled. /// /// Number of loops /// Behavior at the end of each loop /// This builder to allow chaining multiple method calls. [MethodImpl(MethodImplOptions.AggressiveInlining)] public readonly MotionBuilder WithLoops(int loops, LoopType loopType = LoopType.Restart) { CheckBuffer(); buffer.Data.Core.Loops = loops; buffer.Data.Core.LoopType = loopType; return this; } /// /// Specify special parameters for each motion data. /// /// Option value to specify /// This builder to allow chaining multiple method calls. [MethodImpl(MethodImplOptions.AggressiveInlining)] public readonly MotionBuilder WithOptions(TOptions options) { CheckBuffer(); buffer.Data.Options = options; return this; } /// /// Specify the callback when canceled. /// /// Callback when canceled /// This builder to allow chaining multiple method calls. [MethodImpl(MethodImplOptions.AggressiveInlining)] public readonly MotionBuilder WithOnCancel(Action callback) { CheckBuffer(); buffer.CallbackData.OnCancelAction += callback; return this; } /// /// Specify the callback when playback ends. /// /// Callback when playback ends /// This builder to allow chaining multiple method calls. [MethodImpl(MethodImplOptions.AggressiveInlining)] public readonly MotionBuilder WithOnComplete(Action callback) { CheckBuffer(); buffer.CallbackData.OnCompleteAction += callback; return this; } /// /// Cancel Motion when an exception occurs during Bind processing. /// /// Whether to cancel on error /// This builder to allow chaining multiple method calls. [MethodImpl(MethodImplOptions.AggressiveInlining)] public readonly MotionBuilder WithCancelOnError(bool cancelOnError = true) { CheckBuffer(); buffer.CallbackData.CancelOnError = cancelOnError; return this; } /// /// Bind values when scheduling the motion. /// /// Whether to bind on sheduling /// This builder to allow chaining multiple method calls. [MethodImpl(MethodImplOptions.AggressiveInlining)] public readonly MotionBuilder WithBindOnSchedule(bool bindOnSchedule = true) { CheckBuffer(); buffer.BindOnSchedule = bindOnSchedule; return this; } /// /// Specifies the scheduler that schedule the motion. /// /// Scheduler /// This builder to allow chaining multiple method calls. [MethodImpl(MethodImplOptions.AggressiveInlining)] public readonly MotionBuilder WithScheduler(IMotionScheduler scheduler) { CheckBuffer(); buffer.Scheduler = scheduler; return this; } /// /// Create motion and play it without binding it to a specific object. /// /// Handle of the created motion data. public MotionHandle RunWithoutBinding() { CheckBuffer(); SetMotionData(); var scheduler = buffer.Scheduler; return Schedule(scheduler, ref buffer.Data, ref buffer.CallbackData); } /// /// Create motion and bind it to a specific object, property, etc. /// /// Action that handles binding /// Handle of the created motion data. public MotionHandle Bind(Action action) { CheckBuffer(); SetMotionData(); SetCallbackData(action); var scheduler = buffer.Scheduler; return Schedule(scheduler, ref buffer.Data, ref buffer.CallbackData); } /// /// Create motion and bind it to a specific object. Unlike the regular Bind method, it avoids allocation by closure by passing an object. /// /// Type of state /// Motion state /// Action that handles binding /// Handle of the created motion data. public MotionHandle BindWithState(TState state, Action action) where TState : class { CheckBuffer(); SetMotionData(); SetCallbackData(state, action); var scheduler = buffer.Scheduler; return Schedule(scheduler, ref buffer.Data, ref buffer.CallbackData); } /// /// Create motion and bind it to a specific object. Unlike the regular Bind method, it avoids allocation by closure by passing an object. /// /// Type of state /// Type of state /// Motion state /// Action that handles binding /// Handle of the created motion data. public MotionHandle BindWithState(TState1 state1, TState2 state2, Action action) where TState1 : class where TState2 : class { CheckBuffer(); SetMotionData(); SetCallbackData(state1, state2, action); var scheduler = buffer.Scheduler; return Schedule(scheduler, ref buffer.Data, ref buffer.CallbackData); } /// /// Create motion and bind it to a specific object. Unlike the regular Bind method, it avoids allocation by closure by passing an object. /// /// Type of state /// Type of state /// Type of state /// Motion state /// Action that handles binding /// Handle of the created motion data. public MotionHandle BindWithState(TState1 state1, TState2 state2, TState3 state3, Action action) where TState1 : class where TState2 : class where TState3 : class { CheckBuffer(); SetMotionData(); SetCallbackData(state1, state2, state3, action); var scheduler = buffer.Scheduler; return Schedule(scheduler, ref buffer.Data, ref buffer.CallbackData); } /// /// Preserves the internal buffer and prevents the builder from being automatically destroyed after creating the motion data. /// Calling this allows you to create the motion multiple times, but you must call the Dispose method to destroy the builder after use. /// /// This builder to allow chaining multiple method calls. [MethodImpl(MethodImplOptions.AggressiveInlining)] public readonly MotionBuilder Preserve() { CheckBuffer(); buffer.IsPreserved = true; return this; } [MethodImpl(MethodImplOptions.AggressiveInlining)] internal MotionHandle Schedule(IMotionScheduler scheduler, ref MotionData data, ref MotionCallbackData callbackData) { if (buffer.BindOnSchedule && callbackData.UpdateAction != null) { callbackData.InvokeUnsafe( default(TAdapter).Evaluate( ref data.StartValue, ref data.EndValue, ref data.Options, new() { Progress = data.Core.Ease switch { Ease.CustomAnimationCurve => data.Core.AnimationCurve.Evaluate(0f), _ => EaseUtility.Evaluate(0f, data.Core.Ease) } } )); } MotionHandle handle; if (scheduler == null) { #if UNITY_EDITOR if (!UnityEditor.EditorApplication.isPlaying) { handle = EditorMotionDispatcher.Schedule(data, callbackData); } else if (MotionScheduler.DefaultScheduler == MotionScheduler.Update) // avoid virtual method call { handle = MotionDispatcher.Schedule(data, callbackData, PlayerLoopTiming.Update); } else { handle = MotionScheduler.DefaultScheduler.Schedule(ref data, ref callbackData); } #else if (MotionScheduler.DefaultScheduler == MotionScheduler.Update) // avoid virtual method call { handle = MotionDispatcher.Schedule(data, callbackData, PlayerLoopTiming.Update); } else { handle = MotionScheduler.DefaultScheduler.Schedule(ref data, ref callbackData); } #endif } else { handle = scheduler.Schedule(ref data, ref callbackData); } if (MotionTracker.EnableTracking) { MotionTracker.AddTracking(handle, scheduler); } if (!buffer.IsPreserved) Dispose(); return handle; } /// /// Dispose this builder. You need to call this manually after calling Preserve or if you have never created a motion data. /// public void Dispose() { if (buffer == null) return; MotionBuilderBuffer.Return(buffer); buffer = null; } [MethodImpl(MethodImplOptions.AggressiveInlining)] internal readonly void SetMotionData() { buffer.Data.Core.Status = MotionStatus.Scheduled; if (buffer.AnimationCurve != null) { #if LITMOTION_COLLECTIONS_2_0_OR_NEWER buffer.Data.Core.AnimationCurve = new NativeAnimationCurve(buffer.AnimationCurve, Allocator.Temp); #else buffer.Data.Core.AnimationCurve = new UnsafeAnimationCurve(buffer.AnimationCurve, Allocator.Temp); #endif } } [MethodImpl(MethodImplOptions.AggressiveInlining)] internal readonly void SetCallbackData(Action action) { buffer.CallbackData.UpdateAction = action; } [MethodImpl(MethodImplOptions.AggressiveInlining)] internal readonly void SetCallbackData(TState state, Action action) where TState : class { buffer.CallbackData.StateCount = 1; buffer.CallbackData.State1 = state; buffer.CallbackData.UpdateAction = action; } [MethodImpl(MethodImplOptions.AggressiveInlining)] internal readonly void SetCallbackData(TState1 state1, TState2 state2, Action action) where TState1 : class where TState2 : class { buffer.CallbackData.StateCount = 2; buffer.CallbackData.State1 = state1; buffer.CallbackData.State2 = state2; buffer.CallbackData.UpdateAction = action; } [MethodImpl(MethodImplOptions.AggressiveInlining)] internal readonly void SetCallbackData(TState1 state1, TState2 state2, TState3 state3, Action action) where TState1 : class where TState2 : class where TState3 : class { buffer.CallbackData.StateCount = 3; buffer.CallbackData.State1 = state1; buffer.CallbackData.State2 = state2; buffer.CallbackData.State3 = state3; buffer.CallbackData.UpdateAction = action; } [MethodImpl(MethodImplOptions.AggressiveInlining)] readonly void CheckBuffer() { if (buffer == null || buffer.Version != version) throw new InvalidOperationException("MotionBuilder is either not initialized or has already run a Build (or Bind). If you want to build or bind multiple times, call Preseve() for MotionBuilder."); } [MethodImpl(MethodImplOptions.AggressiveInlining)] readonly void CheckEaseType(Ease ease) { if (ease is Ease.CustomAnimationCurve) throw new ArgumentException($"Ease.{ease} cannot be specified directly."); } } }