2025-05-09 15:40:34 +08:00

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