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/AnimancerState.cs

1127 lines
48 KiB
C#

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

// Animancer // Copyright 2020 Kybernetik //
using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;
using UnityEngine;
using UnityEngine.Playables;
using Object = UnityEngine.Object;
#if UNITY_EDITOR
using UnityEditor;
#endif
namespace Animancer
{
/// <summary>
/// Base class for all states in an <see cref="AnimancerPlayable"/> graph.
/// Each state is a wrapper for a <see cref="Playable"/> in the <see cref="PlayableGraph"/>.
/// <para></para>
/// This class can be used as a custom yield instruction to wait until the animation either stops playing or reaches its end.
/// </summary>
/// <remarks>
/// There are various different ways of getting a state:
/// <list type="bullet">
/// <item>
/// Use one of the state's constructors. Generally the first parameter is a layer or mixer which will be used as
/// the state's parent. If not specified, you will need to call SetParent manually. Also note than an
/// AnimancerComponent can be implicitly cast to its first layer.
/// </item>
/// <item>
/// AnimancerController.CreateState creates a new ClipState. You can optionally specify a custom `key` to
/// register it in the dictionary instead of the default (the `clip` itself).
/// </item>
/// <item>
/// AnimancerController.GetOrCreateState looks for an existing state registered with the specified `key` and only
/// creates a new one if it doesnt already exist.
/// </item>
/// <item>
/// AnimancerController.GetState returns an existing state registered with the specified `key` if there is one.
/// </item>
/// <item>
/// AnimancerController.TryGetState is similar but returns a bool to indicate success and returns the `state`
/// as an out parameter.
/// </item>
/// <item>
/// AnimancerController.Play and CrossFade also return the state they play.
/// </item>
/// </list>
/// <para></para>
/// Note that when inheriting from this class, the <see cref="AnimancerNode._Playable"/> field must be assigned in the
/// constructor to avoid throwing <see cref="ArgumentException"/>s throughout the system.
/// </remarks>
public abstract partial class AnimancerState : AnimancerNode, IAnimationClipCollection
{
/************************************************************************************************************************/
#region Hierarchy
/************************************************************************************************************************/
/// <summary>The object which receives the output of the <see cref="Playable"/>.</summary>
public override IPlayableWrapper Parent { get { return _Parent; } }
private AnimancerNode _Parent;
/// <summary>
/// Connects this state to the `parent` mixer at the specified `index`.
/// <para></para>
/// See also <see cref="AnimancerLayer.AddChild(AnimancerState)"/> to connect a state to an available port on a
/// layer.
/// </summary>
public void SetParent(AnimancerNode parent, int index)
{
if (_Parent != null)
_Parent.OnRemoveChild(this);
Index = index;
_Parent = parent;
if (parent != null)
{
SetWeightDirty();
parent.OnAddChild(this);
}
}
/// <summary>[Internal]
/// Called by <see cref="AnimancerNode.OnAddChild(IList{AnimancerState}, AnimancerState)"/> if the specified
/// port is already occupied so it can be cleared without triggering any other calls.
/// </summary>
internal void ClearParent()
{
Index = -1;
_Parent = null;
}
/************************************************************************************************************************/
/// <summary>
/// The <see cref="AnimancerNode.Weight"/> of this state multiplied by the <see cref="AnimancerNode.Weight"/> of each of
/// its parents down the hierarchy to determine how much this state affects the final output.
/// </summary>
/// <exception cref="NullReferenceException">Thrown if this state has no <see cref="AnimancerNode.Parent"/>.</exception>
public float EffectiveWeight
{
get
{
var weight = Weight;
var parent = _Parent;
while (parent != null)
{
weight *= parent.Weight;
parent = parent.Parent as AnimancerNode;
}
return weight;
}
}
/************************************************************************************************************************/
// Layer.
/************************************************************************************************************************/
/// <summary>The root <see cref="AnimancerLayer"/> which this state is connected to.</summary>
public override AnimancerLayer Layer { get { return _Parent.Layer; } }
/// <summary>
/// The index of the <see cref="AnimancerLayer"/> this state is connected to (determined by the
/// <see cref="Parent"/>).
/// </summary>
public int LayerIndex
{
get { return _Parent.Layer.Index; }
set
{
if (_Parent != null && LayerIndex == value)
return;
Root.Layers[value].AddChild(this);
}
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Key and Clip
/************************************************************************************************************************/
internal object _Key;
/// <summary>
/// The object used to identify this state in the root <see cref="AnimancerPlayable.States"/> dictionary.
/// Can be null.
/// </summary>
public object Key
{
get { return _Key; }
set
{
Root.States.Unregister(this);
Root.States.Register(value, this);
}
}
/************************************************************************************************************************/
/// <summary>The <see cref="AnimationClip"/> which this state plays (if any).</summary>
/// <exception cref="NotSupportedException">
/// Thrown if this state type doesn't have a clip and you try to set it.
/// </exception>
public virtual AnimationClip Clip
{
get { return null; }
set { throw new NotSupportedException(GetType() + " does not support setting the Clip."); }
}
/// <summary>The main object to show in the Inspector for this state (if any).</summary>
/// <exception cref="NotSupportedException">
/// Thrown if this state type doesn't have a main object and you try to set it.
/// </exception>
/// <exception cref="InvalidCastException">
/// Thrown if you try to assign something this state can't use.
/// </exception>
public virtual Object MainObject
{
get { return null; }
set { throw new NotSupportedException(GetType() + " does not support setting the MainObject."); }
}
/************************************************************************************************************************/
/// <summary>The average velocity of the root motion caused by this state.</summary>
public virtual Vector3 AverageVelocity
{
get { return default(Vector3); }
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Playing
/************************************************************************************************************************/
/// <summary>Is the <see cref="Time"/> automatically advancing?</summary>
private bool _IsPlaying = true;
/// <summary>
/// Has <see cref="_IsPlaying"/> changed since it was last applied to the <see cref="Playable"/>.
/// </summary>
/// <remarks>
/// Playables start playing by default so we start dirty to pause it during the first update (unless
/// <see cref="IsPlaying"/> is set to true before that).
/// </remarks>
private bool _IsPlayingDirty;
/************************************************************************************************************************/
/// <summary>Is the <see cref="Time"/> automatically advancing?</summary>
///
/// <example>
/// <code>
/// void IsPlayingExample(AnimancerComponent animancer, AnimationClip clip)
/// {
/// var state = animancer.States.GetOrCreate(clip);
///
/// if (state.IsPlaying)
/// Debug.Log(clip + " is playing");
/// else
/// Debug.Log(clip + " is paused");
///
/// state.IsPlaying = false;// Pause the animation.
///
/// state.IsPlaying = true;// Unpause the animation.
/// }
/// </code>
/// </example>
public virtual bool IsPlaying
{
get { return _IsPlaying; }
set
{
if (_IsPlaying == value)
return;
_IsPlaying = value;
// If it was already dirty then we just returned to the previous state so it is no longer dirty.
if (_IsPlayingDirty)
{
_IsPlayingDirty = false;
}
else
{
_IsPlayingDirty = true;
Root.RequireUpdate(this);
}
}
}
/************************************************************************************************************************/
/// <summary>
/// Returns true if this state is playing and is at or fading towards a non-zero
/// <see cref="AnimancerNode.Weight"/>.
/// </summary>
public bool IsActive
{
get
{
return
_IsPlaying &&
TargetWeight > 0;
}
}
/************************************************************************************************************************/
/// <summary>
/// Returns true if this state is not playing and is at 0 <see cref="AnimancerNode.Weight"/>.
/// </summary>
public bool IsStopped
{
get
{
return
!_IsPlaying &&
Weight == 0;
}
}
/************************************************************************************************************************/
/// <summary>
/// Updates the <see cref="AnimancerNode.Weight"/> for fading, applies it to this state's port on the parent
/// mixer, and plays or pauses the <see cref="Playable"/> if its state is dirty.
/// <para></para>
/// If the <see cref="Parent"/>'s <see cref="AnimancerNode.KeepChildrenConnected"/> is set to false, this
/// method will also connect/disconnect this node from the <see cref="Parent"/> in the playable graph.
/// </summary>
protected internal override void Update(out bool needsMoreUpdates)
{
base.Update(out needsMoreUpdates);
if (_IsPlayingDirty)
{
_IsPlayingDirty = false;
if (_IsPlaying)
{
#if UNITY_2017_3_OR_NEWER
_Playable.Play();
#else
_Playable.SetPlayState(PlayState.Playing);
#endif
}
else
{
#if UNITY_2017_3_OR_NEWER
_Playable.Pause();
#else
_Playable.SetPlayState(PlayState.Paused);
#endif
}
}
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Timing
/************************************************************************************************************************/
// Time.
/************************************************************************************************************************/
/// <summary>
/// The current time of the <see cref="Playable"/>, retrieved by <see cref="Time"/> whenever the
/// <see cref="_TimeFrameID"/> is different from the <see cref="AnimancerPlayable.FrameID"/>.</summary>
private float _Time;
/// <summary>
/// The <see cref="AnimancerPlayable.FrameID"/> from when the <see cref="Time"/> was last retrieved from the
/// <see cref="Playable"/>.
/// </summary>
private uint _TimeFrameID;
/************************************************************************************************************************/
/// <summary>
/// The number of seconds that have passed since the start of this animation.
/// <para></para>
/// This value will continue increasing after the animation passes the end of its <see cref="Length"/> while
/// the animated object either freezes in place or starts again from the beginning according to whether it is
/// looping or not.
/// <para></para>
/// Animancer Lite does not allow this value to be changed in a runtime build (except resetting it to 0).
/// </summary>
///
/// <example>
/// <code>
/// void PlayAnimation(AnimancerComponent animancer, AnimationClip clip)
/// {
/// var state = animancer.Play(clip);
///
/// // Skip 0.5 seconds into the animation:
/// state.Time = 0.5f;
///
/// // Skip 50% of the way through the animation (0.5 in a range of 0 to 1):
/// state.NormalizedTime = 0.5f;
///
/// // Skip to the end of the animation and play backwards.
/// state.NormalizedTime = 1;
/// state.Speed = -1;
/// }
/// </code>
/// </example>
///
/// <remarks>
/// This property internally uses <see cref="NewTime"/> whenever the value is out of date or gets changed.
/// </remarks>
public float Time
{
get
{
var frameID = Root.FrameID;
if (_TimeFrameID != frameID)
{
_TimeFrameID = frameID;
_Time = NewTime;
}
return _Time;
}
set
{
if (_TimeFrameID == Root.FrameID)
{
if (_Time == value)
return;
}
else
{
_TimeFrameID = Root.FrameID;
}
Debug.Assert(!float.IsNaN(value), "Time must not be NaN");
_Time = value;
NewTime = value;
}
}
/************************************************************************************************************************/
/// <summary>
/// The internal implementation of <see cref="Time"/> which actually gets and sets the underlying value.
/// </summary>
/// <remarks>
/// Setting this value actually calls <see cref="PlayableExtensions.SetTime"/> twice to ensure that animation
/// events aren't triggered incorrectly. Calling it only once would trigger any animation events between the
/// previous time and the new time. So if an animation plays to the end and you set the time back to 0 (such as
/// by calling <see cref="Stop"/> or playing a different animation), the next time that animation played it
/// would immediately trigger all of its events, then play through and trigger them normally as well.
/// </remarks>
protected virtual float NewTime
{
get { return (float)_Playable.GetTime(); }
set
{
var time = (double)value;
_Playable.SetTime(time);
_Playable.SetTime(time);
if (_EventUpdatable != null)
_EventUpdatable.OnTimeChanged();
}
}
/************************************************************************************************************************/
/// <summary>
/// The <see cref="Time"/> of this state as a portion of the animation's <see cref="Length"/>, meaning the
/// value goes from 0 to 1 as it plays from start to end, regardless of how long that actually takes.
/// <para></para>
/// This value will continue increasing after the animation passes the end of its <see cref="Length"/> while
/// the animated object either freezes in place or starts again from the beginning according to whether it is
/// looping or not.
/// <para></para>
/// The fractional part of the value (<c>NormalizedTime % 1</c>) is the percentage (0-1) of progress in the
/// current loop while the integer part (<c>(int)NormalizedTime</c>) is the number of times the animation has
/// been looped.
/// <para></para>
/// Animancer Lite does not allow this value to be changed to a value other than 0 in a runtime build.
/// </summary>
///
/// <example>
/// <code>
/// void PlayAnimation(AnimancerComponent animancer, AnimationClip clip)
/// {
/// var state = animancer.Play(clip);
///
/// // Skip 0.5 seconds into the animation:
/// state.Time = 0.5f;
///
/// // Skip 50% of the way through the animation (0.5 in a range of 0 to 1):
/// state.NormalizedTime = 0.5f;
///
/// // Skip to the end of the animation and play backwards.
/// state.NormalizedTime = 1;
/// state.Speed = -1;
/// }
/// </code>
/// </example>
public float NormalizedTime
{
get
{
var length = Length;
if (length != 0)
return Time / Length;
else
return 0;
}
set { Time = value * Length; }
}
/************************************************************************************************************************/
// Duration.
/************************************************************************************************************************/
/// <summary>
/// The number of seconds the animation will take to play fully at its current
/// <see cref="AnimancerNode.Speed"/>.
/// <para></para>
/// Setting this value modifies the <see cref="AnimancerNode.Speed"/>, not the <see cref="Length"/>.
/// Animancer Lite does not allow this value to be changed in a runtime build.
/// <para></para>
/// For the time remaining from now until it reaches the end, use <see cref="RemainingDuration"/> instead.
/// </summary>
///
/// <example>
/// <code>
/// void PlayAnimation(AnimancerComponent animancer, AnimationClip clip)
/// {
/// var state = animancer.Play(clip);
///
/// state.Duration = 1;// Play fully in 1 second.
/// state.Duration = 2;// Play fully in 2 seconds.
/// state.Duration = 0.5f;// Play fully in half a second.
/// state.Duration = -1;// Play backwards fully in 1 second.
/// state.NormalizedTime = 1; state.Duration = -1;// Play backwards from the end in 1 second.
/// }
/// </code>
/// </example>
public float Duration
{
get
{
var speed = Speed;
if (speed == 0)
return float.PositiveInfinity;
else
return Length / Math.Abs(speed);
}
set
{
if (value == 0)
Speed = float.PositiveInfinity;
else
Speed = Length / value;
}
}
/// <summary>
/// The number of seconds the animation will take to reach the end at its current <see cref="AnimancerNode.Speed"/>.
/// <para></para>
/// Setting this value modifies the <see cref="AnimancerNode.Speed"/>, not the <see cref="Length"/>.
/// Animancer Lite does not allow this value to be changed in a runtime build.
/// <para></para>
/// For the time it would take to play fully from the start, use <see cref="Duration"/> instead.
/// </summary>
///
/// <example>
/// <code>
/// void PlayAnimation(AnimancerComponent animancer, AnimationClip clip)
/// {
/// var state = animancer.Play(clip);
///
/// state.RemainingDuration = 1;// Play from the current time to the end in 1 second.
/// state.RemainingDuration = 2;// Play from the current time to the end in 2 seconds.
/// state.RemainingDuration = 0.5f;// Play from the current time to the end in half a second.
/// state.RemainingDuration = -1;// Play backwards from the current time to the end in 1 second.
/// }
/// </code>
/// </example>
public float RemainingDuration
{
get
{
var speed = Speed;
if (speed == 0)
return float.PositiveInfinity;
var length = Length;
if (_EventUpdatable != null)
{
if (speed > 0)
length *= _EventUpdatable.Events.NormalizedEndTime;
else
length *= 1 - _EventUpdatable.Events.NormalizedEndTime;
}
var time = Time;
if (IsLooping)
time = Mathf.Repeat(time, length);
return (length - time) / Math.Abs(speed);
}
set
{
if (value == 0)
throw new ArgumentException("Duration cannot be set to 0 because that would require infinite speed.");
var length = Length;
if (_EventUpdatable != null)
{
if (value > 0)
length *= _EventUpdatable.Events.NormalizedEndTime;
else
length *= 1 - _EventUpdatable.Events.NormalizedEndTime;
}
var time = Time;
if (IsLooping)
time = Mathf.Repeat(time, length);
Speed = (length - time) / value;
}
}
/************************************************************************************************************************/
// Length.
/************************************************************************************************************************/
/// <summary>The total time this state takes to play in seconds (when <c>Speed = 1</c>).</summary>
public abstract float Length { get; }
/// <summary>
/// Indicates whether this state will loop back to the start when it reaches the end.
/// </summary>
public virtual bool IsLooping { get { return false; } }
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Inverse Kinematics
/************************************************************************************************************************/
/// <summary>
/// Determines whether <c>OnAnimatorIK(int layerIndex)</c> will be called on the animated object.
/// The initial value is determined by <see cref="AnimancerLayer.DefaultApplyAnimatorIK"/>.
/// <para></para>
/// This is equivalent to the "IK Pass" toggle in Animator Controller layers, except that due to limitations in
/// the Playables API the <c>layerIndex</c> will always be zero.
/// <para></para>
/// It requires Unity 2018.1 or newer, however 2018.3 or newer is recommended because a bug in earlier versions
/// of the Playables API caused this value to only take effect while a state was at
/// <see cref="AnimancerNode.Weight"/> == 1 which meant that IK would not work while fading between animations.
/// <para></para>
/// Returns false and does nothing if this state does not support IK.
/// </summary>
public virtual bool ApplyAnimatorIK
{
get { return false; }
set { }
}
/// <summary>
/// Indicates whether this state is applying IK to the character's feet.
/// The initial value is determined by <see cref="AnimancerLayer.DefaultApplyFootIK"/>.
/// <para></para>
/// This is equivalent to the "Foot IK" toggle in Animator Controller states.
/// <para></para>
/// Returns false and does nothing if this state does not support IK.
/// </summary>
public virtual bool ApplyFootIK
{
get { return false; }
set { }
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Methods
/************************************************************************************************************************/
/// <summary>Constructs a new <see cref="AnimancerState"/>.</summary>
public AnimancerState(AnimancerPlayable root) : base(root)
{
IsPlaying = false;
}
/************************************************************************************************************************/
/// <summary>
/// Plays this animation immediately, without any blending.
/// Sets <see cref="IsPlaying"/> = true, <see cref="AnimancerNode.Weight"/> = 1, and clears the
/// <see cref="Events"/>.
/// <para></para>
/// This method does not change the <see cref="Time"/> so it will continue from its current value.
/// </summary>
public void Play()
{
IsPlaying = true;
Weight = 1;
EventUpdatable.TryClear(_EventUpdatable);
}
/************************************************************************************************************************/
/// <summary>
/// Stops the animation and makes it inactive immediately so it no longer affects the output.
/// Sets <see cref="AnimancerNode.Weight"/> = 0, <see cref="IsPlaying"/> = false, <see cref="Time"/> = 0, and
/// clears the <see cref="Events"/>.
/// <para></para>
/// If you only want to freeze the animation in place, you can set <see cref="IsPlaying"/> = false instead. Or
/// to freeze all animations, you can call <see cref="AnimancerPlayable.PauseGraph"/>.
/// </summary>
public override void Stop()
{
base.Stop();
IsPlaying = false;
Time = 0;
EventUpdatable.TryClear(_EventUpdatable);
}
/************************************************************************************************************************/
/// <summary>
/// Called by <see cref="AnimancerNode.StartFade"/>. Clears the <see cref="Events"/>.
/// </summary>
protected internal override void OnStartFade()
{
EventUpdatable.TryClear(_EventUpdatable);
}
/************************************************************************************************************************/
/// <summary>Destroys the <see cref="Playable"/>.</summary>
public virtual void Destroy()
{
GC.SuppressFinalize(this);
if (_Parent != null)
_Parent.OnRemoveChild(this);
Index = -1;
EventUpdatable.TryClear(_EventUpdatable);
Root.States.Unregister(this);
// For some reason this is slightly faster than _Playable.Destroy().
if (_Playable.IsValid())
Root._Graph.DestroyPlayable(_Playable);
}
/************************************************************************************************************************/
/// <summary>[<see cref="IAnimationClipCollection"/>]
/// Gathers all the animations in this state.
/// </summary>
public virtual void GatherAnimationClips(ICollection<AnimationClip> clips)
{
clips.Gather(Clip);
}
/************************************************************************************************************************/
/// <summary>
/// Returns true if the animation is playing and has not yet passed the
/// <see cref="AnimancerEvent.Sequence.endEvent"/>.
/// <para></para>
/// This method is called by <see cref="IEnumerator.MoveNext"/> so this object can be used as a custom yield
/// instruction to wait until it finishes.
/// </summary>
protected internal override bool IsPlayingAndNotEnding()
{
if (!IsPlaying)
return false;
var speed = EffectiveSpeed;
if (speed > 0)
{
float endTime;
if (_EventUpdatable != null)
{
endTime = _EventUpdatable.Events.endEvent.normalizedTime;
if (float.IsNaN(endTime))
endTime = Length;
else
endTime *= Length;
}
else endTime = Length;
return Time <= endTime;
}
else if (speed < 0)
{
float endTime;
if (_EventUpdatable != null)
{
endTime = _EventUpdatable.Events.endEvent.normalizedTime;
if (float.IsNaN(endTime))
endTime = 0;
else
endTime *= Length;
}
else endTime = 0;
return Time >= endTime;
}
else return true;
}
/************************************************************************************************************************/
#region Descriptions
/************************************************************************************************************************/
#if UNITY_EDITOR
/// <summary>[Editor-Only] Returns a custom drawer for this state.</summary>
protected internal virtual Editor.IAnimancerNodeDrawer GetDrawer()
{
return new Editor.AnimancerStateDrawer<AnimancerState>(this);
}
#endif
/************************************************************************************************************************/
/// <summary>
/// Called by <see cref="AnimancerNode.AppendDescription"/> to append the details of this node.
/// </summary>
protected override void AppendDetails(StringBuilder text, string delimiter)
{
base.AppendDetails(text, delimiter);
text.Append(delimiter).Append("IsPlaying: ").Append(IsPlaying);
text.Append(delimiter).Append("Time (Normalized): ").Append(Time);
text.Append(" (").Append(NormalizedTime).Append(')');
text.Append(delimiter).Append("Length: ").Append(Length);
text.Append(delimiter).Append("IsLooping: ").Append(IsLooping);
if (_Key != null)
text.Append(delimiter).Append("Key: ").Append(_Key);
if (_EventUpdatable != null && _EventUpdatable.Events != null)
_EventUpdatable.Events.endEvent.AppendDetails(text, "EndEvent", delimiter);
var clip = Clip;
if (clip != null)
{
#if UNITY_EDITOR
text.Append(delimiter).Append("AssetPath: ").Append(AssetDatabase.GetAssetPath(clip));
#endif
}
}
/************************************************************************************************************************/
/// <summary>Returns the hierarchy path of this state through its <see cref="Parent"/>s.</summary>
public string GetPath()
{
if (_Parent == null)
return null;
var path = new StringBuilder();
AppendPath(path, _Parent);
AppendPortAndType(path);
return path.ToString();
}
/// <summary>Appends the hierarchy path of this state through its <see cref="Parent"/>s.</summary>
private static void AppendPath(StringBuilder path, AnimancerNode parent)
{
var parentState = parent as AnimancerState;
if (parentState != null && parentState._Parent != null)
{
AppendPath(path, parentState._Parent);
}
else
{
path.Append("Layers[")
.Append(parent.Layer.Index)
.Append("].States");
return;
}
var state = parent as AnimancerState;
if (state != null)
{
state.AppendPortAndType(path);
}
else
{
path.Append(" -> ")
.Append(parent.GetType());
}
}
/// <summary>Appends "[Index] -> GetType().Name".</summary>
private void AppendPortAndType(StringBuilder path)
{
path.Append('[')
.Append(Index)
.Append("] -> ")
.Append(GetType().Name);
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Transition
/************************************************************************************************************************/
/// <summary>
/// Base class for serializable <see cref="ITransition"/>s which can create a particular type of
/// <see cref="AnimancerState"/> when passed into <see cref="AnimancerPlayable.Play(ITransition)"/>.
/// </summary>
/// <remarks>
/// Unfortunately the tool used to generate this documentation does not currently support nested types with
/// identical names, so only one <c>Transition</c> class will actually have a documentation page.
/// <para></para>
/// Even though it has the <see cref="SerializableAttribute"/>, this class won't actually get serialized
/// by Unity because it's generic and abstract. Each child class still needs to include the attribute.
/// </remarks>
[Serializable]
public abstract class Transition<TState> : ITransitionDetailed where TState : AnimancerState
{
/************************************************************************************************************************/
[SerializeField, Tooltip(Strings.ProOnlyTag + "The amount of time the transition will take (in seconds)")]
private float _FadeDuration = AnimancerPlayable.DefaultFadeDuration;
/// <summary>[<see cref="SerializeField"/>] The amount of time the transition will take (in seconds).</summary>
/// <exception cref="ArgumentOutOfRangeException">Thrown when setting the value to a negative number.</exception>
public float FadeDuration
{
get { return _FadeDuration; }
set
{
if (value < 0)
throw new ArgumentOutOfRangeException("value", "must not be negative");
_FadeDuration = value;
}
}
/************************************************************************************************************************/
/// <summary>[<see cref="ITransitionDetailed"/>]
/// Indicates what the value of <see cref="AnimancerState.IsLooping"/> will be for the created state.
/// Returns false unless overridden.
/// </summary>
public virtual bool IsLooping { get { return false; } }
/// <summary>[<see cref="ITransitionDetailed"/>]
/// Determines what <see cref="NormalizedTime"/> to start the animation at.
/// Returns <see cref="float.NaN"/> unless overridden.
/// </summary>
public virtual float NormalizedStartTime { get { return float.NaN; } set { } }
/// <summary>[<see cref="ITransitionDetailed"/>]
/// Determines how fast the animation plays (1x = normal speed).
/// Returns 1 unless overridden.
/// </summary>
public virtual float Speed { get { return 1; } set { } }
/// <summary>[<see cref="ITransitionDetailed"/>]
/// The maximum amount of time the animation is expected to take (in seconds).
/// </summary>
public abstract float MaximumDuration { get; }
/************************************************************************************************************************/
[SerializeField, Tooltip(Strings.ProOnlyTag + "Events which will be triggered as the animation plays")]
private AnimancerEvent.Sequence.Serializable _Events;
/// <summary>[<see cref="SerializeField"/>] Events which will be triggered as the animation plays.</summary>
public AnimancerEvent.Sequence.Serializable Events
{
get { return _Events; }
set { _Events = value; }
}
/************************************************************************************************************************/
/// <summary>
/// The state that was created by this object. Specifically, this is the state that was most recently
/// passed into <see cref="Apply"/> (usually by <see cref="AnimancerPlayable.Play(ITransition)"/>).
/// <para></para>
/// You can use <see cref="AnimancerPlayable.StateDictionary.GetOrCreate(ITransition)"/> or
/// <see cref="AnimancerLayer.GetOrCreateState(ITransition)"/> to get or create the state for a
/// specific object.
/// <para></para>
/// <see cref="State"/> is simply a shorthand for casting this to <typeparamref name="TState"/>.
/// </summary>
public AnimancerState BaseState { get; private set; }
/************************************************************************************************************************/
private TState _State;
/// <summary>
/// The state that was created by this object. Specifically, this is the state that was most recently
/// passed into <see cref="Apply"/> (usually by <see cref="AnimancerPlayable.Play(ITransition)"/>).
/// <para></para>
/// You can use <see cref="AnimancerPlayable.StateDictionary.GetOrCreate(ITransition)"/> or
/// <see cref="AnimancerLayer.GetOrCreateState(ITransition)"/> to get or create the state for a
/// specific object.
/// <para></para>
/// This property is shorthand for casting the <see cref="BaseState"/> to <typeparamref name="TState"/>.
/// </summary>
/// <exception cref="InvalidCastException">
/// Thrown if the <see cref="BaseState"/> is not actually a <typeparamref name="TState"/>. This should only
/// happen if a different type of state was created by something else and registered using the
/// <see cref="Key"/>, causing this <see cref="AnimancerPlayable.Play(ITransition)"/> to pass that
/// state into <see cref="Apply"/> instead of calling <see cref="CreateState"/> to make the correct type of
/// state.
/// </exception>
public TState State
{
get
{
if (_State == null)
_State = (TState)BaseState;
return _State;
}
protected set
{
BaseState = _State = value;
}
}
/************************************************************************************************************************/
/// <summary>
/// The <see cref="AnimancerState.Key"/> which the created state will be registered with.
/// <para></para>
/// By default, a transition is used as its own <see cref="Key"/>, but this property can be overridden.
/// </summary>
public virtual object Key { get { return this; } }
/// <summary>
/// When a transition is passed into <see cref="AnimancerPlayable.Play(ITransition)"/>, this property
/// determines which <see cref="Animancer.FadeMode"/> will be used.
/// </summary>
public virtual FadeMode FadeMode { get { return FadeMode.FixedSpeed; } }
/// <summary>
/// Creates and returns a new <typeparamref name="TState"/> connected to the `layer`.
/// </summary>
public abstract TState CreateState(AnimancerLayer layer);
/// <summary>
/// Creates and returns a new <typeparamref name="TState"/> connected to the `layer`.
/// </summary>
AnimancerState ITransition.CreateState(AnimancerLayer layer)
{
return CreateState(layer);
}
/************************************************************************************************************************/
/// <summary>[<see cref="ITransition"/>]
/// Called by <see cref="AnimancerPlayable.Play(ITransition)"/> to set the <see cref="BaseState"/>
/// and apply any other modifications to the `state`.
/// </summary>
/// <remarks>
/// This method also clears the <see cref="State"/> if necessary, so it will re-cast the
/// <see cref="BaseState"/> when it gets accessed again.
/// </remarks>
public virtual void Apply(AnimancerState state)
{
state.Events = _Events;
BaseState = state;
if (_State != state)
_State = null;
}
/************************************************************************************************************************/
#if UNITY_EDITOR
/************************************************************************************************************************/
/// <summary>[Editor-Only] Don't use Inspector Gadgets Nested Object Drawers.</summary>
private const bool NestedObjectDrawers = false;
/************************************************************************************************************************/
/// <summary>[Editor-Only] Adds context menu functions for this transition.</summary>
void ITransitionDetailed.AddItemsToContextMenu(GenericMenu menu, SerializedProperty property,
Editor.Serialization.PropertyAccessor accessor)
{
AddItemsToContextMenu(menu, property, accessor);
}
/// <summary>[Editor-Only] Adds context menu functions for this transition.</summary>
protected virtual void AddItemsToContextMenu(GenericMenu menu, SerializedProperty property,
Editor.Serialization.PropertyAccessor accessor)
{
var transition = (Transition<TState>)accessor.GetValue(property);
const string EventsPrefix = "Transition Event Details/";
int timeCount, callbackCount;
AnimancerEvent.Sequence.Serializable.GetDetails(transition._Events, out timeCount, out callbackCount);
menu.AddDisabledItem(new GUIContent(EventsPrefix + "Serialized Time Count: " + timeCount));
menu.AddDisabledItem(new GUIContent(EventsPrefix + "Serialized Callback Count: " + callbackCount));
Editor.Serialization.AddPropertyModifierFunction(menu, property, "Reset Transition", Reset);
}
/************************************************************************************************************************/
private static void Reset(SerializedProperty property)
{
var transition = Editor.Serialization.GetValue(property);
if (transition == null)
return;
const System.Reflection.BindingFlags Bindings =
System.Reflection.BindingFlags.Public |
System.Reflection.BindingFlags.NonPublic |
System.Reflection.BindingFlags.Instance;
var type = transition.GetType();
var constructor = type.GetConstructor(Bindings, null, Type.EmptyTypes, null);
if (constructor == null)
{
Debug.LogError("Parameterless constructor not found in " + type);
return;
}
Editor.Serialization.RecordUndo(property);
constructor.Invoke(transition, null);
Editor.Serialization.OnPropertyChanged(property);
}
/************************************************************************************************************************/
#endif
/************************************************************************************************************************/
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
}
}