// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2023 Kybernetik // using System; using System.Collections; using System.Collections.Generic; using UnityEngine; namespace Animancer { partial struct AnimancerEvent { /// /// A variable-size list of s which keeps itself sorted according to their /// . /// /// /// Animancer Lite does not allow events (except for ) in runtime builds. /// /// Documentation: Animancer Events /// /// https://kybernetik.com.au/animancer/api/Animancer/Sequence /// public partial class Sequence : IEnumerable, ICopyable { /************************************************************************************************************************/ #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)." + " The " + nameof(AnimancerEvent) + "." + nameof(DummyCallback) + " can be assigned to make an event do nothing."; #endif /************************************************************************************************************************/ /// All of the s 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; } /************************************************************************************************************************/ /// Indicates whether the sequence has any events in it (including the ). public bool IsEmpty { get { return _EndEvent.callback == null && float.IsNaN(_EndEvent.normalizedTime) && Count == 0; } } /************************************************************************************************************************/ /// The initial that will be used if another value is not specified. public const int DefaultCapacity = 8; /// [Pro-Only] The size of the internal array used to hold events. /// /// When set, the array is reallocated to the given size. /// /// By default, the starts at 0 and increases to the /// when the first event is added. /// public int Capacity { 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(); } } } /************************************************************************************************************************/ #region Modification Detection /************************************************************************************************************************/ private int _Version; /// [Pro-Only] /// The number of times the contents of this sequence have been modified. This applies to general events, /// but not the . /// public int Version { get => _Version; private set { _Version = value; OnSequenceModified(); } } /************************************************************************************************************************/ #if UNITY_ASSERTIONS /// [Assert-Only] /// If this property is set, any attempt to modify this sequence will trigger /// (which will include this value in its message). /// /// This value can be set by . public string ShouldNotModifyReason { get; private set; } #endif /************************************************************************************************************************/ /// [Assert-Conditional] /// Sets the for . /// /// /// If the warning is triggered, the message is formatted as: /// "The being modified should not be modified because " + /// . /// [System.Diagnostics.Conditional(Strings.Assertions)] public void SetShouldNotModifyReason(string reason) { #if UNITY_ASSERTIONS ShouldNotModifyReason = reason; #endif } /************************************************************************************************************************/ /// [Assert-Conditional] Logs if necessary. [System.Diagnostics.Conditional(Strings.Assertions)] public void OnSequenceModified() { #if UNITY_ASSERTIONS if (string.IsNullOrEmpty(ShouldNotModifyReason) || OptionalWarning.LockedEvents.IsDisabled()) return; OptionalWarning.LockedEvents.Log( $"The {nameof(AnimancerEvent)}.{nameof(Sequence)} being modified should not be modified because " + ShouldNotModifyReason + $"\n\nThis warning can be prevented by calling state.Events.{nameof(SetShouldNotModifyReason)}(null);" + $" before making any modifications."); // Clear the reason so it doesn't trigger this warning again. No point in wasting performance. ShouldNotModifyReason = null; #endif } /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ #region End Event /************************************************************************************************************************/ private AnimancerEvent _EndEvent = new AnimancerEvent(float.NaN, null); /// /// A that will be triggered every frame after the has /// passed. If you want it to only get triggered once, you can either have the event clear itself or just /// use a regular event instead. /// /// /// /// void PlayAnimation(AnimancerComponent animancer, AnimationClip clip) /// { /// var state = animancer.Play(clip); /// state.Events.NormalizedEndTime = 0.75f; /// state.Events.OnEnd = OnAnimationEnd; /// /// // Or set the time and callback at the same time: /// state.Events.EndEvent = new AnimancerEvent(0.75f, OnAnimationEnd); /// } /// /// void OnAnimationEnd() /// { /// Debug.Log("Animation ended"); /// } /// /// /// /// Documentation: End Events /// /// Interrupting the animation does not trigger this event. /// /// By default, the will be so that it can choose the /// correct value based on the current play direction: forwards ends at 1 and backwards ends at 0. /// /// Animancer Lite does not allow the to be changed in Runtime Builds. /// /// /// /// public AnimancerEvent EndEvent { get => _EndEvent; set { _EndEvent = value; OnSequenceModified(); } } /************************************************************************************************************************/ /// Shorthand for the EndEvent.callback. /// /// public Action OnEnd { get => _EndEvent.callback; set { _EndEvent.callback = value; OnSequenceModified(); } } /************************************************************************************************************************/ /// [Pro-Only] 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 float NormalizedEndTime { get => _EndEvent.normalizedTime; set { _EndEvent.normalizedTime = value; OnSequenceModified(); } } /************************************************************************************************************************/ /// /// 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 /// . /// 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. /// public static float GetDefaultNormalizedEndTime(float speed) => speed < 0 ? 0 : 1; /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ #region Names /************************************************************************************************************************/ private string[] _Names; /// [Pro-Only] The names of the events (excluding the ). /// This array can be null. public ref string[] Names => ref _Names; /************************************************************************************************************************/ /// [Pro-Only] /// Returns the name of the event at the specified `index` or null if it is outside of the /// array. /// public string GetName(int index) { if (_Names == null || _Names.Length <= index) return null; else return _Names[index]; } /************************************************************************************************************************/ /// [Pro-Only] /// 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, string name) { AnimancerUtilities.Assert((uint)index < (uint)Count, IndexOutOfRangeError); // Capacity can't be 0 at this point. if (_Names == null) { _Names = new string[Capacity]; } else if (_Names.Length <= index) { var names = new string[Capacity]; Array.Copy(_Names, names, _Names.Length); _Names = names; } _Names[index] = name; } /************************************************************************************************************************/ /// [Pro-Only] /// Returns the index of the event with the specified `name` or -1 if there is no such event. /// /// /// /// /// public int IndexOf(string name, int startIndex = 0) { if (_Names == null) return -1; var count = Mathf.Min(Count, _Names.Length); for (; startIndex < count; startIndex++) if (_Names[startIndex] == name) return startIndex; return -1; } /// [Pro-Only] Returns the index of the event with the specified `name`. /// There is no such event. /// public int IndexOfRequired(string name, int startIndex = 0) { startIndex = IndexOf(name, startIndex); if (startIndex >= 0) return startIndex; throw new ArgumentException($"No event exists with the name '{name}'."); } /************************************************************************************************************************/ #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] { 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[string name] => this[IndexOfRequired(name)]; /************************************************************************************************************************/ /// [Assert-Conditional] /// Throws an if the of any events /// is less than 0 or greater than or equal to 1. /// /// This does not include 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); } /************************************************************************************************************************/ /// Returns a string containing the details of all events in this sequence. public string DeepToString(bool multiLine = true) { var text = ObjectPool.AcquireStringBuilder() .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 /// . /// public FastEnumerator GetEnumerator() => new FastEnumerator(_Events, Count); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); /************************************************************************************************************************/ /// [Pro-Only] Returns the index of the `animancerEvent` or -1 if there is no such event. /// public int IndexOf(AnimancerEvent animancerEvent) => IndexOf(Count / 2, animancerEvent); /// [Pro-Only] Returns the index of the `animancerEvent`. /// There is no such event. /// 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 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) if (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) if (otherEvent.callback == animancerEvent.callback) return indexHint; } } return -1; } /// [Pro-Only] Returns the index of the `animancerEvent`. /// There is no such event. /// 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 the 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); AssertEventUniqueness(index, animancerEvent); _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. /// /// public int Add(float normalizedTime, Action callback) => Add(new AnimancerEvent(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. /// /// Use the instead of null. /// 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); AssertEventUniqueness(indexHint, animancerEvent); _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. /// /// public int Add(int indexHint, float normalizedTime, Action callback) => Add(indexHint, new AnimancerEvent(normalizedTime, callback)); /************************************************************************************************************************/ /// [Pro-Only] /// Adds every event in the `enumerable` to this sequence. The is increased by one and if /// required, the is doubled to fit the new event. /// /// 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`. /// public void AddCallback(int index, Action callback) { ref var animancerEvent = ref _Events[index]; AssertCallbackUniqueness(animancerEvent.callback, callback, $"{nameof(callback)} being added"); 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`. /// /// public void AddCallback(string name, Action callback) => AddCallback(IndexOfRequired(name), callback); /************************************************************************************************************************/ /// [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; if (animancerEvent.callback == null) 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`. /// public void RemoveCallback(string name, Action callback) => RemoveCallback(IndexOfRequired(name), callback); /************************************************************************************************************************/ /// [Pro-Only] Replaces the of the event at the specified `index`. /// Use the 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]; AssertCallbackUniqueness(animancerEvent.callback, callback, $"{nameof(callback)} being assigned"); animancerEvent.callback = callback; Version++; } /// [Pro-Only] Replaces the of the event with the specified `name`. /// There is no event with the specified `name`. /// /// public void SetCallback(string name, Action callback) => SetCallback(IndexOfRequired(name), callback); /************************************************************************************************************************/ /// [Assert-Conditional] /// Logs if the `oldCallback` is identical to the /// `newCallback` or just has the same . /// [System.Diagnostics.Conditional(Strings.Assertions)] private static void AssertCallbackUniqueness(Action oldCallback, Action newCallback, string target) { #if UNITY_ASSERTIONS if (oldCallback == DummyCallback || OptionalWarning.DuplicateEvent.IsDisabled()) return; if (oldCallback == newCallback) { OptionalWarning.DuplicateEvent.Log($"The {target}" + " is identical to an existing event in the sequence" + " which may mean that it is being unintentionally added multiple times." + $" If the {nameof(AnimancerEvent)}.{nameof(Sequence)} is owned by a Transition then it will not" + " be cleared each time it is played so the events should only be initialized once on startup." + $" See the documentation for more information: {Strings.DocsURLs.ClearAutomatically}"); } else if (oldCallback?.Method == newCallback?.Method) { OptionalWarning.DuplicateEvent.Log($"The {target}" + " is identical to an existing event in the sequence except for the target object." + " This often happens when a Transition is shared by multiple objects," + " in which case it can be avoided by giving each object its own" + $" {nameof(AnimancerEvent)}.{nameof(Sequence)} as explained in the documentation:" + $" {Strings.DocsURLs.SharedEventSequences}"); } #endif } /// [Assert-Conditional] /// Logs if the event at the specified `index` is identical to /// the `newEvent`. /// [System.Diagnostics.Conditional(Strings.Assertions)] private void AssertEventUniqueness(int index, AnimancerEvent newEvent) { #if UNITY_ASSERTIONS if (OptionalWarning.DuplicateEvent.IsDisabled() || index == 0) return; var previousEvent = _Events[index - 1]; if (previousEvent.normalizedTime != newEvent.normalizedTime) return; AssertCallbackUniqueness(previousEvent.callback, newEvent.callback, $"{nameof(AnimancerEvent)} being added"); #endif } /************************************************************************************************************************/ /// [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 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 (!string.IsNullOrEmpty(name)) 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`. /// public int SetNormalizedTime(string 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`. /// 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, using the assumption /// that elements will usually be added in order. /// private int Insert(float normalizedTime) { var index = Count; 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; 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 != null) { if (_Names.Length < capacity) { var names = new string[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 != null) { 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 (_Names != null && index < _Names.Length) { _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(string name) { var index = IndexOf(name); if (index >= 0) { Remove(index); return true; } else return false; } /// [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) { Remove(index); return true; } else return false; } /************************************************************************************************************************/ /// Removes all events, including the . public void Clear() { if (_Names != null) Array.Clear(_Names, 0, _Names.Length); Array.Clear(_Events, 0, Count); Count = 0; Version++; _EndEvent = new AnimancerEvent(float.NaN, null); } /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ #region Copying /************************************************************************************************************************/ /// public void CopyFrom(Sequence copyFrom) { if (copyFrom == null) { if (_Names != null) Array.Clear(_Names, 0, _Names.Length); Array.Clear(_Events, 0, Count); Count = 0; Capacity = 0; _EndEvent = default; return; } AnimancerUtilities.CopyExactArray(copyFrom._Names, ref _Names); 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; } /************************************************************************************************************************/ /// [Pro-Only] Copies the into this . /// /// The of the new events will be empty and can be set by /// . /// /// If you are going to play the `animation`, consider disabling so that /// 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; Capacity += animationEvents.Length; var index = -1; for (int i = 0; i < animationEvents.Length; i++) { var animationEvent = animationEvents[i]; index = Add(index + 1, new AnimancerEvent(animationEvent.time / length, DummyCallback)); SetName(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 (_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 /************************************************************************************************************************/ } } }