You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
527 lines
23 KiB
C#
527 lines
23 KiB
C#
2 months ago
|
// 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
|
||
|
{
|
||
|
/// <summary>
|
||
|
/// An <see cref="AnimancerState"/> which plays an <see cref="AnimationClip"/>.
|
||
|
/// </summary>
|
||
|
public sealed class ClipState : AnimancerState
|
||
|
{
|
||
|
/************************************************************************************************************************/
|
||
|
#region Fields and Properties
|
||
|
/************************************************************************************************************************/
|
||
|
|
||
|
/// <summary>The <see cref="AnimationClip"/> which this state plays.</summary>
|
||
|
private AnimationClip _Clip;
|
||
|
|
||
|
/// <summary>The <see cref="AnimationClip"/> which this state plays.</summary>
|
||
|
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();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/// <summary>The <see cref="AnimationClip"/> which this state plays.</summary>
|
||
|
public override Object MainObject
|
||
|
{
|
||
|
get { return _Clip; }
|
||
|
set { Clip = (AnimationClip)value; }
|
||
|
}
|
||
|
|
||
|
/************************************************************************************************************************/
|
||
|
|
||
|
/// <summary>The <see cref="AnimationClip.length"/>.</summary>
|
||
|
public override float Length { get { return _Clip.length; } }
|
||
|
|
||
|
/************************************************************************************************************************/
|
||
|
|
||
|
/// <summary>The <see cref="Motion.isLooping"/>.</summary>
|
||
|
public override bool IsLooping { get { return _Clip.isLooping; } }
|
||
|
|
||
|
/************************************************************************************************************************/
|
||
|
|
||
|
/// <summary>The average velocity of the root motion caused by this state.</summary>
|
||
|
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
|
||
|
|
||
|
/// <summary>
|
||
|
/// Determines whether <c>OnAnimatorIK(int layerIndex)</c> will be called on the animated object.
|
||
|
/// The initial value is determined by <see cref="AnimancerLayer.DefaultApplyAnimatorIK"/>.
|
||
|
/// <para></para>
|
||
|
/// This is equivalent to the "IK Pass" toggle in Animator Controller layers.
|
||
|
/// <para></para>
|
||
|
/// 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
|
||
|
/// <see cref="AnimancerNode.Weight"/> == 1 which meant that IK would not work while fading between animations.
|
||
|
/// </summary>
|
||
|
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
|
||
|
}
|
||
|
|
||
|
/************************************************************************************************************************/
|
||
|
|
||
|
/// <summary>
|
||
|
/// Indicates whether this state is applying IK to the character's feet.
|
||
|
/// The initial value is determined by <see cref="AnimancerLayer.DefaultApplyFootIK"/>.
|
||
|
/// <para></para>
|
||
|
/// This is equivalent to the "Foot IK" toggle in Animator Controller states.
|
||
|
/// </summary>
|
||
|
public override bool ApplyFootIK
|
||
|
{
|
||
|
get { return ((AnimationClipPlayable)_Playable).GetApplyFootIK(); }
|
||
|
set { ((AnimationClipPlayable)_Playable).SetApplyFootIK(value); }
|
||
|
}
|
||
|
|
||
|
/************************************************************************************************************************/
|
||
|
|
||
|
/// <summary>
|
||
|
/// Applies the default IK flags from the specified `layer`.
|
||
|
/// </summary>
|
||
|
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
|
||
|
/************************************************************************************************************************/
|
||
|
|
||
|
/// <summary>
|
||
|
/// Constructs a new <see cref="ClipState"/> to play the `clip` without connecting it to the
|
||
|
/// <see cref="PlayableGraph"/>. You must call <see cref="AnimancerState.SetParent(AnimancerNode, int)"/> or it
|
||
|
/// will not actually do anything.
|
||
|
/// </summary>
|
||
|
public ClipState(AnimancerPlayable root, AnimationClip clip)
|
||
|
: base(root)
|
||
|
{
|
||
|
CreatePlayable(clip);
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// Constructs a new <see cref="ClipState"/> to play the `clip` and connects it to a new port on the `layer`s
|
||
|
/// <see cref="Playable"/>.
|
||
|
/// </summary>
|
||
|
public ClipState(AnimancerLayer layer, AnimationClip clip)
|
||
|
: this(layer.Root, clip)
|
||
|
{
|
||
|
layer.AddChild(this);
|
||
|
InitialiseIKDefaults(layer);
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// Constructs a new <see cref="ClipState"/> to play the `clip` and connects it to the `parent`s
|
||
|
/// <see cref="Playable"/> at the specified `index`.
|
||
|
/// </summary>
|
||
|
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);
|
||
|
}
|
||
|
|
||
|
/************************************************************************************************************************/
|
||
|
|
||
|
/// <summary>
|
||
|
/// Returns a string describing the type of this state and the name of the <see cref="Clip"/>.
|
||
|
/// </summary>
|
||
|
public override string ToString()
|
||
|
{
|
||
|
if (_Clip != null)
|
||
|
return string.Concat(base.ToString(), " (", _Clip.name, ")");
|
||
|
else
|
||
|
return base.ToString() + " (null)";
|
||
|
}
|
||
|
|
||
|
/************************************************************************************************************************/
|
||
|
|
||
|
/// <summary>Destroys the <see cref="Playable"/>.</summary>
|
||
|
public override void Destroy()
|
||
|
{
|
||
|
_Clip = null;
|
||
|
base.Destroy();
|
||
|
}
|
||
|
|
||
|
/************************************************************************************************************************/
|
||
|
#endregion
|
||
|
/************************************************************************************************************************/
|
||
|
#region Inspector
|
||
|
#if UNITY_EDITOR
|
||
|
/************************************************************************************************************************/
|
||
|
|
||
|
/// <summary>[Editor-Only] Returns a <see cref="Drawer"/> for this state.</summary>
|
||
|
protected internal override Editor.IAnimancerNodeDrawer GetDrawer()
|
||
|
{
|
||
|
return new Drawer(this);
|
||
|
}
|
||
|
|
||
|
/************************************************************************************************************************/
|
||
|
|
||
|
/// <summary>[Editor-Only] Draws the Inspector GUI for a <see cref="ClipState"/>.</summary>
|
||
|
public sealed class Drawer : Editor.AnimancerStateDrawer<ClipState>
|
||
|
{
|
||
|
/************************************************************************************************************************/
|
||
|
|
||
|
/// <summary>Indicates whether the animation has an event called "End".</summary>
|
||
|
private bool _HasEndEvent;
|
||
|
|
||
|
/************************************************************************************************************************/
|
||
|
|
||
|
/// <summary>
|
||
|
/// Constructs a new <see cref="Drawer"/> to manage the Inspector GUI for the `state`.
|
||
|
/// </summary>
|
||
|
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;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/************************************************************************************************************************/
|
||
|
|
||
|
/// <summary> Draws the details of the target state in the GUI.</summary>
|
||
|
protected override void DoDetailsGUI(IAnimancerComponent owner)
|
||
|
{
|
||
|
base.DoDetailsGUI(owner);
|
||
|
DoAnimationTypeWarningGUI(owner);
|
||
|
DoEndEventWarningGUI();
|
||
|
}
|
||
|
|
||
|
/************************************************************************************************************************/
|
||
|
|
||
|
private string _AnimationTypeWarning;
|
||
|
private Animator _AnimationTypeWarningOwner;
|
||
|
|
||
|
/// <summary>
|
||
|
/// Validates the <see cref="Clip"/> type compared to the owner's <see cref="Animator"/> type.
|
||
|
/// </summary>
|
||
|
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);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/************************************************************************************************************************/
|
||
|
|
||
|
/// <summary>Adds the details of this state to the menu.</summary>
|
||
|
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
|
||
|
/************************************************************************************************************************/
|
||
|
|
||
|
/// <summary>
|
||
|
/// A serializable <see cref="ITransition"/> which can create a <see cref="ClipState"/> when passed
|
||
|
/// into <see cref="AnimancerPlayable.Play(ITransition)"/>.
|
||
|
/// </summary>
|
||
|
/// <remarks>
|
||
|
/// Unfortunately the tool used to generate this documentation does not currently support nested types with
|
||
|
/// identical names, so only one <c>Transition</c> class will actually have a documentation page.
|
||
|
/// </remarks>
|
||
|
[Serializable]
|
||
|
public class Transition : Transition<ClipState>, IAnimationClipCollection
|
||
|
{
|
||
|
/************************************************************************************************************************/
|
||
|
|
||
|
[SerializeField, Tooltip("The animation to play")]
|
||
|
private AnimationClip _Clip;
|
||
|
|
||
|
/// <summary>[<see cref="SerializeField"/>] The animation to play.</summary>
|
||
|
public AnimationClip Clip
|
||
|
{
|
||
|
get { return _Clip; }
|
||
|
set
|
||
|
{
|
||
|
if (value != null)
|
||
|
Validate.NotLegacy(value);
|
||
|
|
||
|
_Clip = value;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// The <see cref="Clip"/> will be used as the <see cref="AnimancerState.Key"/> for the created state to be
|
||
|
/// registered with.
|
||
|
/// </summary>
|
||
|
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;
|
||
|
|
||
|
/// <summary>[<see cref="SerializeField"/>]
|
||
|
/// Determines how fast the animation plays (1x = normal speed, 2x = double speed).
|
||
|
/// </summary>
|
||
|
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;
|
||
|
|
||
|
/// <summary>[<see cref="SerializeField"/>]
|
||
|
/// Determines what <see cref="AnimancerState.NormalizedTime"/> to start the animation at.
|
||
|
/// <para></para>
|
||
|
/// The default value is <see cref="float.NaN"/> which indicates that this value is not used so the
|
||
|
/// animation will continue from its current time.
|
||
|
/// </summary>
|
||
|
public override float NormalizedStartTime
|
||
|
{
|
||
|
get { return _NormalizedStartTime; }
|
||
|
set { _NormalizedStartTime = value; }
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// If this transition will set the <see cref="AnimancerState.NormalizedTime"/>, then it needs to use
|
||
|
/// <see cref="FadeMode.FromStart"/>.
|
||
|
/// </summary>
|
||
|
public override FadeMode FadeMode
|
||
|
{
|
||
|
get
|
||
|
{
|
||
|
return float.IsNaN(_NormalizedStartTime) ? FadeMode.FixedSpeed : FadeMode.FromStart;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/************************************************************************************************************************/
|
||
|
|
||
|
/// <summary>[<see cref="ITransitionDetailed"/>] Returns <see cref="Motion.isLooping"/>.</summary>
|
||
|
public override bool IsLooping
|
||
|
{
|
||
|
get
|
||
|
{
|
||
|
return _Clip != null ? _Clip.isLooping : false;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/// <summary>[<see cref="ITransitionDetailed"/>]
|
||
|
/// The maximum amount of time the animation is expected to take (in seconds).
|
||
|
/// </summary>
|
||
|
public override float MaximumDuration
|
||
|
{
|
||
|
get
|
||
|
{
|
||
|
return _Clip != null ? _Clip.length : 0;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/************************************************************************************************************************/
|
||
|
|
||
|
/// <summary>
|
||
|
/// Creates and returns a new <see cref="ClipState"/> connected to the `layer`.
|
||
|
/// <para></para>
|
||
|
/// This method also assigns it as the <see cref="AnimancerState.Transition{TState}.State"/>.
|
||
|
/// </summary>
|
||
|
public override ClipState CreateState(AnimancerLayer layer)
|
||
|
{
|
||
|
return State = new ClipState(layer, _Clip);
|
||
|
}
|
||
|
|
||
|
/************************************************************************************************************************/
|
||
|
|
||
|
/// <summary>
|
||
|
/// Called by <see cref="AnimancerPlayable.Play(ITransition)"/> to apply the <see cref="Speed"/>
|
||
|
/// and <see cref="NormalizedStartTime"/>.
|
||
|
/// </summary>
|
||
|
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);
|
||
|
}
|
||
|
|
||
|
/************************************************************************************************************************/
|
||
|
|
||
|
/// <summary>Adds the <see cref="Clip"/> to the collection.</summary>
|
||
|
void IAnimationClipCollection.GatherAnimationClips(ICollection<AnimationClip> clips)
|
||
|
{
|
||
|
clips.Gather(_Clip);
|
||
|
}
|
||
|
|
||
|
/************************************************************************************************************************/
|
||
|
#if UNITY_EDITOR
|
||
|
/************************************************************************************************************************/
|
||
|
|
||
|
/// <summary>[Editor-Only] Draws the Inspector GUI for a <see cref="Transition"/>.</summary>
|
||
|
[UnityEditor.CustomPropertyDrawer(typeof(Transition), true)]
|
||
|
public class Drawer : Editor.TransitionDrawer
|
||
|
{
|
||
|
/************************************************************************************************************************/
|
||
|
|
||
|
/// <summary>Constructs a new <see cref="Drawer"/>.</summary>
|
||
|
public Drawer() : base("_Clip") { }
|
||
|
|
||
|
/************************************************************************************************************************/
|
||
|
}
|
||
|
|
||
|
/************************************************************************************************************************/
|
||
|
#endif
|
||
|
/************************************************************************************************************************/
|
||
|
}
|
||
|
|
||
|
/************************************************************************************************************************/
|
||
|
#endregion
|
||
|
/************************************************************************************************************************/
|
||
|
}
|
||
|
}
|
||
|
|