|
|
// 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 doesn’t 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
|
|
|
/************************************************************************************************************************/
|
|
|
}
|
|
|
}
|
|
|
|