// 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
{
///
/// Serializable data which can be used to construct an using
/// s and s.
///
///
/// Documentation:
///
/// Serialized Events
///
/// https://kybernetik.com.au/animancer/api/Animancer/Serializable
[Serializable]
public class Serializable : ICloneable
#if UNITY_EDITOR
, ISerializationCallbackReceiver
#endif
{
/************************************************************************************************************************/
#region Fields and Properties
/************************************************************************************************************************/
[SerializeField]
private float[] _NormalizedTimes;
/// [] The serialized s.
/// The last item is used for the .
public ref float[] NormalizedTimes => ref _NormalizedTimes;
/************************************************************************************************************************/
[SerializeReference, Polymorphic]
private IInvokable[] _Callbacks;
/// [] The serialized s.
///
/// This array only needs to be large enough to hold the last item that isn't null.
///
/// If this array is larger than the , the first item
/// with no corresponding time will be used as the callback
/// and any others after that will be ignored.
///
public ref IInvokable[] Callbacks => ref _Callbacks;
/************************************************************************************************************************/
[SerializeField]
private StringAsset[] _Names;
/// [] The serialized .
public ref StringAsset[] Names => ref _Names;
/************************************************************************************************************************/
#if UNITY_EDITOR
/************************************************************************************************************************/
/// [Editor-Only] [Internal]
/// The name of the array field which stores the s.
///
internal const string NormalizedTimesField = nameof(_NormalizedTimes);
/// [Editor-Only] [Internal]
/// The name of the array field which stores the serialized .
///
internal const string CallbacksField = nameof(_Callbacks);
/// [Editor-Only] [Internal]
/// The name of the array field which stores the serialized .
///
internal const string NamesField = nameof(_Names);
/************************************************************************************************************************/
#endif
/************************************************************************************************************************/
private Sequence _Events;
/// Returns the or null if it wasn't yet initialized.
public Sequence InitializedEvents
=> _Events;
///
/// The runtime compiled from this .
/// Each call after the first will return the same reference.
///
///
/// Unlike , this property will create an empty
/// instead of returning null if there are no events.
///
public Sequence Events
{
get
{
if (_Events == null)
{
GetEventsOptional();
_Events ??= new();
}
return _Events;
}
set => _Events = value;
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Initialization
/************************************************************************************************************************/
///
/// Returns the runtime compiled from this .
/// Each call after the first will return the same reference.
///
///
/// This method returns null if the sequence would be empty anyway and is used by the implicit
/// conversion from to .
///
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;
}
/// Calls .
public static implicit operator Sequence(Serializable serializable)
=> serializable?.GetEventsOptional();
/************************************************************************************************************************/
///
/// Returns the if the `invokable` isn't null.
/// Otherwise, returns null.
///
public static Action GetInvoke(IInvokable invokable)
=> invokable != null
? invokable.Invoke
: InvokeBoundCallback;
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region End Event
/************************************************************************************************************************/
/// Returns the of the .
/// If the value is not set, the value is determined by .
public float GetNormalizedEndTime(float speed = 1)
{
return _NormalizedTimes.IsNullOrEmpty()
? GetDefaultNormalizedEndTime(speed)
: _NormalizedTimes[^1];
}
/************************************************************************************************************************/
/// Sets the of the .
public void SetNormalizedEndTime(float normalizedTime)
{
if (_NormalizedTimes.IsNullOrEmpty())
_NormalizedTimes = new float[] { normalizedTime };
else
_NormalizedTimes[^1] = normalizedTime;
}
/************************************************************************************************************************/
/// Sets the of the .
public void SetEndCallback(IInvokable callback = null)
{
if (_NormalizedTimes.IsNullOrEmpty())
_NormalizedTimes = new float[] { float.NaN };
InsertOptionalItem(ref _Callbacks, _NormalizedTimes.Length - 1, callback);
}
/************************************************************************************************************************/
/// Sets the data of the .
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
/************************************************************************************************************************/
/// Adds an event to the serialized fields.
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;
}
/************************************************************************************************************************/
/// Inserts an `item` at the specified `index` in an optional `array`.
///
/// If the `item` is null then the array only needs
/// to be expanded if it was already larger than the `index`.
///
private static void InsertOptionalItem(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);
}
/************************************************************************************************************************/
/// Removes an event from the serialized fields.
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);
}
/************************************************************************************************************************/
/// Removes all events.
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
/************************************************************************************************************************/
/// Creates a new and copies the contents of this into it.
/// To copy into an existing sequence, use instead.
public Serializable Clone()
{
var clone = new Serializable();
clone.CopyFrom(this);
return clone;
}
///
public Serializable Clone(CloneContext context)
=> Clone();
/************************************************************************************************************************/
///
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
/************************************************************************************************************************/
/// [Editor-Only] Does nothing.
void ISerializationCallbackReceiver.OnAfterDeserialize() { }
/************************************************************************************************************************/
/// [Editor-Only] [Internal]
/// Called by .
///
internal static event Action OnBeforeSerialize;
/// [Editor-Only] Ensures that the events are sorted by time (excluding the end event).
void ISerializationCallbackReceiver.OnBeforeSerialize()
=> OnBeforeSerialize?.Invoke(this);
/************************************************************************************************************************/
/// [Editor-Only] [Internal]
/// Should the arrays be prevented from reducing their size when their last elements are unused?
///
internal static bool DisableCompactArrays { get; set; }
/// [Editor-Only] [Internal]
/// Removes empty data from the ends of the arrays to reduce the serialized data size.
///
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();
_Callbacks = Array.Empty();
_Names = Array.Empty();
return;
}
Trim(ref _Callbacks, _NormalizedTimes.Length, callback => callback != null);
Trim(ref _Names, _NormalizedTimes.Length, name => name != null);
}
/************************************************************************************************************************/
/// [Editor-Only] Removes unimportant values from the end of the `array`.
private static void Trim(ref T[] array, int maxLength, Func 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
/************************************************************************************************************************/
}
}
}
}