From 8c4d383c92ceffa5c27c233fba27e65671632d4d Mon Sep 17 00:00:00 2001 From: AnnulusGames Date: Sat, 23 Nov 2024 20:44:19 +0900 Subject: [PATCH 01/11] Add: MotionHandle.Time --- .../LitMotion/Runtime/Internal/Error.cs | 5 + .../LitMotion/Runtime/Internal/MotionData.cs | 7 +- .../Runtime/Internal/MotionHelper.cs | 149 ++++++++++++++++++ .../Runtime/Internal/MotionHelper.cs.meta | 2 + .../Runtime/Internal/MotionManager.cs | 7 + .../Runtime/Internal/MotionStorage.cs | 31 ++++ .../Assets/LitMotion/Runtime/MotionHandle.cs | 16 ++ .../LitMotion/Runtime/MotionUpdateJob.cs | 130 +-------------- 8 files changed, 215 insertions(+), 132 deletions(-) create mode 100644 src/LitMotion/Assets/LitMotion/Runtime/Internal/MotionHelper.cs create mode 100644 src/LitMotion/Assets/LitMotion/Runtime/Internal/MotionHelper.cs.meta 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..4d80557f 100644 --- a/src/LitMotion/Assets/LitMotion/Runtime/Internal/MotionData.cs +++ b/src/LitMotion/Assets/LitMotion/Runtime/Internal/MotionData.cs @@ -6,20 +6,19 @@ namespace LitMotion [StructLayout(LayoutKind.Sequential)] public struct MotionDataCore { + // state public MotionStatus Status; - public double Time; public float PlaybackSpeed; - public float Duration; + // 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..f546a7ca --- /dev/null +++ b/src/LitMotion/Assets/LitMotion/Runtime/Internal/MotionHelper.cs @@ -0,0 +1,149 @@ +using Unity.Burst; +using Unity.Burst.CompilerServices; +using Unity.Mathematics; + +namespace LitMotion +{ + [BurstCompile] + internal unsafe static class MotionHelper + { + [BurstCompile] + public static void Update(MotionData* ptr, double deltaTime, out TValue result) + where TValue : unmanaged + where TOptions : unmanaged, IMotionOptions + where TAdapter : unmanaged, IMotionAdapter + { + var corePtr = (MotionDataCore*)ptr; + + corePtr->Time = math.max(corePtr->Time + deltaTime * corePtr->PlaybackSpeed, 0.0); + var motionTime = corePtr->Time; + + 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 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 + }; + + 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..09442c3f 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(); @@ -353,6 +354,36 @@ 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(); + + var prevTime = dataPtr->Core.Time; + var deltaTime = time - prevTime; + + MotionHelper.Update(dataPtr + denseIndex, deltaTime, 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/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/MotionUpdateJob.cs b/src/LitMotion/Assets/LitMotion/Runtime/MotionUpdateJob.cs index 571e9598..74ef0dd4 100644 --- a/src/LitMotion/Assets/LitMotion/Runtime/MotionUpdateJob.cs +++ b/src/LitMotion/Assets/LitMotion/Runtime/MotionUpdateJob.cs @@ -42,126 +42,9 @@ 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; + MotionHelper.Update(ptr, deltaTime, out var result); - 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 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) { @@ -169,14 +52,5 @@ public void Execute([AssumeRange(0, int.MaxValue)] int 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 From 7e4e78474a56b6381b3cd844e3e675db80bff742 Mon Sep 17 00:00:00 2001 From: AnnulusGames Date: Sat, 23 Nov 2024 20:51:38 +0900 Subject: [PATCH 02/11] Remove: PlaybackSpeed check --- src/LitMotion/Assets/LitMotion/Runtime/Internal/Error.cs | 5 ----- src/LitMotion/Assets/LitMotion/Runtime/MotionHandle.cs | 1 - 2 files changed, 6 deletions(-) diff --git a/src/LitMotion/Assets/LitMotion/Runtime/Internal/Error.cs b/src/LitMotion/Assets/LitMotion/Runtime/Internal/Error.cs index c370c3e4..8796f862 100644 --- a/src/LitMotion/Assets/LitMotion/Runtime/Internal/Error.cs +++ b/src/LitMotion/Assets/LitMotion/Runtime/Internal/Error.cs @@ -26,11 +26,6 @@ public static void ArgumentNull(string message) throw new ArgumentNullException(message); } - 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."); diff --git a/src/LitMotion/Assets/LitMotion/Runtime/MotionHandle.cs b/src/LitMotion/Assets/LitMotion/Runtime/MotionHandle.cs index eb2a3cad..c346c21e 100644 --- a/src/LitMotion/Assets/LitMotion/Runtime/MotionHandle.cs +++ b/src/LitMotion/Assets/LitMotion/Runtime/MotionHandle.cs @@ -49,7 +49,6 @@ public readonly float PlaybackSpeed } set { - if (value < 0f) Error.PlaybackSpeedMustBeZeroOrGreater(); MotionManager.GetDataRef(this).PlaybackSpeed = value; } } From 71bbf99a0a58a70d7f60fb1dac85beb8f8c4f66d Mon Sep 17 00:00:00 2001 From: AnnulusGames Date: Sat, 23 Nov 2024 20:56:20 +0900 Subject: [PATCH 03/11] Change: MotionHelper.Update -> SetTime --- .../Runtime/Internal/MotionHelper.cs | 20 +++++++++---------- .../Runtime/Internal/MotionStorage.cs | 5 +---- .../LitMotion/Runtime/MotionUpdateJob.cs | 4 +++- 3 files changed, 14 insertions(+), 15 deletions(-) diff --git a/src/LitMotion/Assets/LitMotion/Runtime/Internal/MotionHelper.cs b/src/LitMotion/Assets/LitMotion/Runtime/Internal/MotionHelper.cs index f546a7ca..e78391d3 100644 --- a/src/LitMotion/Assets/LitMotion/Runtime/Internal/MotionHelper.cs +++ b/src/LitMotion/Assets/LitMotion/Runtime/Internal/MotionHelper.cs @@ -8,14 +8,14 @@ namespace LitMotion internal unsafe static class MotionHelper { [BurstCompile] - public static void Update(MotionData* ptr, double deltaTime, out TValue result) + 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; - corePtr->Time = math.max(corePtr->Time + deltaTime * corePtr->PlaybackSpeed, 0.0); + corePtr->Time = math.max(time, 0.0); var motionTime = corePtr->Time; double t; @@ -28,8 +28,8 @@ public static void Update(MotionDataDelayType == DelayType.FirstLoop || corePtr->Delay == 0f) { - var time = motionTime - corePtr->Delay; - isCompleted = corePtr->Loops >= 0 && time > 0f; + var timeSinceStart = motionTime - corePtr->Delay; + isCompleted = corePtr->Loops >= 0 && timeSinceStart > 0f; if (isCompleted) { t = 1f; @@ -38,10 +38,10 @@ public static void Update(MotionDataLoops < 0 ? math.max(0, completedLoops) : math.clamp(completedLoops, 0, corePtr->Loops); - isDelayed = time < 0; + isDelayed = timeSinceStart < 0; } else { @@ -56,11 +56,11 @@ public static void Update(MotionDataDelayType == DelayType.FirstLoop) { - var time = motionTime - corePtr->Delay; - completedLoops = (int)math.floor(time / corePtr->Duration); + var timeSinceStart = motionTime - 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 = time < 0f; + isDelayed = timeSinceStart < 0f; if (isCompleted) { @@ -68,7 +68,7 @@ public static void Update(MotionDataDuration * clampedCompletedLoops; + var currentLoopTime = timeSinceStart - corePtr->Duration * clampedCompletedLoops; t = math.clamp(currentLoopTime / corePtr->Duration, 0f, 1f); } } diff --git a/src/LitMotion/Assets/LitMotion/Runtime/Internal/MotionStorage.cs b/src/LitMotion/Assets/LitMotion/Runtime/Internal/MotionStorage.cs index 09442c3f..e4691f83 100644 --- a/src/LitMotion/Assets/LitMotion/Runtime/Internal/MotionStorage.cs +++ b/src/LitMotion/Assets/LitMotion/Runtime/Internal/MotionStorage.cs @@ -368,10 +368,7 @@ public unsafe void SetTime(MotionHandle handle, double time) var version = slot.Version; if (version <= 0 || version != handle.Version) Error.MotionNotExists(); - var prevTime = dataPtr->Core.Time; - var deltaTime = time - prevTime; - - MotionHelper.Update(dataPtr + denseIndex, deltaTime, out var result); + MotionHelper.SetTime(dataPtr + denseIndex, time, out var result); try { diff --git a/src/LitMotion/Assets/LitMotion/Runtime/MotionUpdateJob.cs b/src/LitMotion/Assets/LitMotion/Runtime/MotionUpdateJob.cs index 74ef0dd4..b014a30a 100644 --- a/src/LitMotion/Assets/LitMotion/Runtime/MotionUpdateJob.cs +++ b/src/LitMotion/Assets/LitMotion/Runtime/MotionUpdateJob.cs @@ -42,7 +42,9 @@ public void Execute([AssumeRange(0, int.MaxValue)] int index) _ => default }; - MotionHelper.Update(ptr, deltaTime, out var result); + var time = corePtr->Time + deltaTime * corePtr->PlaybackSpeed; + + MotionHelper.SetTime(ptr, time, out var result); Output[index] = result; } From 94dbbd2f39a946d54fa669101e8b16be9246922c Mon Sep 17 00:00:00 2001 From: AnnulusGames Date: Sat, 23 Nov 2024 21:02:43 +0900 Subject: [PATCH 04/11] Add: MotionHandle.Preserve() --- .../Assets/LitMotion/Runtime/Internal/MotionData.cs | 1 + .../Assets/LitMotion/Runtime/MotionHandleExtensions.cs | 6 ++++++ src/LitMotion/Assets/LitMotion/Runtime/MotionUpdateJob.cs | 2 +- 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/LitMotion/Assets/LitMotion/Runtime/Internal/MotionData.cs b/src/LitMotion/Assets/LitMotion/Runtime/Internal/MotionData.cs index 4d80557f..18d4cc1e 100644 --- a/src/LitMotion/Assets/LitMotion/Runtime/Internal/MotionData.cs +++ b/src/LitMotion/Assets/LitMotion/Runtime/Internal/MotionData.cs @@ -10,6 +10,7 @@ public struct MotionDataCore public MotionStatus Status; public double Time; public float PlaybackSpeed; + public bool IsPreserved; // parameters public float Duration; 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 b014a30a..2927f696 100644 --- a/src/LitMotion/Assets/LitMotion/Runtime/MotionUpdateJob.cs +++ b/src/LitMotion/Assets/LitMotion/Runtime/MotionUpdateJob.cs @@ -48,7 +48,7 @@ public void Execute([AssumeRange(0, int.MaxValue)] int index) 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; From 4e1bb831d632337bca3cec76bd5f5f39ec65bb59 Mon Sep 17 00:00:00 2001 From: AnnulusGames Date: Sat, 23 Nov 2024 21:14:34 +0900 Subject: [PATCH 05/11] Refactoring --- .../LitMotion/Runtime/Internal/MotionHelper.cs | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/LitMotion/Assets/LitMotion/Runtime/Internal/MotionHelper.cs b/src/LitMotion/Assets/LitMotion/Runtime/Internal/MotionHelper.cs index e78391d3..8a1f36df 100644 --- a/src/LitMotion/Assets/LitMotion/Runtime/Internal/MotionHelper.cs +++ b/src/LitMotion/Assets/LitMotion/Runtime/Internal/MotionHelper.cs @@ -16,7 +16,6 @@ public static void SetTime(MotionDataTime = math.max(time, 0.0); - var motionTime = corePtr->Time; double t; bool isCompleted; @@ -28,7 +27,7 @@ public static void SetTime(MotionDataDelayType == DelayType.FirstLoop || corePtr->Delay == 0f) { - var timeSinceStart = motionTime - corePtr->Delay; + var timeSinceStart = time - corePtr->Delay; isCompleted = corePtr->Loops >= 0 && timeSinceStart > 0f; if (isCompleted) { @@ -45,7 +44,7 @@ public static void SetTime(MotionDataDelay); + 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; @@ -56,7 +55,7 @@ public static void SetTime(MotionDataDelayType == DelayType.FirstLoop) { - var timeSinceStart = motionTime - corePtr->Delay; + 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; @@ -74,9 +73,11 @@ public static void SetTime(MotionDataDuration + 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); + 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; @@ -116,7 +117,7 @@ public static void SetTime(MotionDataDelay + corePtr->Duration * corePtr->Loops : (corePtr->Delay + corePtr->Duration) * corePtr->Loops; - if (corePtr->Loops > 0 && motionTime >= totalDuration) + if (isCompleted) { corePtr->Status = MotionStatus.Completed; } From a5d3994115a86625ef4045b03c855d71d266bbdd Mon Sep 17 00:00:00 2001 From: AnnulusGames Date: Sat, 23 Nov 2024 21:36:55 +0900 Subject: [PATCH 06/11] Fix: bug when time > totalDuration --- .../LitMotion/Runtime/Internal/MotionHelper.cs | 15 +++++++++++---- .../Assets/LitMotion/Runtime/MotionUpdateJob.cs | 3 ++- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/LitMotion/Assets/LitMotion/Runtime/Internal/MotionHelper.cs b/src/LitMotion/Assets/LitMotion/Runtime/Internal/MotionHelper.cs index 8a1f36df..7209c531 100644 --- a/src/LitMotion/Assets/LitMotion/Runtime/Internal/MotionHelper.cs +++ b/src/LitMotion/Assets/LitMotion/Runtime/Internal/MotionHelper.cs @@ -15,7 +15,8 @@ public static void SetTime(MotionDataTime = math.max(time, 0.0); + corePtr->Time = time; + time = math.max(time, 0.0); double t; bool isCompleted; @@ -39,13 +40,17 @@ public static void SetTime(MotionDataLoops < 0 ? math.max(0, completedLoops) : math.clamp(completedLoops, 0, corePtr->Loops); + 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); + 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; @@ -57,7 +62,9 @@ public static void SetTime(MotionDataDelay; completedLoops = (int)math.floor(timeSinceStart / corePtr->Duration); - clampedCompletedLoops = corePtr->Loops < 0 ? math.max(0, completedLoops) : math.clamp(completedLoops, 0, corePtr->Loops); + 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; diff --git a/src/LitMotion/Assets/LitMotion/Runtime/MotionUpdateJob.cs b/src/LitMotion/Assets/LitMotion/Runtime/MotionUpdateJob.cs index 2927f696..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 { From 8a623e5b0d1606b913244bedd32e40addf978517 Mon Sep 17 00:00:00 2001 From: AnnulusGames Date: Sat, 23 Nov 2024 21:41:51 +0900 Subject: [PATCH 07/11] Fix: IsPreserved is not initialized --- .../Assets/LitMotion/Runtime/Internal/MotionStorage.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/LitMotion/Assets/LitMotion/Runtime/Internal/MotionStorage.cs b/src/LitMotion/Assets/LitMotion/Runtime/Internal/MotionStorage.cs index e4691f83..d50bf8de 100644 --- a/src/LitMotion/Assets/LitMotion/Runtime/Internal/MotionStorage.cs +++ b/src/LitMotion/Assets/LitMotion/Runtime/Internal/MotionStorage.cs @@ -72,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; From 769feba50692cdad7b7874d1d6445c59d3fa0f71 Mon Sep 17 00:00:00 2001 From: AnnulusGames Date: Sat, 23 Nov 2024 21:44:54 +0900 Subject: [PATCH 08/11] Update: tests --- .../Tests/Runtime/MotionHandleTest.cs | 47 ++++++++++++++++++- .../Tests/Runtime/PlaybackSpeedTest.cs | 11 ----- 2 files changed, 45 insertions(+), 13 deletions(-) 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 From b0295f9cb9e336d3f5494f7b927a25f2d6204805 Mon Sep 17 00:00:00 2001 From: AnnulusGames Date: Sat, 23 Nov 2024 21:48:35 +0900 Subject: [PATCH 09/11] Revert "Remove: PlaybackSpeed check" This reverts commit 7e4e78474a56b6381b3cd844e3e675db80bff742. --- src/LitMotion/Assets/LitMotion/Runtime/Internal/Error.cs | 5 +++++ src/LitMotion/Assets/LitMotion/Runtime/MotionHandle.cs | 1 + 2 files changed, 6 insertions(+) diff --git a/src/LitMotion/Assets/LitMotion/Runtime/Internal/Error.cs b/src/LitMotion/Assets/LitMotion/Runtime/Internal/Error.cs index 8796f862..c370c3e4 100644 --- a/src/LitMotion/Assets/LitMotion/Runtime/Internal/Error.cs +++ b/src/LitMotion/Assets/LitMotion/Runtime/Internal/Error.cs @@ -26,6 +26,11 @@ public static void ArgumentNull(string message) throw new ArgumentNullException(message); } + 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."); diff --git a/src/LitMotion/Assets/LitMotion/Runtime/MotionHandle.cs b/src/LitMotion/Assets/LitMotion/Runtime/MotionHandle.cs index c346c21e..eb2a3cad 100644 --- a/src/LitMotion/Assets/LitMotion/Runtime/MotionHandle.cs +++ b/src/LitMotion/Assets/LitMotion/Runtime/MotionHandle.cs @@ -49,6 +49,7 @@ public readonly float PlaybackSpeed } set { + if (value < 0f) Error.PlaybackSpeedMustBeZeroOrGreater(); MotionManager.GetDataRef(this).PlaybackSpeed = value; } } From ddd00f68b21072f7266cf3780e13d339feb36073 Mon Sep 17 00:00:00 2001 From: AnnulusGames Date: Sat, 23 Nov 2024 22:04:26 +0900 Subject: [PATCH 10/11] Fix: OnComplete is called every frame after completion --- .../LitMotion/Runtime/Internal/MotionData.cs | 1 + .../Runtime/Internal/MotionHelper.cs | 6 +++++ .../Runtime/Internal/UpdateRunner.cs | 22 +++++++++++-------- 3 files changed, 20 insertions(+), 9 deletions(-) diff --git a/src/LitMotion/Assets/LitMotion/Runtime/Internal/MotionData.cs b/src/LitMotion/Assets/LitMotion/Runtime/Internal/MotionData.cs index 18d4cc1e..8595f16b 100644 --- a/src/LitMotion/Assets/LitMotion/Runtime/Internal/MotionData.cs +++ b/src/LitMotion/Assets/LitMotion/Runtime/Internal/MotionData.cs @@ -11,6 +11,7 @@ public struct MotionDataCore public double Time; public float PlaybackSpeed; public bool IsPreserved; + public bool WasStatusChanged; // parameters public float Duration; diff --git a/src/LitMotion/Assets/LitMotion/Runtime/Internal/MotionHelper.cs b/src/LitMotion/Assets/LitMotion/Runtime/Internal/MotionHelper.cs index 7209c531..f647f0f6 100644 --- a/src/LitMotion/Assets/LitMotion/Runtime/Internal/MotionHelper.cs +++ b/src/LitMotion/Assets/LitMotion/Runtime/Internal/MotionHelper.cs @@ -14,6 +14,10 @@ public static void SetTime(MotionData { var corePtr = (MotionDataCore*)ptr; + var prevStatus = corePtr->Status; + + // reset flag(s) + corePtr->WasStatusChanged = false; corePtr->Time = time; time = math.max(time, 0.0); @@ -137,6 +141,8 @@ public static void SetTime(MotionDataStatus = MotionStatus.Playing; } + corePtr->WasStatusChanged = prevStatus != corePtr->Status; + var context = new MotionEvaluationContext() { Progress = progress 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); + } } } } From c8e935c522a8ca3c239a7544952f9c69a8b08527 Mon Sep 17 00:00:00 2001 From: AnnulusGames Date: Sat, 23 Nov 2024 22:07:19 +0900 Subject: [PATCH 11/11] Fix: TryCompleteCore --- .../Assets/LitMotion/Runtime/Internal/MotionStorage.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/LitMotion/Assets/LitMotion/Runtime/Internal/MotionStorage.cs b/src/LitMotion/Assets/LitMotion/Runtime/Internal/MotionStorage.cs index d50bf8de..617d5df2 100644 --- a/src/LitMotion/Assets/LitMotion/Runtime/Internal/MotionStorage.cs +++ b/src/LitMotion/Assets/LitMotion/Runtime/Internal/MotionStorage.cs @@ -310,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 {