AK056/Packages/com.kybernetik.animancer/Runtime/Core/Events/AnimancerEvent.Sequence.Serializable.cs
2025-05-09 15:40:34 +08:00

457 lines
22 KiB
C#

// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 Kybernetik //
#pragma warning disable CS0649 // Field is never assigned to, and will always have its default value.
using System;
using UnityEngine;
namespace Animancer
{
/// https://kybernetik.com.au/animancer/api/Animancer/AnimancerEvent
partial struct AnimancerEvent
{
/// https://kybernetik.com.au/animancer/api/Animancer/Sequence
partial class Sequence
{
/// <summary>
/// Serializable data which can be used to construct an <see cref="Sequence"/> using
/// <see cref="StringAsset"/>s and <see cref="IInvokable"/>s.
/// </summary>
/// <remarks>
/// <strong>Documentation:</strong>
/// <see href="https://kybernetik.com.au/animancer/docs/manual/events/animancer/serialization">
/// Serialized Events</see>
/// </remarks>
/// https://kybernetik.com.au/animancer/api/Animancer/Serializable
[Serializable]
public class Serializable : ICloneable<Serializable>
#if UNITY_EDITOR
, ISerializationCallbackReceiver
#endif
{
/************************************************************************************************************************/
#region Fields and Properties
/************************************************************************************************************************/
[SerializeField]
private float[] _NormalizedTimes;
/// <summary>[<see cref="SerializeField"/>] The serialized <see cref="normalizedTime"/>s.</summary>
/// <remarks>The last item is used for the <see cref="EndEvent"/>.</remarks>
public ref float[] NormalizedTimes => ref _NormalizedTimes;
/************************************************************************************************************************/
[SerializeReference, Polymorphic]
private IInvokable[] _Callbacks;
/// <summary>[<see cref="SerializeField"/>] The serialized <see cref="callback"/>s.</summary>
/// <remarks>
/// This array only needs to be large enough to hold the last item that isn't null.
/// <para></para>
/// If this array is larger than the <see cref="NormalizedTimes"/>, the first item
/// with no corresponding time will be used as the <see cref="OnEnd"/> callback
/// and any others after that will be ignored.
/// </remarks>
public ref IInvokable[] Callbacks => ref _Callbacks;
/************************************************************************************************************************/
[SerializeField]
private StringAsset[] _Names;
/// <summary>[<see cref="SerializeField"/>] The serialized <see cref="Sequence.Names"/>.</summary>
public ref StringAsset[] Names => ref _Names;
/************************************************************************************************************************/
#if UNITY_EDITOR
/************************************************************************************************************************/
/// <summary>[Editor-Only] [Internal]
/// The name of the array field which stores the <see cref="normalizedTime"/>s.
/// </summary>
internal const string NormalizedTimesField = nameof(_NormalizedTimes);
/// <summary>[Editor-Only] [Internal]
/// The name of the array field which stores the serialized <see cref="Callbacks"/>.
/// </summary>
internal const string CallbacksField = nameof(_Callbacks);
/// <summary>[Editor-Only] [Internal]
/// The name of the array field which stores the serialized <see cref="Names"/>.
/// </summary>
internal const string NamesField = nameof(_Names);
/************************************************************************************************************************/
#endif
/************************************************************************************************************************/
private Sequence _Events;
/// <summary>Returns the <see cref="Events"/> or <c>null</c> if it wasn't yet initialized.</summary>
public Sequence InitializedEvents
=> _Events;
/// <summary>
/// The runtime <see cref="Sequence"/> compiled from this <see cref="Serializable"/>.
/// Each call after the first will return the same reference.
/// </summary>
/// <remarks>
/// Unlike <see cref="GetEventsOptional"/>, this property will create an empty
/// <see cref="Sequence"/> instead of returning null if there are no events.
/// </remarks>
public Sequence Events
{
get
{
if (_Events == null)
{
GetEventsOptional();
_Events ??= new();
}
return _Events;
}
set => _Events = value;
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Initialization
/************************************************************************************************************************/
/// <summary>
/// Returns the runtime <see cref="Sequence"/> compiled from this <see cref="Serializable"/>.
/// Each call after the first will return the same reference.
/// </summary>
/// <remarks>
/// This method returns null if the sequence would be empty anyway and is used by the implicit
/// conversion from <see cref="Serializable"/> to <see cref="Sequence"/>.
/// </remarks>
public Sequence GetEventsOptional()
{
if (_Events != null ||
_NormalizedTimes == null)
return _Events;
var timeCount = _NormalizedTimes.Length;
if (timeCount == 0)
return null;
var callbackCount = _Callbacks != null
? _Callbacks.Length
: 0;
var callback = callbackCount >= timeCount--
? GetInvoke(_Callbacks[timeCount])
: null;
var endEvent = new AnimancerEvent(_NormalizedTimes[timeCount], callback);
_Events = new(timeCount)
{
EndEvent = endEvent,
Count = timeCount,
Names = StringAsset.ToStringReferences(_Names),
};
var events = _Events._Events;
for (int i = 0; i < timeCount; i++)
{
callback = i < callbackCount
? GetInvoke(_Callbacks[i])
: InvokeBoundCallback;
events[i] = new(_NormalizedTimes[i], callback);
}
return _Events;
}
/// <summary>Calls <see cref="GetEventsOptional"/>.</summary>
public static implicit operator Sequence(Serializable serializable)
=> serializable?.GetEventsOptional();
/************************************************************************************************************************/
/// <summary>
/// Returns the <see cref="IInvokable.Invoke"/> if the `invokable` isn't <c>null</c>.
/// Otherwise, returns <c>null</c>.
/// </summary>
public static Action GetInvoke(IInvokable invokable)
=> invokable != null
? invokable.Invoke
: InvokeBoundCallback;
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region End Event
/************************************************************************************************************************/
/// <summary>Returns the <see cref="normalizedTime"/> of the <see cref="EndEvent"/>.</summary>
/// <remarks>If the value is not set, the value is determined by <see cref="GetDefaultNormalizedEndTime"/>.</remarks>
public float GetNormalizedEndTime(float speed = 1)
{
return _NormalizedTimes.IsNullOrEmpty()
? GetDefaultNormalizedEndTime(speed)
: _NormalizedTimes[^1];
}
/************************************************************************************************************************/
/// <summary>Sets the <see cref="normalizedTime"/> of the <see cref="EndEvent"/>.</summary>
public void SetNormalizedEndTime(float normalizedTime)
{
if (_NormalizedTimes.IsNullOrEmpty())
_NormalizedTimes = new float[] { normalizedTime };
else
_NormalizedTimes[^1] = normalizedTime;
}
/************************************************************************************************************************/
/// <summary>Sets the <see cref="callback"/> of the <see cref="EndEvent"/>.</summary>
public void SetEndCallback(IInvokable callback = null)
{
if (_NormalizedTimes.IsNullOrEmpty())
_NormalizedTimes = new float[] { float.NaN };
InsertOptionalItem(ref _Callbacks, _NormalizedTimes.Length - 1, callback);
}
/************************************************************************************************************************/
/// <summary>Sets the data of the <see cref="EndEvent"/>.</summary>
public void SetEndEvent(float normalizedTime = float.NaN, IInvokable callback = null)
{
if (_NormalizedTimes.IsNullOrEmpty())
_NormalizedTimes = new float[] { normalizedTime };
else
_NormalizedTimes[^1] = normalizedTime;
InsertOptionalItem(ref _Callbacks, _NormalizedTimes.Length - 1, callback);
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Other Events
/************************************************************************************************************************/
/// <summary>Adds an event to the serialized fields.</summary>
public int AddEvent(float normalizedTime, IInvokable callback = null, StringAsset name = null)
{
int index;
if (_NormalizedTimes.IsNullOrEmpty())
{
_NormalizedTimes = new float[] { normalizedTime, float.NaN };
index = 0;
}
else
{
index = _NormalizedTimes.Length - 1;
for (int i = 0; i < _NormalizedTimes.Length - 1; i++)
{
if (_NormalizedTimes[i] > normalizedTime)
{
index = i;
break;
}
}
AnimancerUtilities.InsertAt(ref _NormalizedTimes, index, normalizedTime);
}
InsertOptionalItem(ref _Callbacks, index, callback);
InsertOptionalItem(ref _Names, index, name);
return index;
}
/************************************************************************************************************************/
/// <summary>Inserts an `item` at the specified `index` in an optional `array`.</summary>
/// <remarks>
/// If the `item` is <c>null</c> then the array only needs
/// to be expanded if it was already larger than the `index`.
/// </remarks>
private static void InsertOptionalItem<T>(ref T[] array, int index, T item)
where T : class
{
if (item == null &&
(array == null || array.Length < index))
return;
AnimancerUtilities.InsertAt(ref array, index, item);
}
/************************************************************************************************************************/
/// <summary>Removes an event from the serialized fields.</summary>
public void RemoveEvent(int index)
{
if (_NormalizedTimes.IsNullOrEmpty())
return;
AnimancerUtilities.RemoveAt(ref _NormalizedTimes, index);
if (_Callbacks != null && _Callbacks.Length > index)
AnimancerUtilities.RemoveAt(ref _Callbacks, index);
if (_Names != null && _Names.Length > index)
AnimancerUtilities.RemoveAt(ref _Names, index);
}
/************************************************************************************************************************/
/// <summary>Removes all events.</summary>
public void Clear(bool keepEndEvent = false)
{
if (keepEndEvent)
{
if (_NormalizedTimes != null && _NormalizedTimes.Length > 0)
_NormalizedTimes = new float[] { _NormalizedTimes[^1] };
else
_NormalizedTimes = null;
if (_Callbacks != null && _Callbacks.Length > 0)
_Callbacks = new IInvokable[] { _Callbacks[^1] };
else
_Callbacks = null;
}
else
{
_NormalizedTimes = null;
_Callbacks = null;
}
_Names = null;
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Copying
/************************************************************************************************************************/
/// <summary>Creates a new <see cref="Serializable"/> and copies the contents of <c>this</c> into it.</summary>
/// <remarks>To copy into an existing sequence, use <see cref="CopyFrom"/> instead.</remarks>
public Serializable Clone()
{
var clone = new Serializable();
clone.CopyFrom(this);
return clone;
}
/// <inheritdoc/>
public Serializable Clone(CloneContext context)
=> Clone();
/************************************************************************************************************************/
/// <inheritdoc/>
public void CopyFrom(Serializable copyFrom)
{
if (copyFrom == null)
{
_NormalizedTimes = default;
_Callbacks = default;
_Names = default;
return;
}
AnimancerUtilities.CopyExactArray(copyFrom._NormalizedTimes, ref _NormalizedTimes);
AnimancerUtilities.CopyExactArray(copyFrom._Callbacks, ref _Callbacks);
AnimancerUtilities.CopyExactArray(copyFrom._Names, ref _Names);
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Serialization
/************************************************************************************************************************/
#if UNITY_EDITOR
/************************************************************************************************************************/
/// <summary>[Editor-Only] Does nothing.</summary>
void ISerializationCallbackReceiver.OnAfterDeserialize() { }
/************************************************************************************************************************/
/// <summary>[Editor-Only] [Internal]
/// Called by <see cref="ISerializationCallbackReceiver.OnBeforeSerialize"/>.
/// </summary>
internal static event Action<Serializable> OnBeforeSerialize;
/// <summary>[Editor-Only] Ensures that the events are sorted by time (excluding the end event).</summary>
void ISerializationCallbackReceiver.OnBeforeSerialize()
=> OnBeforeSerialize?.Invoke(this);
/************************************************************************************************************************/
/// <summary>[Editor-Only] [Internal]
/// Should the arrays be prevented from reducing their size when their last elements are unused?
/// </summary>
internal static bool DisableCompactArrays { get; set; }
/// <summary>[Editor-Only] [Internal]
/// Removes empty data from the ends of the arrays to reduce the serialized data size.
/// </summary>
internal void CompactArrays()
{
if (DisableCompactArrays)
return;
// If there is only one time and it is NaN, we don't need to store anything.
if (_NormalizedTimes == null ||
(_NormalizedTimes.Length == 1 &&
(_Callbacks == null || _Callbacks.Length == 0) &&
(_Names == null || _Names.Length == 0) &&
float.IsNaN(_NormalizedTimes[0])))
{
_NormalizedTimes = Array.Empty<float>();
_Callbacks = Array.Empty<IInvokable>();
_Names = Array.Empty<StringAsset>();
return;
}
Trim(ref _Callbacks, _NormalizedTimes.Length, callback => callback != null);
Trim(ref _Names, _NormalizedTimes.Length, name => name != null);
}
/************************************************************************************************************************/
/// <summary>[Editor-Only] Removes unimportant values from the end of the `array`.</summary>
private static void Trim<T>(ref T[] array, int maxLength, Func<T, bool> isImportant)
{
if (array == null)
return;
var count = Math.Min(array.Length, maxLength);
while (count >= 1)
{
var item = array[count - 1];
if (isImportant(item))
break;
else
count--;
}
Array.Resize(ref array, count);
}
/************************************************************************************************************************/
#endif
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
}
}
}
}