#if DEVELOPMENT_BUILD || UNITY_EDITOR #define LITMOTION_DEBUG #endif using System; using System.Runtime.CompilerServices; using UnityEngine; namespace LitMotion { internal unsafe sealed 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.ImmediateBind = true; buffer.StartValue = default; buffer.EndValue = default; buffer.Options = default; buffer.Duration = default; buffer.Ease = default; buffer.AnimationCurve = default; buffer.TimeKind = default; buffer.Delay = default; buffer.Loops = 1; buffer.LoopType = default; buffer.State0 = default; buffer.State1 = default; buffer.State2 = default; buffer.StateCount = default; buffer.UpdateAction = default; buffer.OnLoopCompleteAction = default; buffer.OnCompleteAction = default; buffer.OnCancelAction = default; buffer.CancelOnError = default; buffer.SkipValuesDuringDelay = default; buffer.Scheduler = default; #if LITMOTION_DEBUG buffer.DebugName = default; #endif if (buffer.Version != ushort.MaxValue) { buffer.NextNode = PoolRoot; PoolRoot = buffer; } } public ushort Version; public MotionBuilderBuffer NextNode; public TValue StartValue; public TValue EndValue; public TOptions Options; public float Duration; public Ease Ease; public MotionTimeKind TimeKind; public float Delay; public int Loops = 1; public DelayType DelayType; public LoopType LoopType; public bool CancelOnError; public bool SkipValuesDuringDelay; public bool ImmediateBind = true; public object State0; public object State1; public object State2; public byte StateCount; public object UpdateAction; public Action OnLoopCompleteAction; public Action OnCompleteAction; public Action OnCancelAction; public AnimationCurve AnimationCurve; public IMotionScheduler Scheduler; #if LITMOTION_DEBUG public string DebugName; #endif } /// /// 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.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.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.Delay = delay; buffer.DelayType = delayType; buffer.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.Loops = loops; buffer.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.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.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.OnCompleteAction += callback; return this; } /// /// Specify a callback to be performed when each loop finishes. /// /// Callback to be performed when each loop finishes. /// This builder to allow chaining multiple method calls. [MethodImpl(MethodImplOptions.AggressiveInlining)] public readonly MotionBuilder WithOnLoopComplete(Action callback) { CheckBuffer(); buffer.OnLoopCompleteAction += 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.CancelOnError = cancelOnError; return this; } /// /// Bind values ​​immediately when scheduling the motion. /// /// Whether to bind on sheduling /// This builder to allow chaining multiple method calls. [MethodImpl(MethodImplOptions.AggressiveInlining)] public readonly MotionBuilder WithImmediateBind(bool immediateBind = true) { CheckBuffer(); buffer.ImmediateBind = immediateBind; 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; } /// /// Specifies the name that will be displayed in the debugger. /// /// Debug name /// This builder to allow chaining multiple method calls. [MethodImpl(MethodImplOptions.AggressiveInlining)] public readonly MotionBuilder WithDebugName(string debugName) { #if LITMOTION_DEBUG CheckBuffer(); buffer.DebugName = debugName; #endif return this; } /// /// Create motion and play it without binding it to a specific object. /// /// Handle of the created motion data. [MethodImpl(MethodImplOptions.AggressiveInlining)] public MotionHandle RunWithoutBinding() { CheckBuffer(); return ScheduleMotion(); } /// /// Create motion and bind it to a specific object, property, etc. /// /// Action that handles binding /// Handle of the created motion data. [MethodImpl(MethodImplOptions.AggressiveInlining)] public MotionHandle Bind(Action action) { CheckBuffer(); SetCallbackData(action); return ScheduleMotion(); } /// /// 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. [MethodImpl(MethodImplOptions.AggressiveInlining)] public MotionHandle Bind(TState state, Action action) where TState : class { CheckBuffer(); SetCallbackData(state, action); return ScheduleMotion(); } /// /// 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 /// Motion state /// Action that handles binding /// Handle of the created motion data. [MethodImpl(MethodImplOptions.AggressiveInlining)] public MotionHandle Bind(TState0 state0, TState1 state1, Action action) where TState0 : class where TState1 : class { CheckBuffer(); SetCallbackData(state0, state1, action); return ScheduleMotion(); } /// /// 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 /// Motion state /// Motion state /// Action that handles binding /// Handle of the created motion data. [MethodImpl(MethodImplOptions.AggressiveInlining)] public MotionHandle Bind(TState0 state0, TState1 state1, TState2 state2, Action action) where TState0 : class where TState1 : class where TState2 : class { CheckBuffer(); SetCallbackData(state0, state1, state2, action); return ScheduleMotion(); } /// /// Creates a MotionSettings from the values ​​set in the builder. /// /// Configured MotionSettings [MethodImpl(MethodImplOptions.AggressiveInlining)] public readonly MotionSettings ToMotionSettings() { CheckBuffer(); return new MotionSettings() { StartValue = buffer.StartValue, EndValue = buffer.EndValue, Duration = buffer.Duration, Options = buffer.Options, Ease = buffer.Ease, CustomEaseCurve = buffer.AnimationCurve, Delay = buffer.Delay, DelayType = buffer.DelayType, Loops = buffer.Loops, LoopType = buffer.LoopType, CancelOnError = buffer.CancelOnError, SkipValuesDuringDelay = buffer.SkipValuesDuringDelay, ImmediateBind = buffer.ImmediateBind, Scheduler = buffer.Scheduler, }; } [MethodImpl(MethodImplOptions.AggressiveInlining)] internal MotionHandle ScheduleMotion() { MotionHandle handle; if (buffer.Scheduler == null) { #if UNITY_EDITOR if (!UnityEditor.EditorApplication.isPlaying) { handle = EditorMotionDispatcher.Schedule(ref this); } else if (MotionScheduler.DefaultScheduler == MotionScheduler.Update) // avoid virtual method call { handle = MotionDispatcher.Schedule(ref this, PlayerLoopTiming.Update); } else { handle = MotionScheduler.DefaultScheduler.Schedule(ref this); } #else if (MotionScheduler.DefaultScheduler == MotionScheduler.Update) // avoid virtual method call { handle = MotionDispatcher.Schedule(ref this, PlayerLoopTiming.Update); } else { handle = MotionScheduler.DefaultScheduler.Schedule(ref this); } #endif } else { handle = buffer.Scheduler.Schedule(ref this); } if (MotionDebugger.Enabled) { MotionDebugger.AddTracking(handle, buffer.Scheduler); } 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 SetCallbackData(Action action) { buffer.StateCount = 0; buffer.UpdateAction = action; } [MethodImpl(MethodImplOptions.AggressiveInlining)] internal readonly void SetCallbackData(TState state, Action action) where TState : class { buffer.StateCount = 1; buffer.State0 = state; buffer.UpdateAction = action; } [MethodImpl(MethodImplOptions.AggressiveInlining)] internal readonly void SetCallbackData(TState0 state0, TState1 state1, Action action) where TState0 : class where TState1 : class { buffer.StateCount = 2; buffer.State0 = state0; buffer.State1 = state1; buffer.UpdateAction = action; } [MethodImpl(MethodImplOptions.AggressiveInlining)] internal readonly void SetCallbackData(TState0 state0, TState1 state1, TState2 state2, Action action) where TState0 : class where TState1 : class where TState2 : class { buffer.StateCount = 3; buffer.State0 = state0; buffer.State1 = state1; buffer.State2 = state2; buffer.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)."); } [MethodImpl(MethodImplOptions.AggressiveInlining)] readonly void CheckEaseType(Ease ease) { if (ease is Ease.CustomAnimationCurve) throw new ArgumentException($"Ease.{ease} cannot be specified directly."); } } }