// 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
{
/// Plays a single .
///
///
///
/// Documentation:
///
/// Component Types
///
/// Sample:
/// Doors
///
///
/// 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
/************************************************************************************************************************/
/// Initialize before anything else tries to use this component.
public const int DefaultExecutionOrder = -5000;
/************************************************************************************************************************/
[SerializeField, Tooltip("The Animator component which this script controls")]
private Animator _Animator;
/// []
/// The component which this script controls.
///
///
/// If you need to set this value at runtime you are likely better off using a proper
/// .
///
public Animator Animator
{
get => _Animator;
set
{
_Animator = value;
if (IsInitialized)
Play();
}
}
/************************************************************************************************************************/
[SerializeField, Tooltip("The animation that will be played")]
private AnimationClip _Clip;
/// [] The that will be played.
///
/// If you need to set this value at runtime you are likely better off using a proper
/// .
///
public AnimationClip Clip
{
get => _Clip;
set
{
_Clip = value;
if (IsInitialized)
Play();
}
}
///
///
/// This value is cached on startup
/// and is if there's no .
///
public float Length { get; private set; } = float.NaN;
///
/// This value is cached on startup.
public bool IsLooping { get; private set; }
/************************************************************************************************************************/
///
/// 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.
///
///
/// The default value is true.
///
/// This property inverts
/// and is serialized by the .
///
public bool StopOnDisable
{
get => !_Animator.keepAnimatorStateOnDisable;
set => _Animator.keepAnimatorStateOnDisable = !value;
}
/************************************************************************************************************************/
/// The being used to play the .
private PlayableGraph _Graph;
/// The being used to play the .
private AnimationClipPlayable _Playable;
/************************************************************************************************************************/
private bool _IsPlaying;
/// Is the animation playing (true) or paused (false)?
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;
/// [] The normalized time that the animation will start at.
public float NormalizedStartTime
{
get => _NormalizedStartTime;
set => _NormalizedStartTime = value;
}
/************************************************************************************************************************/
/// []
/// The number of seconds that have passed since the start of the animation.
///
///
/// 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.
///
public float Time
{
get => _Playable.IsValid()
? (float)_Playable.GetTime()
: _NormalizedStartTime * Length;
set
{
if (_Playable.IsValid())
SetTime(value);
}
}
///
/// Calls twice
/// to ensure that animation events aren't triggered incorrectly.
///
private void SetTime(double value)
{
_Playable.SetTime(value);
_Playable.SetTime(value);
}
/// []
/// The of this state as a portion of the ,
/// meaning the value goes from 0 to 1 as it plays from start to end,
/// regardless of how long that actually takes.
///
///
/// 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.
///
/// The fractional part of the value (NormalizedTime % 1) is the percentage (0-1)
/// of progress in the current loop while the integer part ((int)NormalizedTime)
/// is the number of times the animation has been looped.
///
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;
/// [] The speed at which the animation is playing (default 1).
/// This component is not yet .
public float Speed
{
get => _Speed;
set
{
_Speed = value;
if (_Playable.IsValid())
_Playable.SetSpeed(value);
}
}
/************************************************************************************************************************/
/// Indicates whether the is valid.
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;
/// [Editor-Only] Should the be automatically applied to the object in Edit Mode?
public ref bool ApplyInEditMode
=> ref _ApplyInEditMode;
/************************************************************************************************************************/
/// [Editor-Only]
/// Tries to find an component on this or its
/// children or parents (in that order).
///
///
/// 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.
///
protected virtual void Reset()
{
gameObject.GetComponentInParentOrChildren(ref _Animator);
}
/************************************************************************************************************************/
/// [Editor-Only]
/// Applies the , , and .
///
/// Called in Edit Mode whenever this script is loaded or a value is changed in the Inspector.
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
/************************************************************************************************************************/
/// Plays the .
public void Play()
=> Play(_Clip);
/// Plays the `clip`.
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();
}
}
/************************************************************************************************************************/
/// Calls .
public void Evaluate()
{
if (_Graph.IsValid())
_Graph.Evaluate();
}
/// Calls .
public void Evaluate(float deltaTime)
{
if (_Graph.IsValid())
_Graph.Evaluate(deltaTime);
}
/************************************************************************************************************************/
/// Plays the on the target .
protected virtual void OnEnable()
{
if (!_IsPlaying)
Play();
}
/************************************************************************************************************************/
///
/// Checks if the animation is done
/// so it can pause the to improve performance.
///
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;
}
}
}
/************************************************************************************************************************/
/// Stops playing and rewinds if .
protected virtual void OnDisable()
{
if (!IsInitialized)
return;
_IsPlaying = false;
_Graph.Stop();
if (StopOnDisable)
SetTime(0);
}
/************************************************************************************************************************/
/// Ensures that the is properly cleaned up.
protected virtual void OnDestroy()
{
if (IsInitialized)
_Graph.Destroy();
}
/************************************************************************************************************************/
#if UNITY_EDITOR
/// [Editor-Only] Ensures that the is destroyed.
~SoloAnimation()
{
UnityEditor.EditorApplication.delayCall += OnDestroy;
}
#endif
/************************************************************************************************************************/
/// [] Adds the to the list.
public void GetAnimationClips(List clips)
{
if (_Clip != null)
clips.Add(_Clip);
}
/************************************************************************************************************************/
}
}