// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 Kybernetik // #pragma warning disable IDE0016 // Use 'throw' expression. using System; using System.Collections; using System.Collections.Generic; using System.Runtime.CompilerServices; using UnityEngine; using Object = UnityEngine.Object; namespace Animancer { /// https://kybernetik.com.au/animancer/api/Animancer/AnimancerEvent partial struct AnimancerEvent { /// /// A variable-size list of s which keeps itself sorted /// according to their . /// /// /// Animancer Lite doesn't allow events (except for ) in runtime builds. /// /// Documentation: /// /// Animancer Events /// /// https://kybernetik.com.au/animancer/api/Animancer/Sequence /// public partial class Sequence : IEnumerable, ICloneable { /************************************************************************************************************************/ #region Fields and Properties /************************************************************************************************************************/ internal const string IndexOutOfRangeError = "index must be within the range of 0 <= index < " + nameof(Count); #if UNITY_ASSERTIONS private const string NullCallbackError = nameof(AnimancerEvent) + " callbacks can't be null (except for End Events). Use " + nameof(AnimancerEvent) + "." + nameof(InvokeBoundCallback) + " or " + nameof(AnimancerEvent) + "." + nameof(DummyCallback) + " instead."; #endif /************************************************************************************************************************/ /// All of the events in this sequence, excluding the . /// This field should never be null. It should use instead. private AnimancerEvent[] _Events; /************************************************************************************************************************/ /// [Pro-Only] The number of events in this sequence, excluding the . public int Count { get; private set; } /************************************************************************************************************************/ /// Does this sequence have no events in it, including the ? public bool IsEmpty { [MethodImpl(MethodImplOptions.AggressiveInlining)] get { return _EndEvent.callback == null && float.IsNaN(_EndEvent.normalizedTime) && Count == 0; } } /************************************************************************************************************************/ /// The initial which will be used if another value is not specified. public const int DefaultCapacity = 4; /// [Pro-Only] The size of the internal array used to hold events. /// /// When set, the array is re-allocated to the given size. /// /// If not specified in the constructor, this value starts at 0 /// and increases to the when the first event is added. /// public int Capacity { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => _Events.Length; set { if (value < Count) throw new ArgumentOutOfRangeException(nameof(value), $"{nameof(Capacity)} cannot be set lower than {nameof(Count)}"); if (value == _Events.Length) return; if (value > 0) { var newEvents = new AnimancerEvent[value]; if (Count > 0) Array.Copy(_Events, 0, newEvents, 0, Count); _Events = newEvents; } else { _Events = Array.Empty(); } } } /************************************************************************************************************************/ /// /// The number of times the contents of this sequence have been modified. /// This applies to general events, but not the . /// public int Version { get; private set; } /************************************************************************************************************************/ #region End Event /************************************************************************************************************************/ private AnimancerEvent _EndEvent = new(float.NaN, null); /// /// A which will be triggered every frame /// after the has passed as long as the animation is playing. /// /// /// /// Interrupting the animation before it ends doesn't trigger this event. /// /// By default, the will be /// so that it chooses the correct value based on the current play direction: /// playing forwards ends at 1 and playing backwards ends at 0. /// /// Documentation: /// /// End Events /// /// /// /// public ref AnimancerEvent EndEvent { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => ref _EndEvent; } /************************************************************************************************************************/ /// /// A callback which will be triggered every frame after the /// has passed as long as the animation is playing. /// /// /// /// Interrupting the animation before it ends doesn't trigger this event. /// /// By default, the will be /// so that it chooses the correct value based on the current play direction: /// playing forwards ends at 1 and playing backwards ends at 0. /// /// Documentation: /// /// End Events /// /// /// /// public ref Action OnEnd { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => ref _EndEvent.callback; } /************************************************************************************************************************/ /// Shorthand for EndEvent.normalizedTime. /// /// This value is by default so that the actual time /// can be determined based on the : /// positive speed ends at 1 and negative speed ends at 0. /// /// Use to access that value. /// /// /// public ref float NormalizedEndTime { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => ref _EndEvent.normalizedTime; } /************************************************************************************************************************/ /// /// Returns the but converts /// to its corresponding default value: positive speed ends at 1 and negative speed ends at 0. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public float GetRealNormalizedEndTime(float speed = 1) => float.IsNaN(_EndEvent.normalizedTime) ? GetDefaultNormalizedEndTime(speed) : _EndEvent.normalizedTime; /************************************************************************************************************************/ /// /// The default for an animation to start /// at when playing forwards is 0 (the start of the animation) /// and when playing backwards is 1 (the end of the animation). /// /// `speed` 0 or will also return 0. /// /// /// This method has nothing to do with events, so it is only here because of /// . /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static float GetDefaultNormalizedStartTime(float speed) => speed < 0 ? 1 : 0; /// /// The default for an /// when playing forwards is 1 (the end of the animation) /// and when playing backwards is 0 (the start of the animation). /// /// `speed` 0 or will also return 1. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static float GetDefaultNormalizedEndTime(float speed) => speed < 0 ? 0 : 1; /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ #region Names /************************************************************************************************************************/ private StringReference[] _Names = Array.Empty(); /// The names of the events, excluding the . /// This array is empty by default and can never be null. public StringReference[] Names { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => _Names; [MethodImpl(MethodImplOptions.AggressiveInlining)] set => _Names = value ?? Array.Empty(); } /************************************************************************************************************************/ /// /// Returns the name of the event at the specified `index` /// or null if it's outside of the array. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public StringReference GetName(int index) => (uint)_Names.Length > (uint)index ? _Names[index] : null; /************************************************************************************************************************/ /// Sets the name of the event at the specified `index`. /// /// If the did not previously include that `index` /// it will be resized with a size equal to the . /// public void SetName(int index, StringReference name) { AnimancerUtilities.Assert((uint)index < (uint)Count, IndexOutOfRangeError); // Capacity can't be 0 at this point because the above assertion would have failed. if (_Names.Length <= index) Array.Resize(ref _Names, Capacity); _Names[index] = name; } /************************************************************************************************************************/ /// /// Returns the index of the first event with the specified `name` /// or -1 if there is no such event. /// /// /// /// /// public int IndexOf(StringReference name, int startIndex = 0) { if (_Names.Length == 0) return -1; var count = Mathf.Min(Count, _Names.Length); for (; startIndex < count; startIndex++) if (_Names[startIndex] == name) return startIndex; return -1; } /// Returns the index of the first event with the specified `name`. /// There is no such event. /// public int IndexOfRequired(StringReference name, int startIndex = 0) { startIndex = IndexOf(name, startIndex); if (startIndex >= 0) return startIndex; throw new ArgumentException( $"No event exists with the name '{name}'." + $" If the specified event isn't required, use IndexOf which will return -1 if not found."); } /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ #region Constructors /************************************************************************************************************************/ /// /// Creates a new which starts at 0 . /// /// Adding anything to the sequence will set the = /// and then double it whenever the would exceed the . /// public Sequence() { _Events = Array.Empty(); } /************************************************************************************************************************/ /// [Pro-Only] /// Creates a new which starts with the specified /// . It will be initially empty, but will have room for the /// given number of elements before any reallocations are required. /// public Sequence(int capacity) { _Events = capacity > 0 ? new AnimancerEvent[capacity] : Array.Empty(); } /************************************************************************************************************************/ /// Creates a new and copies the contents of `copyFrom` into it. /// To copy into an existing sequence, use instead. public Sequence(Sequence copyFrom) { _Events = Array.Empty(); if (copyFrom != null) CopyFrom(copyFrom); } /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ #region Iteration /************************************************************************************************************************/ /// [Pro-Only] Returns the event at the specified `index`. public AnimancerEvent this[int index] { [MethodImpl(MethodImplOptions.AggressiveInlining)] get { AnimancerUtilities.Assert((uint)index < (uint)Count, IndexOutOfRangeError); return _Events[index]; } } /// [Pro-Only] Returns the event with the specified `name`. /// There is no event with the specified `name`. public AnimancerEvent this[StringReference name] { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => this[IndexOfRequired(name)]; } /************************************************************************************************************************/ /// Returns a string containing the details of all events in this sequence. public string DeepToString(bool multiLine = true) { var text = StringBuilderPool.Instance.Acquire() .Append(ToString()) .Append('[') .Append(Count) .Append(']'); text.Append(multiLine ? "\n{" : " {"); for (int i = 0; i < Count; i++) { if (multiLine) text.Append("\n "); else if (i > 0) text.Append(','); text.Append(" ["); text.Append(i) .Append("] "); this[i].AppendDetails(text); var name = GetName(i); if (name != null) { text.Append(", Name: '") .Append(name) .Append('\''); } } if (multiLine) { text.Append("\n [End] "); } else { if (Count > 0) text.Append(','); text.Append(" [End] "); } _EndEvent.AppendDetails(text); if (multiLine) text.Append("\n}\n"); else text.Append(" }"); return text.ReleaseToString(); } /************************************************************************************************************************/ /// [Pro-Only] /// Returns a for the events in this sequence, /// excluding the . /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public FastEnumerator GetEnumerator() => new(_Events, Count); [MethodImpl(MethodImplOptions.AggressiveInlining)] IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); [MethodImpl(MethodImplOptions.AggressiveInlining)] IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); /************************************************************************************************************************/ /// [Pro-Only] Returns the index of the `animancerEvent` or -1 if there is no such event. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public int IndexOf(AnimancerEvent animancerEvent) => IndexOf(Count / 2, animancerEvent); /// [Pro-Only] Returns the index of the `animancerEvent`. /// There is no such event. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public int IndexOfRequired(AnimancerEvent animancerEvent) => IndexOfRequired(Count / 2, animancerEvent); /// [Pro-Only] Returns the index of the `animancerEvent` or -1 if there is no such event. /// public int IndexOf(int indexHint, AnimancerEvent animancerEvent) { if (Count == 0) return -1; if (indexHint >= Count) indexHint = Count - 1; var events = _Events; var otherEvent = events[indexHint]; if (otherEvent == animancerEvent) return indexHint; if (otherEvent.normalizedTime > animancerEvent.normalizedTime) { while (--indexHint >= 0) { otherEvent = events[indexHint]; if (otherEvent.normalizedTime < animancerEvent.normalizedTime) return -1; else if (otherEvent.normalizedTime == animancerEvent.normalizedTime && otherEvent.callback == animancerEvent.callback) return indexHint; } } else { while (otherEvent.normalizedTime == animancerEvent.normalizedTime) { indexHint--; if (indexHint < 0) break; otherEvent = events[indexHint]; } while (++indexHint < Count) { otherEvent = events[indexHint]; if (otherEvent.normalizedTime > animancerEvent.normalizedTime) return -1; else if (otherEvent.normalizedTime == animancerEvent.normalizedTime && otherEvent.callback == animancerEvent.callback) return indexHint; } } return -1; } /// [Pro-Only] Returns the index of the `animancerEvent`. /// There is no such event. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public int IndexOfRequired(int indexHint, AnimancerEvent animancerEvent) { indexHint = IndexOf(indexHint, animancerEvent); if (indexHint >= 0) return indexHint; throw new ArgumentException($"Event not found in {nameof(Sequence)} '{animancerEvent}'."); } /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ #region Modification /************************************************************************************************************************/ /// [Pro-Only] /// Adds the given event to this sequence. The is increased by one /// and if required, the is doubled to fit the new event. /// /// /// This methods returns the index at which the event is added, which is determined by /// its to keep the sequence sorted in ascending order. /// If there are already any events with the same , /// the new event is added immediately after them. /// /// /// Use or instead of null. /// public int Add(AnimancerEvent animancerEvent) { #if UNITY_ASSERTIONS if (animancerEvent.callback == null) throw new ArgumentNullException($"{nameof(AnimancerEvent)}.{nameof(callback)}", NullCallbackError); #endif var index = Insert(animancerEvent.normalizedTime); _Events[index] = animancerEvent; return index; } /// [Pro-Only] /// Adds the given event to this sequence. The is increased by one /// and if required, the is doubled to fit the new event. /// /// /// This methods returns the index at which the event is added, which is determined by /// its to keep the sequence sorted in ascending order. /// If there are already any events with the same , /// the new event is added immediately after them. /// /// /// Use or instead of null. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public int Add(float normalizedTime, Action callback) => Add(new(normalizedTime, callback)); /// [Pro-Only] /// Adds the given event to this sequence. The is increased by one /// and if required, the is doubled to fit the new event. /// /// /// This methods returns the index at which the event is added, which is determined by /// its to keep the sequence sorted in ascending order. /// If there are already any events with the same , /// the new event is added immediately after them. /// public int Add(int indexHint, AnimancerEvent animancerEvent) { #if UNITY_ASSERTIONS if (animancerEvent.callback == null) throw new ArgumentNullException($"{nameof(AnimancerEvent)}.{nameof(callback)}", NullCallbackError); #endif indexHint = Insert(indexHint, animancerEvent.normalizedTime); _Events[indexHint] = animancerEvent; return indexHint; } /// [Pro-Only] /// Adds the given event to this sequence. The is increased by one /// and if required, the is doubled to fit the new event. /// /// /// This methods returns the index at which the event is added, which is determined by /// its to keep the sequence sorted in ascending order. /// If there are already any events with the same , /// the new event is added immediately after them. /// /// /// Use or instead of null. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public int Add(int indexHint, float normalizedTime, Action callback) => Add(indexHint, new(normalizedTime, callback)); /************************************************************************************************************************/ /// [Pro-Only] /// Adds every event in the `enumerable` to this sequence using . /// /// /// Use or instead of null. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public void AddRange(IEnumerable enumerable) { foreach (var item in enumerable) Add(item); } /************************************************************************************************************************/ /// [Pro-Only] Adds the specified `callback` to the event at the specified `index`. /// /// Use or instead of null. /// public void AddCallback(int index, Action callback) { ref var animancerEvent = ref _Events[index]; if (animancerEvent.callback == DummyCallback) { #if UNITY_ASSERTIONS if (callback == null) throw new ArgumentNullException(nameof(callback), NullCallbackError); #endif animancerEvent.callback = callback; } else { animancerEvent.callback += callback; } Version++; } /// [Pro-Only] Adds the specified `callback` to the event with the specified `name`. /// There is no event with the specified `name`. /// /// Use or instead of null. /// /// /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public void AddCallback(StringReference name, Action callback) => AddCallback(IndexOfRequired(name), callback); /// [Pro-Only] /// Adds the specified `callback` to every event with the specified `name` /// and returns the number of events that were found. /// /// /// Use or instead of null. /// /// /// public int AddCallbacks(StringReference name, Action callback) { var count = 0; var index = -1; while (true) { index = IndexOf(name, index + 1); if (index < 0) return count; count++; AddCallback(index, callback); } } /************************************************************************************************************************/ /// [Pro-Only] /// Adds the specified `callback` to the event at the specified `index`. /// will be used to get the callback's parameter. /// /// The `callback` is null. /// /// public Action AddCallback(int index, Action callback) { ref var animancerEvent = ref _Events[index]; AssertContainsParameter(animancerEvent.callback); var parametized = Parametize(callback); animancerEvent.callback += parametized; Version++; return parametized; } /// [Pro-Only] /// Adds the specified `callback` to the event with the specified `name`. /// will be used to get the callback's parameter. /// /// There is no event with the specified `name`. /// The `callback` is null. /// /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public Action AddCallback(StringReference name, Action callback) => AddCallback(IndexOfRequired(name), callback); /// [Pro-Only] /// Adds the specified `callback` to every event with the specified `name` /// and returns the number of events that were found. /// will be used to get the callback's parameter. /// /// The `callback` is null. /// /// public int AddCallbacks(StringReference name, Action callback) { Action parametized = null; var count = 0; var index = -1; while (true) { index = IndexOf(name, index + 1); if (index < 0) return count; AssertContainsParameter(_Events[index].callback); parametized ??= Parametize(callback); count++; AddCallback(index, parametized); } } /************************************************************************************************************************/ /// [Pro-Only] Removes the specified `callback` from the event at the specified `index`. /// /// If the would become null, /// it is instead set to the since they are not allowed to be null. /// public void RemoveCallback(int index, Action callback) { ref var animancerEvent = ref _Events[index]; animancerEvent.callback -= callback; animancerEvent.callback ??= DummyCallback; Version++; } /// [Pro-Only] Removes the specified `callback` from the event with the specified `name`. /// /// If the would become null, /// it is instead set to the since they are not allowed to be null. /// /// There is no event with the specified `name`. /// /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public void RemoveCallback(StringReference name, Action callback) => RemoveCallback(IndexOfRequired(name), callback); /// [Pro-Only] /// Removes the specified `callback` from every event with the specified `name` /// and returns the number of events that were found. /// /// /// If a would become null, /// it is instead set to the since they are not allowed to be null. /// /// /// public int RemoveCallbacks(StringReference name, Action callback) { var count = 0; var index = -1; while (true) { index = IndexOf(name, index + 1); if (index < 0) return count; count++; RemoveCallback(index, callback); } } /************************************************************************************************************************/ /// [Pro-Only] Replaces the of the event at the specified `index`. /// /// Use or instead of null. /// public void SetCallback(int index, Action callback) { #if UNITY_ASSERTIONS if (callback == null) throw new ArgumentNullException(nameof(callback), NullCallbackError); #endif ref var animancerEvent = ref _Events[index]; animancerEvent.callback = callback; Version++; } /// [Pro-Only] Replaces the of the event with the specified `name`. /// There is no event with the specified `name`. /// /// Use or instead of null. /// /// /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public void SetCallback(StringReference name, Action callback) => SetCallback(IndexOfRequired(name), callback); /// [Pro-Only] /// Replaces the of every event with the specified `name` /// and returns the number of events that were found. /// /// /// Use or instead of null. /// /// /// public int SetCallbacks(StringReference name, Action callback) { var count = 0; var index = -1; while (true) { index = IndexOf(name, index + 1); if (index < 0) return count; count++; SetCallback(index, callback); } } /************************************************************************************************************************/ /// [Pro-Only] Sets the of the event at the specified `index`. /// /// If multiple events have the same , this method will /// avoid re-arranging them where calling then /// would always re-add the moved event /// as the last one with that time. /// public int SetNormalizedTime(int index, float normalizedTime) { #if UNITY_ASSERTIONS if (!normalizedTime.IsFinite()) throw new ArgumentOutOfRangeException(nameof(normalizedTime), normalizedTime, $"{nameof(normalizedTime)} {Strings.MustBeFinite}"); #endif var events = _Events; var animancerEvent = events[index]; if (animancerEvent.normalizedTime == normalizedTime) return index; var moveTo = index; if (animancerEvent.normalizedTime < normalizedTime) { while (moveTo < Count - 1) { if (events[moveTo + 1].normalizedTime >= normalizedTime) break; else moveTo++; } } else { while (moveTo > 0) { if (events[moveTo - 1].normalizedTime <= normalizedTime) break; else moveTo--; } } if (index != moveTo) { var name = GetName(index); Remove(index); index = moveTo; Insert(index); if (!name.IsNullOrEmpty()) SetName(index, name); } animancerEvent.normalizedTime = normalizedTime; events[index] = animancerEvent; Version++; return index; } /// [Pro-Only] Sets the of the event with the specified `name`. /// /// If multiple events have the same , this method will /// avoid re-arranging them where calling then /// would always re-add the moved event /// as the last one with that time. /// /// There is no event with the specified `name`. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public int SetNormalizedTime(StringReference name, float normalizedTime) => SetNormalizedTime(IndexOfRequired(name), normalizedTime); /// [Pro-Only] Sets the of the matching `animancerEvent`. /// /// If multiple events have the same , this method will /// avoid re-arranging them where calling then /// would always re-add the moved event /// as the last one with that time. /// /// There is no event matching the `animancerEvent`. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public int SetNormalizedTime(AnimancerEvent animancerEvent, float normalizedTime) => SetNormalizedTime(IndexOfRequired(animancerEvent), normalizedTime); /************************************************************************************************************************/ /// [Pro-Only] /// Determines the index where a new event with the specified `normalizedTime` should /// be added in order to keep this sequence sorted, increases the /// by one, doubles the if required, moves any existing events /// to open up the chosen index, and returns that index. /// /// /// /// This overload starts searching for the desired index from the end of the sequence, /// based on the assumption that elements will usually be added in order. /// private int Insert(float normalizedTime) { var index = Count; var events = _Events; while (index > 0 && events[index - 1].normalizedTime > normalizedTime) index--; Insert(index); return index; } /// [Pro-Only] /// Determines the index where a new event with the specified `normalizedTime` should /// be added in order to keep this sequence sorted, increases the /// by one, doubles the if required, moves any existing events /// to open up the chosen index, and returns that index. /// /// This overload starts searching for the desired index from the `hint`. /// private int Insert(int indexHint, float normalizedTime) { if (Count == 0) { Count = 0; } else { if (indexHint >= Count) indexHint = Count - 1; var events = _Events; if (events[indexHint].normalizedTime > normalizedTime) { while (indexHint > 0 && events[indexHint - 1].normalizedTime > normalizedTime) indexHint--; } else { while (indexHint < Count && events[indexHint].normalizedTime <= normalizedTime) indexHint++; } } Insert(indexHint); return indexHint; } /************************************************************************************************************************/ /// [Pro-Only] /// Increases the by one, doubles the if required, /// and moves any existing events to open up the `index`. /// private void Insert(int index) { AnimancerUtilities.Assert((uint)index <= (uint)Count, IndexOutOfRangeError); var capacity = _Events.Length; if (Count == capacity) { if (capacity == 0) { capacity = DefaultCapacity; _Events = new AnimancerEvent[DefaultCapacity]; } else { capacity *= 2; if (capacity < DefaultCapacity) capacity = DefaultCapacity; var events = new AnimancerEvent[capacity]; Array.Copy(_Events, 0, events, 0, index); if (Count > index) Array.Copy(_Events, index, events, index + 1, Count - index); _Events = events; } } else if (Count > index) { Array.Copy(_Events, index, _Events, index + 1, Count - index); } if (_Names.Length > 0) { if (_Names.Length < capacity) { var names = new StringReference[capacity]; Array.Copy(_Names, 0, names, 0, Math.Min(_Names.Length, index)); if (index <= Count && index < _Names.Length) Array.Copy(_Names, index, names, index + 1, Count - index); _Names = names; } else { if (Count > index) Array.Copy(_Names, index, _Names, index + 1, Count - index); _Names[index] = null; } } Count++; Version++; } /************************************************************************************************************************/ /// [Pro-Only] /// Removes the event at the specified `index` from this sequence by decrementing the /// and copying all events after the removed one down one place. /// public void Remove(int index) { AnimancerUtilities.Assert((uint)index < (uint)Count, IndexOutOfRangeError); Count--; if (index < Count) { Array.Copy(_Events, index + 1, _Events, index, Count - index); if (_Names.Length > 0) { var nameCount = Mathf.Min(Count + 1, _Names.Length); if (index + 1 < nameCount) Array.Copy(_Names, index + 1, _Names, index, nameCount - index - 1); _Names[nameCount - 1] = default; } } else if ((uint)_Names.Length > (uint)index) { _Names[index] = default; } _Events[Count] = default; Version++; } /// [Pro-Only] /// Removes the event with the specified `name` from this sequence by decrementing the /// and copying all events after the removed one down one place. /// Returns true if the event was found and removed. /// public bool Remove(StringReference name) { var index = IndexOf(name); if (index < 0) return false; Remove(index); return true; } /// [Pro-Only] /// Removes the `animancerEvent` from this sequence by decrementing the /// and copying all events after the removed one down one place. /// Returns true if the event was found and removed. /// public bool Remove(AnimancerEvent animancerEvent) { var index = IndexOf(animancerEvent); if (index < 0) return false; Remove(index); return true; } /************************************************************************************************************************/ /// Removes all events, including the . public void Clear() { Array.Clear(_Names, 0, _Names.Length); Array.Clear(_Events, 0, Count); Count = 0; Version++; _EndEvent = new(float.NaN, null); } /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ #region Copying /************************************************************************************************************************/ /// Creates a new and copies the contents of this into it. /// To copy into an existing sequence, use instead. [MethodImpl(MethodImplOptions.AggressiveInlining)] public Sequence Clone() => new(this); /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public Sequence Clone(CloneContext context) => new(this); /************************************************************************************************************************/ /// public void CopyFrom(Sequence copyFrom) { if (copyFrom == null) { Array.Clear(_Names, 0, _Names.Length); Array.Clear(_Events, 0, Count); Count = 0; Capacity = 0; _EndEvent = default; return; } CopyNamesFrom(copyFrom._Names, copyFrom.Count); var sourceCount = copyFrom.Count; if (Count > sourceCount) Array.Clear(_Events, Count, sourceCount - Count); else if (_Events.Length < sourceCount) Capacity = sourceCount; Count = sourceCount; Array.Copy(copyFrom._Events, 0, _Events, 0, sourceCount); _EndEvent = copyFrom._EndEvent; } /************************************************************************************************************************/ /// Copies the given array into the . private void CopyNamesFrom(StringReference[] copyFrom, int maxCount) { if (_Names.Length == 0) { // Both empty. if (copyFrom.Length == 0) return; // Copying into empty. maxCount = Math.Min(copyFrom.Length, maxCount); _Names = new StringReference[copyFrom.Length]; Array.Copy(copyFrom, _Names, maxCount); return; } // Copying empty into not empty. if (copyFrom.Length == 0) { Array.Clear(_Names, 0, _Names.Length); return; } // Copying into large enough array. maxCount = Math.Min(copyFrom.Length, maxCount); if (_Names.Length >= maxCount) { Array.Copy(copyFrom, _Names, maxCount); Array.Clear(_Names, maxCount, _Names.Length - maxCount); } else// Need larger array. { _Names = new StringReference[copyFrom.Length]; Array.Copy(copyFrom, _Names, maxCount); } } /************************************************************************************************************************/ /// [Pro-Only] Copies the into this . /// /// The of the new events will be empty and can be set by /// . /// /// If you're going to play the `animation`, consider disabling /// so the events copied by this method are not triggered as s. /// Otherwise they would still trigger in addition to the s copied here. /// public void AddAllEvents(AnimationClip animation) { if (animation == null) return; var length = animation.length; var animationEvents = animation.events; if (animationEvents.Length == 0) return; var capacity = Count + animationEvents.Length; if (Capacity < capacity) Capacity = Mathf.Max(Mathf.NextPowerOfTwo(capacity), DefaultCapacity); if (_Names.Length < Capacity) Array.Resize(ref _Names, Capacity); var index = -1; for (int i = 0; i < animationEvents.Length; i++) { var animationEvent = animationEvents[i]; index = Add(index + 1, new(animationEvent.time / length, InvokeBoundCallback)); _Names[index] = animationEvent.functionName; } } /************************************************************************************************************************/ /// [] [Pro-Only] /// Copies all the events from this sequence into the `array`, starting at the `index`. /// public void CopyTo(AnimancerEvent[] array, int index) { Array.Copy(_Events, 0, array, index, Count); } /************************************************************************************************************************/ /// Are all events in this sequence identical to the ones in the `other` sequence? public bool ContentsAreEqual(Sequence other) { if (other == null || _EndEvent != other._EndEvent) return false; if (Count != other.Count) return false; for (int i = Count - 1; i >= 0; i--) if (this[i] != other[i]) return false; return true; } /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ #region Assertions /************************************************************************************************************************/ /// [Assert-Conditional] /// Throws an /// if any event is outside the range of 0 <= normalizedTime < 1. /// /// /// This excludes the since it works differently to other events. /// [System.Diagnostics.Conditional(Strings.Assertions)] public void AssertNormalizedTimes(AnimancerState state) { if (Count == 0 || (_Events[0].normalizedTime >= 0 && _Events[Count - 1].normalizedTime < 1)) return; throw new ArgumentOutOfRangeException(nameof(normalizedTime), "Events on looping animations are triggered every loop and must be" + $" within the range of 0 <= {nameof(normalizedTime)} < 1.\n{state}\n{DeepToString()}"); } /************************************************************************************************************************/ /// [Assert-Conditional] /// Calls if `isLooping` is true. /// [System.Diagnostics.Conditional(Strings.Assertions)] public void AssertNormalizedTimes(AnimancerState state, bool isLooping) { if (isLooping) AssertNormalizedTimes(state); } /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ } } }