// Animancer // Copyright 2020 Kybernetik // using System; using System.Collections; using System.Collections.Generic; using System.Text; using UnityEngine; namespace Animancer { partial struct AnimancerEvent { /// /// A variable-size list of s which keeps itself sorted by /// . /// /// Animancer Lite does not allow the use of events in a runtime build, except for . /// public sealed partial class Sequence : IEnumerable { /************************************************************************************************************************/ #region Fields and Properties /************************************************************************************************************************/ private const string NoCallbackError = "Event has no callback", IndexTooHighError = "index must be less than Count and not negative"; /// /// A zero length array of s which is used by all lists before any elements are /// added to them (unless their is set manually). /// public static readonly AnimancerEvent[] EmptyArray = new AnimancerEvent[0]; /// The initial that will be used if another value is not specified. public const int DefaultCapacity = 8; /************************************************************************************************************************/ /// /// An which denotes the end of the animation. Its values can be accessed via /// and . /// /// 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 a runtime build. /// /// /// /// /// void PlayAnimation(AnimancerComponent animancer, AnimationClip clip) /// { /// var state = animancer.Play(clip); /// state.Events.OnEnd = OnAnimationEnd; /// state.Events.NormalizedEndTime = 0.75f; /// /// // Or set the time and callback at the same time: /// state.Events.endEvent = new AnimancerEvent(0.75f, OnAnimationEnd); /// } /// /// void OnAnimationEnd() /// { /// Debug.Log("Animation ended"); /// } /// /// /// /// /// See the documentation for more information about /// /// End Events. /// public AnimancerEvent endEvent = new AnimancerEvent(float.NaN, null); /// The internal array in which the events are stored (excluding the ). private AnimancerEvent[] _Events; /// [Pro-Only] The number of events in this sequence (excluding the ). public int Count { get; private set; } /// [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; private set; } /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ #region Constructors /************************************************************************************************************************/ /// /// Creates a new which starts at 0 . /// /// Adding anything to the list will set the = /// and then double it whenever the would exceed the . /// public Sequence() { _Events = EmptyArray; } /************************************************************************************************************************/ /// [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] : EmptyArray; } /************************************************************************************************************************/ /// /// Creates a new , copying the contents of `copyFrom` into it. /// public Sequence(Sequence copyFrom) { CopyFrom(copyFrom); } /************************************************************************************************************************/ /// [Pro-Only] /// Creates a new , copying and sorting the contents of the `collection` into it. /// The and will be equal to the /// . /// public Sequence(ICollection collection) { if (collection == null) throw new ArgumentNullException("collection"); var count = collection.Count; if (count == 0) { _Events = EmptyArray; } else { _Events = new AnimancerEvent[count]; AddRange(collection); } } /************************************************************************************************************************/ /// [Pro-Only] /// Creates a new , copying and sorting the contents of the `enumerable` into it. /// public Sequence(IEnumerable enumerable) { if (enumerable == null) throw new ArgumentNullException("enumerable"); _Events = EmptyArray; AddRange(enumerable); } /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ #region Iteration /************************************************************************************************************************/ /// /// Indicates whether the list has any events in it or the event's /// is not at the default value (1). /// public bool IsEmpty { get { return endEvent.callback == null && float.IsNaN(endEvent.normalizedTime) && Count == 0; } } /************************************************************************************************************************/ /// [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 { return _Events.Length; } set { if (value < Count) throw new ArgumentOutOfRangeException("value", "Capacity cannot be set lower than 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 = EmptyArray; } } } /************************************************************************************************************************/ /// [Pro-Only] Gets the event at the specified `index`. public AnimancerEvent this[int index] { get { Debug.Assert((uint)index < (uint)Count, IndexTooHighError); return _Events[index]; } } /************************************************************************************************************************/ /// [Assert] /// 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.Assert)] public void AssertNormalizedTimes() { if (Count == 0 || (_Events[0].normalizedTime >= 0 && _Events[Count - 1].normalizedTime < 1)) return; throw new ArgumentOutOfRangeException("The normalized time of an event in the Sequence is" + " < 0 or >= 1, which is not allowed on looping animations. " + DeepToString()); } /// [Assert] /// Calls if `isLooping` is true. /// [System.Diagnostics.Conditional(Strings.Assert)] public void AssertNormalizedTimes(bool isLooping) { if (isLooping) AssertNormalizedTimes(); } /************************************************************************************************************************/ /// Returns a string containing the details of all events in this sequence. public string DeepToString(bool multiLine = true) { var text = new StringBuilder() .Append(ToString()) .Append(" [") .Append(Count) .Append(multiLine ? "]\n{" : "] { "); for (int i = 0; i < Count; i++) { if (multiLine) text.Append("\n "); else if (i > 0) text.Append(", "); text.Append(this[i]); } text.Append(multiLine ? "\n}\nendEvent=" : " } (endEvent=") .Append(endEvent); if (!multiLine) text.Append(")"); return text.ToString(); } /************************************************************************************************************************/ /// [Pro-Only] Returns an for this sequence. public Enumerator GetEnumerator() { return new Enumerator(this); } IEnumerator IEnumerable.GetEnumerator() { return new Enumerator(this); } IEnumerator IEnumerable.GetEnumerator() { return new Enumerator(this); } /************************************************************************************************************************/ /// [Pro-Only] /// An iterator that can cycle through every event in a except for the /// . /// public struct Enumerator : IEnumerator { /************************************************************************************************************************/ /// The target . public readonly Sequence Sequence; private int _Index; private int _Version; private AnimancerEvent _Current; private const string InvalidVersion = "AnimancerEvent.Sequence was modified. Enumeration operation may not execute."; /************************************************************************************************************************/ /// The event this iterator is currently pointing to. public AnimancerEvent Current { get { return _Current; } } /// The event this iterator is currently pointing to. object IEnumerator.Current { get { if (_Index == 0 || _Index == Sequence.Count + 1) throw new InvalidOperationException( "Operation is not valid due to the current state of the object."); return Current; } } /************************************************************************************************************************/ /// Creates a new . public Enumerator(Sequence sequence) { Sequence = sequence; _Index = 0; _Version = sequence.Version; _Current = default(AnimancerEvent); } /************************************************************************************************************************/ void IDisposable.Dispose() { } /************************************************************************************************************************/ /// /// Moves to the next event in the and returns true if there is one. /// /// /// Thrown if the has changed since this iterator was created. /// public bool MoveNext() { if (_Version != Sequence.Version) throw new InvalidOperationException(InvalidVersion); if ((uint)_Index < (uint)Sequence.Count) { _Current = Sequence._Events[_Index]; _Index++; return true; } else { _Index = Sequence.Count + 1; _Current = default(AnimancerEvent); return false; } } /************************************************************************************************************************/ /// /// Returns this iterator to the start of the . /// /// /// Thrown if the has changed since this iterator was created. /// void IEnumerator.Reset() { if (_Version != Sequence.Version) throw new InvalidOperationException(InvalidVersion); _Index = 0; _Current = default(AnimancerEvent); } /************************************************************************************************************************/ } /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ #region Modification /************************************************************************************************************************/ /// [Pro-Only] /// Adds the given event to this list. 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 /// in order to keep the list sorted in ascending order. If there are already /// any events with the same , the new event is added immediately after them. /// public int Add(AnimancerEvent animancerEvent) { Debug.Assert(animancerEvent.callback != null, NoCallbackError); var index = Insert(animancerEvent.normalizedTime); _Events[index] = animancerEvent; return index; } /// [Pro-Only] /// Adds the given event to this list. 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 /// in order to keep the list 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) { return Add(new AnimancerEvent(normalizedTime, callback)); } /// [Pro-Only] /// Adds the given event to this list. 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 /// in order to keep the list 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) { Debug.Assert(animancerEvent.callback != null, NoCallbackError); indexHint = Insert(indexHint, animancerEvent.normalizedTime); _Events[indexHint] = animancerEvent; return indexHint; } /// [Pro-Only] /// Adds the given event to this list. 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 /// in order to keep the list 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) { return Add(indexHint, new AnimancerEvent(normalizedTime, callback)); } /************************************************************************************************************************/ /// [Pro-Only] /// Adds every event in the `enumerable` to this list. 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 /// in order to keep the list sorted in ascending order. If there are already /// any events with the same , the new event is added immediately after them. /// public void AddRange(IEnumerable enumerable) { foreach (var item in enumerable) Add(item); } /************************************************************************************************************************/ /// [Pro-Only] /// Replaces the of the event at the specified `index`. /// public void Set(int index, Action callback) { var animancerEvent = _Events[index]; animancerEvent.callback = callback; _Events[index] = animancerEvent; Version++; } /************************************************************************************************************************/ /// [Pro-Only] /// Determines the index where a new event with the specified `normalizedTime` should be added in order to /// keep this list 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 list, 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 list 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 hint, float normalizedTime) { if (hint >= Count) return Insert(normalizedTime); if (_Events[hint].normalizedTime > normalizedTime) { while (hint > 0 && _Events[hint - 1].normalizedTime > normalizedTime) hint--; } else { while (hint < Count && _Events[hint].normalizedTime <= normalizedTime) hint++; } Insert(hint); return hint; } /************************************************************************************************************************/ /// [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) { Debug.Assert((uint)index <= (uint)Count, "index must be less than or equal to Count"); var capacity = _Events.Length; if (Count == capacity) { if (capacity == 0) { _Events = new AnimancerEvent[DefaultCapacity]; } else { capacity *= 2; if (capacity < DefaultCapacity) capacity = DefaultCapacity; var newEvents = new AnimancerEvent[capacity]; Array.Copy(_Events, 0, newEvents, 0, index); if (Count > index) Array.Copy(_Events, index, newEvents, index + 1, Count - index); _Events = newEvents; } } else if (Count > index) { Array.Copy(_Events, index, _Events, index + 1, Count - index); } Count++; Version++; } /************************************************************************************************************************/ /// [Pro-Only] /// Removes the event at the specified `index` from this list by decrementing the and /// copying all events after the removed one down one place. /// public void Remove(int index) { Debug.Assert((uint)index < (uint)Count, IndexTooHighError); Count--; if (index < Count) Array.Copy(_Events, index + 1, _Events, index, Count - index); _Events[Count] = default(AnimancerEvent); Version++; } /// [Pro-Only] /// Removes the `animancerEvent` from this list 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 = Array.IndexOf(_Events, animancerEvent); if (index >= 0) { Remove(index); return true; } else return false; } /************************************************************************************************************************/ /// [Pro-Only] /// Removes all events except the . /// /// public void RemoveAll() { Array.Clear(_Events, 0, Count); Count = 0; Version++; } /// /// Removes all events, including the . /// /// public void Clear() { RemoveAll(); endEvent = new AnimancerEvent(float.NaN, null); } /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ #region On End /************************************************************************************************************************/ /// /// Shorthand for the endEvent.callback. This callback is triggered when the animation passes the /// (not when the state is interrupted or exited for whatever reason). /// /// Unlike regular events, this callback will be triggered every frame while it is past the end so if you /// want to ensure that your callback only occurs once, you will need to clear it as part of that callback. /// /// This callback is automatically cleared by , /// , and . /// /// /// /// /// void PlayAnimation(AnimancerComponent animancer, AnimationClip clip) /// { /// var state = animancer.Play(clip); /// state.Events.OnEnd = OnAnimationEnd; /// state.Events.NormalizedEndTime = 0.75f; /// /// // Or set the time and callback at the same time: /// state.Events.endEvent = new AnimancerEvent(0.75f, OnAnimationEnd); /// } /// /// void OnAnimationEnd() /// { /// Debug.Log("Animation ended"); /// } /// /// /// /// /// See the documentation for more information about /// /// End Events. /// public Action OnEnd { get { return endEvent.callback; } set { endEvent.callback = value; } } /************************************************************************************************************************/ /// [Pro-Only] /// Shorthand for endEvent.normalizedTime. /// /// By default, this value 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 this value to be changed in a runtime build. /// /// /// /// /// void PlayAnimation(AnimancerComponent animancer, AnimationClip clip) /// { /// var state = animancer.Play(clip); /// state.Events.OnEnd = OnAnimationEnd; /// state.Events.NormalizedEndTime = 0.75f; /// /// // Or set the time and callback at the same time: /// state.Events.endEvent = new AnimancerEvent(0.75f, OnAnimationEnd); /// } /// /// void OnAnimationEnd() /// { /// Debug.Log("Animation ended"); /// } /// /// /// /// /// See the documentation for more information about /// /// End Events. /// public float NormalizedEndTime { get { return endEvent.normalizedTime; } set { endEvent.normalizedTime = value; } } /************************************************************************************************************************/ /// /// 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) { return 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) { return speed < 0 ? 0 : 1; } /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ #region Copying /************************************************************************************************************************/ /// /// Copies all the events from the `source` to replace the previous contents of this list. /// public void CopyFrom(Sequence source) { var sourceCount = source.Count; if (Count > sourceCount) Array.Clear(_Events, Count, sourceCount - Count); else if (_Events.Length < sourceCount) Capacity = sourceCount; Count = sourceCount; Array.Copy(source._Events, 0, _Events, 0, sourceCount); endEvent = source.endEvent; } /************************************************************************************************************************/ /// [] /// Copies all the events from this list into the `array`, starting at the `index`. /// public void CopyTo(AnimancerEvent[] array, int index) { Array.Copy(_Events, 0, array, index, Count); } /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ } } }