464 lines
17 KiB
C#
464 lines
17 KiB
C#
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 Kybernetik //
|
|
|
|
using Animancer.Units;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using UnityEngine;
|
|
using UnityEngine.Animations;
|
|
using UnityEngine.Playables;
|
|
|
|
namespace Animancer
|
|
{
|
|
/// <summary>Plays a single <see cref="AnimationClip"/>.</summary>
|
|
///
|
|
/// <remarks>
|
|
/// <para></para>
|
|
/// <strong>Documentation:</strong>
|
|
/// <see href="https://kybernetik.com.au/animancer/docs/manual/playing/component-types">
|
|
/// Component Types</see>
|
|
/// <para></para>
|
|
/// <strong>Sample:</strong>
|
|
/// <see href="https://kybernetik.com.au/animancer/docs/samples/fine-control/doors">Doors</see>
|
|
/// </remarks>
|
|
///
|
|
/// https://kybernetik.com.au/animancer/api/Animancer/SoloAnimation
|
|
///
|
|
[AddComponentMenu(Strings.MenuPrefix + "Solo Animation")]
|
|
[AnimancerHelpUrl(typeof(SoloAnimation))]
|
|
[DefaultExecutionOrder(DefaultExecutionOrder)]
|
|
public class SoloAnimation : MonoBehaviour, IAnimationClipSource
|
|
{
|
|
/************************************************************************************************************************/
|
|
#region Fields and Properties
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>Initialize before anything else tries to use this component.</summary>
|
|
public const int DefaultExecutionOrder = -5000;
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
[SerializeField, Tooltip("The Animator component which this script controls")]
|
|
private Animator _Animator;
|
|
|
|
/// <summary>[<see cref="SerializeField"/>]
|
|
/// The <see cref="UnityEngine.Animator"/> component which this script controls.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// If you need to set this value at runtime you are likely better off using a proper
|
|
/// <see cref="AnimancerComponent"/>.
|
|
/// </remarks>
|
|
public Animator Animator
|
|
{
|
|
get => _Animator;
|
|
set
|
|
{
|
|
_Animator = value;
|
|
if (IsInitialized)
|
|
Play();
|
|
}
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
[SerializeField, Tooltip("The animation that will be played")]
|
|
private AnimationClip _Clip;
|
|
|
|
/// <summary>[<see cref="SerializeField"/>] The <see cref="AnimationClip"/> that will be played.</summary>
|
|
/// <remarks>
|
|
/// If you need to set this value at runtime you are likely better off using a proper
|
|
/// <see cref="AnimancerComponent"/>.
|
|
/// </remarks>
|
|
public AnimationClip Clip
|
|
{
|
|
get => _Clip;
|
|
set
|
|
{
|
|
_Clip = value;
|
|
if (IsInitialized)
|
|
Play();
|
|
}
|
|
}
|
|
|
|
/// <summary><see cref="AnimationClip.length"/></summary>
|
|
/// <remarks>
|
|
/// This value is cached on startup
|
|
/// and is <see cref="float.NaN"/> if there's no <see cref="Clip"/>.
|
|
/// </remarks>
|
|
public float Length { get; private set; } = float.NaN;
|
|
|
|
/// <summary><see cref="Motion.isLooping"/></summary>
|
|
/// <remarks>This value is cached on startup.</remarks>
|
|
public bool IsLooping { get; private set; }
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>
|
|
/// Should disabling this object stop and rewind the animation?
|
|
/// Otherwise, it will simply be paused and will resume from its current state when re-enabled.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// The default value is true.
|
|
/// <para></para>
|
|
/// This property inverts <see cref="Animator.keepAnimatorStateOnDisable"/>
|
|
/// and is serialized by the <see cref="UnityEngine.Animator"/>.
|
|
/// </remarks>
|
|
public bool StopOnDisable
|
|
{
|
|
get => !_Animator.keepAnimatorStateOnDisable;
|
|
set => _Animator.keepAnimatorStateOnDisable = !value;
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>The <see cref="PlayableGraph"/> being used to play the <see cref="Clip"/>.</summary>
|
|
private PlayableGraph _Graph;
|
|
|
|
/// <summary>The <see cref="AnimationClipPlayable"/> being used to play the <see cref="Clip"/>.</summary>
|
|
private AnimationClipPlayable _Playable;
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
private bool _IsPlaying;
|
|
|
|
/// <summary>Is the animation playing (true) or paused (false)?</summary>
|
|
public bool IsPlaying
|
|
{
|
|
get => _IsPlaying;
|
|
set
|
|
{
|
|
_IsPlaying = value;
|
|
|
|
if (value)
|
|
{
|
|
if (!IsInitialized)
|
|
{
|
|
Play();
|
|
}
|
|
else
|
|
{
|
|
_Graph.Play();
|
|
|
|
#if UNITY_EDITOR
|
|
// In Edit Mode, unpausing the graph doesn't work properly unless we force it to change.
|
|
if (!UnityEditor.EditorApplication.isPlayingOrWillChangePlaymode)
|
|
_Graph.Evaluate(0.00001f);
|
|
#endif
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (IsInitialized)
|
|
_Graph.Stop();
|
|
}
|
|
}
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
[SerializeField, Range(0, 1)]
|
|
[Tooltip("The normalized time that the animation will start at")]
|
|
private float _NormalizedStartTime;
|
|
|
|
/// <summary>[<see cref="SerializeField"/>] The normalized time that the animation will start at.</summary>
|
|
public float NormalizedStartTime
|
|
{
|
|
get => _NormalizedStartTime;
|
|
set => _NormalizedStartTime = value;
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>[<see cref="SerializeField"/>]
|
|
/// The number of seconds that have passed since the start of the animation.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// This value will continue increasing after the animation passes the end of its length
|
|
/// and it will either freeze in place or start again from the beginning according to
|
|
/// whether it's looping or not.
|
|
/// </remarks>
|
|
public float Time
|
|
{
|
|
get => _Playable.IsValid()
|
|
? (float)_Playable.GetTime()
|
|
: _NormalizedStartTime * Length;
|
|
set
|
|
{
|
|
if (_Playable.IsValid())
|
|
SetTime(value);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Calls <see cref="PlayableExtensions.SetTime{U}"/> twice
|
|
/// to ensure that animation events aren't triggered incorrectly.
|
|
/// </summary>
|
|
private void SetTime(double value)
|
|
{
|
|
_Playable.SetTime(value);
|
|
_Playable.SetTime(value);
|
|
}
|
|
|
|
/// <summary>[<see cref="SerializeField"/>]
|
|
/// The <see cref="Time"/> of this state as a portion of the <see cref="AnimationClip.length"/>,
|
|
/// meaning the value goes from 0 to 1 as it plays from start to end,
|
|
/// regardless of how long that actually takes.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// This value will continue increasing after the animation passes the end of its length
|
|
/// and it will either freeze in place or start again from the beginning according to
|
|
/// whether it's 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.
|
|
/// </remarks>
|
|
public float NormalizedTime
|
|
{
|
|
get => _Playable.IsValid()
|
|
? (float)_Playable.GetTime() / Length
|
|
: _NormalizedStartTime;
|
|
set
|
|
{
|
|
if (_Playable.IsValid())
|
|
SetTime(value * Length);
|
|
}
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
[SerializeField, Multiplier, Tooltip("The speed at which the animation plays (default 1)")]
|
|
private float _Speed = 1;
|
|
|
|
/// <summary>[<see cref="SerializeField"/>] The speed at which the animation is playing (default 1).</summary>
|
|
/// <exception cref="ArgumentException">This component is not yet <see cref="IsInitialized"/>.</exception>
|
|
public float Speed
|
|
{
|
|
get => _Speed;
|
|
set
|
|
{
|
|
_Speed = value;
|
|
if (_Playable.IsValid())
|
|
_Playable.SetSpeed(value);
|
|
}
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>Indicates whether the <see cref="PlayableGraph"/> is valid.</summary>
|
|
public bool IsInitialized
|
|
=> _Graph.IsValid();
|
|
|
|
/************************************************************************************************************************/
|
|
#endregion
|
|
/************************************************************************************************************************/
|
|
|
|
#if UNITY_EDITOR
|
|
/************************************************************************************************************************/
|
|
|
|
[SerializeField, Tooltip("Should the " + nameof(Clip) + " be automatically applied to the object in Edit Mode?")]
|
|
private bool _ApplyInEditMode;
|
|
|
|
/// <summary>[Editor-Only] Should the <see cref="Clip"/> be automatically applied to the object in Edit Mode?</summary>
|
|
public ref bool ApplyInEditMode
|
|
=> ref _ApplyInEditMode;
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>[Editor-Only]
|
|
/// Tries to find an <see cref="UnityEngine.Animator"/> component on this <see cref="GameObject"/> or its
|
|
/// children or parents (in that order).
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Called by the Unity Editor when this component is first added (in Edit Mode) and whenever the Reset command
|
|
/// is executed from its context menu.
|
|
/// </remarks>
|
|
protected virtual void Reset()
|
|
{
|
|
gameObject.GetComponentInParentOrChildren(ref _Animator);
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>[Editor-Only]
|
|
/// Applies the <see cref="Speed"/>, <see cref="FootIK"/>, and <see cref="ApplyInEditMode"/>.
|
|
/// </summary>
|
|
/// <remarks>Called in Edit Mode whenever this script is loaded or a value is changed in the Inspector.</remarks>
|
|
protected virtual void OnValidate()
|
|
{
|
|
if (!UnityEditor.EditorApplication.isPlaying)
|
|
{
|
|
if (_ApplyInEditMode && enabled)
|
|
{
|
|
if (!IsInitialized)
|
|
{
|
|
Play();
|
|
IsPlaying = false;
|
|
_Graph.Evaluate();
|
|
}
|
|
|
|
if (NormalizedTime != _NormalizedStartTime)
|
|
{
|
|
NormalizedTime = _NormalizedStartTime;
|
|
_Graph.Evaluate();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (IsInitialized)
|
|
_Graph.Destroy();
|
|
}
|
|
}
|
|
|
|
if (IsInitialized)
|
|
Speed = Speed;
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
#endif
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>Plays the <see cref="Clip"/>.</summary>
|
|
public void Play()
|
|
=> Play(_Clip);
|
|
|
|
/// <summary>Plays the `clip`.</summary>
|
|
public void Play(AnimationClip clip)
|
|
{
|
|
if (clip == null)
|
|
{
|
|
Length = 0;
|
|
IsLooping = false;
|
|
return;
|
|
}
|
|
|
|
Length = clip.length;
|
|
IsLooping = clip.isLooping;
|
|
|
|
if (_Animator == null)
|
|
return;
|
|
|
|
if (_Graph.IsValid())
|
|
_Graph.Destroy();
|
|
|
|
_Playable = AnimationPlayableUtilities.PlayClip(_Animator, clip, out _Graph);
|
|
|
|
_Playable.SetSpeed(_Speed);
|
|
|
|
SetTime(_NormalizedStartTime * Length);
|
|
|
|
if (_Speed != 0)
|
|
{
|
|
_IsPlaying = true;
|
|
}
|
|
else
|
|
{
|
|
_IsPlaying = false;
|
|
_Graph.Stop();
|
|
}
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>Calls <see cref="PlayableGraph.Evaluate()"/>.</summary>
|
|
public void Evaluate()
|
|
{
|
|
if (_Graph.IsValid())
|
|
_Graph.Evaluate();
|
|
}
|
|
|
|
/// <summary>Calls <see cref="PlayableGraph.Evaluate(float)"/>.</summary>
|
|
public void Evaluate(float deltaTime)
|
|
{
|
|
if (_Graph.IsValid())
|
|
_Graph.Evaluate(deltaTime);
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>Plays the <see cref="Clip"/> on the target <see cref="Animator"/>.</summary>
|
|
protected virtual void OnEnable()
|
|
{
|
|
if (!_IsPlaying)
|
|
Play();
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>
|
|
/// Checks if the animation is done
|
|
/// so it can pause the <see cref="PlayableGraph"/> to improve performance.
|
|
/// </summary>
|
|
protected virtual void LateUpdate()
|
|
{
|
|
if (!IsPlaying ||
|
|
IsLooping ||
|
|
!_Playable.IsValid())
|
|
return;
|
|
|
|
var time = (float)_Playable.GetTime();
|
|
if (_Speed >= 0)
|
|
{
|
|
if (time >= Length)
|
|
{
|
|
IsPlaying = false;
|
|
Time = Length;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (time <= 0)
|
|
{
|
|
IsPlaying = false;
|
|
Time = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>Stops playing and rewinds if <see cref="StopOnDisable"/>.</summary>
|
|
protected virtual void OnDisable()
|
|
{
|
|
if (!IsInitialized)
|
|
return;
|
|
|
|
_IsPlaying = false;
|
|
_Graph.Stop();
|
|
|
|
if (StopOnDisable)
|
|
SetTime(0);
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>Ensures that the <see cref="PlayableGraph"/> is properly cleaned up.</summary>
|
|
protected virtual void OnDestroy()
|
|
{
|
|
if (IsInitialized)
|
|
_Graph.Destroy();
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
#if UNITY_EDITOR
|
|
/// <summary>[Editor-Only] Ensures that the <see cref="PlayableGraph"/> is destroyed.</summary>
|
|
~SoloAnimation()
|
|
{
|
|
UnityEditor.EditorApplication.delayCall += OnDestroy;
|
|
}
|
|
#endif
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>[<see cref="IAnimationClipSource"/>] Adds the <see cref="Clip"/> to the list.</summary>
|
|
public void GetAnimationClips(List<AnimationClip> clips)
|
|
{
|
|
if (_Clip != null)
|
|
clips.Add(_Clip);
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
}
|
|
}
|
|
|