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