// Animancer // Copyright 2020 Kybernetik // using System; using System.Collections.Generic; using UnityEngine; using UnityEngine.Animations; using UnityEngine.Playables; namespace Animancer { /// Plays a single on startup. [AddComponentMenu(Strings.MenuPrefix + "Solo Animation")] [HelpURL(Strings.APIDocumentationURL + "/SoloAnimation")] [DefaultExecutionOrder(-5000)]// Initialise before anything else tries to use this component. public sealed class SoloAnimation : MonoBehaviour, IAnimationClipSource { /************************************************************************************************************************/ #region Fields and Properties /************************************************************************************************************************/ [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 { return _Animator; } set { _Animator = value; Awake(); } } /************************************************************************************************************************/ [SerializeField, Tooltip("The AnimationClip which will be played by OnEnable")] private AnimationClip _Clip; /// [] /// The which will be played by . /// /// If you need to set this value at runtime you are likely better off using a proper /// . /// public AnimationClip Clip { get { return _Clip; } set { _Clip = value; Awake(); } } /************************************************************************************************************************/ #if UNITY_2018_1_OR_NEWER /// /// 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 . /// /// It requires Unity 2018.1 or newer. /// public bool StopOnDisable { get { return !_Animator.keepAnimatorStateOnDisable; } set { _Animator.keepAnimatorStateOnDisable = !value; } } #endif /************************************************************************************************************************/ /// /// The being used to play the . /// private PlayableGraph _Graph; /// /// The being used to play the . /// private AnimationClipPlayable _Playable; /************************************************************************************************************************/ private bool _IsPlaying; /// /// Indicates whether the animation is playing (true) or paused (false). /// public bool IsPlaying { get { return _IsPlaying; } set { _IsPlaying = value; if (!IsInitialised) return; if (value) _Graph.Play(); else _Graph.Stop(); } } /************************************************************************************************************************/ [SerializeField, Tooltip("The speed at which the animation plays (default 1)")] private float _Speed = 1; /// [] /// The speed at which the animation is playing (default 1). /// /// Thrown if this component is not yet . public float Speed { get { return _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; /// [] /// Determines whether Foot IK will be applied to the model (if it is Humanoid). /// /// The developers of Unity have states 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. /// /// Thrown if this component is not yet . public bool FootIK { get { return _FootIK; } set { _FootIK = value; _Playable.SetApplyFootIK(value); } } /************************************************************************************************************************/ /// /// The number of seconds that have passed since the start of the animation. /// /// Thrown if this component is not yet . public float Time { get { return (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. /// /// Thrown if this component is not yet . public float NormalizedTime { get { return Time / _Clip.length; } set { Time = value * _Clip.length; } } /************************************************************************************************************************/ /// Indicates whether the is valid. public bool IsInitialised { get { return _Graph.IsValid(); } } /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ #if UNITY_EDITOR /************************************************************************************************************************/ /// [Editor-Only] /// 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. /// /// Tries to find an component on this or its /// children or parents (in that order). /// private void Reset() { _Animator = Editor.AnimancerEditorUtilities.GetComponentInHierarchy(gameObject); } /************************************************************************************************************************/ /// [Editor-Only] /// Called by the Unity Editor in Edit Mode whenever an instance of this script is loaded or a value is changed /// in the Inspector. /// /// Tries to find an component on this or its /// parents or children (in that order). /// private void OnValidate() { if (IsInitialised) { Speed = Speed; FootIK = FootIK; } } /************************************************************************************************************************/ #endif /************************************************************************************************************************/ /// /// Called by Unity when this component is first created. /// /// Initialises everything needed to play the . /// private void Awake() { 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); } /************************************************************************************************************************/ /// /// Called by Unity when this component becomes enabled and active. /// /// Plays the on the target . /// private void OnEnable() { IsPlaying = true; } /************************************************************************************************************************/ /// /// Called by Unity every frame while this component is enabled and active. /// /// Checks if the animation is done so it can pause the to improve performance. /// private void Update() { if (!IsPlaying) return; if (_Graph.IsDone()) { IsPlaying = false; } else if (_Speed < 0 && Time <= 0) { IsPlaying = false; Time = 0; } } /************************************************************************************************************************/ /// /// Called by Unity when this component becomes disabled or inactive. /// /// Ensures that the is properly cleaned up. /// private void OnDisable() { IsPlaying = false; #if UNITY_2018_1_OR_NEWER if (_Animator.keepAnimatorStateOnDisable) return; #endif if (IsInitialised) { // We need to call SetTime twice to ensure that animation events aren't triggered incorrectly. _Playable.SetTime(0); _Playable.SetTime(0); } } /************************************************************************************************************************/ /// /// Called by Unity when this component is destroyed. /// /// Ensures that the is properly cleaned up. /// private void OnDestroy() { if (IsInitialised) _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 { [UnityEditor.CustomEditor(typeof(SoloAnimation)), UnityEditor.CanEditMultipleObjects] internal sealed class SoloAnimationEditor : UnityEditor.Editor { /************************************************************************************************************************/ /// The animator referenced by each target. private Animator[] _Animators; /// A encapsulating the . private UnityEditor.SerializedObject _SerializedAnimator; #if UNITY_2018_1_OR_NEWER /// The property. private UnityEditor.SerializedProperty _KeepStateOnDisable; #endif /************************************************************************************************************************/ 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(); } /************************************************************************************************************************/ private void RefreshSerializedAnimator() { var targets = this.targets; if (_Animators == null || _Animators.Length != targets.Length) _Animators = new Animator[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); #if UNITY_2018_1_OR_NEWER _KeepStateOnDisable = _SerializedAnimator.FindProperty("m_KeepAnimatorControllerStateOnDisable"); #endif } /************************************************************************************************************************/ /// /// Draws a toggle inverted from the field. /// private void DoStopOnDisableGUI() { #if UNITY_2018_1_OR_NEWER var area = AnimancerGUI.LayoutSingleLineRect(); var label = AnimancerGUI.TempContent("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(); label = UnityEditor.EditorGUI.BeginProperty(area, label, _KeepStateOnDisable); _KeepStateOnDisable.boolValue = !UnityEditor.EditorGUI.Toggle(area, label, !_KeepStateOnDisable.boolValue); UnityEditor.EditorGUI.EndProperty(); _KeepStateOnDisable.serializedObject.ApplyModifiedProperties(); } else { var enabled = GUI.enabled; GUI.enabled = false; UnityEditor.EditorGUI.Toggle(area, label, false); GUI.enabled = enabled; } #endif } /************************************************************************************************************************/ /// Draws the target's runtime details. private void DoRuntimeDetailsGUI() { if (!UnityEditor.EditorApplication.isPlayingOrWillChangePlaymode || targets.Length != 1) return; AnimancerGUI.BeginVerticalBox(GUI.skin.box); var target = (SoloAnimation)this.target; if (!target.IsInitialised) { GUILayout.Label("Not Initialised"); } 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 = target.NormalizedTime.Wrap01(); 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(); } /************************************************************************************************************************/ private void OnDisable() { if (_SerializedAnimator != null) { _SerializedAnimator.Dispose(); _SerializedAnimator = null; #if UNITY_2018_1_OR_NEWER _KeepStateOnDisable = null; #endif } } /************************************************************************************************************************/ } } /************************************************************************************************************************/ #endif /************************************************************************************************************************/