You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
CrowdControl/Assets/Plugins/Animancer/Internal/Core/AnimancerEvent.Sequence.cs

802 lines
36 KiB
C#

1 month ago
// Animancer // Copyright 2020 Kybernetik //
using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;
using UnityEngine;
namespace Animancer
{
partial struct AnimancerEvent
{
/// <summary>
/// A variable-size list of <see cref="AnimancerEvent"/>s which keeps itself sorted by
/// <see cref="normalizedTime"/>.
/// <para></para>
/// Animancer Lite does not allow the use of events in a runtime build, except for <see cref="OnEnd"/>.
/// </summary>
public sealed partial class Sequence : IEnumerable<AnimancerEvent>
{
/************************************************************************************************************************/
#region Fields and Properties
/************************************************************************************************************************/
private const string
NoCallbackError = "Event has no callback",
IndexTooHighError = "index must be less than Count and not negative";
/// <summary>
/// A zero length array of <see cref="AnimancerEvent"/>s which is used by all lists before any elements are
/// added to them (unless their <see cref="Capacity"/> is set manually).
/// </summary>
public static readonly AnimancerEvent[] EmptyArray = new AnimancerEvent[0];
/// <summary>The initial <see cref="Capacity"/> that will be used if another value is not specified.</summary>
public const int DefaultCapacity = 8;
/************************************************************************************************************************/
/// <summary>
/// An <see cref="AnimancerEvent"/> which denotes the end of the animation. Its values can be accessed via
/// <see cref="OnEnd"/> and <see cref="NormalizedEndTime"/>.
/// <para></para>
/// By default, the <see cref="normalizedTime"/> will be <see cref="float.NaN"/> so that it can choose the
/// correct value based on the current play direction: forwards ends at 1 and backwards ends at 0.
/// <para></para>
/// Animancer Lite does not allow the <see cref="normalizedTime"/> to be changed in a runtime build.
/// </summary>
///
/// <example>
/// <code>
/// 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");
/// }
/// </code>
/// </example>
///
/// <remarks>
/// See the documentation for more information about
/// <see href="https://kybernetik.com.au/animancer/docs/manual/events/end">
/// End Events</see>.
/// </remarks>
public AnimancerEvent endEvent = new AnimancerEvent(float.NaN, null);
/// <summary>The internal array in which the events are stored (excluding the <see cref="endEvent"/>).</summary>
private AnimancerEvent[] _Events;
/// <summary>[Pro-Only] The number of events in this sequence (excluding the <see cref="endEvent"/>).</summary>
public int Count { get; private set; }
/// <summary>[Pro-Only]
/// The number of times the contents of this sequence have been modified. This applies to general events,
/// but not the <see cref="endEvent"/>.
/// </summary>
public int Version { get; private set; }
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Constructors
/************************************************************************************************************************/
/// <summary>
/// Creates a new <see cref="Sequence"/> which starts at 0 <see cref="Capacity"/>.
/// <para></para>
/// Adding anything to the list will set the <see cref="Capacity"/> = <see cref="DefaultCapacity"/>
/// and then double it whenever the <see cref="Count"/> would exceed the <see cref="Capacity"/>.
/// </summary>
public Sequence()
{
_Events = EmptyArray;
}
/************************************************************************************************************************/
/// <summary>[Pro-Only]
/// Creates a new <see cref="Sequence"/> which starts with the specified <see cref="Capacity"/>. It will be
/// initially empty, but will have room for the given number of elements before any reallocations are
/// required.
/// </summary>
public Sequence(int capacity)
{
_Events = capacity > 0 ? new AnimancerEvent[capacity] : EmptyArray;
}
/************************************************************************************************************************/
/// <summary>
/// Creates a new <see cref="Sequence"/>, copying the contents of `copyFrom` into it.
/// </summary>
public Sequence(Sequence copyFrom)
{
CopyFrom(copyFrom);
}
/************************************************************************************************************************/
/// <summary>[Pro-Only]
/// Creates a new <see cref="Sequence"/>, copying and sorting the contents of the `collection` into it.
/// The <see cref="Count"/> and <see cref="Capacity"/> will be equal to the
/// <see cref="ICollection{T}.Count"/>.
/// </summary>
public Sequence(ICollection<AnimancerEvent> collection)
{
if (collection == null)
throw new ArgumentNullException("collection");
var count = collection.Count;
if (count == 0)
{
_Events = EmptyArray;
}
else
{
_Events = new AnimancerEvent[count];
AddRange(collection);
}
}
/************************************************************************************************************************/
/// <summary>[Pro-Only]
/// Creates a new <see cref="Sequence"/>, copying and sorting the contents of the `enumerable` into it.
/// </summary>
public Sequence(IEnumerable<AnimancerEvent> enumerable)
{
if (enumerable == null)
throw new ArgumentNullException("enumerable");
_Events = EmptyArray;
AddRange(enumerable);
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Iteration
/************************************************************************************************************************/
/// <summary>
/// Indicates whether the list has any events in it or the <see cref="endEvent"/> event's
/// <see cref="normalizedTime"/> is not at the default value (1).
/// </summary>
public bool IsEmpty
{
get
{
return
endEvent.callback == null &&
float.IsNaN(endEvent.normalizedTime) &&
Count == 0;
}
}
/************************************************************************************************************************/
/// <summary>[Pro-Only]
/// The size of the internal array used to hold events.
/// <para></para>
/// When set, the array is reallocated to the given size.
/// <para></para>
/// By default, the <see cref="Capacity"/> starts at 0 and increases to the <see cref="DefaultCapacity"/>
/// when the first event is added.
/// </summary>
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;
}
}
}
/************************************************************************************************************************/
/// <summary>[Pro-Only] Gets the event at the specified `index`.</summary>
public AnimancerEvent this[int index]
{
get
{
Debug.Assert((uint)index < (uint)Count, IndexTooHighError);
return _Events[index];
}
}
/************************************************************************************************************************/
/// <summary>[Assert]
/// Throws an <see cref="ArgumentOutOfRangeException"/> if the <see cref="normalizedTime"/> of any events
/// is less than 0 or greater than or equal to 1.
/// <para></para>
/// This does not include the <see cref="endEvent"/> since it works differently to other events.
/// </summary>
[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());
}
/// <summary>[Assert]
/// Calls <see cref="AssertNormalizedTimes()"/> if `isLooping` is true.
/// </summary>
[System.Diagnostics.Conditional(Strings.Assert)]
public void AssertNormalizedTimes(bool isLooping)
{
if (isLooping)
AssertNormalizedTimes();
}
/************************************************************************************************************************/
/// <summary>Returns a string containing the details of all events in this sequence.</summary>
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();
}
/************************************************************************************************************************/
/// <summary>[Pro-Only] Returns an <see cref="Enumerator"/> for this sequence.</summary>
public Enumerator GetEnumerator()
{
return new Enumerator(this);
}
IEnumerator<AnimancerEvent> IEnumerable<AnimancerEvent>.GetEnumerator()
{
return new Enumerator(this);
}
IEnumerator IEnumerable.GetEnumerator()
{
return new Enumerator(this);
}
/************************************************************************************************************************/
/// <summary>[Pro-Only]
/// An iterator that can cycle through every event in a <see cref="Sequence"/> except for the
/// <see cref="endEvent"/>.
/// </summary>
public struct Enumerator : IEnumerator<AnimancerEvent>
{
/************************************************************************************************************************/
/// <summary>The target <see cref="AnimancerEvent.Sequence"/>.</summary>
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.";
/************************************************************************************************************************/
/// <summary>The event this iterator is currently pointing to.</summary>
public AnimancerEvent Current { get { return _Current; } }
/// <summary>The event this iterator is currently pointing to.</summary>
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;
}
}
/************************************************************************************************************************/
/// <summary>Creates a new <see cref="Enumerator"/>.</summary>
public Enumerator(Sequence sequence)
{
Sequence = sequence;
_Index = 0;
_Version = sequence.Version;
_Current = default(AnimancerEvent);
}
/************************************************************************************************************************/
void IDisposable.Dispose() { }
/************************************************************************************************************************/
/// <summary>
/// Moves to the next event in the <see cref="Sequence"/> and returns true if there is one.
/// </summary>
/// <exception cref="InvalidOperationException">
/// Thrown if the <see cref="Version"/> has changed since this iterator was created.
/// </exception>
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;
}
}
/************************************************************************************************************************/
/// <summary>
/// Returns this iterator to the start of the <see cref="Sequence"/>.
/// </summary>
/// <exception cref="InvalidOperationException">
/// Thrown if the <see cref="Version"/> has changed since this iterator was created.
/// </exception>
void IEnumerator.Reset()
{
if (_Version != Sequence.Version)
throw new InvalidOperationException(InvalidVersion);
_Index = 0;
_Current = default(AnimancerEvent);
}
/************************************************************************************************************************/
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Modification
/************************************************************************************************************************/
/// <summary>[Pro-Only]
/// Adds the given event to this list. The <see cref="Count"/> is increased by one and if required, the
/// <see cref="Capacity"/> is doubled to fit the new event.
/// <para></para>
/// This methods returns the index at which the event is added, which is determined by its
/// <see cref="normalizedTime"/> in order to keep the list sorted in ascending order. If there are already
/// any events with the same <see cref="normalizedTime"/>, the new event is added immediately after them.
/// </summary>
public int Add(AnimancerEvent animancerEvent)
{
Debug.Assert(animancerEvent.callback != null, NoCallbackError);
var index = Insert(animancerEvent.normalizedTime);
_Events[index] = animancerEvent;
return index;
}
/// <summary>[Pro-Only]
/// Adds the given event to this list. The <see cref="Count"/> is increased by one and if required, the
/// <see cref="Capacity"/> is doubled to fit the new event.
/// <para></para>
/// This methods returns the index at which the event is added, which is determined by its
/// <see cref="normalizedTime"/> in order to keep the list sorted in ascending order. If there are already
/// any events with the same <see cref="normalizedTime"/>, the new event is added immediately after them.
/// </summary>
public int Add(float normalizedTime, Action callback)
{
return Add(new AnimancerEvent(normalizedTime, callback));
}
/// <summary>[Pro-Only]
/// Adds the given event to this list. The <see cref="Count"/> is increased by one and if required, the
/// <see cref="Capacity"/> is doubled to fit the new event.
/// <para></para>
/// This methods returns the index at which the event is added, which is determined by its
/// <see cref="normalizedTime"/> in order to keep the list sorted in ascending order. If there are already
/// any events with the same <see cref="normalizedTime"/>, the new event is added immediately after them.
/// </summary>
public int Add(int indexHint, AnimancerEvent animancerEvent)
{
Debug.Assert(animancerEvent.callback != null, NoCallbackError);
indexHint = Insert(indexHint, animancerEvent.normalizedTime);
_Events[indexHint] = animancerEvent;
return indexHint;
}
/// <summary>[Pro-Only]
/// Adds the given event to this list. The <see cref="Count"/> is increased by one and if required, the
/// <see cref="Capacity"/> is doubled to fit the new event.
/// <para></para>
/// This methods returns the index at which the event is added, which is determined by its
/// <see cref="normalizedTime"/> in order to keep the list sorted in ascending order. If there are already
/// any events with the same <see cref="normalizedTime"/>, the new event is added immediately after them.
/// </summary>
public int Add(int indexHint, float normalizedTime, Action callback)
{
return Add(indexHint, new AnimancerEvent(normalizedTime, callback));
}
/************************************************************************************************************************/
/// <summary>[Pro-Only]
/// Adds every event in the `enumerable` to this list. The <see cref="Count"/> is increased by one and if
/// required, the <see cref="Capacity"/> is doubled to fit the new event.
/// <para></para>
/// This methods returns the index at which the event is added, which is determined by its
/// <see cref="normalizedTime"/> in order to keep the list sorted in ascending order. If there are already
/// any events with the same <see cref="normalizedTime"/>, the new event is added immediately after them.
/// </summary>
public void AddRange(IEnumerable<AnimancerEvent> enumerable)
{
foreach (var item in enumerable)
Add(item);
}
/************************************************************************************************************************/
/// <summary>[Pro-Only]
/// Replaces the <see cref="callback"/> of the event at the specified `index`.
/// </summary>
public void Set(int index, Action callback)
{
var animancerEvent = _Events[index];
animancerEvent.callback = callback;
_Events[index] = animancerEvent;
Version++;
}
/************************************************************************************************************************/
/// <summary>[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 <see cref="Count"/> by one, doubles the <see cref="Capacity"/> if
/// required, moves any existing events to open up the chosen index, and returns that index.
/// <para></para>
/// 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.
/// </summary>
private int Insert(float normalizedTime)
{
var index = Count;
while (index > 0 && _Events[index - 1].normalizedTime > normalizedTime)
index--;
Insert(index);
return index;
}
/// <summary>[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 <see cref="Count"/> by one, doubles the <see cref="Capacity"/> if
/// required, moves any existing events to open up the chosen index, and returns that index.
/// <para></para>
/// This overload starts searching for the desired index from the `hint`.
/// </summary>
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;
}
/************************************************************************************************************************/
/// <summary>[Pro-Only]
/// Increases the <see cref="Count"/> by one, doubles the <see cref="Capacity"/> if required, and moves any
/// existing events to open up the `index`.
/// </summary>
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++;
}
/************************************************************************************************************************/
/// <summary>[Pro-Only]
/// Removes the event at the specified `index` from this list by decrementing the <see cref="Count"/> and
/// copying all events after the removed one down one place.
/// </summary>
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++;
}
/// <summary>[Pro-Only]
/// Removes the `animancerEvent` from this list by decrementing the <see cref="Count"/> and copying all
/// events after the removed one down one place. Returns true if the event was found and removed.
/// </summary>
public bool Remove(AnimancerEvent animancerEvent)
{
var index = Array.IndexOf(_Events, animancerEvent);
if (index >= 0)
{
Remove(index);
return true;
}
else return false;
}
/************************************************************************************************************************/
/// <summary>[Pro-Only]
/// Removes all events except the <see cref="endEvent"/>.
/// <seealso cref="Clear"/>
/// </summary>
public void RemoveAll()
{
Array.Clear(_Events, 0, Count);
Count = 0;
Version++;
}
/// <summary>
/// Removes all events, including the <see cref="endEvent"/>.
/// <seealso cref="RemoveAll"/>
/// </summary>
public void Clear()
{
RemoveAll();
endEvent = new AnimancerEvent(float.NaN, null);
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region On End
/************************************************************************************************************************/
/// <summary>
/// Shorthand for the <c>endEvent.callback</c>. This callback is triggered when the animation passes the
/// <see cref="NormalizedEndTime"/> (not when the state is interrupted or exited for whatever reason).
/// <para></para>
/// 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.
/// <para></para>
/// This callback is automatically cleared by <see cref="AnimancerState.Play"/>,
/// <see cref="AnimancerState.OnStartFade"/>, and <see cref="AnimancerState.Stop"/>.
/// </summary>
///
/// <example>
/// <code>
/// 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");
/// }
/// </code>
/// </example>
///
/// <remarks>
/// See the documentation for more information about
/// <see href="https://kybernetik.com.au/animancer/docs/manual/events/end">
/// End Events</see>.
/// </remarks>
public Action OnEnd
{
get { return endEvent.callback; }
set { endEvent.callback = value; }
}
/************************************************************************************************************************/
/// <summary>[Pro-Only]
/// Shorthand for <c>endEvent.normalizedTime</c>.
/// <para></para>
/// By default, this value will be <see cref="float.NaN"/> so that it can choose the correct value based on
/// the current play direction: forwards ends at 1 and backwards ends at 0.
/// <para></para>
/// Animancer Lite does not allow this value to be changed in a runtime build.
/// </summary>
///
/// <example>
/// <code>
/// 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");
/// }
/// </code>
/// </example>
///
/// <remarks>
/// See the documentation for more information about
/// <see href="https://kybernetik.com.au/animancer/docs/manual/events/end">
/// End Events</see>.
/// </remarks>
public float NormalizedEndTime
{
get { return endEvent.normalizedTime; }
set { endEvent.normalizedTime = value; }
}
/************************************************************************************************************************/
/// <summary>
/// The default <see cref="AnimancerState.NormalizedTime"/> 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).
/// <para></para>
/// `speed` 0 or <see cref="float.NaN"/> will also return 0.
/// </summary>
/// <remarks>
/// This method has nothing to do with events, so it is only here because of
/// <see cref="GetDefaultNormalizedEndTime"/>.
/// </remarks>
public static float GetDefaultNormalizedStartTime(float speed)
{
return speed < 0 ? 1 : 0;
}
/// <summary>
/// The default <see cref="normalizedTime"/> for an <see cref="endEvent"/> when playing forwards is 1 (the
/// end of the animation) and when playing backwards is 0 (the start of the animation).
/// <para></para>
/// `speed` 0 or <see cref="float.NaN"/> will also return 1.
/// </summary>
public static float GetDefaultNormalizedEndTime(float speed)
{
return speed < 0 ? 0 : 1;
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Copying
/************************************************************************************************************************/
/// <summary>
/// Copies all the events from the `source` to replace the previous contents of this list.
/// </summary>
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;
}
/************************************************************************************************************************/
/// <summary>[<see cref="ICollection{T}"/>]
/// Copies all the events from this list into the `array`, starting at the `index`.
/// </summary>
public void CopyTo(AnimancerEvent[] array, int index)
{
Array.Copy(_Events, 0, array, index, Count);
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
}
}
}