diff --git a/src/LitMotion/Assets/LitMotion/Runtime/Internal/Error.cs b/src/LitMotion/Assets/LitMotion/Runtime/Internal/Error.cs index bf265310..c370c3e4 100644 --- a/src/LitMotion/Assets/LitMotion/Runtime/Internal/Error.cs +++ b/src/LitMotion/Assets/LitMotion/Runtime/Internal/Error.cs @@ -31,6 +31,11 @@ public static void PlaybackSpeedMustBeZeroOrGreater() throw new ArgumentOutOfRangeException("Playback speed must be 0 or greater."); } + public static void TimeMustBeZeroOrGreater() + { + throw new ArgumentOutOfRangeException("Time must be 0 or greater."); + } + public static void MotionNotExists() { throw new InvalidOperationException("Motion has been destroyed or no longer exists."); diff --git a/src/LitMotion/Assets/LitMotion/Runtime/Internal/MotionData.cs b/src/LitMotion/Assets/LitMotion/Runtime/Internal/MotionData.cs index 195f0583..8595f16b 100644 --- a/src/LitMotion/Assets/LitMotion/Runtime/Internal/MotionData.cs +++ b/src/LitMotion/Assets/LitMotion/Runtime/Internal/MotionData.cs @@ -6,20 +6,21 @@ namespace LitMotion [StructLayout(LayoutKind.Sequential)] public struct MotionDataCore { + // state public MotionStatus Status; - public double Time; public float PlaybackSpeed; - public float Duration; + public bool IsPreserved; + public bool WasStatusChanged; + // parameters + public float Duration; public Ease Ease; - #if LITMOTION_COLLECTIONS_2_0_OR_NEWER public NativeAnimationCurve AnimationCurve; #else public UnsafeAnimationCurve AnimationCurve; #endif - public MotionTimeKind TimeKind; public float Delay; public int Loops; diff --git a/src/LitMotion/Assets/LitMotion/Runtime/Internal/MotionHelper.cs b/src/LitMotion/Assets/LitMotion/Runtime/Internal/MotionHelper.cs new file mode 100644 index 00000000..f647f0f6 --- /dev/null +++ b/src/LitMotion/Assets/LitMotion/Runtime/Internal/MotionHelper.cs @@ -0,0 +1,163 @@ +using Unity.Burst; +using Unity.Burst.CompilerServices; +using Unity.Mathematics; + +namespace LitMotion +{ + [BurstCompile] + internal unsafe static class MotionHelper + { + [BurstCompile] + public static void SetTime(MotionData* ptr, double time, out TValue result) + where TValue : unmanaged + where TOptions : unmanaged, IMotionOptions + where TAdapter : unmanaged, IMotionAdapter + { + var corePtr = (MotionDataCore*)ptr; + var prevStatus = corePtr->Status; + + // reset flag(s) + corePtr->WasStatusChanged = false; + + corePtr->Time = time; + time = math.max(time, 0.0); + + double t; + bool isCompleted; + bool isDelayed; + int completedLoops; + int clampedCompletedLoops; + + if (Hint.Unlikely(corePtr->Duration <= 0f)) + { + if (corePtr->DelayType == DelayType.FirstLoop || corePtr->Delay == 0f) + { + var timeSinceStart = time - corePtr->Delay; + isCompleted = corePtr->Loops >= 0 && timeSinceStart > 0f; + if (isCompleted) + { + t = 1f; + completedLoops = corePtr->Loops; + } + else + { + t = 0f; + completedLoops = timeSinceStart < 0f ? -1 : 0; + } + clampedCompletedLoops = corePtr->Loops < 0 + ? math.max(0, completedLoops) + : math.clamp(completedLoops, 0, corePtr->Loops); + isDelayed = timeSinceStart < 0; + } + else + { + completedLoops = (int)math.floor(time / corePtr->Delay); + clampedCompletedLoops = corePtr->Loops < 0 + ? math.max(0, completedLoops) + : math.clamp(completedLoops, 0, corePtr->Loops); + isCompleted = corePtr->Loops >= 0 && clampedCompletedLoops > corePtr->Loops - 1; + isDelayed = !isCompleted; + t = isCompleted ? 1f : 0f; + } + } + else + { + if (corePtr->DelayType == DelayType.FirstLoop) + { + var timeSinceStart = time - corePtr->Delay; + completedLoops = (int)math.floor(timeSinceStart / corePtr->Duration); + clampedCompletedLoops = corePtr->Loops < 0 + ? math.max(0, completedLoops) + : math.clamp(completedLoops, 0, corePtr->Loops); + isCompleted = corePtr->Loops >= 0 && clampedCompletedLoops > corePtr->Loops - 1; + isDelayed = timeSinceStart < 0f; + + if (isCompleted) + { + t = 1f; + } + else + { + var currentLoopTime = timeSinceStart - corePtr->Duration * clampedCompletedLoops; + t = math.clamp(currentLoopTime / corePtr->Duration, 0f, 1f); + } + } + else + { + var currentLoopTime = math.fmod(time, corePtr->Duration + corePtr->Delay) - corePtr->Delay; + completedLoops = (int)math.floor(time / (corePtr->Duration + corePtr->Delay)); + clampedCompletedLoops = corePtr->Loops < 0 + ? math.max(0, completedLoops) + : math.clamp(completedLoops, 0, corePtr->Loops); + isCompleted = corePtr->Loops >= 0 && clampedCompletedLoops > corePtr->Loops - 1; + isDelayed = currentLoopTime < 0; + + if (isCompleted) + { + t = 1f; + } + else + { + t = math.clamp(currentLoopTime / corePtr->Duration, 0f, 1f); + } + } + } + + float progress; + switch (corePtr->LoopType) + { + default: + case LoopType.Restart: + progress = GetEasedValue(corePtr, (float)t); + break; + case LoopType.Flip: + progress = GetEasedValue(corePtr, (float)t); + if ((clampedCompletedLoops + (int)t) % 2 == 1) progress = 1f - progress; + break; + case LoopType.Incremental: + progress = GetEasedValue(corePtr, 1f) * clampedCompletedLoops + GetEasedValue(corePtr, (float)math.fmod(t, 1f)); + break; + case LoopType.Yoyo: + progress = (clampedCompletedLoops + (int)t) % 2 == 1 + ? GetEasedValue(corePtr, (float)(1f - t)) + : GetEasedValue(corePtr, (float)t); + break; + } + + var totalDuration = corePtr->DelayType == DelayType.FirstLoop + ? corePtr->Delay + corePtr->Duration * corePtr->Loops + : (corePtr->Delay + corePtr->Duration) * corePtr->Loops; + + if (isCompleted) + { + corePtr->Status = MotionStatus.Completed; + } + else if (isDelayed) + { + corePtr->Status = MotionStatus.Delayed; + } + else + { + corePtr->Status = MotionStatus.Playing; + } + + corePtr->WasStatusChanged = prevStatus != corePtr->Status; + + var context = new MotionEvaluationContext() + { + Progress = progress + }; + + result = default(TAdapter).Evaluate(ref ptr->StartValue, ref ptr->EndValue, ref ptr->Options, context); + } + + static float GetEasedValue(MotionDataCore* data, float value) + { + return data->Ease switch + { + Ease.CustomAnimationCurve => data->AnimationCurve.Evaluate(value), + _ => EaseUtility.Evaluate(value, data->Ease) + }; + } + } +} \ No newline at end of file diff --git a/src/LitMotion/Assets/LitMotion/Runtime/Internal/MotionHelper.cs.meta b/src/LitMotion/Assets/LitMotion/Runtime/Internal/MotionHelper.cs.meta new file mode 100644 index 00000000..cb235ae0 --- /dev/null +++ b/src/LitMotion/Assets/LitMotion/Runtime/Internal/MotionHelper.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: b7b3337d974654c029ff5bfa7e15049c \ No newline at end of file diff --git a/src/LitMotion/Assets/LitMotion/Runtime/Internal/MotionManager.cs b/src/LitMotion/Assets/LitMotion/Runtime/Internal/MotionManager.cs index 321068e0..604888c0 100644 --- a/src/LitMotion/Assets/LitMotion/Runtime/Internal/MotionManager.cs +++ b/src/LitMotion/Assets/LitMotion/Runtime/Internal/MotionManager.cs @@ -68,6 +68,13 @@ public static bool IsActive(MotionHandle handle) return list[handle.StorageId].IsActive(handle); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void SetTime(MotionHandle handle, double time) + { + CheckTypeId(handle); + list[handle.StorageId].SetTime(handle, time); + } + // For MotionTracker public static (Type ValueType, Type OptionsType, Type AdapterType) GetMotionType(MotionHandle handle) { diff --git a/src/LitMotion/Assets/LitMotion/Runtime/Internal/MotionStorage.cs b/src/LitMotion/Assets/LitMotion/Runtime/Internal/MotionStorage.cs index 7cdfaf5c..617d5df2 100644 --- a/src/LitMotion/Assets/LitMotion/Runtime/Internal/MotionStorage.cs +++ b/src/LitMotion/Assets/LitMotion/Runtime/Internal/MotionStorage.cs @@ -13,6 +13,7 @@ internal interface IMotionStorage bool TryComplete(MotionHandle handle); void Cancel(MotionHandle handle); void Complete(MotionHandle handle); + void SetTime(MotionHandle handle, double time); ref MotionDataCore GetDataRef(MotionHandle handle); ref ManagedMotionData GetManagedDataRef(MotionHandle handle); void Reset(); @@ -71,9 +72,10 @@ public unsafe MotionHandle Create(ref MotionBuilder ref var dataRef = ref unmanagedDataArray[tail]; ref var managedDataRef = ref managedDataArray[tail]; + dataRef.Core.Status = MotionStatus.Scheduled; dataRef.Core.Time = 0; dataRef.Core.PlaybackSpeed = 1f; - dataRef.Core.Status = MotionStatus.Scheduled; + dataRef.Core.IsPreserved = false; dataRef.Core.Duration = buffer.Duration; dataRef.Core.Delay = buffer.Delay; @@ -308,8 +310,7 @@ int TryCompleteCore(MotionHandle handle) ref var managedData = ref managedDataArray[denseIndex]; - // To avoid duplication of Complete processing, it is treated as canceled internally. - unmanagedData.Core.Status = MotionStatus.Canceled; + unmanagedData.Core.Status = MotionStatus.Completed; var endProgress = unmanagedData.Core.LoopType switch { @@ -353,6 +354,33 @@ int TryCompleteCore(MotionHandle handle) return 0; } + public unsafe void SetTime(MotionHandle handle, double time) + { + ref var slot = ref sparseSetCore.GetSlotRefUnchecked(handle.Index); + + var denseIndex = slot.DenseIndex; + if (denseIndex < 0 || denseIndex >= tail) Error.MotionNotExists(); + + fixed (MotionData* ptr = unmanagedDataArray) + { + var dataPtr = ptr + denseIndex; + + var version = slot.Version; + if (version <= 0 || version != handle.Version) Error.MotionNotExists(); + + MotionHelper.SetTime(dataPtr + denseIndex, time, out var result); + + try + { + managedDataArray[denseIndex].UpdateUnsafe(result); + } + catch (Exception ex) + { + MotionDispatcher.GetUnhandledExceptionHandler()?.Invoke(ex); + } + } + } + public ref ManagedMotionData GetManagedDataRef(MotionHandle handle) { ref var slot = ref GetSlotWithVarify(handle); diff --git a/src/LitMotion/Assets/LitMotion/Runtime/Internal/UpdateRunner.cs b/src/LitMotion/Assets/LitMotion/Runtime/Internal/UpdateRunner.cs index dc9db464..b573b4a0 100644 --- a/src/LitMotion/Assets/LitMotion/Runtime/Internal/UpdateRunner.cs +++ b/src/LitMotion/Assets/LitMotion/Runtime/Internal/UpdateRunner.cs @@ -62,7 +62,8 @@ public unsafe void Update(double time, double unscaledTime, double realtime) var outputPtr = (TValue*)output.GetUnsafePtr(); for (int i = 0; i < managedDataSpan.Length; i++) { - var status = (dataPtr + i)->Core.Status; + var currentDataPtr = dataPtr + i; + var status = currentDataPtr->Core.Status; ref var managedData = ref managedDataSpan[i]; if (status == MotionStatus.Playing || (status == MotionStatus.Delayed && !managedData.SkipValuesDuringDelay)) { @@ -75,7 +76,7 @@ public unsafe void Update(double time, double unscaledTime, double realtime) MotionDispatcher.GetUnhandledExceptionHandler()?.Invoke(ex); if (managedData.CancelOnError) { - (dataPtr + i)->Core.Status = MotionStatus.Canceled; + currentDataPtr->Core.Status = MotionStatus.Canceled; managedData.OnCancelAction?.Invoke(); } } @@ -91,19 +92,22 @@ public unsafe void Update(double time, double unscaledTime, double realtime) MotionDispatcher.GetUnhandledExceptionHandler()?.Invoke(ex); if (managedData.CancelOnError) { - (dataPtr + i)->Core.Status = MotionStatus.Canceled; + currentDataPtr->Core.Status = MotionStatus.Canceled; managedData.OnCancelAction?.Invoke(); continue; } } - try + if (currentDataPtr->Core.WasStatusChanged) { - managedData.OnCompleteAction?.Invoke(); - } - catch (Exception ex) - { - MotionDispatcher.GetUnhandledExceptionHandler()?.Invoke(ex); + try + { + managedData.OnCompleteAction?.Invoke(); + } + catch (Exception ex) + { + MotionDispatcher.GetUnhandledExceptionHandler()?.Invoke(ex); + } } } } diff --git a/src/LitMotion/Assets/LitMotion/Runtime/MotionHandle.cs b/src/LitMotion/Assets/LitMotion/Runtime/MotionHandle.cs index a3441e03..eb2a3cad 100644 --- a/src/LitMotion/Assets/LitMotion/Runtime/MotionHandle.cs +++ b/src/LitMotion/Assets/LitMotion/Runtime/MotionHandle.cs @@ -22,6 +22,22 @@ public struct MotionHandle : IEquatable /// public int Version; + /// + /// Motion time + /// + public readonly double Time + { + get + { + return MotionManager.GetDataRef(this).Time; + } + set + { + if (value < 0f) Error.TimeMustBeZeroOrGreater(); + MotionManager.SetTime(this, value); + } + } + /// /// Motion playback speed. /// diff --git a/src/LitMotion/Assets/LitMotion/Runtime/MotionHandleExtensions.cs b/src/LitMotion/Assets/LitMotion/Runtime/MotionHandleExtensions.cs index a073e335..33ebe16d 100644 --- a/src/LitMotion/Assets/LitMotion/Runtime/MotionHandleExtensions.cs +++ b/src/LitMotion/Assets/LitMotion/Runtime/MotionHandleExtensions.cs @@ -23,6 +23,12 @@ public static bool IsActive(this MotionHandle handle) return MotionManager.IsActive(handle); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Preserve(this MotionHandle handle) + { + MotionManager.GetDataRef(handle).IsPreserved = true; + } + /// /// Complete motion. /// diff --git a/src/LitMotion/Assets/LitMotion/Runtime/MotionUpdateJob.cs b/src/LitMotion/Assets/LitMotion/Runtime/MotionUpdateJob.cs index 571e9598..5da497a0 100644 --- a/src/LitMotion/Assets/LitMotion/Runtime/MotionUpdateJob.cs +++ b/src/LitMotion/Assets/LitMotion/Runtime/MotionUpdateJob.cs @@ -32,7 +32,8 @@ public void Execute([AssumeRange(0, int.MaxValue)] int index) var ptr = DataPtr + index; var corePtr = (MotionDataCore*)ptr; - if (Hint.Likely(corePtr->Status is MotionStatus.Scheduled or MotionStatus.Delayed or MotionStatus.Playing)) + if (Hint.Likely(corePtr->Status is MotionStatus.Scheduled or MotionStatus.Delayed or MotionStatus.Playing) || + Hint.Unlikely(corePtr->IsPreserved && corePtr->Status is MotionStatus.Completed)) { var deltaTime = corePtr->TimeKind switch { @@ -42,141 +43,17 @@ public void Execute([AssumeRange(0, int.MaxValue)] int index) _ => default }; - corePtr->Time = math.max(corePtr->Time + deltaTime * corePtr->PlaybackSpeed, 0.0); - var motionTime = corePtr->Time; + var time = corePtr->Time + deltaTime * corePtr->PlaybackSpeed; - double t; - bool isCompleted; - bool isDelayed; - int completedLoops; - int clampedCompletedLoops; + MotionHelper.SetTime(ptr, time, out var result); - if (Hint.Unlikely(corePtr->Duration <= 0f)) - { - if (corePtr->DelayType == DelayType.FirstLoop || corePtr->Delay == 0f) - { - var time = motionTime - corePtr->Delay; - isCompleted = corePtr->Loops >= 0 && time > 0f; - if (isCompleted) - { - t = 1f; - completedLoops = corePtr->Loops; - } - else - { - t = 0f; - completedLoops = time < 0f ? -1 : 0; - } - clampedCompletedLoops = corePtr->Loops < 0 ? math.max(0, completedLoops) : math.clamp(completedLoops, 0, corePtr->Loops); - isDelayed = time < 0; - } - else - { - completedLoops = (int)math.floor(motionTime / corePtr->Delay); - clampedCompletedLoops = corePtr->Loops < 0 ? math.max(0, completedLoops) : math.clamp(completedLoops, 0, corePtr->Loops); - isCompleted = corePtr->Loops >= 0 && clampedCompletedLoops > corePtr->Loops - 1; - isDelayed = !isCompleted; - t = isCompleted ? 1f : 0f; - } - } - else - { - if (corePtr->DelayType == DelayType.FirstLoop) - { - var time = motionTime - corePtr->Delay; - completedLoops = (int)math.floor(time / corePtr->Duration); - clampedCompletedLoops = corePtr->Loops < 0 ? math.max(0, completedLoops) : math.clamp(completedLoops, 0, corePtr->Loops); - isCompleted = corePtr->Loops >= 0 && clampedCompletedLoops > corePtr->Loops - 1; - isDelayed = time < 0f; - - if (isCompleted) - { - t = 1f; - } - else - { - var currentLoopTime = time - corePtr->Duration * clampedCompletedLoops; - t = math.clamp(currentLoopTime / corePtr->Duration, 0f, 1f); - } - } - else - { - var currentLoopTime = math.fmod(motionTime, corePtr->Duration + corePtr->Delay) - corePtr->Delay; - completedLoops = (int)math.floor(motionTime / (corePtr->Duration + corePtr->Delay)); - clampedCompletedLoops = corePtr->Loops < 0 ? math.max(0, completedLoops) : math.clamp(completedLoops, 0, corePtr->Loops); - isCompleted = corePtr->Loops >= 0 && clampedCompletedLoops > corePtr->Loops - 1; - isDelayed = currentLoopTime < 0; - - if (isCompleted) - { - t = 1f; - } - else - { - t = math.clamp(currentLoopTime / corePtr->Duration, 0f, 1f); - } - } - } - - float progress; - switch (corePtr->LoopType) - { - default: - case LoopType.Restart: - progress = GetEasedValue(corePtr, (float)t); - break; - case LoopType.Flip: - progress = GetEasedValue(corePtr, (float)t); - if ((clampedCompletedLoops + (int)t) % 2 == 1) progress = 1f - progress; - break; - case LoopType.Incremental: - progress = GetEasedValue(corePtr, 1f) * clampedCompletedLoops + GetEasedValue(corePtr, (float)math.fmod(t, 1f)); - break; - case LoopType.Yoyo: - progress = (clampedCompletedLoops + (int)t) % 2 == 1 - ? GetEasedValue(corePtr, (float)(1f - t)) - : GetEasedValue(corePtr, (float)t); - break; - } - - var totalDuration = corePtr->DelayType == DelayType.FirstLoop - ? corePtr->Delay + corePtr->Duration * corePtr->Loops - : (corePtr->Delay + corePtr->Duration) * corePtr->Loops; - - if (corePtr->Loops > 0 && motionTime >= totalDuration) - { - corePtr->Status = MotionStatus.Completed; - } - else if (isDelayed) - { - corePtr->Status = MotionStatus.Delayed; - } - else - { - corePtr->Status = MotionStatus.Playing; - } - - var context = new MotionEvaluationContext() - { - Progress = progress - }; - - Output[index] = default(TAdapter).Evaluate(ref ptr->StartValue, ref ptr->EndValue, ref ptr->Options, context); + Output[index] = result; } - else if (corePtr->Status is MotionStatus.Completed or MotionStatus.Canceled) + else if ((!corePtr->IsPreserved && corePtr->Status is MotionStatus.Completed) || corePtr->Status is MotionStatus.Canceled) { CompletedIndexList.AddNoResize(index); corePtr->Status = MotionStatus.Disposed; } } - - static float GetEasedValue(MotionDataCore* data, float value) - { - return data->Ease switch - { - Ease.CustomAnimationCurve => data->AnimationCurve.Evaluate(value), - _ => EaseUtility.Evaluate(value, data->Ease) - }; - } } } \ No newline at end of file diff --git a/src/LitMotion/Assets/LitMotion/Tests/Runtime/MotionHandleTest.cs b/src/LitMotion/Assets/LitMotion/Tests/Runtime/MotionHandleTest.cs index 51d61c92..58591a16 100644 --- a/src/LitMotion/Assets/LitMotion/Tests/Runtime/MotionHandleTest.cs +++ b/src/LitMotion/Assets/LitMotion/Tests/Runtime/MotionHandleTest.cs @@ -25,6 +25,28 @@ public IEnumerator Test_Cancel() Assert.IsTrue(!handle.IsActive()); } + [UnityTest] + public IEnumerator Test_TryCancel() + { + var value = 0f; + var endValue = 10f; + var handle = LMotion.Create(0f, endValue, 2f) + .Bind(x => + { + value = x; + Debug.Log(x); + }); + yield return new WaitForSeconds(1f); + var tryResult = handle.TryCancel(); + Assert.IsTrue(tryResult); + yield return new WaitForSeconds(1f); + Assert.IsTrue(value < endValue); + Assert.IsTrue(!handle.IsActive()); + + tryResult = handle.TryCancel(); + Assert.IsFalse(tryResult); + } + [UnityTest] public IEnumerator Test_Complete() { @@ -42,6 +64,27 @@ public IEnumerator Test_Complete() Assert.IsTrue(!handle.IsActive()); } + [UnityTest] + public IEnumerator Test_TryComplete() + { + var value = 0f; + var endValue = 10f; + var handle = LMotion.Create(0f, endValue, 2f) + .Bind(x => + { + value = x; + Debug.Log(x); + }); + yield return new WaitForSeconds(1f); + var tryResult = handle.TryComplete(); + Assert.IsTrue(tryResult); + Assert.AreApproximatelyEqual(value, endValue); + Assert.IsTrue(!handle.IsActive()); + + tryResult = handle.TryComplete(); + Assert.IsFalse(tryResult); + } + [UnityTest] public IEnumerator Test_Complete_WithYoyoLoop() { @@ -73,9 +116,9 @@ public IEnumerator Test_CompleteAndCancel_WithInfiniteLoop() Debug.Log(x); }); yield return new WaitForSeconds(1f); - handle.Complete(); + handle.TryComplete(); Assert.IsTrue(handle.IsActive()); - handle.Cancel(); + handle.TryCancel(); Assert.IsTrue(!handle.IsActive()); } diff --git a/src/LitMotion/Assets/LitMotion/Tests/Runtime/PlaybackSpeedTest.cs b/src/LitMotion/Assets/LitMotion/Tests/Runtime/PlaybackSpeedTest.cs index d4780953..bfe48b9d 100644 --- a/src/LitMotion/Assets/LitMotion/Tests/Runtime/PlaybackSpeedTest.cs +++ b/src/LitMotion/Assets/LitMotion/Tests/Runtime/PlaybackSpeedTest.cs @@ -51,16 +51,5 @@ public IEnumerator Test_PlaybackSpeed_2x_Speed() yield return handle.ToYieldInteraction(); Assert.That(Time.time - time, Is.EqualTo(0.5f).Using(new FloatEqualityComparer(0.05f))); } - - [Test] - public void Test_PlaybackSpeed_Minus() - { - var handle = LMotion.Create(0f, 10f, 1f).RunWithoutBinding(); - Assert.Throws(() => - { - handle.PlaybackSpeed = -1f; - }); - } - } } \ No newline at end of file