// 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 /************************************************************************************************************************/ } } } }