// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2023 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 /// /// /// Solo Animation /// /// https://kybernetik.com.au/animancer/api/Animancer/SoloAnimation /// [AddComponentMenu(Strings.MenuPrefix + "Solo Animation")] [DefaultExecutionOrder(DefaultExecutionOrder)] [HelpURL(Strings.DocsURLs.APIDocumentation + "/" + nameof(SoloAnimation))] 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(); } } /************************************************************************************************************************/ /// /// If true, disabling this object will stop and rewind the animation. Otherwise it will simply be paused /// and will resume from its current state when it is re-enabled. /// /// /// The default value is true. /// /// This property wraps and inverts its value. /// The value is serialized by the . /// public bool StopOnDisable { #if UNITY_2022_2_OR_NEWER get => !_Animator.keepAnimatorStateOnDisable; set => _Animator.keepAnimatorStateOnDisable = !value; #else get => !_Animator.keepAnimatorControllerStateOnDisable; set => _Animator.keepAnimatorControllerStateOnDisable = !value; #endif } /************************************************************************************************************************/ /// 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(); } else { if (IsInitialized) _Graph.Stop(); } } } /************************************************************************************************************************/ [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; _Playable.SetSpeed(value); IsPlaying = value != 0; } } /************************************************************************************************************************/ [SerializeField, Tooltip("Determines whether Foot IK will be applied to the model (if it is Humanoid)")] private bool _FootIK; /// [] Should Foot IK will be applied to the model (if it is Humanoid)? /// /// The developers of Unity have stated that they believe it looks better with this enabled, but more often /// than not it just makes the legs end up in a slightly different pose to what the animator intended. /// /// This component is not yet . public bool FootIK { get => _FootIK; set { _FootIK = value; _Playable.SetApplyFootIK(value); } } /************************************************************************************************************************/ /// The number of seconds that have passed since the start of the animation. /// This component is not yet . public float Time { get => (float)_Playable.GetTime(); set { // We need to call SetTime twice to ensure that animation events aren't triggered incorrectly. _Playable.SetTime(value); _Playable.SetTime(value); IsPlaying = true; } } /// /// 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 is 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. /// /// This component is not yet . public float NormalizedTime { get => Time / _Clip.length; set => Time = value * _Clip.length; } /************************************************************************************************************************/ /// 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 (IsInitialized) { Speed = Speed; FootIK = FootIK; } else if (_ApplyInEditMode && enabled) { _Clip.EditModeSampleAnimation(_Animator); } } /************************************************************************************************************************/ #endif /************************************************************************************************************************/ /// Plays the . public void Play() => Play(_Clip); /// Plays the `clip`. public void Play(AnimationClip clip) { if (clip == null || _Animator == null) return; if (_Graph.IsValid()) _Graph.Destroy(); _Playable = AnimationPlayableUtilities.PlayClip(_Animator, clip, out _Graph); _Playable.SetSpeed(_Speed); if (!_FootIK) _Playable.SetApplyFootIK(false); if (!clip.isLooping) _Playable.SetDuration(clip.length); _IsPlaying = true; } /************************************************************************************************************************/ /// Plays the on the target . protected virtual void OnEnable() { IsPlaying = true; } /************************************************************************************************************************/ /// /// Checks if the animation is done so it can pause the to improve performance. /// protected virtual void Update() { if (!IsPlaying) return; if (_Graph.IsDone()) { IsPlaying = false; } else if (_Speed < 0 && Time <= 0) { IsPlaying = false; Time = 0; } } /************************************************************************************************************************/ /// Ensures that the is properly cleaned up. protected virtual void OnDisable() { IsPlaying = false; if (IsInitialized && StopOnDisable) { // Call SetTime twice to ensure that animation events aren't triggered incorrectly. _Playable.SetTime(0); _Playable.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); } /************************************************************************************************************************/ } } /************************************************************************************************************************/ #if UNITY_EDITOR /************************************************************************************************************************/ namespace Animancer.Editor { /// [Editor-Only] A custom Inspector for . /// https://kybernetik.com.au/animancer/api/Animancer.Editor/SoloAnimationEditor /// [UnityEditor.CustomEditor(typeof(SoloAnimation)), UnityEditor.CanEditMultipleObjects] public class SoloAnimationEditor : UnityEditor.Editor { /************************************************************************************************************************/ /// The animator referenced by each target. [NonSerialized] private Animator[] _Animators; /// A encapsulating the . [NonSerialized] private UnityEditor.SerializedObject _SerializedAnimator; /// The property. [NonSerialized] private UnityEditor.SerializedProperty _KeepStateOnDisable; /************************************************************************************************************************/ /// public override void OnInspectorGUI() { DoSerializedFieldsGUI(); RefreshSerializedAnimator(); DoStopOnDisableGUI(); DoRuntimeDetailsGUI(); } /************************************************************************************************************************/ /// Draws the target's serialized fields. private void DoSerializedFieldsGUI() { serializedObject.Update(); var property = serializedObject.GetIterator(); property.NextVisible(true); if (property.name != "m_Script") UnityEditor.EditorGUILayout.PropertyField(property, true); while (property.NextVisible(false)) { UnityEditor.EditorGUILayout.PropertyField(property, true); } serializedObject.ApplyModifiedProperties(); } /************************************************************************************************************************/ /// Ensures that the cached references relating to the target's are correct. private void RefreshSerializedAnimator() { var targets = this.targets; AnimancerUtilities.SetLength(ref _Animators, targets.Length); var dirty = false; var hasAll = true; for (int i = 0; i < _Animators.Length; i++) { var animator = (targets[i] as SoloAnimation).Animator; if (_Animators[i] != animator) { _Animators[i] = animator; dirty = true; } if (animator == null) hasAll = false; } if (!dirty) return; OnDisable(); if (!hasAll) return; _SerializedAnimator = new UnityEditor.SerializedObject(_Animators); _KeepStateOnDisable = _SerializedAnimator.FindProperty("m_KeepAnimatorControllerStateOnDisable"); } /************************************************************************************************************************/ /// /// Draws a toggle inverted from the field. /// private void DoStopOnDisableGUI() { var area = AnimancerGUI.LayoutSingleLineRect(); using (ObjectPool.Disposable.AcquireContent(out var label, "Stop On Disable", "If true, disabling this object will stop and rewind all animations." + " Otherwise they will simply be paused and will resume from their current states when it is re-enabled.")) { if (_KeepStateOnDisable != null) { _KeepStateOnDisable.serializedObject.Update(); var content = UnityEditor.EditorGUI.BeginProperty(area, label, _KeepStateOnDisable); _KeepStateOnDisable.boolValue = !UnityEditor.EditorGUI.Toggle(area, content, !_KeepStateOnDisable.boolValue); UnityEditor.EditorGUI.EndProperty(); _KeepStateOnDisable.serializedObject.ApplyModifiedProperties(); } else { using (new UnityEditor.EditorGUI.DisabledScope(true)) UnityEditor.EditorGUI.Toggle(area, label, false); } } } /************************************************************************************************************************/ /// Draws the target's runtime details. private void DoRuntimeDetailsGUI() { if (!UnityEditor.EditorApplication.isPlaying || targets.Length != 1) return; AnimancerGUI.BeginVerticalBox(GUI.skin.box); var target = (SoloAnimation)this.target; if (!target.IsInitialized) { GUILayout.Label("Not Initialized"); } else { UnityEditor.EditorGUI.BeginChangeCheck(); var isPlaying = UnityEditor.EditorGUILayout.Toggle("Is Playing", target.IsPlaying); if (UnityEditor.EditorGUI.EndChangeCheck()) target.IsPlaying = isPlaying; UnityEditor.EditorGUI.BeginChangeCheck(); var time = UnityEditor.EditorGUILayout.FloatField("Time", target.Time); if (UnityEditor.EditorGUI.EndChangeCheck()) target.Time = time; time = AnimancerUtilities.Wrap01(target.NormalizedTime); if (time == 0 && target.Time != 0) time = 1; UnityEditor.EditorGUI.BeginChangeCheck(); time = UnityEditor.EditorGUILayout.Slider("Normalized Time", time, 0, 1); if (UnityEditor.EditorGUI.EndChangeCheck()) target.NormalizedTime = time; } AnimancerGUI.EndVerticalBox(GUI.skin.box); Repaint(); } /************************************************************************************************************************/ /// Cleans up cached references relating to the target's . protected virtual void OnDisable() { if (_SerializedAnimator != null) { _SerializedAnimator.Dispose(); _SerializedAnimator = null; _KeepStateOnDisable = null; } } /************************************************************************************************************************/ } } /************************************************************************************************************************/ #endif /************************************************************************************************************************/