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