Files
2026-09-21 12:15:57 +03:00

513 lines
19 KiB
C#

#if DEVELOPMENT_BUILD || UNITY_EDITOR
#define LITMOTION_DEBUG
#endif
using System;
using System.Runtime.CompilerServices;
using LitMotion.Collections;
using Unity.Collections;
using Unity.Collections.LowLevel.Unsafe;
namespace LitMotion
{
internal record MotionDebugInfo
{
public object StartValue;
public object EndValue;
public object Options;
}
internal interface IMotionStorage
{
bool IsValid(MotionHandle handle);
bool IsActive(MotionHandle handle);
bool IsPlaying(MotionHandle handle);
bool TryCancel(MotionHandle handle, bool checkIsInSequence = true);
bool TryComplete(MotionHandle handle, bool checkIsInSequence = true);
void Cancel(MotionHandle handle, bool checkIsInSequence = true);
void Complete(MotionHandle handle, bool checkIsInSequence = true);
void SetTime(MotionHandle handle, double time, bool checkIsInSequence = true);
ref MotionData GetDataRef(MotionHandle handle, bool checkIsInSequence = true);
ref ManagedMotionData GetManagedDataRef(MotionHandle handle, bool checkIsInSequence = true);
void AddToSequence(MotionHandle handle, out double motionDuration);
MotionDebugInfo GetDebugInfo(MotionHandle handle);
void Reset();
}
internal sealed class MotionStorage<TValue, TOptions, TAdapter> : IMotionStorage
where TValue : unmanaged
where TOptions : unmanaged, IMotionOptions
where TAdapter : unmanaged, IMotionAdapter<TValue, TOptions>
{
const int InitialCapacity = 32;
public int Id { get; }
public int Count => tail;
SparseSetCore sparseSetCore = new(InitialCapacity);
SparseIndex[] sparseIndexLookup = new SparseIndex[InitialCapacity];
MotionData<TValue, TOptions>[] unmanagedDataArray = new MotionData<TValue, TOptions>[InitialCapacity];
ManagedMotionData[] managedDataArray = new ManagedMotionData[InitialCapacity];
AllocatorHelper<RewindableAllocator> allocator;
int tail;
public MotionStorage(int id)
{
Id = id;
allocator = RewindableAllocatorFactory.CreateAllocator();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Span<MotionData<TValue, TOptions>> GetDataSpan()
{
return unmanagedDataArray.AsSpan();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Span<ManagedMotionData> GetManagedDataSpan()
{
return managedDataArray.AsSpan();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void EnsureCapacity(int minimumCapacity)
{
sparseSetCore.EnsureCapacity(minimumCapacity);
ArrayHelper.EnsureCapacity(ref sparseIndexLookup, minimumCapacity);
ArrayHelper.EnsureCapacity(ref unmanagedDataArray, minimumCapacity);
ArrayHelper.EnsureCapacity(ref managedDataArray, minimumCapacity);
}
public unsafe MotionHandle Create(ref MotionBuilder<TValue, TOptions, TAdapter> builder)
{
EnsureCapacity(tail + 1);
var buffer = builder.buffer;
ref var dataRef = ref unmanagedDataArray[tail];
ref var managedDataRef = ref managedDataArray[tail];
ref var state = ref dataRef.Core.State;
ref var parameters = ref dataRef.Core.Parameters;
state.Status = MotionStatus.Scheduled;
state.Time = 0;
state.PlaybackSpeed = 1f;
state.IsPreserved = false;
parameters.TimeKind = buffer.TimeKind;
parameters.Duration = buffer.Duration;
parameters.Delay = buffer.Delay;
parameters.DelayType = buffer.DelayType;
parameters.Ease = buffer.Ease;
parameters.Loops = buffer.Loops;
parameters.LoopType = buffer.LoopType;
dataRef.StartValue = buffer.StartValue;
dataRef.EndValue = buffer.EndValue;
dataRef.Options = buffer.Options;
if (buffer.Ease == Ease.CustomAnimationCurve)
{
if (parameters.AnimationCurve.IsCreated)
{
parameters.AnimationCurve.CopyFrom(buffer.AnimationCurve);
}
else
{
#if LITMOTION_COLLECTIONS_2_0_OR_NEWER
parameters.AnimationCurve = new NativeAnimationCurve(buffer.AnimationCurve, allocator.Allocator.Handle);
#else
parameters.AnimationCurve = new UnsafeAnimationCurve(buffer.AnimationCurve, allocator.Allocator.Handle);
#endif
}
}
managedDataRef.CancelOnError = buffer.CancelOnError;
managedDataRef.SkipValuesDuringDelay = buffer.SkipValuesDuringDelay;
managedDataRef.UpdateAction = buffer.UpdateAction;
managedDataRef.OnLoopCompleteAction = buffer.OnLoopCompleteAction;
managedDataRef.OnCancelAction = buffer.OnCancelAction;
managedDataRef.OnCompleteAction = buffer.OnCompleteAction;
managedDataRef.StateCount = buffer.StateCount;
managedDataRef.State0 = buffer.State0;
managedDataRef.State1 = buffer.State1;
managedDataRef.State2 = buffer.State2;
#if LITMOTION_DEBUG
managedDataRef.DebugName = buffer.DebugName;
#endif
if (buffer.ImmediateBind && buffer.UpdateAction != null)
{
managedDataRef.UpdateUnsafe(
default(TAdapter).Evaluate(
ref dataRef.StartValue,
ref dataRef.EndValue,
ref dataRef.Options,
new()
{
Progress = parameters.Ease switch
{
Ease.CustomAnimationCurve => buffer.AnimationCurve.Evaluate(0f),
_ => EaseUtility.Evaluate(0f, parameters.Ease)
},
Time = state.Time,
}
));
}
var sparseIndex = sparseSetCore.Alloc(tail);
sparseIndexLookup[tail] = sparseIndex;
tail++;
return new MotionHandle()
{
Index = sparseIndex.Index,
Version = sparseIndex.Version,
StorageId = Id
};
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
void RemoveAt(int denseIndex)
{
tail--;
// swap elements
unmanagedDataArray[denseIndex] = unmanagedDataArray[tail];
unmanagedDataArray[tail] = default;
managedDataArray[denseIndex] = managedDataArray[tail];
managedDataArray[tail] = default;
// swap sparse index
var prevSparseIndex = sparseIndexLookup[denseIndex];
var currentSparseIndex = sparseIndexLookup[denseIndex] = sparseIndexLookup[tail];
sparseIndexLookup[tail] = default;
// update slot
if (currentSparseIndex.Version != 0)
{
ref var slot = ref sparseSetCore.GetSlotRefUnchecked(currentSparseIndex.Index);
slot.DenseIndex = denseIndex;
}
// free slot
if (prevSparseIndex.Version != 0)
{
sparseSetCore.Free(prevSparseIndex);
}
}
public void RemoveAll(NativeList<int> denseIndexList)
{
var list = new NativeArray<SparseIndex>(denseIndexList.Length, Allocator.Temp, NativeArrayOptions.UninitializedMemory);
for (int i = 0; i < list.Length; i++)
{
list[i] = sparseIndexLookup[denseIndexList[i]];
}
for (int i = 0; i < list.Length; i++)
{
RemoveAt(sparseSetCore.GetSlotRefUnchecked(list[i].Index).DenseIndex);
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool IsValid(MotionHandle handle)
{
ref var slot = ref sparseSetCore.GetSlotRefUnchecked(handle.Index);
if (IsDenseIndexOutOfRange(slot.DenseIndex)) return false;
if (IsInvalidVersion(slot.Version, handle)) return false;
return true;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool IsActive(MotionHandle handle)
{
ref var slot = ref sparseSetCore.GetSlotRefUnchecked(handle.Index);
if (IsDenseIndexOutOfRange(slot.DenseIndex)) return false;
if (IsInvalidVersion(slot.Version, handle)) return false;
ref var state = ref unmanagedDataArray[slot.DenseIndex].Core.State;
return state.Status is MotionStatus.Scheduled or MotionStatus.Delayed or MotionStatus.Playing ||
(state.Status is MotionStatus.Completed && state.IsPreserved);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool IsPlaying(MotionHandle handle)
{
ref var slot = ref sparseSetCore.GetSlotRefUnchecked(handle.Index);
if (IsDenseIndexOutOfRange(slot.DenseIndex)) return false;
if (IsInvalidVersion(slot.Version, handle)) return false;
ref var state = ref unmanagedDataArray[slot.DenseIndex].Core.State;
return state.Status is MotionStatus.Scheduled or MotionStatus.Delayed or MotionStatus.Playing;
}
public bool TryCancel(MotionHandle handle, bool checkIsInSequence = true)
{
return TryCancelCore(handle, checkIsInSequence) == 0;
}
public void Cancel(MotionHandle handle, bool checkIsInSequence = true)
{
switch (TryCancelCore(handle, checkIsInSequence))
{
case 1:
Error.MotionNotExists();
return;
case 2:
Error.MotionHasBeenCanceledOrCompleted();
return;
case 3:
Error.MotionIsInSequence();
return;
}
}
int TryCancelCore(MotionHandle handle, bool checkIsInSequence)
{
ref var slot = ref sparseSetCore.GetSlotRefUnchecked(handle.Index);
var denseIndex = slot.DenseIndex;
if (IsDenseIndexOutOfRange(denseIndex) || IsInvalidVersion(slot.Version, handle))
{
return 1;
}
ref var dataRef = ref unmanagedDataArray[denseIndex];
ref var state = ref dataRef.Core.State;
if (state.Status is MotionStatus.None or MotionStatus.Canceled ||
(state.Status is MotionStatus.Completed && !state.IsPreserved))
{
return 2;
}
if (checkIsInSequence && state.IsInSequence)
{
return 3;
}
state.Status = MotionStatus.Canceled;
ref var managedData = ref managedDataArray[denseIndex];
managedData.InvokeOnCancel();
return 0;
}
public bool TryComplete(MotionHandle handle, bool checkIsInSequence = true)
{
return TryCompleteCore(handle, checkIsInSequence) == 0;
}
public void Complete(MotionHandle handle, bool checkIsInSequence = true)
{
switch (TryCompleteCore(handle, checkIsInSequence))
{
case 1:
Error.MotionNotExists();
return;
case 2:
Error.MotionHasBeenCanceledOrCompleted();
return;
case 3:
Error.MotionIsInSequence();
return;
case 4:
throw new InvalidOperationException("Complete was ignored because it is not possible to complete a motion that loops infinitely. If you want to end the motion, call Cancel() instead.");
}
}
int TryCompleteCore(MotionHandle handle, bool checkIsInSequence)
{
ref var slot = ref sparseSetCore.GetSlotRefUnchecked(handle.Index);
if (IsDenseIndexOutOfRange(slot.DenseIndex) || IsInvalidVersion(slot.Version, handle))
{
return 1;
}
ref var dataRef = ref unmanagedDataArray[slot.DenseIndex];
ref var state = ref dataRef.Core.State;
ref var parameters = ref dataRef.Core.Parameters;
if (state.Status is MotionStatus.None or MotionStatus.Canceled or MotionStatus.Completed)
{
return 2;
}
if (checkIsInSequence && state.IsInSequence)
{
return 3;
}
if (parameters.Loops < 0)
{
return 4;
}
dataRef.Complete<TAdapter>(out var result);
ref var managedData = ref managedDataArray[slot.DenseIndex];
try
{
managedData.UpdateUnsafe(result);
}
catch (Exception ex)
{
MotionDispatcher.GetUnhandledExceptionHandler()?.Invoke(ex);
}
if (state.WasLoopCompleted)
{
managedData.InvokeOnLoopComplete(state.CompletedLoops);
}
managedData.InvokeOnComplete();
return 0;
}
public unsafe void SetTime(MotionHandle handle, double time, bool checkIsInSequence = true)
{
ref var slot = ref sparseSetCore.GetSlotRefUnchecked(handle.Index);
var denseIndex = slot.DenseIndex;
if (IsDenseIndexOutOfRange(denseIndex)) Error.MotionNotExists();
var version = slot.Version;
if (version <= 0 || version != handle.Version) Error.MotionNotExists();
fixed (MotionData<TValue, TOptions>* arrayPtr = unmanagedDataArray)
{
var dataPtr = arrayPtr + denseIndex;
ref var state = ref dataPtr->Core.State;
if (checkIsInSequence && state.IsInSequence) Error.MotionIsInSequence();
dataPtr->Update<TAdapter>(time, out var result);
var status = state.Status;
ref var managedData = ref managedDataArray[denseIndex];
if (status is MotionStatus.Playing or MotionStatus.Completed || (status == MotionStatus.Delayed && !managedData.SkipValuesDuringDelay))
{
try
{
managedData.UpdateUnsafe(result);
}
catch (Exception ex)
{
MotionDispatcher.GetUnhandledExceptionHandler()?.Invoke(ex);
if (managedData.CancelOnError)
{
state.Status = MotionStatus.Canceled;
managedData.OnCancelAction?.Invoke();
return;
}
}
if (state.WasLoopCompleted)
{
managedData.InvokeOnLoopComplete(state.CompletedLoops);
}
if (status is MotionStatus.Completed && state.WasStatusChanged)
{
managedData.InvokeOnComplete();
}
}
}
}
public void AddToSequence(MotionHandle handle, out double motionDuration)
{
ref var slot = ref GetSlotWithVarify(handle, true);
ref var dataRef = ref unmanagedDataArray[slot.DenseIndex];
if (dataRef.Core.State.Status is not MotionStatus.Scheduled)
{
throw new ArgumentException("Cannot add a running motion to a sequence.");
}
motionDuration = handle.TotalDuration;
if (double.IsInfinity(motionDuration))
{
throw new ArgumentException("Cannot add an infinitely looping motion to a sequence.");
}
dataRef.Core.State.IsPreserved = true;
dataRef.Core.State.IsInSequence = true;
}
public ref ManagedMotionData GetManagedDataRef(MotionHandle handle, bool checkIsInSequence = true)
{
ref var slot = ref GetSlotWithVarify(handle, checkIsInSequence);
return ref managedDataArray[slot.DenseIndex];
}
public ref MotionData GetDataRef(MotionHandle handle, bool checkIsInSequence = true)
{
ref var slot = ref GetSlotWithVarify(handle, checkIsInSequence);
return ref UnsafeUtility.As<MotionData<TValue, TOptions>, MotionData>(ref unmanagedDataArray[slot.DenseIndex]);
}
public MotionDebugInfo GetDebugInfo(MotionHandle handle)
{
ref var slot = ref GetSlotWithVarify(handle, false);
ref var dataRef = ref unmanagedDataArray[slot.DenseIndex];
return new()
{
StartValue = dataRef.StartValue,
EndValue = dataRef.EndValue,
Options = dataRef.Options,
};
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
ref SparseSetCore.Slot GetSlotWithVarify(MotionHandle handle, bool checkIsInSequence = true)
{
ref var slot = ref sparseSetCore.GetSlotRefUnchecked(handle.Index);
if (IsDenseIndexOutOfRange(slot.DenseIndex)) Error.MotionNotExists();
ref var dataRef = ref unmanagedDataArray[slot.DenseIndex];
if (IsInvalidVersion(slot.Version, handle) || dataRef.Core.State.Status == MotionStatus.None)
{
Error.MotionNotExists();
}
if (checkIsInSequence && dataRef.Core.State.IsInSequence)
{
Error.MotionIsInSequence();
}
return ref slot;
}
public void Reset()
{
sparseSetCore.Reset();
sparseIndexLookup.AsSpan().Clear();
unmanagedDataArray.AsSpan().Clear();
managedDataArray.AsSpan().Clear();
tail = 0;
allocator.Allocator.Rewind();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
bool IsDenseIndexOutOfRange(int denseIndex)
{
return denseIndex < 0 || denseIndex >= tail;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
static bool IsInvalidVersion(int version, MotionHandle handle)
{
return version <= 0 || version != handle.Version;
}
}
}