// Animancer // Copyright 2020 Kybernetik // using System; using System.Collections.Generic; using System.Text; using UnityEngine; using UnityEngine.Animations; using UnityEngine.Playables; using Object = UnityEngine.Object; namespace Animancer { /// /// An which plays an . /// public sealed class ClipState : AnimancerState { /************************************************************************************************************************/ #region Fields and Properties /************************************************************************************************************************/ /// The which this state plays. private AnimationClip _Clip; /// The which this state plays. public override AnimationClip Clip { get { return _Clip; } set { if (ReferenceEquals(_Clip, value)) return; if (ReferenceEquals(_Key, _Clip)) Key = value; if (_Playable.IsValid()) Root._Graph.DestroyPlayable(_Playable); CreatePlayable(value); SetWeightDirty(); } } /// The which this state plays. public override Object MainObject { get { return _Clip; } set { Clip = (AnimationClip)value; } } /************************************************************************************************************************/ /// The . public override float Length { get { return _Clip.length; } } /************************************************************************************************************************/ /// The . public override bool IsLooping { get { return _Clip.isLooping; } } /************************************************************************************************************************/ /// The average velocity of the root motion caused by this state. public override Vector3 AverageVelocity { get { return _Clip.averageSpeed; } } /************************************************************************************************************************/ #region Inverse Kinematics /************************************************************************************************************************/ #if !UNITY_2018_1_OR_NEWER private const string IKNotSupported = "ApplyAnimatorIK is not supported by this version of Unity." + " Please upgrade to Unity 2018.1 or newer." ; #endif /// /// Determines whether OnAnimatorIK(int layerIndex) will be called on the animated object. /// The initial value is determined by . /// /// This is equivalent to the "IK Pass" toggle in Animator Controller layers. /// /// It requires Unity 2018.1 or newer, however 2018.3 or newer is recommended because a bug in earlier versions /// of the Playables API caused this value to only take effect while a state was at /// == 1 which meant that IK would not work while fading between animations. /// public override bool ApplyAnimatorIK { #if UNITY_2018_1_OR_NEWER get { return ((AnimationClipPlayable)_Playable).GetApplyPlayableIK(); } set { ((AnimationClipPlayable)_Playable).SetApplyPlayableIK(value); } #else get { throw new NotSupportedException(IKNotSupported); } set { throw new NotSupportedException(IKNotSupported); } #endif } /************************************************************************************************************************/ /// /// Indicates whether this state is applying IK to the character's feet. /// The initial value is determined by . /// /// This is equivalent to the "Foot IK" toggle in Animator Controller states. /// public override bool ApplyFootIK { get { return ((AnimationClipPlayable)_Playable).GetApplyFootIK(); } set { ((AnimationClipPlayable)_Playable).SetApplyFootIK(value); } } /************************************************************************************************************************/ /// /// Applies the default IK flags from the specified `layer`. /// private void InitialiseIKDefaults(AnimancerLayer layer) { // Foot IK is actually enabled by default so we disable it if necessary. if (!layer.DefaultApplyFootIK) ApplyFootIK = false; #if UNITY_2018_1_OR_NEWER if (layer.DefaultApplyAnimatorIK) ApplyAnimatorIK = true; #endif } /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ #region Methods /************************************************************************************************************************/ /// /// Constructs a new to play the `clip` without connecting it to the /// . You must call or it /// will not actually do anything. /// public ClipState(AnimancerPlayable root, AnimationClip clip) : base(root) { CreatePlayable(clip); } /// /// Constructs a new to play the `clip` and connects it to a new port on the `layer`s /// . /// public ClipState(AnimancerLayer layer, AnimationClip clip) : this(layer.Root, clip) { layer.AddChild(this); InitialiseIKDefaults(layer); } /// /// Constructs a new to play the `clip` and connects it to the `parent`s /// at the specified `index`. /// public ClipState(AnimancerNode parent, int index, AnimationClip clip) : this(parent.Root, clip) { SetParent(parent, index); InitialiseIKDefaults(parent.Layer); } /************************************************************************************************************************/ private void CreatePlayable(AnimationClip clip) { if (clip == null) throw new ArgumentNullException("clip"); Validate.NotLegacy(clip); _Clip = clip; _Playable = AnimationClipPlayable.Create(Root._Graph, clip); } /************************************************************************************************************************/ /// /// Returns a string describing the type of this state and the name of the . /// public override string ToString() { if (_Clip != null) return string.Concat(base.ToString(), " (", _Clip.name, ")"); else return base.ToString() + " (null)"; } /************************************************************************************************************************/ /// Destroys the . public override void Destroy() { _Clip = null; base.Destroy(); } /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ #region Inspector #if UNITY_EDITOR /************************************************************************************************************************/ /// [Editor-Only] Returns a for this state. protected internal override Editor.IAnimancerNodeDrawer GetDrawer() { return new Drawer(this); } /************************************************************************************************************************/ /// [Editor-Only] Draws the Inspector GUI for a . public sealed class Drawer : Editor.AnimancerStateDrawer { /************************************************************************************************************************/ /// Indicates whether the animation has an event called "End". private bool _HasEndEvent; /************************************************************************************************************************/ /// /// Constructs a new to manage the Inspector GUI for the `state`. /// public Drawer(ClipState state) : base(state) { var events = state._Clip.events; for (int i = events.Length - 1; i >= 0; i--) { if (events[i].functionName == "End") { _HasEndEvent = true; break; } } } /************************************************************************************************************************/ /// Draws the details of the target state in the GUI. protected override void DoDetailsGUI(IAnimancerComponent owner) { base.DoDetailsGUI(owner); DoAnimationTypeWarningGUI(owner); DoEndEventWarningGUI(); } /************************************************************************************************************************/ private string _AnimationTypeWarning; private Animator _AnimationTypeWarningOwner; /// /// Validates the type compared to the owner's type. /// private void DoAnimationTypeWarningGUI(IAnimancerComponent owner) { // Validate the clip type compared to the owner. if (owner.Animator == null) { _AnimationTypeWarning = null; return; } if (_AnimationTypeWarningOwner != owner.Animator) { _AnimationTypeWarning = null; _AnimationTypeWarningOwner = owner.Animator; } if (_AnimationTypeWarning == null) { var ownerAnimationType = Editor.AnimancerEditorUtilities.GetAnimationType(_AnimationTypeWarningOwner); var clipAnimationType = Editor.AnimancerEditorUtilities.GetAnimationType(Target._Clip); if (ownerAnimationType == clipAnimationType) { _AnimationTypeWarning = ""; } else { var text = new StringBuilder() .Append("Possible animation type mismatch:\n - Animator type is ") .Append(ownerAnimationType) .Append("\n - AnimationClip type is ") .Append(clipAnimationType) .Append("\nThis means that the clip may not work correctly," + " however this check is not totally accurate. Click here for more info."); _AnimationTypeWarning = text.ToString(); } } if (_AnimationTypeWarning != "") { UnityEditor.EditorGUILayout.HelpBox(_AnimationTypeWarning, UnityEditor.MessageType.Warning); if (Editor.AnimancerGUI.TryUseClickEventInLastRect()) UnityEditor.EditorUtility.OpenWithDefaultApp( Strings.DocsURLs.AnimationTypes); } } /************************************************************************************************************************/ private void DoEndEventWarningGUI() { if (_HasEndEvent && Target.Events.OnEnd == null && Target.TargetWeight != 0) { UnityEditor.EditorGUILayout.HelpBox("This animation has an event called 'End'" + " but no 'OnEnd' callback is currently registered for this state. Click here for more info.", UnityEditor.MessageType.Warning); if (Editor.AnimancerGUI.TryUseClickEventInLastRect()) UnityEditor.EditorUtility.OpenWithDefaultApp( Strings.DocsURLs.EndEvents); } } /************************************************************************************************************************/ /// Adds the details of this state to the menu. protected override void AddContextMenuFunctions(UnityEditor.GenericMenu menu) { menu.AddDisabledItem(new GUIContent(DetailsPrefix + "Animation Type: " + Editor.AnimancerEditorUtilities.GetAnimationType(Target._Clip))); base.AddContextMenuFunctions(menu); menu.AddItem(new GUIContent("Inverse Kinematics/Apply Animator IK"), Target.ApplyAnimatorIK, () => Target.ApplyAnimatorIK = !Target.ApplyAnimatorIK); menu.AddItem(new GUIContent("Inverse Kinematics/Apply Foot IK"), Target.ApplyFootIK, () => Target.ApplyFootIK = !Target.ApplyFootIK); } /************************************************************************************************************************/ } /************************************************************************************************************************/ #endif #endregion /************************************************************************************************************************/ #region Transition /************************************************************************************************************************/ /// /// A serializable which can create a when passed /// into . /// /// /// Unfortunately the tool used to generate this documentation does not currently support nested types with /// identical names, so only one Transition class will actually have a documentation page. /// [Serializable] public class Transition : Transition, IAnimationClipCollection { /************************************************************************************************************************/ [SerializeField, Tooltip("The animation to play")] private AnimationClip _Clip; /// [] The animation to play. public AnimationClip Clip { get { return _Clip; } set { if (value != null) Validate.NotLegacy(value); _Clip = value; } } /// /// The will be used as the for the created state to be /// registered with. /// public override object Key { get { return _Clip; } } /************************************************************************************************************************/ [SerializeField, Tooltip(Strings.ProOnlyTag + "How fast the animation plays (1x = normal speed, 2x = double speed)")] private float _Speed = 1; /// [] /// Determines how fast the animation plays (1x = normal speed, 2x = double speed). /// public override float Speed { get { return _Speed; } set { _Speed = value; } } /************************************************************************************************************************/ [SerializeField, Tooltip(Strings.ProOnlyTag + "If enabled, the animation's time will start at this value when played")] [UnityEngine.Serialization.FormerlySerializedAs("_StartTime")] private float _NormalizedStartTime = float.NaN; /// [] /// Determines what to start the animation at. /// /// The default value is which indicates that this value is not used so the /// animation will continue from its current time. /// public override float NormalizedStartTime { get { return _NormalizedStartTime; } set { _NormalizedStartTime = value; } } /// /// If this transition will set the , then it needs to use /// . /// public override FadeMode FadeMode { get { return float.IsNaN(_NormalizedStartTime) ? FadeMode.FixedSpeed : FadeMode.FromStart; } } /************************************************************************************************************************/ /// [] Returns . public override bool IsLooping { get { return _Clip != null ? _Clip.isLooping : false; } } /// [] /// The maximum amount of time the animation is expected to take (in seconds). /// public override float MaximumDuration { get { return _Clip != null ? _Clip.length : 0; } } /************************************************************************************************************************/ /// /// Creates and returns a new connected to the `layer`. /// /// This method also assigns it as the . /// public override ClipState CreateState(AnimancerLayer layer) { return State = new ClipState(layer, _Clip); } /************************************************************************************************************************/ /// /// Called by to apply the /// and . /// public override void Apply(AnimancerState state) { base.Apply(state); if (!float.IsNaN(_Speed)) state.Speed = _Speed; if (!float.IsNaN(_NormalizedStartTime)) state.NormalizedTime = _NormalizedStartTime; else if (state.Weight == 0) state.NormalizedTime = AnimancerEvent.Sequence.GetDefaultNormalizedStartTime(_Speed); } /************************************************************************************************************************/ /// Adds the to the collection. void IAnimationClipCollection.GatherAnimationClips(ICollection clips) { clips.Gather(_Clip); } /************************************************************************************************************************/ #if UNITY_EDITOR /************************************************************************************************************************/ /// [Editor-Only] Draws the Inspector GUI for a . [UnityEditor.CustomPropertyDrawer(typeof(Transition), true)] public class Drawer : Editor.TransitionDrawer { /************************************************************************************************************************/ /// Constructs a new . public Drawer() : base("_Clip") { } /************************************************************************************************************************/ } /************************************************************************************************************************/ #endif /************************************************************************************************************************/ } /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ } }