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