chore: initial commit
This commit is contained in:
881
Packages/com.kybernetik.animancer/Runtime/AnimancerComponent.cs
Normal file
881
Packages/com.kybernetik.animancer/Runtime/AnimancerComponent.cs
Normal file
@@ -0,0 +1,881 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
using Animancer.TransitionLibraries;
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Playables;
|
||||
|
||||
namespace Animancer
|
||||
{
|
||||
/// <summary>
|
||||
/// The main component through which other scripts can interact with <see cref="Animancer"/>. It allows you to play
|
||||
/// animations on an <see cref="UnityEngine.Animator"/> without using a <see cref="RuntimeAnimatorController"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This class can be used as a custom yield instruction to wait until all animations finish playing.
|
||||
/// <para></para>
|
||||
/// This class is mostly just a wrapper that connects an <see cref="AnimancerGraph"/> to an
|
||||
/// <see cref="UnityEngine.Animator"/>.
|
||||
/// <para></para>
|
||||
/// <strong>Documentation:</strong>
|
||||
/// <see href="https://kybernetik.com.au/animancer/docs/manual/playing/component-types">
|
||||
/// Component Types</see>
|
||||
/// </remarks>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer/AnimancerComponent
|
||||
///
|
||||
[AddComponentMenu(Strings.MenuPrefix + "Animancer Component")]
|
||||
[AnimancerHelpUrl(typeof(AnimancerComponent))]
|
||||
[DefaultExecutionOrder(DefaultExecutionOrder)]
|
||||
public class AnimancerComponent : MonoBehaviour,
|
||||
IAnimancerComponent,
|
||||
IEnumerator,
|
||||
IAnimationClipSource,
|
||||
IAnimationClipCollection
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
#region Fields and Properties
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Initialize before anything else tries to use this component.</summary>
|
||||
public const int DefaultExecutionOrder = -5000;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip("Animancer works by using Unity's Playables API to control an Animator component." +
|
||||
"\n\nThe Animator's Controller field should be empty unless you intend to use it.")]
|
||||
private Animator _Animator;
|
||||
|
||||
/// <summary>[<see cref="SerializeField"/>]
|
||||
/// Animancer works by using Unity's Playables API to control an <see cref="UnityEngine.Animator"/> component.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The <see cref="Animator.runtimeAnimatorController"/> should be empty unless you intend to use it.
|
||||
/// </remarks>
|
||||
public Animator Animator
|
||||
{
|
||||
get => _Animator;
|
||||
set
|
||||
{
|
||||
_Animator = value;
|
||||
if (IsGraphInitialized)
|
||||
{
|
||||
_Graph.DestroyOutput();
|
||||
_Graph.Initialize(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
/// <inheritdoc/>
|
||||
string IAnimancerComponent.AnimatorFieldName
|
||||
=> nameof(_Animator);
|
||||
#endif
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip(Strings.ProOnlyTag + "An optional Transition Library" +
|
||||
" which can modify the way Animancer transitions between animations.")]
|
||||
private TransitionLibraryAsset _Transitions;
|
||||
|
||||
/// <summary>[<see cref="SerializeField"/>] [Pro-Only]
|
||||
/// An optional <see cref="TransitionLibraryAsset"/>
|
||||
/// which can modify the way Animancer transitions between animations.
|
||||
/// </summary>
|
||||
public TransitionLibraryAsset Transitions
|
||||
{
|
||||
get => _Transitions;
|
||||
set
|
||||
{
|
||||
_Transitions = value;
|
||||
if (IsGraphInitialized)
|
||||
_Graph.Transitions = value?.Library;
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private AnimancerGraph _Graph;
|
||||
|
||||
/// <summary>
|
||||
/// The internal system which manages the playing animations.
|
||||
/// Accessing this property will automatically initialize it.
|
||||
/// </summary>
|
||||
public AnimancerGraph Graph
|
||||
{
|
||||
get
|
||||
{
|
||||
InitializeGraph();
|
||||
return _Graph;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Has the <see cref="Graph"/> been initialized?</summary>
|
||||
public bool IsGraphInitialized
|
||||
=> _Graph != null
|
||||
&& _Graph.IsValidOrDispose();
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>The layers which each manage their own set of animations.</summary>
|
||||
public AnimancerLayerList Layers
|
||||
=> Graph.Layers;
|
||||
|
||||
/// <summary>The states managed by this component.</summary>
|
||||
public AnimancerStateDictionary States
|
||||
=> Graph.States;
|
||||
|
||||
/// <summary>Dynamic parameters which anything can get or set.</summary>
|
||||
public ParameterDictionary Parameters
|
||||
=> Graph.Parameters;
|
||||
|
||||
/// <summary>A dictionary of callbacks to be triggered by any event with a matching name.</summary>
|
||||
public NamedEventDictionary Events
|
||||
=> Graph.Events;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Returns the <see cref="Graph"/>.</summary>
|
||||
public static implicit operator AnimancerGraph(AnimancerComponent animancer)
|
||||
=> animancer.Graph;
|
||||
|
||||
/// <summary>Returns layer 0.</summary>
|
||||
public static implicit operator AnimancerLayer(AnimancerComponent animancer)
|
||||
=> animancer.Graph.Layers[0];
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
[SerializeField, Tooltip("Determines what happens when this component is disabled" +
|
||||
" or its " + nameof(GameObject) + " becomes inactive (i.e. in OnDisable):" +
|
||||
"\n• [" + nameof(DisableAction.Stop) + "] and reset all animations" +
|
||||
"\n• [" + nameof(DisableAction.Pause) + "] to later resume from the current state" +
|
||||
"\n• [" + nameof(DisableAction.Continue) + "] playing while inactive" +
|
||||
"\n• [" + nameof(DisableAction.Reset) + "] to the original values" +
|
||||
"\n• [" + nameof(DisableAction.Destroy) + "] all layers and states" +
|
||||
"\n• If you're only destroying objects and not disabling them," +
|
||||
" using " + nameof(DisableAction.Continue) + " is the most efficient" +
|
||||
" because it avoids wasting performance stopping things that will be destroyed anyway.")]
|
||||
private DisableAction _ActionOnDisable;
|
||||
|
||||
#if UNITY_EDITOR
|
||||
/// <summary>[Editor-Only]
|
||||
/// The name of the serialized backing field for the <see cref="ActionOnDisable"/> property.
|
||||
/// </summary>
|
||||
string IAnimancerComponent.ActionOnDisableFieldName
|
||||
=> nameof(_ActionOnDisable);
|
||||
#endif
|
||||
|
||||
/// <summary>[<see cref="SerializeField"/>]
|
||||
/// Determines what happens when this component is disabled
|
||||
/// or its <see cref="GameObject"/> becomes inactive
|
||||
/// (i.e. in <see cref="OnDisable"/>).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The default value is <see cref="DisableAction.Stop"/>.
|
||||
/// <para></para>
|
||||
/// If you're only destroying objects and not disabling them,
|
||||
/// using <see cref="DisableAction.Continue"/> is the most efficient
|
||||
/// because it avoids wasting performance stopping things that will be destroyed anyway.
|
||||
/// </remarks>
|
||||
public ref DisableAction ActionOnDisable
|
||||
=> ref _ActionOnDisable;
|
||||
|
||||
/// <inheritdoc/>
|
||||
bool IAnimancerComponent.ResetOnDisable
|
||||
=> _ActionOnDisable == DisableAction.Reset;
|
||||
|
||||
/// <summary>
|
||||
/// An action to perform when disabling an <see cref="AnimancerComponent"/>.
|
||||
/// See <see cref="ActionOnDisable"/>.
|
||||
/// </summary>
|
||||
public enum DisableAction
|
||||
{
|
||||
/// <summary>
|
||||
/// Stop and reset all animations, but leave all animated values as they are (unlike <see cref="Reset"/>).
|
||||
/// </summary>
|
||||
/// <remarks>Calls <see cref="Stop()"/> and <see cref="AnimancerGraph.PauseGraph"/>.</remarks>
|
||||
Stop,
|
||||
|
||||
/// <summary>Pause to later resume from the current state.</summary>
|
||||
/// <remarks>Calls <see cref="AnimancerGraph.PauseGraph"/>.</remarks>
|
||||
Pause,
|
||||
|
||||
/// <summary>Keep playing while inactive.</summary>
|
||||
Continue,
|
||||
|
||||
/// <summary>
|
||||
/// Stop all animations, rewind them, and force the object back into its original state
|
||||
/// (often called the bind pose).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The <see cref="AnimancerComponent"/> must be either above the <see cref="UnityEngine.Animator"/> in
|
||||
/// the Inspector or on a child object so that so that this <see cref="OnDisable"/> gets called first.
|
||||
/// <para></para>
|
||||
/// Calls <see cref="Stop()"/>, <see cref="Animator.Rebind"/>, and <see cref="AnimancerGraph.PauseGraph"/>.
|
||||
/// </remarks>
|
||||
Reset,
|
||||
|
||||
/// <summary>
|
||||
/// Destroy the <see cref="PlayableGraph"/> and all its layers and states. This means that any layers or
|
||||
/// states referenced by other scripts will no longer be valid so they will need to be recreated if you
|
||||
/// want to use this object again.
|
||||
/// </summary>
|
||||
/// <remarks>Calls <see cref="AnimancerGraph.Destroy()"/>.</remarks>
|
||||
Destroy,
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#region Update Mode
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Determines when animations are updated and which time source is used.</summary>
|
||||
/// <remarks>
|
||||
/// Note that changing to or from <see cref="AnimatorUpdateMode.AnimatePhysics"/>
|
||||
/// at runtime has no effect due to limitations in the Playables API.
|
||||
/// </remarks>
|
||||
/// <exception cref="NullReferenceException">No <see cref="Animator"/> is assigned.</exception>
|
||||
public AnimatorUpdateMode UpdateMode
|
||||
{
|
||||
get => _Animator.updateMode;
|
||||
set
|
||||
{
|
||||
_Animator.updateMode = value;
|
||||
|
||||
if (!IsGraphInitialized)
|
||||
return;
|
||||
|
||||
// UnscaledTime on the Animator is actually identical to Normal when using the Playables API so we need
|
||||
// to set the graph's DirectorUpdateMode to determine how it gets its delta time.
|
||||
_Graph.UpdateMode = value == AnimatorUpdateMode.UnscaledTime ?
|
||||
DirectorUpdateMode.UnscaledGameTime :
|
||||
DirectorUpdateMode.GameTime;
|
||||
|
||||
#if UNITY_EDITOR
|
||||
if (InitialUpdateMode == null)
|
||||
{
|
||||
InitialUpdateMode = value;
|
||||
}
|
||||
else if (UnityEditor.EditorApplication.isPlaying)
|
||||
{
|
||||
if (Editor.AnimancerGraphCleanup.HasChangedToOrFromAnimatePhysics(InitialUpdateMode, value))
|
||||
Debug.LogWarning(
|
||||
$"Changing the {nameof(Animator)}.{nameof(Animator.updateMode)} to or from " +
|
||||
#if UNITY_2023_1_OR_NEWER
|
||||
nameof(AnimatorUpdateMode.Fixed) +
|
||||
#else
|
||||
nameof(AnimatorUpdateMode.AnimatePhysics) +
|
||||
#endif
|
||||
" at runtime will have no effect." +
|
||||
" You must set it in the Unity Editor or on startup.", this);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
#if UNITY_EDITOR
|
||||
/// <inheritdoc/>
|
||||
public AnimatorUpdateMode? InitialUpdateMode { get; private set; }
|
||||
#endif
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endregion
|
||||
/************************************************************************************************************************/
|
||||
#endregion
|
||||
/************************************************************************************************************************/
|
||||
#region Initialization
|
||||
/************************************************************************************************************************/
|
||||
|
||||
#if UNITY_EDITOR
|
||||
/// <summary>[Editor-Only]
|
||||
/// Destroys the <see cref="Graph"/> if it was initialized and searches for an <see cref="Animator"/> on
|
||||
/// this object, or it's children or parents.
|
||||
/// </summary>
|
||||
protected virtual void Reset()
|
||||
{
|
||||
OnDestroy();
|
||||
gameObject.GetComponentInParentOrChildren(ref _Animator);
|
||||
}
|
||||
#endif
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Ensures that the <see cref="PlayableGraph"/> is playing.</summary>
|
||||
protected virtual void OnEnable()
|
||||
{
|
||||
if (IsGraphInitialized)
|
||||
{
|
||||
_Graph.UnpauseGraph();
|
||||
|
||||
#if UNITY_EDITOR
|
||||
AnimancerGraph.ClearInactiveInitializationStackTrace(this);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Acts according to the <see cref="ActionOnDisable"/>.</summary>
|
||||
protected virtual void OnDisable()
|
||||
{
|
||||
if (!IsGraphInitialized)
|
||||
return;
|
||||
|
||||
switch (_ActionOnDisable)
|
||||
{
|
||||
case DisableAction.Stop:
|
||||
_Graph.Stop();
|
||||
_Graph.PauseGraph();
|
||||
break;
|
||||
|
||||
case DisableAction.Pause:
|
||||
_Graph.PauseGraph();
|
||||
break;
|
||||
|
||||
case DisableAction.Continue:
|
||||
break;
|
||||
|
||||
case DisableAction.Reset:
|
||||
Debug.Assert(_Animator.isActiveAndEnabled,
|
||||
$"{nameof(DisableAction)}.{nameof(DisableAction.Reset)} failed because the {nameof(Animator)}" +
|
||||
$" is not enabled. This most likely means you are disabling the {nameof(GameObject)} and the" +
|
||||
$" {nameof(Animator)} is above the {nameof(AnimancerComponent)} in the Inspector so it got" +
|
||||
$" disabled right before this method was called." +
|
||||
$" See the Inspector of {this} to fix the issue" +
|
||||
$" or use {nameof(DisableAction)}.{nameof(DisableAction.Stop)}" +
|
||||
$" and call {nameof(Animator)}.{nameof(Animator.Rebind)} manually" +
|
||||
$" before disabling the {nameof(GameObject)}.",
|
||||
this);
|
||||
|
||||
_Graph.Stop();
|
||||
_Animator.Rebind();
|
||||
_Graph.PauseGraph();
|
||||
break;
|
||||
|
||||
case DisableAction.Destroy:
|
||||
_Graph.Destroy();
|
||||
_Graph = null;
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(ActionOnDisable));
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Creates and initializes the <see cref="Graph"/> if it wasn't already initialized.</summary>
|
||||
public void InitializeGraph()
|
||||
{
|
||||
if (IsGraphInitialized)
|
||||
return;
|
||||
|
||||
TryGetAnimator();
|
||||
|
||||
AnimancerGraph.SetNextGraphName(name + " (Animancer)");
|
||||
_Graph = new(_Transitions?.Library);
|
||||
_Graph.Initialize(this);
|
||||
OnInitializeGraph();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Sets the <see cref="Graph"/> and connects it to the <see cref="Animator"/>.</summary>
|
||||
/// <exception cref="InvalidOperationException">
|
||||
/// The <see cref="AnimancerGraph"/> is already initialized.
|
||||
/// You must call <see cref="AnimancerGraph.Destroy"/> before re-initializing it.
|
||||
/// </exception>
|
||||
public void InitializeGraph(AnimancerGraph graph, bool createOutput = true)
|
||||
{
|
||||
if (IsGraphInitialized)
|
||||
throw new InvalidOperationException(
|
||||
$"The {nameof(AnimancerGraph)} is already initialized." +
|
||||
$" Either call this method before anything else uses it or call" +
|
||||
$" animancerComponent.{nameof(Graph)}.{nameof(AnimancerGraph.Destroy)}" +
|
||||
$" before re-initializing it.");
|
||||
|
||||
TryGetAnimator();
|
||||
|
||||
_Graph = graph;
|
||||
_Graph.Transitions = _Transitions?.Library;
|
||||
_Graph.Initialize(this, createOutput);
|
||||
OnInitializeGraph();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Called right after the <see cref="Graph"/> is initialized.</summary>
|
||||
protected virtual void OnInitializeGraph()
|
||||
{
|
||||
#if UNITY_ASSERTIONS
|
||||
ValidateGraphInitialization();
|
||||
#endif
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// Tries to ensure that an <see cref="Animator"/> is present using
|
||||
/// <see cref="Component.TryGetComponent{T}(out T)"/> if necessary.
|
||||
/// </summary>
|
||||
public bool TryGetAnimator()
|
||||
=> _Animator != null
|
||||
|| TryGetComponent(out _Animator);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
#if UNITY_ASSERTIONS
|
||||
/// <summary>[Assert-Only]
|
||||
/// Validates various conditions relating to <see cref="AnimancerGraph"/> initialization.
|
||||
/// </summary>
|
||||
private void ValidateGraphInitialization()
|
||||
{
|
||||
#if UNITY_EDITOR
|
||||
if (_Animator != null)
|
||||
InitialUpdateMode = UpdateMode;
|
||||
|
||||
#if UNITY_IMGUI
|
||||
if (OptionalWarning.CreateGraphDuringGuiEvent.IsEnabled())
|
||||
{
|
||||
var currentEvent = Event.current;
|
||||
if (currentEvent != null)
|
||||
{
|
||||
var eventType = currentEvent.type;
|
||||
if (eventType == EventType.Layout ||
|
||||
eventType == EventType.Repaint)
|
||||
{
|
||||
OptionalWarning.CreateGraphDuringGuiEvent.Log(
|
||||
$"An {nameof(AnimancerGraph)} is being initialized" +
|
||||
$" during a {eventType} event which is likely undesirable.",
|
||||
this);
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
|
||||
if (_Animator != null)
|
||||
{
|
||||
if (!_Animator.enabled)
|
||||
OptionalWarning.AnimatorDisabled.Log(Strings.AnimatorDisabledMessage, this);
|
||||
|
||||
if (_Animator.isHuman &&
|
||||
_Animator.runtimeAnimatorController != null)
|
||||
OptionalWarning.NativeControllerHumanoid.Log(
|
||||
$"An Animator Controller is assigned to the {nameof(Animator)} component" +
|
||||
$" but the Rig is Humanoid so it can't be blended with Animancer." +
|
||||
$" See the documentation for more information:" +
|
||||
$" {Strings.DocsURLs.AnimatorControllersNative.AsHtmlLink()}",
|
||||
this);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Ensures that the <see cref="Graph"/> is properly cleaned up.</summary>
|
||||
protected virtual void OnDestroy()
|
||||
{
|
||||
if (IsGraphInitialized)
|
||||
{
|
||||
_Graph.Destroy();
|
||||
_Graph = null;
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
#if UNITY_EDITOR
|
||||
/// <summary>[Editor-Only]
|
||||
/// Ensures that the <see cref="AnimancerGraph"/> is destroyed in Edit Mode, but not in Play Mode since we want
|
||||
/// to let Unity complain if that happens.
|
||||
/// </summary>
|
||||
~AnimancerComponent()
|
||||
{
|
||||
if (_Graph != null)
|
||||
{
|
||||
UnityEditor.EditorApplication.delayCall += () =>
|
||||
{
|
||||
if (!UnityEditor.EditorApplication.isPlayingOrWillChangePlaymode)
|
||||
OnDestroy();
|
||||
};
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endregion
|
||||
/************************************************************************************************************************/
|
||||
#region Play Management
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Returns the `clip` itself.</summary>
|
||||
/// <remarks>
|
||||
/// This method is used to determine the dictionary key to use for an animation when none is specified by the
|
||||
/// caller, such as in <see cref="Play(AnimationClip)"/>.
|
||||
/// </remarks>
|
||||
public virtual object GetKey(AnimationClip clip)
|
||||
=> clip;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
// Play Immediately.
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Stops all other animations on the same layer, plays the `clip`, and returns its state.</summary>
|
||||
/// <remarks>
|
||||
/// The animation will continue playing from its current <see cref="AnimancerState.Time"/>.
|
||||
/// To restart it from the beginning you can use <c>...Play(clip).Time = 0;</c>.
|
||||
/// <para></para>
|
||||
/// This method is safe to call repeatedly without checking whether the `clip` was already playing.
|
||||
/// </remarks>
|
||||
public AnimancerState Play(AnimationClip clip)
|
||||
=> Graph.Layers[0].Play(States.GetOrCreate(clip));
|
||||
|
||||
/// <summary>Stops all other animations on the same layer, plays the `state`, and returns it.</summary>
|
||||
/// <remarks>
|
||||
/// The animation will continue playing from its current <see cref="AnimancerState.Time"/>.
|
||||
/// To restart it from the beginning you can use <c>...Play(state).Time = 0;</c>.
|
||||
/// <para></para>
|
||||
/// This method is safe to call repeatedly without checking whether the `state` was already playing.
|
||||
/// </remarks>
|
||||
public AnimancerState Play(AnimancerState state)
|
||||
=> Graph.Layers[0].Play(state);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
// Cross Fade.
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// Starts fading in the `clip` while fading out all other states in the same layer over the course of the
|
||||
/// `fadeDuration`. Returns its state.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// If the `state` was already playing and fading in with less time remaining than the `fadeDuration`, this
|
||||
/// method will allow it to complete the existing fade rather than starting a slower one.
|
||||
/// <para></para>
|
||||
/// If the layer currently has 0 <see cref="AnimancerNode.Weight"/>, this method will fade in the layer itself
|
||||
/// and simply <see cref="AnimancerState.Play"/> the `state`.
|
||||
/// <para></para>
|
||||
/// This method is safe to call repeatedly without checking whether the `clip` was already playing.
|
||||
/// <para></para>
|
||||
/// <em>Animancer Lite only allows the default `fadeDuration` (0.25 seconds) in runtime builds.</em>
|
||||
/// </remarks>
|
||||
public AnimancerState Play(AnimationClip clip, float fadeDuration, FadeMode mode = default)
|
||||
=> Graph.Layers[0].Play(States.GetOrCreate(clip), fadeDuration, mode);
|
||||
|
||||
/// <summary>
|
||||
/// Starts fading in the `state` while fading out all others in the same layer over the course of the
|
||||
/// `fadeDuration`. Returns the `state`.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// If the `state` was already playing and fading in with less time remaining than the `fadeDuration`, this
|
||||
/// method will allow it to complete the existing fade rather than starting a slower one.
|
||||
/// <para></para>
|
||||
/// If the layer currently has 0 <see cref="AnimancerNode.Weight"/>, this method will fade in the layer itself
|
||||
/// and simply <see cref="AnimancerState.Play"/> the `state`.
|
||||
/// <para></para>
|
||||
/// This method is safe to call repeatedly without checking whether the `state` was already playing.
|
||||
/// <para></para>
|
||||
/// <em>Animancer Lite only allows the default `fadeDuration` (0.25 seconds) in runtime builds.</em>
|
||||
/// </remarks>
|
||||
public AnimancerState Play(AnimancerState state, float fadeDuration, FadeMode mode = default)
|
||||
=> Graph.Layers[0].Play(state, fadeDuration, mode);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
// Transition.
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// Creates a state for the `transition` if it didn't already exist, then calls
|
||||
/// <see cref="Play(AnimancerState)"/> or <see cref="Play(AnimancerState, float, FadeMode)"/>
|
||||
/// depending on <see cref="ITransition.CrossFadeFromStart"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This method is safe to call repeatedly without checking whether the `transition` was already playing.
|
||||
/// </remarks>
|
||||
public AnimancerState Play(ITransition transition)
|
||||
=> Graph.Layers[0].Play(transition);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a state for the `transition` if it didn't already exist, then calls
|
||||
/// <see cref="Play(AnimancerState)"/> or <see cref="Play(AnimancerState, float, FadeMode)"/>
|
||||
/// depending on <see cref="ITransition.CrossFadeFromStart"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This method is safe to call repeatedly without checking whether the `transition` was already playing.
|
||||
/// </remarks>
|
||||
public AnimancerState Play(ITransition transition, float fadeDuration, FadeMode mode = default)
|
||||
=> Graph.Layers[0].Play(transition, fadeDuration, mode);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
// Try Play.
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// Stops all other animations on the base layer,
|
||||
/// plays the animation registered with the `key`,
|
||||
/// and returns the animation's state.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// If no state is registered with the `key`, this method does nothing and returns null.
|
||||
/// <para></para>
|
||||
/// The animation will continue playing from its current <see cref="AnimancerState.Time"/>.
|
||||
/// To restart it from the beginning you can simply set the returned state's time to 0.
|
||||
/// <para></para>
|
||||
/// This method is safe to call repeatedly without checking whether the animation was already playing.
|
||||
/// </remarks>
|
||||
/// <exception cref="ArgumentNullException">The `key` is null.</exception>
|
||||
public AnimancerState TryPlay(object key)
|
||||
=> Graph.Layers[0].TryPlay(key);
|
||||
|
||||
/// <summary>
|
||||
/// Stops all other animations on the base layer,
|
||||
/// plays the animation registered with the `key`,
|
||||
/// and returns the animation's state.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// If no state is registered with the `key`, this method does nothing and returns null.
|
||||
/// <para></para>
|
||||
/// The animation will continue playing from its current <see cref="AnimancerState.Time"/>.
|
||||
/// To restart it from the beginning you can simply set the returned state's time to 0.
|
||||
/// <para></para>
|
||||
/// This method is safe to call repeatedly without checking whether the animation was already playing.
|
||||
/// </remarks>
|
||||
public AnimancerState TryPlay(IHasKey hasKey)
|
||||
=> TryPlay(hasKey.Key);
|
||||
|
||||
/// <summary>
|
||||
/// Starts fading in the animation registered with the `key` while fading out all others in the same layer
|
||||
/// over the course of the `fadeDuration`. Or if no state is registered with that `key`, this method does
|
||||
/// nothing and returns null.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// If the `state` was already playing and fading in with less time remaining than the `fadeDuration`, this
|
||||
/// method will allow it to complete the existing fade rather than starting a slower one.
|
||||
/// <para></para>
|
||||
/// If the layer currently has 0 <see cref="AnimancerNode.Weight"/>, this method will fade in the layer itself
|
||||
/// and simply <see cref="AnimancerState.Play"/> the `state`.
|
||||
/// <para></para>
|
||||
/// This method is safe to call repeatedly without checking whether the animation was already playing.
|
||||
/// <para></para>
|
||||
/// <em>Animancer Lite only allows the default `fadeDuration` (0.25 seconds) in runtime builds.</em>
|
||||
/// </remarks>
|
||||
/// <exception cref="ArgumentNullException">The `key` is null.</exception>
|
||||
public AnimancerState TryPlay(object key, float fadeDuration, FadeMode mode = default)
|
||||
=> Graph.Layers[0].TryPlay(key, fadeDuration, mode);
|
||||
|
||||
/// <summary>
|
||||
/// Starts fading in the animation registered with the `key`
|
||||
/// while fading out all others in the same layer over the course of the `fadeDuration`
|
||||
/// and returns the animation's state.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// If no state is registered with the `key`, this method does nothing and returns null.
|
||||
/// <para></para>
|
||||
/// If the `state` was already playing and fading in with less time remaining than the `fadeDuration`,
|
||||
/// this method allows it to continue the existing fade rather than starting a slower one.
|
||||
/// <para></para>
|
||||
/// If the layer currently has 0 <see cref="AnimancerNode.Weight"/>, this method will
|
||||
/// fade in the layer itself and simply <see cref="AnimancerState.Play"/> the `state`.
|
||||
/// <para></para>
|
||||
/// This method is safe to call repeatedly without checking whether the animation was already playing.
|
||||
/// <para></para>
|
||||
/// <em>Animancer Lite only allows the default `fadeDuration` (0.25 seconds) in runtime builds.</em>
|
||||
/// </remarks>
|
||||
public AnimancerState TryPlay(
|
||||
IHasKey hasKey,
|
||||
float fadeDuration,
|
||||
FadeMode mode = default)
|
||||
=> TryPlay(hasKey.Key, fadeDuration, mode);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// Gets the state associated with the `clip`, stops and rewinds it to the start, then returns it.
|
||||
/// </summary>
|
||||
public AnimancerState Stop(AnimationClip clip)
|
||||
=> Stop(GetKey(clip));
|
||||
|
||||
/// <summary>
|
||||
/// Gets the state registered with the <see cref="IHasKey.Key"/>, stops and rewinds it to the start, then
|
||||
/// returns it.
|
||||
/// </summary>
|
||||
public AnimancerState Stop(IHasKey hasKey)
|
||||
=> _Graph?.Stop(hasKey);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the state associated with the `key`, stops and rewinds it to the start, then returns it.
|
||||
/// </summary>
|
||||
public AnimancerState Stop(object key)
|
||||
=> _Graph?.Stop(key);
|
||||
|
||||
/// <summary>Stops all animations and rewinds them to the start.</summary>
|
||||
public void Stop()
|
||||
{
|
||||
if (IsGraphInitialized)
|
||||
_Graph.Stop();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if a state is registered for the `clip` and it is currently playing.
|
||||
/// <para></para>
|
||||
/// The actual dictionary key is determined using <see cref="GetKey"/>.
|
||||
/// </summary>
|
||||
public bool IsPlaying(AnimationClip clip)
|
||||
=> IsPlaying(GetKey(clip));
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if a state is registered with the <see cref="IHasKey.Key"/> and it is currently playing.
|
||||
/// </summary>
|
||||
public bool IsPlaying(IHasKey hasKey)
|
||||
=> IsGraphInitialized
|
||||
&& _Graph.IsPlaying(hasKey);
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if a state is registered with the `key` and it is currently playing.
|
||||
/// </summary>
|
||||
public bool IsPlaying(object key)
|
||||
=> IsGraphInitialized
|
||||
&& _Graph.IsPlaying(key);
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if at least one animation is being played.
|
||||
/// </summary>
|
||||
public bool IsPlaying()
|
||||
=> IsGraphInitialized
|
||||
&& _Graph.IsPlaying();
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the `clip` is currently being played by at least one state.
|
||||
/// <para></para>
|
||||
/// This method is inefficient because it searches through every state to find any that are playing the `clip`,
|
||||
/// unlike <see cref="IsPlaying(AnimationClip)"/> which only checks the state registered using the `clip`s key.
|
||||
/// </summary>
|
||||
public bool IsPlayingClip(AnimationClip clip)
|
||||
=> IsGraphInitialized
|
||||
&& _Graph.IsPlayingClip(clip);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// Immediately applies the current states of all animations to the animated objects.
|
||||
/// </summary>
|
||||
public void Evaluate()
|
||||
=> Graph.Evaluate();
|
||||
|
||||
/// <summary>
|
||||
/// Advances time by the specified value (in seconds)
|
||||
/// and immediately applies the current states of all animations to the animated objects.
|
||||
/// </summary>
|
||||
public void Evaluate(float deltaTime)
|
||||
=> Graph.Evaluate(deltaTime);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#region Key Error Methods
|
||||
#if UNITY_EDITOR
|
||||
/************************************************************************************************************************/
|
||||
// These are overloads of other methods that take a System.Object key to ensure the user doesn't try to use an
|
||||
// AnimancerState as a key, since the whole point of a key is to identify a state in the first place.
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>[Warning]
|
||||
/// You should not use an <see cref="AnimancerState"/> as a key.
|
||||
/// Just call <see cref="AnimancerState.Stop"/>.
|
||||
/// </summary>
|
||||
[Obsolete("You should not use an AnimancerState as a key. Just call AnimancerState.Stop().", true)]
|
||||
public AnimancerState Stop(AnimancerState key)
|
||||
{
|
||||
key.Stop();
|
||||
return key;
|
||||
}
|
||||
|
||||
/// <summary>[Warning]
|
||||
/// You should not use an <see cref="AnimancerState"/> as a key.
|
||||
/// Just check <see cref="AnimancerState.IsPlaying"/>.
|
||||
/// </summary>
|
||||
[Obsolete("You should not use an AnimancerState as a key. Just check AnimancerState.IsPlaying.", true)]
|
||||
public bool IsPlaying(AnimancerState key)
|
||||
=> key.IsPlaying;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endif
|
||||
#endregion
|
||||
/************************************************************************************************************************/
|
||||
#endregion
|
||||
/************************************************************************************************************************/
|
||||
#region Enumeration
|
||||
/************************************************************************************************************************/
|
||||
// IEnumerator for yielding in a coroutine to wait until all animations have stopped.
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Are any animations are still playing?</summary>
|
||||
/// <remarks>This allows this object to be used as a custom yield instruction.</remarks>
|
||||
bool IEnumerator.MoveNext()
|
||||
=> IsGraphInitialized
|
||||
&& ((IEnumerator)_Graph).MoveNext();
|
||||
|
||||
/// <summary>Returns null.</summary>
|
||||
object IEnumerator.Current
|
||||
=> null;
|
||||
|
||||
/// <summary>Does nothing.</summary>
|
||||
void IEnumerator.Reset()
|
||||
{ }
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>[<see cref="IAnimationClipSource"/>]
|
||||
/// Calls <see cref="GatherAnimationClips(ICollection{AnimationClip})"/>.
|
||||
/// </summary>
|
||||
public void GetAnimationClips(List<AnimationClip> clips)
|
||||
{
|
||||
var set = SetPool.Acquire<AnimationClip>();
|
||||
set.UnionWith(clips);
|
||||
|
||||
GatherAnimationClips(set);
|
||||
|
||||
clips.Clear();
|
||||
|
||||
foreach (var clip in set)
|
||||
if (clip != null)
|
||||
clips.Add(clip);
|
||||
|
||||
SetPool.Release(set);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>[<see cref="IAnimationClipCollection"/>]
|
||||
/// Gathers all the animations in the <see cref="Transitions"/> and <see cref="Graph"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// In the Unity Editor this method also gathers animations from other components on parent and child objects.
|
||||
/// </remarks>
|
||||
public virtual void GatherAnimationClips(ICollection<AnimationClip> clips)
|
||||
{
|
||||
if (_Transitions != null)
|
||||
_Transitions.GatherAnimationClips(clips);
|
||||
|
||||
if (IsGraphInitialized)
|
||||
_Graph.GatherAnimationClips(clips);
|
||||
|
||||
#if UNITY_EDITOR
|
||||
Editor.AnimationGatherer.GatherFromGameObject(gameObject, clips);
|
||||
|
||||
if (_Animator != null && _Animator.gameObject != gameObject)
|
||||
Editor.AnimationGatherer.GatherFromGameObject(_Animator.gameObject, clips);
|
||||
#endif
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endregion
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0ad50f81b1d25c441943c37a89ba23f6
|
||||
labels:
|
||||
- Component
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0024209230bdd0d46a82810456402e2c
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,472 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Animancer
|
||||
{
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer/ControllerState
|
||||
partial class ControllerState
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private SerializableParameterBindings _SerializedParameterBindings;
|
||||
|
||||
/// <summary>Serialized data used to create <see cref="ParameterBinding{T}"/>s at runtime.</summary>
|
||||
public SerializableParameterBindings SerializedParameterBindings
|
||||
{
|
||||
get => _SerializedParameterBindings;
|
||||
set
|
||||
{
|
||||
_SerializedParameterBindings = value;
|
||||
DeserializeParameterBindings();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Deserializes the <see cref="SerializedParameterBindings"/>.</summary>
|
||||
private void DeserializeParameterBindings()
|
||||
{
|
||||
if (Graph == null)
|
||||
return;
|
||||
|
||||
DisposeParameterBindings();
|
||||
_SerializedParameterBindings?.Deserialize(this);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private List<IDisposable> _ParameterBindings;
|
||||
|
||||
/// <summary>
|
||||
/// Adds an object to a list for <see cref="IDisposable.Dispose"/>
|
||||
/// to be called in <see cref="Destroy"/>.
|
||||
/// </summary>
|
||||
private void AddParameterBinding(IDisposable disposable)
|
||||
{
|
||||
_ParameterBindings ??= new();
|
||||
_ParameterBindings.Add(disposable);
|
||||
}
|
||||
|
||||
/// <summary>Disposes everything added by <see cref="AddParameterBinding"/>.</summary>
|
||||
private void DisposeParameterBindings()
|
||||
{
|
||||
if (_ParameterBindings == null)
|
||||
return;
|
||||
|
||||
for (int i = _ParameterBindings.Count - 1; i >= 0; i--)
|
||||
_ParameterBindings[i].Dispose();
|
||||
|
||||
_ParameterBindings.Clear();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// Configures all parameters in the <see cref="Controller"/>
|
||||
/// to follow the value of a parameter with the same name in the <see cref="AnimancerGraph.Parameters"/>.
|
||||
/// </summary>
|
||||
public void BindAllParameters()
|
||||
{
|
||||
var count = Playable.GetParameterCount();
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var parameter = Playable.GetParameter(i);
|
||||
BindParameter(parameter.name, parameter);
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// Configures a parameter in the <see cref="Controller"/>
|
||||
/// to follow the value of a parameter with the same name in the <see cref="AnimancerGraph.Parameters"/>.
|
||||
/// </summary>
|
||||
public void BindParameter(StringReference name)
|
||||
=> BindParameter(name, name);
|
||||
|
||||
/// <summary>
|
||||
/// Configures a parameter in the <see cref="Controller"/>
|
||||
/// to follow the value of a parameter in the <see cref="AnimancerGraph.Parameters"/>.
|
||||
/// </summary>
|
||||
public void BindParameter(StringReference animancerParameter, string controllerParameterName)
|
||||
=> BindParameter(animancerParameter, Animator.StringToHash(controllerParameterName));
|
||||
|
||||
/// <summary>
|
||||
/// Configures a parameter in the <see cref="Controller"/>
|
||||
/// to follow the value of a parameter in the <see cref="AnimancerGraph.Parameters"/>.
|
||||
/// </summary>
|
||||
public void BindParameter(StringReference animancerParameter, int controllerParameterHash)
|
||||
{
|
||||
var count = Playable.GetParameterCount();
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var parameter = Playable.GetParameter(i);
|
||||
if (parameter.nameHash == controllerParameterHash)
|
||||
{
|
||||
BindParameter(animancerParameter, parameter);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// Configures all parameters in the <see cref="Controller"/>
|
||||
/// to follow the value of a parameter with the same name in the <see cref="AnimancerGraph.Parameters"/>.
|
||||
/// </summary>
|
||||
public void BindParameter(
|
||||
StringReference animancerParameter,
|
||||
AnimatorControllerParameter controllerParameter)
|
||||
{
|
||||
switch (controllerParameter.type)
|
||||
{
|
||||
case AnimatorControllerParameterType.Float:
|
||||
BindFloat(animancerParameter, controllerParameter.nameHash);
|
||||
break;
|
||||
|
||||
case AnimatorControllerParameterType.Int:
|
||||
BindInt(animancerParameter, controllerParameter.nameHash);
|
||||
break;
|
||||
|
||||
case AnimatorControllerParameterType.Bool:
|
||||
case AnimatorControllerParameterType.Trigger:
|
||||
BindBool(animancerParameter, controllerParameter.nameHash);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// Configures a parameter in the <see cref="Controller"/>
|
||||
/// to follow the value of a parameter with the same name in the <see cref="AnimancerGraph.Parameters"/>.
|
||||
/// </summary>
|
||||
public ParameterBinding<bool> BindBool(StringReference name)
|
||||
=> BindBool(name, name);
|
||||
|
||||
/// <summary>
|
||||
/// Configures a parameter in the <see cref="Controller"/>
|
||||
/// to follow the value of a parameter in the <see cref="AnimancerGraph.Parameters"/>.
|
||||
/// </summary>
|
||||
public ParameterBinding<bool> BindBool(StringReference animancerParameter, string controllerParameterName)
|
||||
=> BindBool(animancerParameter, Animator.StringToHash(controllerParameterName));
|
||||
|
||||
/// <summary>
|
||||
/// Configures a parameter in the <see cref="Controller"/>
|
||||
/// to follow the value of a parameter in the <see cref="AnimancerGraph.Parameters"/>.
|
||||
/// </summary>
|
||||
public ParameterBinding<bool> BindBool(StringReference animancerParameter, int controllerParameterHash)
|
||||
{
|
||||
var parameter = Graph.Parameters.GetOrCreate<bool>(animancerParameter);
|
||||
var binding = new ParameterBinding<bool>(
|
||||
parameter,
|
||||
value => Playable.SetBool(controllerParameterHash, value));
|
||||
AddParameterBinding(binding);
|
||||
return binding;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// Configures a parameter in the <see cref="Controller"/>
|
||||
/// to follow the value of a parameter with the same name in the <see cref="AnimancerGraph.Parameters"/>.
|
||||
/// </summary>
|
||||
public ParameterBinding<float> BindFloat(StringReference name)
|
||||
=> BindFloat(name, name);
|
||||
|
||||
/// <summary>
|
||||
/// Configures a parameter in the <see cref="Controller"/>
|
||||
/// to follow the value of a parameter in the <see cref="AnimancerGraph.Parameters"/>.
|
||||
/// </summary>
|
||||
public ParameterBinding<float> BindFloat(StringReference animancerParameter, string controllerParameterName)
|
||||
=> BindFloat(animancerParameter, Animator.StringToHash(controllerParameterName));
|
||||
|
||||
/// <summary>
|
||||
/// Configures a parameter in the <see cref="Controller"/>
|
||||
/// to follow the value of a parameter in the <see cref="AnimancerGraph.Parameters"/>.
|
||||
/// </summary>
|
||||
public ParameterBinding<float> BindFloat(StringReference animancerParameter, int controllerParameterHash)
|
||||
{
|
||||
var parameter = Graph.Parameters.GetOrCreate<float>(animancerParameter);
|
||||
var binding = new ParameterBinding<float>(
|
||||
parameter,
|
||||
value => Playable.SetFloat(controllerParameterHash, value));
|
||||
AddParameterBinding(binding);
|
||||
return binding;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// Configures a parameter in the <see cref="Controller"/>
|
||||
/// to follow the value of a parameter with the same name in the <see cref="AnimancerGraph.Parameters"/>.
|
||||
/// </summary>
|
||||
public ParameterBinding<int> BindInt(StringReference name)
|
||||
=> BindInt(name, name);
|
||||
|
||||
/// <summary>
|
||||
/// Configures a parameter in the <see cref="Controller"/>
|
||||
/// to follow the value of a parameter in the <see cref="AnimancerGraph.Parameters"/>.
|
||||
/// </summary>
|
||||
public ParameterBinding<int> BindInt(StringReference animancerParameter, string controllerParameterName)
|
||||
=> BindInt(animancerParameter, Animator.StringToHash(controllerParameterName));
|
||||
|
||||
/// <summary>
|
||||
/// Configures a parameter in the <see cref="Controller"/>
|
||||
/// to follow the value of a parameter in the <see cref="AnimancerGraph.Parameters"/>.
|
||||
/// </summary>
|
||||
public ParameterBinding<int> BindInt(StringReference animancerParameter, int controllerParameterHash)
|
||||
{
|
||||
var parameter = Graph.Parameters.GetOrCreate<int>(animancerParameter);
|
||||
var binding = new ParameterBinding<int>(
|
||||
parameter,
|
||||
value => Playable.SetInteger(controllerParameterHash, value));
|
||||
AddParameterBinding(binding);
|
||||
return binding;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>An <see cref="IDisposable"/> binding to <see cref="Parameter{T}.OnValueChanged"/>.</summary>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer/ParameterBinding_1
|
||||
public class ParameterBinding<T> : IDisposable
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>The parameter being watched.</summary>
|
||||
public readonly Parameter<T> Parameter;
|
||||
|
||||
/// <summary>The callback to invoke when the parameter changes.</summary>
|
||||
public readonly Action<T> OnParameterChanged;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// Invokes `onParameterChanged` and adds it to the <see cref="Parameter{T}.OnValueChanged"/>
|
||||
/// to be removed by <see cref="Dispose"/>.
|
||||
/// </summary>
|
||||
public ParameterBinding(
|
||||
Parameter<T> parameter,
|
||||
Action<T> onParameterChanged)
|
||||
{
|
||||
Parameter = parameter;
|
||||
OnParameterChanged = onParameterChanged;
|
||||
|
||||
OnParameterChanged(Parameter.Value);
|
||||
Parameter.OnValueChanged += OnParameterChanged;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// Removes <see cref="OnParameterChanged"/> from the <see cref="Parameter{T}.OnValueChanged"/>.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
Parameter.OnValueChanged -= OnParameterChanged;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// A serializable array of data which can create <see cref="ParameterBinding{T}"/>s at runtime.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This data contains a <see cref="Bindings"/> array and <see cref="Mode"/> flag:
|
||||
/// <list type="bullet">
|
||||
///
|
||||
/// <item>
|
||||
/// If the array is empty,
|
||||
/// <c>true</c> will bind all parameters by name
|
||||
/// and <c>false</c> will bind nothing.
|
||||
/// </item>
|
||||
///
|
||||
/// <item>
|
||||
/// Otherwise, <c>true</c> will bind <c>[i * 2]</c> in the <see cref="RuntimeAnimatorController"/>
|
||||
/// to <c>[i * 2 + 1]</c> in the <see cref="AnimancerGraph.Parameters"/>.
|
||||
/// </item>
|
||||
///
|
||||
/// <item>
|
||||
/// And <c>false</c> will bind each of its parameters to the same name in both systems.
|
||||
/// </item>
|
||||
///
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer/SerializableParameterBindings
|
||||
[Serializable]
|
||||
public class SerializableParameterBindings :
|
||||
ICloneable<SerializableParameterBindings>
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
[SerializeField]
|
||||
private bool _Mode;
|
||||
|
||||
/// <summary>[<see cref="SerializeField"/>]
|
||||
/// Modifies the way the <see cref="Bindings"/> array is interpreted.
|
||||
/// </summary>
|
||||
/// <remarks>See the <see cref="SerializableParameterBindings"/> class for details.</remarks>
|
||||
public ref bool Mode
|
||||
=> ref _Mode;
|
||||
|
||||
#if UNITY_EDITOR
|
||||
/// <summary>[Editor-Only] The name of the serialized backing field of <see cref="Mode"/>.</summary>
|
||||
public const string ModeFieldName = nameof(_Mode);
|
||||
#endif
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>[<see cref="SerializeField"/>]
|
||||
/// Should all parameters in the <see cref="RuntimeAnimatorController"/> be bound by name?
|
||||
/// </summary>
|
||||
/// <remarks>See the <see cref="SerializableParameterBindings"/> class for details.</remarks>
|
||||
public bool BindAllParameters
|
||||
{
|
||||
get => _Mode && _Bindings.Length == 0;
|
||||
set
|
||||
{
|
||||
_Mode = value;
|
||||
|
||||
if (value)
|
||||
{
|
||||
_Bindings = Array.Empty<StringAsset>();
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.Assert(
|
||||
_Bindings.Length == 0,
|
||||
$"{nameof(BindAllParameters)} can't be disabled unless the {nameof(Bindings)}" +
|
||||
$" array is empty because it changes the meaning of that array.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>[<see cref="SerializeField"/>]
|
||||
/// Should the <see cref="Bindings"/> be grouped into pairs
|
||||
/// to bind each <see cref="RuntimeAnimatorController"/> parameter
|
||||
/// to the subsequent parameter in <see cref="AnimancerGraph.Parameters"/>?
|
||||
/// </summary>
|
||||
/// <remarks>See the <see cref="SerializableParameterBindings"/> class for details.</remarks>
|
||||
public bool RebindNames
|
||||
{
|
||||
get => _Mode && _Bindings.Length > 0;
|
||||
set
|
||||
{
|
||||
_Mode = value;
|
||||
|
||||
if (value)
|
||||
{
|
||||
if (_Bindings.Length % 2 != 0)
|
||||
Array.Resize(ref _Bindings, _Bindings.Length + 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.Assert(
|
||||
_Bindings.Length == 0,
|
||||
$"{nameof(RebindNames)} can't be disabled unless the {nameof(Bindings)}" +
|
||||
$" array is empty because it changes the meaning of that array.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
[SerializeField]
|
||||
private StringAsset[] _Bindings = Array.Empty<StringAsset>();
|
||||
|
||||
/// <summary>[<see cref="SerializeField"/>]
|
||||
/// Parameter names used to have parameters in the <see cref="RuntimeAnimatorController"/>
|
||||
/// follow the value of parameters in the <see cref="AnimancerGraph.Parameters"/>.
|
||||
/// </summary>
|
||||
/// <remarks>See the <see cref="SerializableParameterBindings"/> class for details.</remarks>
|
||||
public StringAsset[] Bindings
|
||||
{
|
||||
get => _Bindings;
|
||||
set
|
||||
{
|
||||
Debug.Assert(
|
||||
value != null,
|
||||
$"{nameof(Bindings)} can't be null. Use Array.Empty<StringAsset>() instead.");
|
||||
|
||||
_Bindings = value;
|
||||
}
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
/// <summary>[Editor-Only] The name of the serialized backing field of <see cref="Bindings"/>.</summary>
|
||||
public const string BindingsFieldName = nameof(_Bindings);
|
||||
#endif
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Creates runtime bindings for the `state`.</summary>
|
||||
/// <remarks>See the <see cref="SerializableParameterBindings"/> class for details.</remarks>
|
||||
public void Deserialize(ControllerState state)
|
||||
{
|
||||
if (_Bindings.Length == 0)
|
||||
{
|
||||
if (_Mode)
|
||||
state.BindAllParameters();
|
||||
// Else do nothing.
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_Mode)
|
||||
{
|
||||
for (int i = 0; i < _Bindings.Length - 1; i += 2)
|
||||
{
|
||||
var controller = _Bindings[i];
|
||||
var animancer = _Bindings[i + 1];
|
||||
if (controller == null ||
|
||||
animancer == null)
|
||||
continue;
|
||||
|
||||
state.BindParameter(animancer, controller);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
for (int i = 0; i < _Bindings.Length; i++)
|
||||
{
|
||||
var name = _Bindings[i];
|
||||
if (name == null)
|
||||
continue;
|
||||
|
||||
state.BindParameter(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public SerializableParameterBindings Clone(CloneContext context)
|
||||
{
|
||||
var bindingCount = Bindings != null ? Bindings.Length : 0;
|
||||
var clone = new SerializableParameterBindings()
|
||||
{
|
||||
BindAllParameters = BindAllParameters,
|
||||
Bindings = new StringAsset[bindingCount],
|
||||
};
|
||||
|
||||
for (int i = 0; i < bindingCount; i++)
|
||||
clone.Bindings[i] = context.GetCloneOrOriginal(Bindings[i]);
|
||||
|
||||
return clone;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 07d44cf3338366546b1b8a27bb72c14a
|
||||
timeCreated: 1515060256
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,887 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Animations;
|
||||
using UnityEngine.Playables;
|
||||
using Object = UnityEngine.Object;
|
||||
|
||||
namespace Animancer
|
||||
{
|
||||
/// <summary>[Pro-Only]
|
||||
/// An <see cref="AnimancerState"/> which plays a <see cref="RuntimeAnimatorController"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This state can be controlled very similarly to an <see cref="Animator"/>
|
||||
/// via its <see cref="Playable"/> property.
|
||||
/// <para></para>
|
||||
/// <strong>Documentation:</strong>
|
||||
/// <see href="https://kybernetik.com.au/animancer/docs/manual/animator-controllers">
|
||||
/// Animator Controllers</see>
|
||||
/// </remarks>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer/ControllerState
|
||||
///
|
||||
public partial class ControllerState : AnimancerState,
|
||||
ICopyable<ControllerState>,
|
||||
IParametizedState,
|
||||
IUpdatable
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
#region Fields and Properties
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private RuntimeAnimatorController _Controller;
|
||||
|
||||
/// <summary>The <see cref="RuntimeAnimatorController"/> which this state plays.</summary>
|
||||
public RuntimeAnimatorController Controller
|
||||
{
|
||||
get => _Controller;
|
||||
set => ChangeMainObject(ref _Controller, value);
|
||||
}
|
||||
|
||||
/// <summary>The <see cref="RuntimeAnimatorController"/> which this state plays.</summary>
|
||||
public override Object MainObject
|
||||
{
|
||||
get => Controller;
|
||||
set => Controller = (RuntimeAnimatorController)value;
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
/// <inheritdoc/>
|
||||
public override Type MainObjectType
|
||||
=> typeof(RuntimeAnimatorController);
|
||||
#endif
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private new AnimatorControllerPlayable _Playable;
|
||||
|
||||
/// <summary>The internal system which plays the <see cref="RuntimeAnimatorController"/>.</summary>
|
||||
public new AnimatorControllerPlayable Playable
|
||||
{
|
||||
get
|
||||
{
|
||||
Validate.AssertPlayable(this);
|
||||
return _Playable;
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Determines what a layer does when <see cref="AnimancerNode.Stop"/> is called.</summary>
|
||||
public enum ActionOnStop
|
||||
{
|
||||
/// <summary>Reset the layer to the first state it was in.</summary>
|
||||
DefaultState,
|
||||
|
||||
/// <summary>Rewind the current state's time to 0.</summary>
|
||||
RewindTime,
|
||||
|
||||
/// <summary>Allow the current state to stay at its current time.</summary>
|
||||
Continue,
|
||||
}
|
||||
|
||||
/// <summary>Determines what each layer does when <see cref="AnimancerNode.Stop"/> is called.</summary>
|
||||
/// <remarks>
|
||||
/// If empty, all layers will reset to their <see cref="ActionOnStop.DefaultState"/>.
|
||||
/// <para></para>
|
||||
/// If this array is smaller than the <see cref="AnimatorControllerPlayable.GetLayerCount"/>,
|
||||
/// any additional layers will use the last value in this array.
|
||||
/// </remarks>
|
||||
public ActionOnStop[] ActionsOnStop { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="AnimatorStateInfo.shortNameHash"/> of the default state on each layer,
|
||||
/// used to reset to those states when <see cref="ApplyActionsOnStop"/>
|
||||
/// is called for layers using <see cref="ActionOnStop.DefaultState"/>.
|
||||
/// </summary>
|
||||
/// <remarks>Gathered automatically by <see cref="GatherDefaultStates"/>.</remarks>
|
||||
public int[] DefaultStateHashes { get; set; }
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#if UNITY_ASSERTIONS
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>[Assert-Only] Animancer Events work badly on <see cref="ControllerState"/>s.</summary>
|
||||
protected internal override string UnsupportedEventsMessage =>
|
||||
"Animancer Events on " + nameof(ControllerState) + "s will probably not work as expected." +
|
||||
" The events will be associated with the entire Animator Controller and be triggered by any of the" +
|
||||
" states inside it. If you want to use events in an Animator Controller you will likely need to use" +
|
||||
" Unity's regular Animation Event system.";
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endif
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>[Assert-Conditional] Asserts that the `value` is valid for a parameter.</summary>
|
||||
/// <exception cref="ArgumentOutOfRangeException">The `value` is NaN or Infinity.</exception>
|
||||
[System.Diagnostics.Conditional(Strings.Assertions)]
|
||||
public void AssertParameterValue(float value, [CallerMemberName] string parameterName = null)
|
||||
{
|
||||
if (!value.IsFinite())
|
||||
{
|
||||
MarkAsUsed(this);
|
||||
throw new ArgumentOutOfRangeException(parameterName, Strings.MustBeFinite);
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>IK cannot be dynamically enabled on a <see cref="ControllerState"/>.</summary>
|
||||
public override void CopyIKFlags(AnimancerNodeBase copyFrom) { }
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>IK cannot be dynamically enabled on a <see cref="ControllerState"/>.</summary>
|
||||
public override bool ApplyAnimatorIK
|
||||
{
|
||||
get => false;
|
||||
set
|
||||
{
|
||||
#if UNITY_ASSERTIONS
|
||||
if (value)
|
||||
OptionalWarning.UnsupportedIK.Log(
|
||||
$"IK cannot be dynamically enabled on a {nameof(ControllerState)}." +
|
||||
" You must instead enable it on the desired layer inside the Animator Controller.",
|
||||
_Controller);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>IK cannot be dynamically enabled on a <see cref="ControllerState"/>.</summary>
|
||||
public override bool ApplyFootIK
|
||||
{
|
||||
get => false;
|
||||
set
|
||||
{
|
||||
#if UNITY_ASSERTIONS
|
||||
if (value)
|
||||
OptionalWarning.UnsupportedIK.Log(
|
||||
$"IK cannot be dynamically enabled on a {nameof(ControllerState)}." +
|
||||
" You must instead enable it on the desired state inside the Animator Controller.",
|
||||
_Controller);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Returns the hash of a parameter being wrapped by this state.</summary>
|
||||
/// <exception cref="NotSupportedException">This state doesn't wrap any parameters.</exception>
|
||||
public virtual int GetParameterHash(int index)
|
||||
{
|
||||
MarkAsUsed(this);
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual void GetParameters(List<StateParameterDetails> parameters) { }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual void SetParameters(List<StateParameterDetails> parameters) { }
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endregion
|
||||
/************************************************************************************************************************/
|
||||
#region Public API
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Creates a new <see cref="ControllerState"/> to play the `controller`.</summary>
|
||||
public ControllerState(RuntimeAnimatorController controller)
|
||||
{
|
||||
_Controller = controller != null
|
||||
? controller
|
||||
: throw new ArgumentNullException(nameof(controller));
|
||||
}
|
||||
|
||||
/// <summary>Creates a new <see cref="ControllerState"/> to play the `controller`.</summary>
|
||||
public ControllerState(RuntimeAnimatorController controller, params ActionOnStop[] actionsOnStop)
|
||||
: this(controller)
|
||||
{
|
||||
ActionsOnStop = actionsOnStop;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Creates and assigns the <see cref="AnimatorControllerPlayable"/> managed by this state.</summary>
|
||||
protected override void CreatePlayable(out Playable playable)
|
||||
{
|
||||
playable = _Playable = AnimatorControllerPlayable.Create(Graph._PlayableGraph, _Controller);
|
||||
GatherDefaultStates();
|
||||
DeserializeParameterBindings();
|
||||
|
||||
#if UNITY_ASSERTIONS
|
||||
var animator = Graph.Component?.Animator;
|
||||
if (animator != null && animator.runtimeAnimatorController != null)
|
||||
{
|
||||
var usingType = Graph.Component is HybridAnimancerComponent
|
||||
? Graph.Component.GetType()
|
||||
: GetType();
|
||||
|
||||
OptionalWarning.NativeControllerState.Log(
|
||||
$"An Animator Controller is assigned to the {nameof(Animator)} component" +
|
||||
$" while also using a {usingType.Name}." +
|
||||
$" Most likely only one of them is being used so the other should be removed." +
|
||||
$" See the documentation for more information:" +
|
||||
$" {Strings.DocsURLs.AnimatorControllers.AsHtmlLink()}",
|
||||
this);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// Stores the values of all parameters and calls <see cref="AnimancerNode.RecreatePlayable"/>,
|
||||
/// then restores the parameter values.
|
||||
/// </summary>
|
||||
public override void RecreatePlayable()
|
||||
{
|
||||
if (!_Playable.IsValid())
|
||||
{
|
||||
CreatePlayable();
|
||||
return;
|
||||
}
|
||||
|
||||
var parameterNameToValue = DictionaryPool.Acquire<string, object>();
|
||||
|
||||
var parameterCount = _Playable.GetParameterCount();
|
||||
for (int i = 0; i < parameterCount; i++)
|
||||
{
|
||||
var parameter = _Playable.GetParameter(i);
|
||||
var value = AnimancerUtilities.GetParameterValue(_Playable, parameter);
|
||||
parameterNameToValue[parameter.name] = value;
|
||||
}
|
||||
|
||||
base.RecreatePlayable();
|
||||
|
||||
parameterCount = _Playable.GetParameterCount();
|
||||
for (int i = 0; i < parameterCount; i++)
|
||||
{
|
||||
var parameter = _Playable.GetParameter(i);
|
||||
if (parameterNameToValue.TryGetValue(parameter.name, out var value))
|
||||
AnimancerUtilities.TrySetParameterValue(_Playable, parameter, value);
|
||||
}
|
||||
|
||||
DictionaryPool.Release(parameterNameToValue);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// Returns the current state on the specified `layer`,
|
||||
/// or the next state if it is currently in a transition.
|
||||
/// </summary>
|
||||
public AnimatorStateInfo GetStateInfo(int layerIndex)
|
||||
{
|
||||
if (!_Playable.IsValid())
|
||||
return default;
|
||||
|
||||
Validate.AssertPlayable(this);
|
||||
return _Playable.IsInTransition(layerIndex)
|
||||
? _Playable.GetNextAnimatorStateInfo(layerIndex)
|
||||
: _Playable.GetCurrentAnimatorStateInfo(layerIndex);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="AnimatorStateInfo.normalizedTime"/> * <see cref="AnimatorStateInfo.length"/> of layer 0.
|
||||
/// </summary>
|
||||
public override double RawTime
|
||||
{
|
||||
get
|
||||
{
|
||||
var info = GetStateInfo(0);
|
||||
return info.normalizedTime * info.length;
|
||||
}
|
||||
set
|
||||
{
|
||||
Validate.AssertPlayable(this);
|
||||
_Playable.PlayInFixedTime(0, 0, (float)value);
|
||||
|
||||
// Setting the time requires it to be playing.
|
||||
// This will leave it at the specified time.
|
||||
if (!IsPlaying)
|
||||
{
|
||||
_Playable.Play();
|
||||
Graph.RequirePostUpdate(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
int IUpdatable.UpdatableIndex { get; set; } = IUpdatable.List.NotInList;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Pauses the <see cref="Playable"/> if necessary after <see cref="RawTime"/> was set.</summary>
|
||||
void IUpdatable.Update()
|
||||
{
|
||||
if (!IsPlaying)
|
||||
_Playable.Pause();
|
||||
|
||||
AnimancerGraph.Current.CancelPostUpdate(this);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void SetGraph(AnimancerGraph graph)
|
||||
{
|
||||
if (Graph == graph)
|
||||
return;
|
||||
|
||||
Graph?.CancelPostUpdate(this);
|
||||
|
||||
base.SetGraph(graph);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>The current <see cref="AnimatorStateInfo.length"/> of layer 0.</summary>
|
||||
public override float Length => GetStateInfo(0).length;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>The current <see cref="AnimatorStateInfo.loop"/> of layer 0.</summary>
|
||||
public override bool IsLooping => GetStateInfo(0).loop;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override AnimancerEvent.DispatchInfo GetEventDispatchInfo()
|
||||
{
|
||||
var state = GetStateInfo(0);
|
||||
return new(
|
||||
state.length,
|
||||
state.normalizedTime,
|
||||
state.loop);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Gathers the <see cref="DefaultStateHashes"/> from the current states on each layer.</summary>
|
||||
/// <remarks>This is called by <see cref="CreatePlayable(out UnityEngine.Playables.Playable)"/>.</remarks>
|
||||
public void GatherDefaultStates()
|
||||
{
|
||||
Validate.AssertPlayable(this);
|
||||
|
||||
var layerCount = _Playable.GetLayerCount();
|
||||
|
||||
if (DefaultStateHashes == null || DefaultStateHashes.Length != layerCount)
|
||||
DefaultStateHashes = new int[layerCount];
|
||||
|
||||
while (--layerCount >= 0)
|
||||
DefaultStateHashes[layerCount] = _Playable.GetCurrentAnimatorStateInfo(layerCount).shortNameHash;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// Stops the animation and makes it inactive immediately so it no longer affects the output.
|
||||
/// Also calls <see cref="ApplyActionsOnStop"/>.
|
||||
/// </summary>
|
||||
protected internal override void StopWithoutWeight()
|
||||
{
|
||||
// Don't call base.StopWithoutWeight(); because it sets Time = 0;
|
||||
// which uses PlayInFixedTime and interferes with resetting to the default states.
|
||||
|
||||
SetIsPlaying(false);
|
||||
UpdateIsActive();
|
||||
|
||||
ApplyActionsOnStop();
|
||||
|
||||
_SmoothingVelocities?.Clear();
|
||||
}
|
||||
|
||||
/// <summary>Applies the <see cref="ActionsOnStop"/> to their corresponding layers.</summary>
|
||||
/// <exception cref="NullReferenceException"><see cref="DefaultStateHashes"/> is null.</exception>
|
||||
public void ApplyActionsOnStop()
|
||||
{
|
||||
Validate.AssertPlayable(this);
|
||||
|
||||
var layerCount = Math.Min(DefaultStateHashes.Length, _Playable.GetLayerCount());
|
||||
|
||||
if (ActionsOnStop == null || ActionsOnStop.Length == 0)
|
||||
{
|
||||
for (int i = layerCount - 1; i >= 0; i--)
|
||||
_Playable.Play(DefaultStateHashes[i], i, 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
for (int i = layerCount - 1; i >= 0; i--)
|
||||
{
|
||||
var index = i < ActionsOnStop.Length
|
||||
? i
|
||||
: ActionsOnStop.Length - 1;
|
||||
|
||||
switch (ActionsOnStop[index])
|
||||
{
|
||||
case ActionOnStop.DefaultState:
|
||||
_Playable.Play(DefaultStateHashes[i], i, 0);
|
||||
break;
|
||||
|
||||
case ActionOnStop.RewindTime:
|
||||
_Playable.Play(0, i, 0);
|
||||
break;
|
||||
|
||||
case ActionOnStop.Continue:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void GatherAnimationClips(ICollection<AnimationClip> clips)
|
||||
{
|
||||
if (_Controller != null)
|
||||
clips.Gather(_Controller.animationClips);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Destroy()
|
||||
{
|
||||
_Controller = null;
|
||||
DisposeParameterBindings();
|
||||
base.Destroy();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override AnimancerState Clone(CloneContext context)
|
||||
{
|
||||
var clone = new ControllerState(_Controller);
|
||||
clone.CopyFrom(this, context);
|
||||
return clone;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public sealed override void CopyFrom(AnimancerState copyFrom, CloneContext context)
|
||||
=> this.CopyFromBase(copyFrom, context);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual void CopyFrom(ControllerState copyFrom, CloneContext context)
|
||||
{
|
||||
ActionsOnStop = copyFrom.ActionsOnStop;
|
||||
|
||||
if (copyFrom.Graph != null &&
|
||||
Graph != null)
|
||||
{
|
||||
var layerCount = copyFrom._Playable.GetLayerCount();
|
||||
for (int i = 0; i < layerCount; i++)
|
||||
{
|
||||
var info = copyFrom._Playable.GetCurrentAnimatorStateInfo(i);
|
||||
_Playable.Play(info.shortNameHash, i, info.normalizedTime);
|
||||
}
|
||||
|
||||
var parameterCount = copyFrom._Playable.GetParameterCount();
|
||||
for (int i = 0; i < parameterCount; i++)
|
||||
{
|
||||
AnimancerUtilities.CopyParameterValue(
|
||||
copyFrom._Playable,
|
||||
_Playable,
|
||||
copyFrom._Playable.GetParameter(i));
|
||||
}
|
||||
}
|
||||
|
||||
CopySmoothingVelocitiesFrom(copyFrom);
|
||||
|
||||
base.CopyFrom(copyFrom, context);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endregion
|
||||
/************************************************************************************************************************/
|
||||
#region Animator Controller Wrappers
|
||||
/************************************************************************************************************************/
|
||||
#region Cross Fade
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// The default constant for fade duration parameters which causes it to use the
|
||||
/// <see cref="AnimancerGraph.DefaultFadeDuration"/> instead.
|
||||
/// </summary>
|
||||
public const float DefaultFadeDuration = -1;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// Returns the `fadeDuration` if it is zero or positive.
|
||||
/// Otherwise returns the <see cref="AnimancerGraph.DefaultFadeDuration"/>.
|
||||
/// </summary>
|
||||
public static float GetFadeDuration(float fadeDuration)
|
||||
=> fadeDuration >= 0
|
||||
? fadeDuration
|
||||
: AnimancerGraph.DefaultFadeDuration;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Starts a transition from the current state to the specified state using normalized times.</summary>
|
||||
/// <remarks>If `fadeDuration` is negative, it uses the <see cref="AnimancerGraph.DefaultFadeDuration"/>.</remarks>
|
||||
public void CrossFade(
|
||||
int stateNameHash,
|
||||
float fadeDuration = DefaultFadeDuration,
|
||||
int layer = -1,
|
||||
float normalizedTime = float.NegativeInfinity)
|
||||
=> Playable.CrossFade(stateNameHash, GetFadeDuration(fadeDuration), layer, normalizedTime);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Starts a transition from the current state to the specified state using normalized times.</summary>
|
||||
/// <remarks>If `fadeDuration` is negative, it uses the <see cref="AnimancerGraph.DefaultFadeDuration"/>.</remarks>
|
||||
public void CrossFade(
|
||||
string stateName,
|
||||
float fadeDuration = DefaultFadeDuration,
|
||||
int layer = -1,
|
||||
float normalizedTime = float.NegativeInfinity)
|
||||
=> Playable.CrossFade(stateName, GetFadeDuration(fadeDuration), layer, normalizedTime);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Starts a transition from the current state to the specified state using times in seconds.</summary>
|
||||
/// <remarks>If `fadeDuration` is negative, it uses the <see cref="AnimancerGraph.DefaultFadeDuration"/>.</remarks>
|
||||
public void CrossFadeInFixedTime(
|
||||
int stateNameHash,
|
||||
float fadeDuration = DefaultFadeDuration,
|
||||
int layer = -1,
|
||||
float fixedTime = 0)
|
||||
=> Playable.CrossFadeInFixedTime(stateNameHash, GetFadeDuration(fadeDuration), layer, fixedTime);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Starts a transition from the current state to the specified state using times in seconds.</summary>
|
||||
/// <remarks>If `fadeDuration` is negative, it uses the <see cref="AnimancerGraph.DefaultFadeDuration"/>.</remarks>
|
||||
public void CrossFadeInFixedTime(
|
||||
string stateName,
|
||||
float fadeDuration = DefaultFadeDuration,
|
||||
int layer = -1,
|
||||
float fixedTime = 0)
|
||||
=> Playable.CrossFadeInFixedTime(stateName, GetFadeDuration(fadeDuration), layer, fixedTime);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endregion
|
||||
/************************************************************************************************************************/
|
||||
#region Play
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Plays the specified state immediately, starting from a particular normalized time.</summary>
|
||||
public void Play(
|
||||
int stateNameHash,
|
||||
int layer = -1,
|
||||
float normalizedTime = float.NegativeInfinity)
|
||||
=> Playable.Play(stateNameHash, layer, normalizedTime);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Plays the specified state immediately, starting from a particular normalized time.</summary>
|
||||
public void Play(
|
||||
string stateName,
|
||||
int layer = -1,
|
||||
float normalizedTime = float.NegativeInfinity)
|
||||
=> Playable.Play(stateName, layer, normalizedTime);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Plays the specified state immediately, starting from a particular time (in seconds).</summary>
|
||||
public void PlayInFixedTime(
|
||||
int stateNameHash,
|
||||
int layer = -1,
|
||||
float fixedTime = 0)
|
||||
=> Playable.PlayInFixedTime(stateNameHash, layer, fixedTime);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Plays the specified state immediately, starting from a particular time (in seconds).</summary>
|
||||
public void PlayInFixedTime(
|
||||
string stateName,
|
||||
int layer = -1,
|
||||
float fixedTime = 0)
|
||||
=> Playable.PlayInFixedTime(stateName, layer, fixedTime);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endregion
|
||||
/************************************************************************************************************************/
|
||||
#region Parameters
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Gets the value of the specified boolean parameter.</summary>
|
||||
public bool GetBool(int id)
|
||||
=> Playable.GetBool(id);
|
||||
|
||||
/// <summary>Gets the value of the specified boolean parameter.</summary>
|
||||
public bool GetBool(string name)
|
||||
=> Playable.GetBool(name);
|
||||
|
||||
/// <summary>Sets the value of the specified boolean parameter.</summary>
|
||||
public void SetBool(int id, bool value)
|
||||
=> Playable.SetBool(id, value);
|
||||
|
||||
/// <summary>Sets the value of the specified boolean parameter.</summary>
|
||||
public void SetBool(string name, bool value)
|
||||
=> Playable.SetBool(name, value);
|
||||
|
||||
/// <summary>Gets the value of the specified float parameter.</summary>
|
||||
public float GetFloat(int id)
|
||||
=> Playable.GetFloat(id);
|
||||
|
||||
/// <summary>Gets the value of the specified float parameter.</summary>
|
||||
public float GetFloat(string name)
|
||||
=> Playable.GetFloat(name);
|
||||
|
||||
/// <summary>Sets the value of the specified float parameter.</summary>
|
||||
public void SetFloat(int id, float value)
|
||||
=> Playable.SetFloat(id, value);
|
||||
|
||||
/// <summary>Sets the value of the specified float parameter.</summary>
|
||||
public void SetFloat(string name, float value)
|
||||
=> Playable.SetFloat(name, value);
|
||||
|
||||
/// <summary>Gets the value of the specified integer parameter.</summary>
|
||||
public int GetInteger(int id)
|
||||
=> Playable.GetInteger(id);
|
||||
|
||||
/// <summary>Gets the value of the specified integer parameter.</summary>
|
||||
public int GetInteger(string name)
|
||||
=> Playable.GetInteger(name);
|
||||
|
||||
/// <summary>Sets the value of the specified integer parameter.</summary>
|
||||
public void SetInteger(int id, int value)
|
||||
=> Playable.SetInteger(id, value);
|
||||
|
||||
/// <summary>Sets the value of the specified integer parameter.</summary>
|
||||
public void SetInteger(string name, int value)
|
||||
=> Playable.SetInteger(name, value);
|
||||
|
||||
/// <summary>Sets the specified trigger parameter to true.</summary>
|
||||
public void SetTrigger(int id)
|
||||
=> Playable.SetTrigger(id);
|
||||
/// <summary>Sets the specified trigger parameter to true.</summary>
|
||||
///
|
||||
public void SetTrigger(string name)
|
||||
=> Playable.SetTrigger(name);
|
||||
/// <summary>Resets the specified trigger parameter to false.</summary>
|
||||
///
|
||||
public void ResetTrigger(int id)
|
||||
=> Playable.ResetTrigger(id);
|
||||
/// <summary>Resets the specified trigger parameter to false.</summary>
|
||||
///
|
||||
public void ResetTrigger(string name)
|
||||
=> Playable.ResetTrigger(name);
|
||||
|
||||
/// <summary>Indicates whether the specified parameter is controlled by an <see cref="AnimationClip"/>.</summary>
|
||||
public bool IsParameterControlledByCurve(int id)
|
||||
=> Playable.IsParameterControlledByCurve(id);
|
||||
|
||||
/// <summary>Indicates whether the specified parameter is controlled by an <see cref="AnimationClip"/>.</summary>
|
||||
public bool IsParameterControlledByCurve(string name)
|
||||
=> Playable.IsParameterControlledByCurve(name);
|
||||
|
||||
/// <summary>Gets the details of one of the <see cref="Controller"/>'s parameters.</summary>
|
||||
public AnimatorControllerParameter GetParameter(int index)
|
||||
=> Playable.GetParameter(index);
|
||||
|
||||
/// <summary>Gets the number of parameters in the <see cref="Controller"/>.</summary>
|
||||
public int GetParameterCount()
|
||||
=> Playable.GetParameterCount();
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>The number of parameters in the <see cref="Controller"/>.</summary>
|
||||
public int parameterCount => Playable.GetParameterCount();
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private AnimatorControllerParameter[] _Parameters;
|
||||
|
||||
/// <summary>The parameters in the <see cref="Controller"/>.</summary>
|
||||
/// <remarks>
|
||||
/// This property allocates a new array when first accessed. To avoid that, you can use
|
||||
/// <see cref="GetParameterCount"/> and <see cref="GetParameter"/> instead.
|
||||
/// </remarks>
|
||||
public AnimatorControllerParameter[] parameters
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_Parameters == null)
|
||||
{
|
||||
var count = GetParameterCount();
|
||||
_Parameters = new AnimatorControllerParameter[count];
|
||||
for (int i = 0; i < count; i++)
|
||||
_Parameters[i] = GetParameter(i);
|
||||
}
|
||||
|
||||
return _Parameters;
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endregion
|
||||
/************************************************************************************************************************/
|
||||
#region Smoothed Set Float
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private Dictionary<int, float> _SmoothingVelocities;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Sets the value of the specified float parameter with smoothing.</summary>
|
||||
/// <remarks>Consider using a <see cref="SmoothedFloatParameter"/> instead.</remarks>
|
||||
public float SetFloat(
|
||||
string name,
|
||||
float value,
|
||||
float dampTime,
|
||||
float deltaTime,
|
||||
float maxSpeed = float.PositiveInfinity)
|
||||
=> SetFloat(Animator.StringToHash(name), value, dampTime, deltaTime, maxSpeed);
|
||||
|
||||
/// <summary>Sets the value of the specified float parameter with smoothing.</summary>
|
||||
/// <remarks>Consider using a <see cref="SmoothedFloatParameter"/> instead.</remarks>
|
||||
public float SetFloat(
|
||||
int id,
|
||||
float value,
|
||||
float dampTime,
|
||||
float deltaTime,
|
||||
float maxSpeed = float.PositiveInfinity)
|
||||
{
|
||||
_SmoothingVelocities ??= new();
|
||||
|
||||
_SmoothingVelocities.TryGetValue(id, out var velocity);
|
||||
|
||||
value = Mathf.SmoothDamp(GetFloat(id), value, ref velocity, dampTime, maxSpeed, deltaTime);
|
||||
SetFloat(id, value);
|
||||
|
||||
_SmoothingVelocities[id] = velocity;
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Copies the smoothing velocities.</summary>
|
||||
private void CopySmoothingVelocitiesFrom(ControllerState copyFrom)
|
||||
{
|
||||
if (copyFrom._SmoothingVelocities != null)
|
||||
{
|
||||
if (_SmoothingVelocities == null)
|
||||
_SmoothingVelocities = new();
|
||||
else
|
||||
_SmoothingVelocities.Clear();
|
||||
|
||||
foreach (var item in copyFrom._SmoothingVelocities)
|
||||
_SmoothingVelocities[item.Key] = item.Value;
|
||||
}
|
||||
else
|
||||
{
|
||||
_SmoothingVelocities?.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endregion
|
||||
/************************************************************************************************************************/
|
||||
#region Misc
|
||||
/************************************************************************************************************************/
|
||||
// Layers.
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Gets the weight of the layer at the specified index.</summary>
|
||||
public float GetLayerWeight(int layerIndex)
|
||||
=> Playable.GetLayerWeight(layerIndex);
|
||||
/// <summary>Sets the weight of the layer at the specified index.</summary>
|
||||
public void SetLayerWeight(int layerIndex, float weight)
|
||||
=> Playable.SetLayerWeight(layerIndex, weight);
|
||||
|
||||
/// <summary>Gets the number of layers in the <see cref="Controller"/>.</summary>
|
||||
public int GetLayerCount()
|
||||
=> Playable.GetLayerCount();
|
||||
/// <summary>The number of layers in the <see cref="Controller"/>.</summary>
|
||||
public int layerCount
|
||||
=> Playable.GetLayerCount();
|
||||
|
||||
/// <summary>Gets the index of the layer with the specified name.</summary>
|
||||
public int GetLayerIndex(string layerName)
|
||||
=> Playable.GetLayerIndex(layerName);
|
||||
/// <summary>Gets the name of the layer with the specified index.</summary>
|
||||
public string GetLayerName(int layerIndex)
|
||||
=> Playable.GetLayerName(layerIndex);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
// States.
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Returns information about the current state.</summary>
|
||||
public AnimatorStateInfo GetCurrentAnimatorStateInfo(int layerIndex = 0)
|
||||
=> Playable.GetCurrentAnimatorStateInfo(layerIndex);
|
||||
/// <summary>Returns information about the next state being transitioned towards.</summary>
|
||||
public AnimatorStateInfo GetNextAnimatorStateInfo(int layerIndex = 0)
|
||||
=> Playable.GetNextAnimatorStateInfo(layerIndex);
|
||||
|
||||
/// <summary>Indicates whether the specified layer contains the specified state.</summary>
|
||||
public bool HasState(int layerIndex, int stateID)
|
||||
=> Playable.HasState(layerIndex, stateID);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
// Transitions.
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Indicates whether the specified layer is currently executing a transition.</summary>
|
||||
public bool IsInTransition(int layerIndex = 0)
|
||||
=> Playable.IsInTransition(layerIndex);
|
||||
|
||||
/// <summary>Gets information about the current transition.</summary>
|
||||
public AnimatorTransitionInfo GetAnimatorTransitionInfo(int layerIndex = 0)
|
||||
=> Playable.GetAnimatorTransitionInfo(layerIndex);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
// Clips.
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Gets information about the <see cref="AnimationClip"/>s currently being played.</summary>
|
||||
public AnimatorClipInfo[] GetCurrentAnimatorClipInfo(int layerIndex = 0)
|
||||
=> Playable.GetCurrentAnimatorClipInfo(layerIndex);
|
||||
|
||||
/// <summary>Gets information about the <see cref="AnimationClip"/>s currently being played.</summary>
|
||||
public void GetCurrentAnimatorClipInfo(int layerIndex, List<AnimatorClipInfo> clips)
|
||||
=> Playable.GetCurrentAnimatorClipInfo(layerIndex, clips);
|
||||
/// <summary>Gets the number of <see cref="AnimationClip"/>s currently being played.</summary>
|
||||
///
|
||||
public int GetCurrentAnimatorClipInfoCount(int layerIndex = 0)
|
||||
=> Playable.GetCurrentAnimatorClipInfoCount(layerIndex);
|
||||
|
||||
/// <summary>Gets information about the <see cref="AnimationClip"/>s currently being transitioned towards.</summary>
|
||||
public AnimatorClipInfo[] GetNextAnimatorClipInfo(int layerIndex = 0)
|
||||
=> Playable.GetNextAnimatorClipInfo(layerIndex);
|
||||
|
||||
/// <summary>Gets information about the <see cref="AnimationClip"/>s currently being transitioned towards.</summary>
|
||||
public void GetNextAnimatorClipInfo(int layerIndex, List<AnimatorClipInfo> clips)
|
||||
=> Playable.GetNextAnimatorClipInfo(layerIndex, clips);
|
||||
|
||||
/// <summary>Gets the number of <see cref="AnimationClip"/>s currently being transitioned towards.</summary>
|
||||
public int GetNextAnimatorClipInfoCount(int layerIndex = 0)
|
||||
=> Playable.GetNextAnimatorClipInfoCount(layerIndex);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endregion
|
||||
/************************************************************************************************************************/
|
||||
#endregion
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b23544c0fb7a6c8438dfae8cd9e105b5
|
||||
timeCreated: 1515060256
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Packages/com.kybernetik.animancer/Runtime/Core.meta
Normal file
8
Packages/com.kybernetik.animancer/Runtime/Core.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f933bcd99582bff4b908b541fc73d8c7
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
1342
Packages/com.kybernetik.animancer/Runtime/Core/AnimancerGraph.cs
Normal file
1342
Packages/com.kybernetik.animancer/Runtime/Core/AnimancerGraph.cs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bc4986a9d73f3b4459b9d99e8ce9066b
|
||||
timeCreated: 1515048758
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,305 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Playables;
|
||||
|
||||
namespace Animancer
|
||||
{
|
||||
/// <summary>A list of <see cref="AnimancerLayer"/>s with methods to control their mixing and masking.</summary>
|
||||
/// <remarks>
|
||||
/// The default implementation of this class is <see cref="AnimancerLayerMixerList"/>.
|
||||
/// <para></para>
|
||||
/// <strong>Documentation:</strong>
|
||||
/// <see href="https://kybernetik.com.au/animancer/docs/manual/blending/layers">
|
||||
/// Layers</see>
|
||||
/// </remarks>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer/AnimancerLayerList
|
||||
public abstract class AnimancerLayerList :
|
||||
IEnumerable<AnimancerLayer>,
|
||||
IAnimationClipCollection
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
#region Fields
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>The <see cref="AnimancerGraph"/> containing this list.</summary>
|
||||
public readonly AnimancerGraph Graph;
|
||||
|
||||
/// <summary>The layers which each manage their own set of animations.</summary>
|
||||
/// <remarks>This field should never be null so it shouldn't need null-checking.</remarks>
|
||||
private AnimancerLayer[] _Layers;
|
||||
|
||||
/// <summary>The number of layers that have actually been created.</summary>
|
||||
private int _Count;
|
||||
|
||||
/// <summary>The <see cref="UnityEngine.Playables.Playable"/> which blends the layers.</summary>
|
||||
public Playable Playable { get; protected set; }
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Creates a new <see cref="AnimancerLayerList"/>.</summary>
|
||||
/// <remarks>The <see cref="Playable"/> must be assigned by the end of the derived constructor.</remarks>
|
||||
protected AnimancerLayerList(AnimancerGraph graph, int capacity)
|
||||
{
|
||||
Graph = graph;
|
||||
_Layers = new AnimancerLayer[capacity];
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endregion
|
||||
/************************************************************************************************************************/
|
||||
#region List Operations
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>[Pro-Only] The number of layers in this list.</summary>
|
||||
/// <exception cref="ArgumentOutOfRangeException">
|
||||
/// The value is set higher than the <see cref="DefaultCapacity"/>. This is simply a safety measure,
|
||||
/// so if you do actually need more layers you can just increase the limit.
|
||||
/// </exception>
|
||||
/// <exception cref="IndexOutOfRangeException">The value is set to a negative number.</exception>
|
||||
public int Count
|
||||
{
|
||||
get => _Count;
|
||||
set
|
||||
{
|
||||
var count = _Count;
|
||||
|
||||
if (value == count)
|
||||
return;
|
||||
|
||||
CheckAgain:
|
||||
|
||||
if (value > count)// Increasing.
|
||||
{
|
||||
Add();
|
||||
count++;
|
||||
goto CheckAgain;
|
||||
}
|
||||
else// Decreasing.
|
||||
{
|
||||
while (value < count--)
|
||||
{
|
||||
var layer = _Layers[count];
|
||||
if (layer._Playable.IsValid())
|
||||
Graph._PlayableGraph.DestroySubgraph(layer._Playable);
|
||||
layer.DestroyStates();
|
||||
}
|
||||
|
||||
Array.Clear(_Layers, value, _Count - value);
|
||||
|
||||
_Count = value;
|
||||
|
||||
Playable.SetInputCount(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>[Pro-Only]
|
||||
/// If the <see cref="Count"/> is below the specified `min`, this method increases it to that value.
|
||||
/// </summary>
|
||||
public void SetMinCount(int min)
|
||||
{
|
||||
if (Count < min)
|
||||
Count = min;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>[Pro-Only]
|
||||
/// The maximum number of layers that can be created before an <see cref="ArgumentOutOfRangeException"/> will
|
||||
/// be thrown (default 4).
|
||||
/// <para></para>
|
||||
/// Lowering this value will not affect layers that have already been created.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <strong>Example:</strong>
|
||||
/// To set this value automatically when the application starts, place a method like this in any class:
|
||||
/// <para></para><code>
|
||||
/// [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
|
||||
/// private static void SetMaxLayerCount()
|
||||
/// {
|
||||
/// Animancer.AnimancerLayerList.DefaultCapacity = 8;
|
||||
/// }
|
||||
/// </code>
|
||||
/// Otherwise you can set the <see cref="Capacity"/> of each individual list:
|
||||
/// <para></para><code>
|
||||
/// AnimancerComponent animancer;
|
||||
/// animancer.Layers.Capacity = 8;
|
||||
/// </code></remarks>
|
||||
public static int DefaultCapacity { get; set; } = 4;
|
||||
|
||||
/// <summary>[Pro-Only]
|
||||
/// If the <see cref="DefaultCapacity"/> is below the specified `min`, this method increases it to that value.
|
||||
/// </summary>
|
||||
public static void SetMinDefaultCapacity(int min)
|
||||
{
|
||||
if (DefaultCapacity < min)
|
||||
DefaultCapacity = min;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>[Pro-Only]
|
||||
/// The maximum number of layers that can be created before an <see cref="ArgumentOutOfRangeException"/> will
|
||||
/// be thrown. The initial capacity is determined by <see cref="DefaultCapacity"/>.
|
||||
/// </summary>
|
||||
///
|
||||
/// <remarks>
|
||||
/// Lowering this value will destroy any layers beyond the specified value.
|
||||
/// <para></para>
|
||||
/// Changing this value will cause the allocation of a new array and garbage collection of the old one,
|
||||
/// so you should generally set the <see cref="DefaultCapacity"/> before initializing this list.
|
||||
/// </remarks>
|
||||
///
|
||||
/// <exception cref="ArgumentOutOfRangeException">The value is not greater than 0.</exception>
|
||||
public int Capacity
|
||||
{
|
||||
get => _Layers.Length;
|
||||
set
|
||||
{
|
||||
if (value <= 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(Capacity), $"must be greater than 0 ({value} <= 0)");
|
||||
|
||||
if (_Count > value)
|
||||
Count = value;
|
||||
|
||||
Array.Resize(ref _Layers, value);
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>[Pro-Only] Creates and returns a new <see cref="AnimancerLayer"/> at the end of this list.</summary>
|
||||
/// <remarks>If the <see cref="Capacity"/> would be exceeded, it will be doubled.</remarks>
|
||||
public virtual AnimancerLayer Add()
|
||||
{
|
||||
var index = _Count;
|
||||
|
||||
if (index >= _Layers.Length)
|
||||
Capacity *= 2;
|
||||
|
||||
var layer = new AnimancerLayer(Graph, index);
|
||||
|
||||
_Count = index + 1;
|
||||
Playable.SetInputCount(_Count);
|
||||
Graph._PlayableGraph.Connect(Playable, layer._Playable, index, 0);
|
||||
|
||||
_Layers[index] = layer;
|
||||
return layer;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Returns the layer at the specified index. If it didn't already exist, this method creates it.</summary>
|
||||
/// <remarks>To only get an existing layer without creating new ones, use <see cref="GetLayer"/> instead.</remarks>
|
||||
public AnimancerLayer this[int index]
|
||||
{
|
||||
get
|
||||
{
|
||||
SetMinCount(index + 1);
|
||||
return _Layers[index];
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Returns the layer at the specified index.</summary>
|
||||
/// <remarks>To create a new layer if the target doesn't exist, use <see cref="this[int]"/> instead.</remarks>
|
||||
public AnimancerLayer GetLayer(int index)
|
||||
=> _Layers[index];
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endregion
|
||||
/************************************************************************************************************************/
|
||||
#region Enumeration
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Returns an enumerator that will iterate through all layers.</summary>
|
||||
public FastEnumerator<AnimancerLayer> GetEnumerator()
|
||||
=> new(_Layers, _Count);
|
||||
|
||||
/// <inheritdoc/>
|
||||
IEnumerator<AnimancerLayer> IEnumerable<AnimancerLayer>.GetEnumerator()
|
||||
=> GetEnumerator();
|
||||
|
||||
/// <inheritdoc/>
|
||||
IEnumerator IEnumerable.GetEnumerator()
|
||||
=> GetEnumerator();
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>[<see cref="IAnimationClipCollection"/>] Gathers all the animations in all layers.</summary>
|
||||
public void GatherAnimationClips(ICollection<AnimationClip> clips)
|
||||
=> clips.GatherFromSource(_Layers);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endregion
|
||||
/************************************************************************************************************************/
|
||||
#region Layer Details
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>[Pro-Only]
|
||||
/// Is the layer at the specified index is set to additive blending?
|
||||
/// Otherwise it will override lower layers.
|
||||
/// </summary>
|
||||
public virtual bool IsAdditive(int index)
|
||||
=> false;
|
||||
|
||||
/// <summary>[Pro-Only]
|
||||
/// Sets the layer at the specified index to blend additively with earlier layers (if true)
|
||||
/// or to override them (if false). Newly created layers will override by default.
|
||||
/// </summary>
|
||||
public virtual void SetAdditive(int index, bool value) { }
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>[Pro-Only]
|
||||
/// Sets an <see cref="AvatarMask"/> to determine which bones the layer at the specified index will affect.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Don't assign the same mask repeatedly unless you have modified it.
|
||||
/// This property doesn't check if the mask is the same
|
||||
/// so repeatedly assigning the same thing will simply waste performance.
|
||||
/// </remarks>
|
||||
public virtual void SetMask(int index, AvatarMask mask) { }
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>[Editor-Conditional] Sets the Inspector display name of the layer at the specified index.</summary>
|
||||
[System.Diagnostics.Conditional(Strings.UnityEditor)]
|
||||
public void SetDebugName(int index, string name)
|
||||
=> this[index].SetDebugName(name);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// The average velocity of the root motion of all currently playing animations,
|
||||
/// taking their current <see cref="AnimancerNode.Weight"/> into account.
|
||||
/// </summary>
|
||||
public Vector3 AverageVelocity
|
||||
{
|
||||
get
|
||||
{
|
||||
var velocity = default(Vector3);
|
||||
|
||||
for (int i = 0; i < _Count; i++)
|
||||
{
|
||||
var layer = _Layers[i];
|
||||
velocity += layer.AverageVelocity * layer.Weight;
|
||||
}
|
||||
|
||||
return velocity;
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endregion
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d4b81a5fd72a01f488ba1bec416742bc
|
||||
timeCreated: 1515048758
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,75 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
using UnityEngine;
|
||||
using UnityEngine.Animations;
|
||||
|
||||
namespace Animancer
|
||||
{
|
||||
/// <summary>An <see cref="AnimancerLayerList"/> which uses an <see cref="AnimationLayerMixerPlayable"/>.</summary>
|
||||
/// <remarks>
|
||||
/// <strong>Documentation:</strong>
|
||||
/// <see href="https://kybernetik.com.au/animancer/docs/manual/blending/layers">
|
||||
/// Layers</see>
|
||||
/// </remarks>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer/AnimancerLayerMixerList
|
||||
public class AnimancerLayerMixerList : AnimancerLayerList
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Creates a new <see cref="AnimancerLayerMixerList"/>.</summary>
|
||||
public AnimancerLayerMixerList(AnimancerGraph graph)
|
||||
: base(graph, DefaultCapacity)
|
||||
{
|
||||
LayerMixer = AnimationLayerMixerPlayable.Create(graph._PlayableGraph, 1);
|
||||
Playable = LayerMixer;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>The <see cref="AnimationLayerMixerPlayable"/> which blends the layers.</summary>
|
||||
public AnimationLayerMixerPlayable LayerMixer { get; protected set; }
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool IsAdditive(int index)
|
||||
=> LayerMixer.IsLayerAdditive((uint)index);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void SetAdditive(int index, bool value)
|
||||
{
|
||||
SetMinCount(index + 1);
|
||||
LayerMixer.SetLayerAdditive((uint)index, value);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private static AvatarMask _DefaultMask;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void SetMask(int index, AvatarMask mask)
|
||||
{
|
||||
var layer = this[index];
|
||||
|
||||
if (mask == null)
|
||||
{
|
||||
// If the existing mask was already null, do nothing.
|
||||
// If it was destroyed, we still need to continue and set it to the default.
|
||||
if (layer._Mask is null)
|
||||
return;
|
||||
|
||||
_DefaultMask ??= new();
|
||||
|
||||
mask = _DefaultMask;
|
||||
}
|
||||
|
||||
// Don't check if the same mask was already assigned because it might have been modified.
|
||||
layer._Mask = mask;
|
||||
|
||||
LayerMixer.SetLayerMaskFromAvatarMask((uint)index, mask);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a09a302426304ec4b80cfd37106827f4
|
||||
timeCreated: 1515048758
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,537 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Animancer
|
||||
{
|
||||
/// <summary>A dictionary of <see cref="AnimancerState"/>s mapped to their <see cref="AnimancerState.Key"/>.</summary>
|
||||
/// <remarks>
|
||||
/// <strong>Documentation:</strong>
|
||||
/// <see href="https://kybernetik.com.au/animancer/docs/manual/playing/states">
|
||||
/// States</see>
|
||||
/// </remarks>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer/AnimancerStateDictionary
|
||||
public class AnimancerStateDictionary :
|
||||
IAnimationClipCollection,
|
||||
IEnumerable<AnimancerState>
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>The <see cref="AnimancerGraph"/> at the root of the graph.</summary>
|
||||
private readonly AnimancerGraph Graph;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary><see cref="AnimancerState.Key"/> mapped to <see cref="AnimancerState"/>.</summary>
|
||||
private readonly Dictionary<object, AnimancerState>
|
||||
States = new();
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>[Internal] Creates a new <see cref="AnimancerStateDictionary"/>.</summary>
|
||||
internal AnimancerStateDictionary(AnimancerGraph graph)
|
||||
=> Graph = graph;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>The number of states that have been registered with a <see cref="AnimancerState.Key"/>.</summary>
|
||||
public int Count
|
||||
=> States.Count;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void AppendDescriptionOrOrphans(
|
||||
StringBuilder text,
|
||||
string separator = "\n")
|
||||
{
|
||||
string stateSeparator = null;
|
||||
|
||||
foreach (var state in States.Values)
|
||||
{
|
||||
if (state.Parent != null)
|
||||
continue;
|
||||
|
||||
if (stateSeparator is null)
|
||||
{
|
||||
text.Append(separator)
|
||||
.Append("Orphan States:");
|
||||
|
||||
separator += Strings.Indent;
|
||||
stateSeparator = separator + Strings.Indent;
|
||||
}
|
||||
|
||||
text.Append(separator);
|
||||
state.AppendDescription(text, stateSeparator);
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#region Create
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Creates and returns a new <see cref="ClipState"/> to play the `clip`.</summary>
|
||||
/// <remarks>
|
||||
/// To create a state on a specific layer, use <c>animancer.Layers[x].CreateState(clip)</c> instead.
|
||||
/// <para></para>
|
||||
/// <see cref="AnimancerGraph.GetKey"/> is used to determine the <see cref="AnimancerState.Key"/>.
|
||||
/// </remarks>
|
||||
public ClipState Create(AnimationClip clip)
|
||||
=> Create(Graph.GetKey(clip), clip);
|
||||
|
||||
/// <summary>
|
||||
/// Creates and returns a new <see cref="ClipState"/> to play the `clip` and registers it with the `key`.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// To create a state on a specific layer, use <c>animancer.Layers[x].CreateState(key, clip)</c> instead.
|
||||
/// </remarks>
|
||||
public ClipState Create(object key, AnimationClip clip)
|
||||
{
|
||||
var state = new ClipState(clip);
|
||||
state.SetGraph(Graph);
|
||||
state._Key = key;
|
||||
Register(state);
|
||||
return state;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Calls <see cref="GetOrCreate(AnimationClip, bool)"/> for each of the specified clips.</summary>
|
||||
public void CreateIfNew(AnimationClip clip0, AnimationClip clip1)
|
||||
{
|
||||
GetOrCreate(clip0);
|
||||
GetOrCreate(clip1);
|
||||
}
|
||||
|
||||
/// <summary>Calls <see cref="GetOrCreate(AnimationClip, bool)"/> for each of the specified clips.</summary>
|
||||
public void CreateIfNew(AnimationClip clip0, AnimationClip clip1, AnimationClip clip2)
|
||||
{
|
||||
GetOrCreate(clip0);
|
||||
GetOrCreate(clip1);
|
||||
GetOrCreate(clip2);
|
||||
}
|
||||
|
||||
/// <summary>Calls <see cref="GetOrCreate(AnimationClip, bool)"/> for each of the specified clips.</summary>
|
||||
public void CreateIfNew(AnimationClip clip0, AnimationClip clip1, AnimationClip clip2, AnimationClip clip3)
|
||||
{
|
||||
GetOrCreate(clip0);
|
||||
GetOrCreate(clip1);
|
||||
GetOrCreate(clip2);
|
||||
GetOrCreate(clip3);
|
||||
}
|
||||
|
||||
/// <summary>Calls <see cref="GetOrCreate(AnimationClip, bool)"/> for each of the specified `clips`.</summary>
|
||||
public void CreateIfNew(params AnimationClip[] clips)
|
||||
{
|
||||
if (clips == null)
|
||||
return;
|
||||
|
||||
var count = clips.Length;
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var clip = clips[i];
|
||||
if (clip != null)
|
||||
GetOrCreate(clip);
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endregion
|
||||
/************************************************************************************************************************/
|
||||
#region Access
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="AnimancerLayer.CurrentState"/> on layer 0.
|
||||
/// <para></para>
|
||||
/// Specifically, this is the state that was most recently started using any of the Play methods on that layer.
|
||||
/// States controlled individually via methods in the <see cref="AnimancerState"/> itself will not register in
|
||||
/// this property.
|
||||
/// </summary>
|
||||
public AnimancerState Current
|
||||
=> Graph.Layers[0].CurrentState;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Calls <see cref="AnimancerGraph.GetKey"/> then returns the state registered with that key.</summary>
|
||||
/// <exception cref="ArgumentNullException">The key is null.</exception>
|
||||
/// <exception cref="KeyNotFoundException">No state is registered with the key.</exception>
|
||||
public AnimancerState this[AnimationClip clip]
|
||||
=> States[Graph.GetKey(clip)];
|
||||
|
||||
/// <summary>Returns the state registered with the <see cref="IHasKey.Key"/>.</summary>
|
||||
/// <exception cref="ArgumentNullException">The `key` is null.</exception>
|
||||
/// <exception cref="KeyNotFoundException">No state is registered with the `key`.</exception>
|
||||
public AnimancerState this[IHasKey hasKey]
|
||||
=> States[hasKey.Key];
|
||||
|
||||
/// <summary>Returns the state registered with the `key`.</summary>
|
||||
/// <exception cref="ArgumentNullException">The `key` is null.</exception>
|
||||
/// <exception cref="KeyNotFoundException">No state is registered with the `key`.</exception>
|
||||
public AnimancerState this[object key]
|
||||
=> States[key];
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// Calls <see cref="AnimancerGraph.GetKey"/> then passes the key to
|
||||
/// <see cref="TryGet(object, out AnimancerState)"/> and returns the result.
|
||||
/// </summary>
|
||||
public bool TryGet(AnimationClip clip, out AnimancerState state)
|
||||
{
|
||||
if (clip == null)
|
||||
{
|
||||
state = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
return TryGet(Graph.GetKey(clip), out state);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Passes the <see cref="IHasKey.Key"/> into <see cref="TryGet(object, out AnimancerState)"/>
|
||||
/// and returns the result.
|
||||
/// </summary>
|
||||
public bool TryGet(IHasKey hasKey, out AnimancerState state)
|
||||
=> TryGet(hasKey?.Key, out state);
|
||||
|
||||
/// <summary>
|
||||
/// If a `state` is registered with the `key`, this method outputs it and returns true.
|
||||
/// Otherwise the `state` is set to null and this method returns false.
|
||||
/// </summary>
|
||||
public bool TryGet(object key, out AnimancerState state)
|
||||
{
|
||||
if (key == null)
|
||||
{
|
||||
state = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
return States.TryGetValue(key, out state);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// Passes the <see cref="IHasKey.Key"/> into <see cref="TryGetAlias(object, out AnimancerState)"/>
|
||||
/// and returns the result.
|
||||
/// </summary>
|
||||
public bool TryGetAlias(IHasKey hasKey, out AnimancerState state)
|
||||
=> TryGetAlias(hasKey?.Key, out state);
|
||||
|
||||
/// <summary>
|
||||
/// If a `state` is registered with the `key` or the <see cref="TransitionLibraries.TransitionLibrary"/>,
|
||||
/// is using it as an alias, this method outputs it and returns true.
|
||||
/// Otherwise the `state` is set to null and this method returns false.
|
||||
/// </summary>
|
||||
public bool TryGetAlias(object key, out AnimancerState state)
|
||||
{
|
||||
if (key == null)
|
||||
{
|
||||
state = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Graph.Transitions != null &&
|
||||
Graph.Transitions.TryGetTransition(key, out var group))
|
||||
key = group.Transition.Key;
|
||||
|
||||
return States.TryGetValue(key, out state);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// Calls <see cref="AnimancerGraph.GetKey"/> and returns the state registered with that key
|
||||
/// or creates one if it doesn't exist.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// If the state already exists but has the wrong <see cref="AnimancerState.Clip"/>, the `allowSetClip`
|
||||
/// parameter determines what will happen. False causes it to throw an <see cref="ArgumentException"/> while
|
||||
/// true allows it to change the <see cref="AnimancerState.Clip"/>. Note that the change is somewhat costly to
|
||||
/// performance so use with caution.
|
||||
/// </remarks>
|
||||
/// <exception cref="ArgumentException"/>
|
||||
public AnimancerState GetOrCreate(AnimationClip clip, bool allowSetClip = false)
|
||||
=> GetOrCreate(Graph.GetKey(clip), clip, allowSetClip);
|
||||
|
||||
/// <summary>
|
||||
/// Returns the state registered with the `transition`s <see cref="IHasKey.Key"/> if there is one.
|
||||
/// Otherwise this method uses <see cref="ITransition.CreateState"/> to create a new one
|
||||
/// and registers it with that key before returning it.
|
||||
/// </summary>
|
||||
public AnimancerState GetOrCreate(ITransition transition)
|
||||
{
|
||||
var key = transition.Key;
|
||||
if (!TryGet(key, out var state))
|
||||
{
|
||||
state = transition.CreateState();
|
||||
state._Key = key;
|
||||
state.SetGraph(Graph);
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the state which registered with the `key` or creates one if it doesn't exist.
|
||||
/// <para></para>
|
||||
/// If the state already exists but has the wrong <see cref="AnimancerState.Clip"/>, the `allowSetClip`
|
||||
/// parameter determines what will happen. False causes it to throw an <see cref="ArgumentException"/>
|
||||
/// while true allows it to change the <see cref="AnimancerState.Clip"/>.
|
||||
/// Note that the change is somewhat costly to performance so use with caution.
|
||||
/// </summary>
|
||||
/// <exception cref="ArgumentException"/>
|
||||
/// <remarks>See also: <see cref="AnimancerLayer.GetOrCreateState(object, AnimationClip, bool)"/></remarks>
|
||||
public AnimancerState GetOrCreate(object key, AnimationClip clip, bool allowSetClip = false)
|
||||
{
|
||||
if (TryGet(key, out var state))
|
||||
{
|
||||
// If a state exists with the 'key' but has the wrong clip, either change it or complain.
|
||||
if (!ReferenceEquals(state.Clip, clip))
|
||||
{
|
||||
if (allowSetClip)
|
||||
{
|
||||
state.Clip = clip;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new ArgumentException(GetClipMismatchError(key, state.Clip, clip));
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
state = Create(key, clip);
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Returns an error message explaining that a state already exists with the specified `key`.</summary>
|
||||
public static string GetClipMismatchError(object key, AnimationClip oldClip, AnimationClip newClip)
|
||||
=> $"A state already exists using the specified '{nameof(key)}', but has a different {nameof(AnimationClip)}:" +
|
||||
$"\n• Key: {key}" +
|
||||
$"\n• Old Clip: {oldClip}" +
|
||||
$"\n• New Clip: {newClip}";
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>[Internal]
|
||||
/// Registers the `state` in this dictionary so the <see cref="AnimancerState.Key"/> can be used to get it
|
||||
/// later on using any of the lookup methods such as <see cref="this[object]"/> or
|
||||
/// <see cref="TryGet(object, out AnimancerState)"/>.
|
||||
/// </summary>
|
||||
/// <remarks>Does nothing if the <see cref="AnimancerState.Key"/> is <c>null</c>.</remarks>
|
||||
internal void Register(AnimancerState state)
|
||||
{
|
||||
var key = state._Key;
|
||||
if (key != null)
|
||||
{
|
||||
#if UNITY_ASSERTIONS
|
||||
if (state.Graph != Graph)
|
||||
throw new ArgumentException(
|
||||
$"{nameof(AnimancerStateDictionary)} cannot register a state with a different {nameof(Graph)}: " + state);
|
||||
#endif
|
||||
|
||||
States.Add(key, state);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>[Internal] Removes the `state` from this dictionary (the opposite of <see cref="Register"/>).</summary>
|
||||
internal void Unregister(AnimancerState state)
|
||||
{
|
||||
var key = state._Key;
|
||||
if (key != null)
|
||||
States.Remove(key);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#region Enumeration
|
||||
/************************************************************************************************************************/
|
||||
// IEnumerable for 'foreach' statements.
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Returns an enumerator that will iterate through all registered states.</summary>
|
||||
public Dictionary<object, AnimancerState>.ValueCollection.Enumerator GetEnumerator()
|
||||
=> States.Values.GetEnumerator();
|
||||
|
||||
/// <inheritdoc/>
|
||||
IEnumerator<AnimancerState> IEnumerable<AnimancerState>.GetEnumerator()
|
||||
=> GetEnumerator();
|
||||
|
||||
/// <inheritdoc/>
|
||||
IEnumerator IEnumerable.GetEnumerator()
|
||||
=> GetEnumerator();
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>[<see cref="IAnimationClipCollection"/>]
|
||||
/// Adds all the animations of states with a <see cref="AnimancerState.Key"/> to the `clips`.
|
||||
/// </summary>
|
||||
public void GatherAnimationClips(ICollection<AnimationClip> clips)
|
||||
{
|
||||
foreach (var state in States.Values)
|
||||
clips.GatherFromSource(state);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endregion
|
||||
/************************************************************************************************************************/
|
||||
#endregion
|
||||
/************************************************************************************************************************/
|
||||
#region Destroy
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// Calls <see cref="AnimancerState.Destroy"/> on the state associated with the `clip` (if any).
|
||||
/// Returns true if the state existed.
|
||||
/// </summary>
|
||||
public bool Destroy(AnimationClip clip)
|
||||
{
|
||||
if (clip == null)
|
||||
return false;
|
||||
|
||||
return Destroy(Graph.GetKey(clip));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calls <see cref="AnimancerState.Destroy"/> on the state associated with the <see cref="IHasKey.Key"/>
|
||||
/// (if any). Returns true if the state existed.
|
||||
/// </summary>
|
||||
public bool Destroy(IHasKey hasKey)
|
||||
{
|
||||
if (hasKey == null)
|
||||
return false;
|
||||
|
||||
return Destroy(hasKey.Key);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calls <see cref="AnimancerState.Destroy"/> on the state associated with the `key` (if any).
|
||||
/// Returns true if the state existed.
|
||||
/// </summary>
|
||||
public bool Destroy(object key)
|
||||
{
|
||||
if (!TryGet(key, out var state))
|
||||
return false;
|
||||
|
||||
state.Destroy();
|
||||
return true;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Calls <see cref="Destroy(AnimationClip)"/> on each of the `clips`.</summary>
|
||||
public void DestroyAll(IList<AnimationClip> clips)
|
||||
{
|
||||
if (clips == null)
|
||||
return;
|
||||
|
||||
for (int i = clips.Count - 1; i >= 0; i--)
|
||||
Destroy(clips[i]);
|
||||
}
|
||||
|
||||
/// <summary>Calls <see cref="Destroy(AnimationClip)"/> on each of the `clips`.</summary>
|
||||
public void DestroyAll(IEnumerable<AnimationClip> clips)
|
||||
{
|
||||
if (clips == null)
|
||||
return;
|
||||
|
||||
foreach (var clip in clips)
|
||||
Destroy(clip);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// Calls <see cref="Destroy(AnimationClip)"/> on all states gathered by
|
||||
/// <see cref="IAnimationClipSource.GetAnimationClips"/>.
|
||||
/// </summary>
|
||||
public void DestroyAll(IAnimationClipSource source)
|
||||
{
|
||||
if (source == null)
|
||||
return;
|
||||
|
||||
var clips = ListPool.Acquire<AnimationClip>();
|
||||
source.GetAnimationClips(clips);
|
||||
DestroyAll(clips);
|
||||
ListPool.Release(clips);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calls <see cref="Destroy(AnimationClip)"/> on all states gathered by
|
||||
/// <see cref="IAnimationClipCollection.GatherAnimationClips"/>.
|
||||
/// </summary>
|
||||
public void DestroyAll(IAnimationClipCollection source)
|
||||
{
|
||||
if (source == null)
|
||||
return;
|
||||
|
||||
var clips = SetPool.Acquire<AnimationClip>();
|
||||
source.GatherAnimationClips(clips);
|
||||
DestroyAll(clips);
|
||||
SetPool.Release(clips);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endregion
|
||||
/************************************************************************************************************************/
|
||||
#region Key Error Methods
|
||||
#if UNITY_EDITOR
|
||||
/************************************************************************************************************************/
|
||||
// These are overloads of other methods that take a System.Object key to ensure the user doesn't try to use an
|
||||
// AnimancerState as a key, since the whole point of a key is to identify a state in the first place.
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>[Warning]
|
||||
/// You should not use an <see cref="AnimancerState"/> as a key.
|
||||
/// The whole point of a key is to identify a state in the first place.
|
||||
/// </summary>
|
||||
[Obsolete("You should not use an AnimancerState as a key. The whole point of a key is to identify a state in the first place.", true)]
|
||||
public AnimancerState this[AnimancerState key]
|
||||
=> key;
|
||||
|
||||
/// <summary>[Warning]
|
||||
/// You should not use an <see cref="AnimancerState"/> as a key.
|
||||
/// The whole point of a key is to identify a state in the first place.
|
||||
/// </summary>
|
||||
[Obsolete("You should not use an AnimancerState as a key. The whole point of a key is to identify a state in the first place.", true)]
|
||||
public bool TryGet(AnimancerState key, out AnimancerState state)
|
||||
{
|
||||
state = key;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>[Warning]
|
||||
/// You should not use an <see cref="AnimancerState"/> as a key.
|
||||
/// The whole point of a key is to identify a state in the first place.
|
||||
/// </summary>
|
||||
[Obsolete("You should not use an AnimancerState as a key. The whole point of a key is to identify a state in the first place.", true)]
|
||||
public AnimancerState GetOrCreate(AnimancerState key, AnimationClip clip)
|
||||
=> key;
|
||||
|
||||
/// <summary>[Warning]
|
||||
/// You should not use an <see cref="AnimancerState"/> as a key.
|
||||
/// Just call <see cref="AnimancerState.Destroy"/>.
|
||||
/// </summary>
|
||||
[Obsolete("You should not use an AnimancerState as a key. Just call AnimancerState.Destroy.", true)]
|
||||
public bool Destroy(AnimancerState key)
|
||||
{
|
||||
key.Destroy();
|
||||
return true;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endif
|
||||
#endregion
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3cf7dec197d38394fb2b06ad8799801f
|
||||
timeCreated: 1515048758
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
1444
Packages/com.kybernetik.animancer/Runtime/Core/AnimancerUtilities.cs
Normal file
1444
Packages/com.kybernetik.animancer/Runtime/Core/AnimancerUtilities.cs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5951f73829d4fe5478c69d982a08e77e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 89969125b8d531b45b2e18a548ac6b92
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,551 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Animancer
|
||||
{
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer/AnimancerEvent
|
||||
partial struct AnimancerEvent
|
||||
{
|
||||
/// <summary>Details about a state which determine how it triggers events.</summary>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer/DispatchInfo
|
||||
public readonly struct DispatchInfo
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary><see cref="AnimancerState.Length"/></summary>
|
||||
public readonly float Length;
|
||||
|
||||
/// <summary><see cref="AnimancerState.NormalizedTime"/></summary>
|
||||
public readonly float NormalizedTime;
|
||||
|
||||
/// <summary><see cref="AnimancerState.IsLooping"/></summary>
|
||||
public readonly bool IsLooping;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Creates a new <see cref="DispatchInfo"/>.</summary>
|
||||
public DispatchInfo(float length, float normalizedTime, bool isLooping)
|
||||
{
|
||||
Length = length;
|
||||
NormalizedTime = normalizedTime;
|
||||
IsLooping = isLooping;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A system which triggers events in an <see cref="Sequence"/>
|
||||
/// based on a target <see cref="State"/>.
|
||||
/// </summary>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer/Dispatcher
|
||||
public class Dispatcher : IHasDescription
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>The target state.</summary>
|
||||
public readonly AnimancerState State;
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="AnimancerState.OwnedEvents"/> and
|
||||
/// <see cref="AnimancerState.SharedEvents"/>.
|
||||
/// </summary>
|
||||
/// <remarks>Should never be null.</remarks>
|
||||
public Sequence Events { get; private set; }
|
||||
|
||||
/// <summary><see cref="AnimancerState.HasOwnedEvents"/></summary>
|
||||
public bool HasOwnEvents { get; private set; }
|
||||
|
||||
private float _PreviousNormalizedTime;
|
||||
private int _NextEventIndex = RecalculateEventIndex;
|
||||
private int _SequenceVersion = -1;// When version changes, next event index is invalid.
|
||||
private bool _WasPlayingForwards;// When direction changes, next event index is invalid.
|
||||
|
||||
/// <summary>
|
||||
/// A special value for the <see cref="_NextEventIndex"/>
|
||||
/// which indicates that it needs to be recalculated.
|
||||
/// </summary>
|
||||
private const int RecalculateEventIndex = int.MinValue;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Creates a new <see cref="Dispatcher"/>.</summary>
|
||||
public Dispatcher(AnimancerState state)
|
||||
{
|
||||
|
||||
State = state;
|
||||
_PreviousNormalizedTime = state.NormalizedTime;
|
||||
|
||||
#if UNITY_ASSERTIONS
|
||||
OptionalWarning.UnsupportedEvents.Log(state.UnsupportedEventsMessage, state.Graph?.Component);
|
||||
#endif
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// Setters for <see cref="AnimancerState.OwnedEvents"/>
|
||||
/// and <see cref="AnimancerState.SharedEvents"/>.
|
||||
/// </summary>
|
||||
public void SetEvents(Sequence events, bool isOwned)
|
||||
{
|
||||
Events = events;
|
||||
_NextEventIndex = RecalculateEventIndex;
|
||||
_SequenceVersion = Events.Version;
|
||||
HasOwnEvents = isOwned;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Sets <see cref="HasOwnEvents"/> to <c>false</c>.</summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void DismissEventOwnership()
|
||||
=> HasOwnEvents = false;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary><see cref="AnimancerState.Events(object, out Sequence)"/>.</summary>
|
||||
public bool InitializeEvents(out Sequence events)
|
||||
{
|
||||
if (HasOwnEvents)
|
||||
{
|
||||
events = Events;
|
||||
return false;
|
||||
}
|
||||
|
||||
Events = events = new(Events);
|
||||
_NextEventIndex = RecalculateEventIndex;
|
||||
_SequenceVersion = Events.Version;
|
||||
HasOwnEvents = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>[Internal]
|
||||
/// Notifies this dispatcher that the target's <see cref="Time"/> has changed.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
internal void OnSetTime()
|
||||
{
|
||||
// The Playable's time won't move in the same frame it was set,
|
||||
// so we'll just let the next frame grab its time.
|
||||
_PreviousNormalizedTime = float.NaN;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void UpdateEvents(bool raiseEvents)
|
||||
{
|
||||
var info = State.GetEventDispatchInfo();
|
||||
|
||||
// If we aren't raising events or don't have a previous time, just keep track of the time.
|
||||
if (!raiseEvents || float.IsNaN(_PreviousNormalizedTime))
|
||||
{
|
||||
_PreviousNormalizedTime = info.NormalizedTime;
|
||||
|
||||
// Since we aren't paying attention to the events,
|
||||
// we also aren't paying attention to which index the time corresponds to.
|
||||
_NextEventIndex = RecalculateEventIndex;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// If the sequence is modified, we need to recalculate the next event index.
|
||||
var sequenceVersion = Events.Version;
|
||||
if (_SequenceVersion != sequenceVersion)
|
||||
{
|
||||
_SequenceVersion = sequenceVersion;
|
||||
_NextEventIndex = RecalculateEventIndex;
|
||||
}
|
||||
|
||||
if (info.Length > 0)
|
||||
{
|
||||
if (_PreviousNormalizedTime == info.NormalizedTime)
|
||||
return;
|
||||
|
||||
CheckGeneralEvents(info.NormalizedTime, info.IsLooping);
|
||||
|
||||
CheckEndEvent(info.NormalizedTime);
|
||||
|
||||
_PreviousNormalizedTime = info.NormalizedTime;
|
||||
}
|
||||
else// Length zero, negative, or NaN.
|
||||
{
|
||||
UpdateZeroLength();
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>If the state has zero length, trigger its events every frame.</summary>
|
||||
private void UpdateZeroLength()
|
||||
{
|
||||
var speed = State.EffectiveSpeed;
|
||||
if (speed == 0)
|
||||
return;
|
||||
|
||||
if (Events.Count > 0)
|
||||
{
|
||||
int playDirectionInt;
|
||||
if (speed < 0)
|
||||
{
|
||||
playDirectionInt = -1;
|
||||
if (_NextEventIndex == RecalculateEventIndex ||
|
||||
_WasPlayingForwards)
|
||||
{
|
||||
_NextEventIndex = Events.Count - 1;
|
||||
_WasPlayingForwards = false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
playDirectionInt = 1;
|
||||
if (_NextEventIndex == RecalculateEventIndex ||
|
||||
!_WasPlayingForwards)
|
||||
{
|
||||
_NextEventIndex = 0;
|
||||
_WasPlayingForwards = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!InvokeAllEvents(Events, 1, playDirectionInt))
|
||||
return;
|
||||
}
|
||||
|
||||
var endEvent = Events.EndEvent;
|
||||
if (endEvent.callback != null)
|
||||
endEvent.DelayInvoke(EndEventName, State);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>General events are triggered on the frame when their time passes.</summary>
|
||||
/// <remarks>Looping animations trigger their events every loop.</remarks>
|
||||
private void CheckGeneralEvents(float normalizedTime, bool isLooping)
|
||||
{
|
||||
var count = Events.Count;
|
||||
if (count == 0)
|
||||
{
|
||||
_NextEventIndex = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
ValidateNextEventIndex(
|
||||
isLooping,
|
||||
ref normalizedTime,
|
||||
out var playDirectionFloat,
|
||||
out var playDirectionInt);
|
||||
|
||||
if (isLooping)// Looping.
|
||||
{
|
||||
var animancerEvent = Events[_NextEventIndex];
|
||||
var eventTime = animancerEvent.normalizedTime * playDirectionFloat;
|
||||
|
||||
var loopDelta = GetLoopDelta(_PreviousNormalizedTime, normalizedTime, eventTime);
|
||||
if (loopDelta == 0)
|
||||
return;
|
||||
|
||||
// For each additional loop, invoke all events without needing to check their times.
|
||||
if (!InvokeAllEvents(Events, loopDelta - 1, playDirectionInt))
|
||||
return;
|
||||
|
||||
var loopStartIndex = _NextEventIndex;
|
||||
|
||||
Invoke:
|
||||
animancerEvent.DelayInvoke(Events.GetName(_NextEventIndex), State);
|
||||
|
||||
if (!NextEventLooped(Events, playDirectionInt) ||
|
||||
_NextEventIndex == loopStartIndex)
|
||||
return;
|
||||
|
||||
animancerEvent = Events[_NextEventIndex];
|
||||
eventTime = animancerEvent.normalizedTime * playDirectionFloat;
|
||||
if (loopDelta == GetLoopDelta(_PreviousNormalizedTime, normalizedTime, eventTime))
|
||||
goto Invoke;
|
||||
}
|
||||
else// Non-Looping.
|
||||
{
|
||||
while ((uint)_NextEventIndex < (uint)count)
|
||||
{
|
||||
var animancerEvent = Events[_NextEventIndex];
|
||||
var eventTime = animancerEvent.normalizedTime * playDirectionFloat;
|
||||
|
||||
if (normalizedTime <= eventTime)
|
||||
return;
|
||||
|
||||
animancerEvent.DelayInvoke(Events.GetName(_NextEventIndex), State);
|
||||
|
||||
_NextEventIndex += playDirectionInt;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private void ValidateNextEventIndex(
|
||||
bool isLooping,
|
||||
ref float normalizedTime,
|
||||
out float playDirectionFloat,
|
||||
out int playDirectionInt)
|
||||
{
|
||||
if (normalizedTime < _PreviousNormalizedTime)// Playing Backwards.
|
||||
{
|
||||
var previousTime = _PreviousNormalizedTime;
|
||||
_PreviousNormalizedTime = -previousTime;
|
||||
normalizedTime = -normalizedTime;
|
||||
playDirectionFloat = -1;
|
||||
playDirectionInt = -1;
|
||||
|
||||
if (_NextEventIndex == RecalculateEventIndex ||
|
||||
_WasPlayingForwards)
|
||||
{
|
||||
_NextEventIndex = Events.Count - 1;
|
||||
_WasPlayingForwards = false;
|
||||
|
||||
if (isLooping)
|
||||
previousTime = AnimancerUtilities.Wrap01(previousTime);
|
||||
|
||||
while (Events[_NextEventIndex].normalizedTime > previousTime)
|
||||
{
|
||||
_NextEventIndex--;
|
||||
|
||||
if (_NextEventIndex < 0)
|
||||
{
|
||||
if (isLooping)
|
||||
_NextEventIndex = Events.Count - 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Events.AssertNormalizedTimes(State, isLooping);
|
||||
}
|
||||
}
|
||||
else// Playing Forwards.
|
||||
{
|
||||
playDirectionFloat = 1;
|
||||
playDirectionInt = 1;
|
||||
|
||||
if (_NextEventIndex == RecalculateEventIndex ||
|
||||
!_WasPlayingForwards)
|
||||
{
|
||||
_NextEventIndex = 0;
|
||||
_WasPlayingForwards = true;
|
||||
|
||||
var previousTime = _PreviousNormalizedTime;
|
||||
if (isLooping)
|
||||
previousTime = AnimancerUtilities.Wrap01(previousTime);
|
||||
|
||||
var max = Events.Count - 1;
|
||||
while (Events[_NextEventIndex].normalizedTime < previousTime)
|
||||
{
|
||||
_NextEventIndex++;
|
||||
|
||||
if (_NextEventIndex > max)
|
||||
{
|
||||
if (isLooping)
|
||||
_NextEventIndex = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Events.AssertNormalizedTimes(State, isLooping);
|
||||
}
|
||||
}
|
||||
|
||||
// This method could be slightly optimised for playback direction changes by using the current index
|
||||
// as the starting point instead of iterating from the edge of the sequence, but that would make it
|
||||
// significantly more complex for something that shouldn't happen very often and would only matter if
|
||||
// there are lots of events (in which case the optimisation would be tiny compared to the cost of
|
||||
// actually invoking all those events and running the rest of the application).
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the number of times an event at `eventTime` should be invoked when the
|
||||
/// <see cref="AnimancerState.NormalizedTime"/> goes from `previousTime` to `nextTime` on a looping animation.
|
||||
/// </summary>
|
||||
private static int GetLoopDelta(float previousTime, float nextTime, float eventTime)
|
||||
{
|
||||
previousTime -= eventTime;
|
||||
nextTime -= eventTime;
|
||||
|
||||
var previousLoopCount = Mathf.FloorToInt(previousTime);
|
||||
var nextLoopCount = Mathf.FloorToInt(nextTime);
|
||||
|
||||
var loopCount = nextLoopCount - previousLoopCount;
|
||||
|
||||
// Previous time must be inclusive.
|
||||
// And next time must be exclusive.
|
||||
// So if the previous time is exactly on a looped increment of the event time, count one more.
|
||||
// And if the next time is exactly on a looped increment of the event time, count one less.
|
||||
if (previousTime == previousLoopCount)
|
||||
loopCount++;
|
||||
if (nextTime == nextLoopCount)
|
||||
loopCount--;
|
||||
|
||||
return loopCount;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private static int _MaximumFullLoopCount = 3;
|
||||
|
||||
/// <summary>
|
||||
/// The maximum number of times a looping animation can trigger all of its events in a single frame.
|
||||
/// Default 3, Minimum 1.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This limit should only ever be reached when a state has a very short length and high speed.
|
||||
/// </remarks>
|
||||
public static int MaximumFullLoopCount
|
||||
{
|
||||
get => _MaximumFullLoopCount;
|
||||
set => _MaximumFullLoopCount = Math.Max(value, 1);
|
||||
}
|
||||
|
||||
private bool InvokeAllEvents(Sequence events, int count, int playDirectionInt)
|
||||
{
|
||||
if (count > _MaximumFullLoopCount)
|
||||
count = _MaximumFullLoopCount;
|
||||
|
||||
var loopStartIndex = _NextEventIndex;
|
||||
while (count-- > 0)
|
||||
{
|
||||
do
|
||||
{
|
||||
events[_NextEventIndex].DelayInvoke(events.GetName(_NextEventIndex), State);
|
||||
|
||||
if (!NextEventLooped(events, playDirectionInt))
|
||||
return false;
|
||||
}
|
||||
while (_NextEventIndex != loopStartIndex);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private bool NextEventLooped(Sequence events, int playDirectionInt)
|
||||
{
|
||||
_NextEventIndex += playDirectionInt;
|
||||
|
||||
var count = events.Count;
|
||||
if (_NextEventIndex >= count)
|
||||
_NextEventIndex = 0;
|
||||
else if (_NextEventIndex < 0)
|
||||
_NextEventIndex = count - 1;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>End events are triggered every frame after their time passes.</summary>
|
||||
/// <remarks>
|
||||
/// This ensures that assigning the event after the time has passed
|
||||
/// will still trigger it rather than leaving it playing indefinitely.
|
||||
/// </remarks>
|
||||
private void CheckEndEvent(float normalizedTime)
|
||||
{
|
||||
var endEvent = Events.EndEvent;
|
||||
if (endEvent.callback == null)
|
||||
return;
|
||||
|
||||
if (normalizedTime > _PreviousNormalizedTime)// Playing Forwards.
|
||||
{
|
||||
var eventTime = float.IsNaN(endEvent.normalizedTime)
|
||||
? 1
|
||||
: endEvent.normalizedTime;
|
||||
|
||||
if (normalizedTime > eventTime)
|
||||
endEvent.DelayInvoke(EndEventName, State);
|
||||
}
|
||||
else// Playing Backwards.
|
||||
{
|
||||
var eventTime = float.IsNaN(endEvent.normalizedTime)
|
||||
? 0
|
||||
: endEvent.normalizedTime;
|
||||
|
||||
if (normalizedTime < eventTime)
|
||||
endEvent.DelayInvoke(EndEventName, State);
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// Sets the <see cref="AnimancerState.NormalizedTime"/>
|
||||
/// to the <see cref="AnimancerState.NormalizedEndTime"/>
|
||||
/// and invokes any remaining <see cref="AnimancerEvent"/>s.
|
||||
/// </summary>
|
||||
public void FinishImmediately()
|
||||
{
|
||||
var index = _NextEventIndex;
|
||||
if (index == RecalculateEventIndex && Events.Count > 0)
|
||||
{
|
||||
var normalizedTime = State.NormalizedTime;
|
||||
ValidateNextEventIndex(
|
||||
State.IsLooping,
|
||||
ref normalizedTime,
|
||||
out _,
|
||||
out _);
|
||||
index = _NextEventIndex;
|
||||
}
|
||||
|
||||
State.NormalizedTime = State.NormalizedEndTime;
|
||||
|
||||
if (Events.Count > 0)
|
||||
{
|
||||
if (State.EffectiveSpeed < 0)// Backwards.
|
||||
{
|
||||
for (int i = index; i >= 0; i--)
|
||||
Events[i].DelayInvoke(Events.GetName(i), State);
|
||||
}
|
||||
else// Forwards, 0, or NaN.
|
||||
{
|
||||
for (int i = index; i < Events.Count; i++)
|
||||
Events[i].DelayInvoke(Events.GetName(i), State);
|
||||
}
|
||||
}
|
||||
|
||||
var endEvent = Events.EndEvent;
|
||||
if (endEvent.callback != null)
|
||||
endEvent.DelayInvoke(EndEventName, State);
|
||||
|
||||
Invoker.InvokeAllAndClear();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Returns "<see cref="Dispatcher"/> (Target State)".</summary>
|
||||
public override string ToString()
|
||||
=> State != null
|
||||
? $"{nameof(Dispatcher)} ({State})"
|
||||
: $"{nameof(Dispatcher)} (No Target State)";
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void AppendDescription(StringBuilder text, string separator = "\n")
|
||||
{
|
||||
text.AppendField(separator, "State", State.GetPath());
|
||||
text.AppendField(separator, "IsLooping", State.IsLooping);
|
||||
text.AppendField(separator, "PreviousNormalizedTime", _PreviousNormalizedTime);
|
||||
text.AppendField(separator, "NextEventIndex", _NextEventIndex);
|
||||
text.AppendField(separator, "SequenceVersion", _SequenceVersion);
|
||||
text.AppendField(separator, "WasPlayingForwards", _WasPlayingForwards);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2b948ae61bb921c45b0af9e45ecc16b9
|
||||
timeCreated: 1515060256
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,206 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
using UnityEngine;
|
||||
using Object = UnityEngine.Object;
|
||||
|
||||
namespace Animancer
|
||||
{
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer/AnimancerEvent
|
||||
partial struct AnimancerEvent
|
||||
{
|
||||
/// <summary>An <see cref="AnimancerEvent"/> and other associated details used to invoke it.</summary>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer/Invocation
|
||||
public readonly struct Invocation
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>The details of the event currently being triggered.</summary>
|
||||
/// <remarks>Cleared after the event is invoked.</remarks>
|
||||
public static Invocation Current { get; private set; }
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>The <see cref="AnimancerEvent"/>.</summary>
|
||||
public readonly AnimancerEvent Event;
|
||||
|
||||
/// <summary>The name of the <see cref="Event"/>.</summary>
|
||||
public readonly StringReference Name;
|
||||
|
||||
/// <summary>The <see cref="AnimancerState"/> triggering the <see cref="Event"/>.</summary>
|
||||
public readonly AnimancerState State;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Creates a new <see cref="Invocation"/>.</summary>
|
||||
public Invocation(
|
||||
AnimancerEvent animancerEvent,
|
||||
StringReference eventName,
|
||||
AnimancerState state)
|
||||
{
|
||||
Event = animancerEvent;
|
||||
State = state;
|
||||
Name = eventName;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// Sets the <see cref="Current"/>, invokes the <see cref="callback"/>,
|
||||
/// then reverts the <see cref="Current"/>.
|
||||
/// </summary>
|
||||
/// <remarks>This method catches and logs any exception thrown by the <see cref="callback"/>.</remarks>
|
||||
/// <exception cref="NullReferenceException">The <see cref="callback"/> is null.</exception>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public readonly void Invoke()
|
||||
{
|
||||
#if UNITY_ASSERTIONS
|
||||
var oldLayer = State.Layer;
|
||||
var oldCommandCount = oldLayer.CommandCount;
|
||||
#endif
|
||||
|
||||
var previous = Current;
|
||||
var parameter = CurrentParameter;
|
||||
|
||||
Current = this;
|
||||
CurrentParameter = null;
|
||||
|
||||
try
|
||||
{
|
||||
Event.callback();
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Debug.LogException(exception, State?.Graph?.Component as Object);
|
||||
}
|
||||
|
||||
Current = previous;
|
||||
CurrentParameter = parameter;
|
||||
|
||||
#if UNITY_ASSERTIONS
|
||||
if (Name == EndEventName)
|
||||
AssertEndEventInvoked(oldLayer, oldCommandCount);
|
||||
#endif
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// Returns the callback registered in the <see cref="AnimancerGraph.Events"/>
|
||||
/// with the <see cref="Name"/> (or null if there isn't one).
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public readonly Action GetBoundCallback()
|
||||
=> Name.IsNullOrEmpty()
|
||||
? null
|
||||
: State.Graph._Events?.Get(Name);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Returns a string describing the contents of this invocation.</summary>
|
||||
public override string ToString()
|
||||
=> $"{nameof(AnimancerEvent)}.{nameof(Invocation)}(" +
|
||||
$"{nameof(Name)}={AnimancerUtilities.ToStringOrNull(Name)}, " +
|
||||
$"NormalizedTime={Event.normalizedTime:0.##}, " +
|
||||
$"Callback=({AnimancerReflection.ToStringDetailed(Event.callback)}), " +
|
||||
$"{nameof(State)}={AnimancerUtilities.ToStringOrNull(State)})";
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// Invokes the callback bound to the <see cref="Name"/>
|
||||
/// in the <see cref="AnimancerGraph.Events"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Logs <see cref="OptionalWarning.UselessEvent"/> if no callback is bound.
|
||||
/// </remarks>
|
||||
public void InvokeBoundCallback()
|
||||
{
|
||||
if (Name != null &&
|
||||
State.Graph._Events != null &&
|
||||
State.Graph._Events.TryGetValue(Name, out var callback))
|
||||
{
|
||||
callback();
|
||||
}
|
||||
#if UNITY_ASSERTIONS
|
||||
// If the callback doesn't do anything else, then this is a useless event.
|
||||
else if (Event.callback == AnimancerEvent.InvokeBoundCallback &&
|
||||
OptionalWarning.UselessEvent.IsEnabled())
|
||||
{
|
||||
OptionalWarning.UselessEvent.Log(
|
||||
$"An {nameof(AnimancerEvent)} attempted to invoke the callback bound to the name" +
|
||||
$" '{AnimancerUtilities.ToStringOrNull(Name)}' but there is no callback" +
|
||||
$" bound to that name so the event may not be configured correctly." +
|
||||
$"\n• Normalized Time: {Event.normalizedTime}" +
|
||||
$"\n• State: {State}" +
|
||||
$"\n• Object: {AnimancerUtilities.ToStringOrNull(State.Graph?.Component)}",
|
||||
State.Graph?.Component);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#if UNITY_ASSERTIONS
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>[Assert-Only]
|
||||
/// Call after invoking an end event to assert <see cref="OptionalWarning.EndEventInterrupt"/>.
|
||||
/// </summary>
|
||||
private readonly void AssertEndEventInvoked(AnimancerLayer oldLayer, int oldCommandCount)
|
||||
{
|
||||
if (ShouldLogEndEventInterrupt(oldLayer, oldCommandCount))
|
||||
{
|
||||
OptionalWarning.EndEventInterrupt.Log(
|
||||
$"An End Event callback didn't stop the animation." +
|
||||
$" Animancer doesn't handle End Events automatically," +
|
||||
$" so the controlling script is responsible for stopping the animation," +
|
||||
$" often by playing a different one." +
|
||||
$"\n• State: {State}" +
|
||||
$"\n• Callback: {Event.callback.ToStringDetailed()}" +
|
||||
$"\n• End Events are triggered every frame after their time has passed:" +
|
||||
$" {Strings.DocsURLs.EndEvents.AsHtmlLink()}" +
|
||||
$"\n• To avoid this behaviour, use a regular Animancer Event instead:" +
|
||||
$" {Strings.DocsURLs.AnimancerEvents.AsHtmlLink()}",
|
||||
State.Graph?.Component);
|
||||
|
||||
OptionalWarning.EndEventInterrupt.Disable();
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>[Assert-Only] Should <see cref="OptionalWarning.EndEventInterrupt"/> be logged?</summary>
|
||||
private readonly bool ShouldLogEndEventInterrupt(AnimancerLayer oldLayer, int oldCommandCount)
|
||||
{
|
||||
if (!OptionalWarning.EndEventInterrupt.IsEnabled())
|
||||
return false;
|
||||
|
||||
var events = State.SharedEvents;
|
||||
if (events == null ||
|
||||
events.OnEnd != Event.callback)
|
||||
return false;
|
||||
|
||||
var newLayer = State.Layer;
|
||||
if (oldLayer != newLayer ||
|
||||
oldCommandCount != newLayer.CommandCount ||
|
||||
!State.Graph.IsGraphPlaying ||
|
||||
!State.IsPlaying)
|
||||
return false;
|
||||
|
||||
var speed = State.EffectiveSpeed;
|
||||
if (speed > 0)
|
||||
return State.NormalizedTime > State.NormalizedEndTime;
|
||||
else if (speed < 0)
|
||||
return State.NormalizedTime < State.NormalizedEndTime;
|
||||
else// Speed 0.
|
||||
return false;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endif
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 589b19114284c8148a2f1389b152a66c
|
||||
timeCreated: 1515060256
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,136 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Animancer
|
||||
{
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer/AnimancerEvent
|
||||
partial struct AnimancerEvent
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Events ready to be invoked by the next <see cref="Invoker.InvokeAllAndClear"/>.</summary>
|
||||
/// <remarks>
|
||||
/// This field should be inside the Invoker class.
|
||||
/// But that can potentially cause a TypeLoadException if Invoker initializes before AnimancerEvent.
|
||||
/// Having it out in AnimancerEvent avoids that possibility.
|
||||
/// </remarks>
|
||||
private static readonly List<Invocation>
|
||||
InvocationQueue = new();
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Gathers delegates in a static list to be invoked at a later time by any child class.</summary>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer/Invoker
|
||||
[DefaultExecutionOrder(-30000)]// Run as soon as possible in whatever update cycle is being executed.
|
||||
[ExecuteAlways]
|
||||
public abstract class Invoker : MonoBehaviour
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Ensures that an appropriate <see cref="Invoker"/> has been created.</summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static Invoker Initialize(bool fixedUpdate)
|
||||
=> fixedUpdate
|
||||
? InvokerFixed.Initialize()
|
||||
: InvokerDynamic.Initialize();
|
||||
|
||||
/// <summary>Ensures that an appropriate <see cref="Invoker"/> has been created.</summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static Invoker Initialize(AnimatorUpdateMode updateMode)
|
||||
{
|
||||
const AnimatorUpdateMode FixedUpdateMode =
|
||||
#if UNITY_2023_1_OR_NEWER
|
||||
AnimatorUpdateMode.Fixed;
|
||||
#else
|
||||
AnimatorUpdateMode.AnimatePhysics;
|
||||
#endif
|
||||
|
||||
return Initialize(updateMode == FixedUpdateMode);
|
||||
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>[Internal] Adds an event to the queue to be invoked by the next update.</summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
internal static void Add(Invocation invocation)
|
||||
{
|
||||
#if UNITY_ASSERTIONS
|
||||
if (!HasEnabledInstance)
|
||||
Debug.LogWarning(
|
||||
$"There is no currently enabled {nameof(AnimancerEvent)}.{nameof(Invoker)}" +
|
||||
$" so events will not be invoked.");
|
||||
#endif
|
||||
|
||||
InvocationQueue.Add(invocation);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// In case <see cref="InvokeAllAndClear"/> gets called recursively,
|
||||
/// we need to avoid invoking the same event multiple times
|
||||
/// without the performance cost of immediately removing them each from the queue.
|
||||
/// </summary>
|
||||
private static int _CurrentInvocation;
|
||||
|
||||
/// <summary>Invokes all queued events and clears the queue.</summary>
|
||||
public static void InvokeAllAndClear()
|
||||
{
|
||||
while (_CurrentInvocation < InvocationQueue.Count)
|
||||
InvocationQueue[_CurrentInvocation++].Invoke();
|
||||
|
||||
InvocationQueue.Clear();
|
||||
_CurrentInvocation = 0;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Returns an enumerator for all invocations currently in the queue.</summary>
|
||||
public static List<Invocation>.Enumerator EnumerateInvocationQueue()
|
||||
=> InvocationQueue.GetEnumerator();
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#if UNITY_ASSERTIONS
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private static readonly List<Invoker>
|
||||
Instances = new();
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>[Assert-Only] Registers this instance.</summary>
|
||||
protected virtual void Awake()
|
||||
=> Instances.Add(this);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>[Assert-Only] Un-registers this instance.</summary>
|
||||
protected virtual void OnDestroy()
|
||||
=> Instances.Remove(this);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>[Assert-Only] Is there any <see cref="Behaviour.enabled"/> instance?</summary>
|
||||
private static bool HasEnabledInstance
|
||||
{
|
||||
get
|
||||
{
|
||||
for (int i = 0; i < Instances.Count; i++)
|
||||
if (Instances[i].enabled)
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endif
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 38222fa6fab7a29479a5946d01fec2be
|
||||
timeCreated: 1515060256
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,59 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
using System.Runtime.CompilerServices;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Animancer
|
||||
{
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer/AnimancerEvent
|
||||
partial struct AnimancerEvent
|
||||
{
|
||||
/// <summary>Executes <see cref="Invoker.InvokeAllAndClear"/> after animations in the Dynamic Update cycle.</summary>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer/InvokerDynamic
|
||||
[AnimancerHelpUrl(typeof(InvokerDynamic))]
|
||||
[AddComponentMenu("")]// Singleton creates itself.
|
||||
public class InvokerDynamic : Invoker
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private static InvokerDynamic _Instance;
|
||||
|
||||
/// <summary>Creates the singleton instance.</summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static InvokerDynamic Initialize()
|
||||
=> AnimancerUtilities.InitializeSingleton(ref _Instance);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Should this system execute events?</summary>
|
||||
/// <remarks>If disabled, this system will not be re-enabled automatically.</remarks>
|
||||
public static bool Enabled
|
||||
{
|
||||
get => _Instance != null && _Instance.enabled;
|
||||
set
|
||||
{
|
||||
if (value)
|
||||
{
|
||||
Initialize();
|
||||
_Instance.enabled = true;
|
||||
}
|
||||
else if (_Instance != null)
|
||||
{
|
||||
_Instance.enabled = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>After animation update with dynamic timestep.</summary>
|
||||
protected virtual void LateUpdate()
|
||||
{
|
||||
InvokeAllAndClear();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 108ccdfde47cfab418af3bd716413114
|
||||
timeCreated: 1515060256
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,76 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
using System.Collections;
|
||||
using System.Runtime.CompilerServices;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Animancer
|
||||
{
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer/AnimancerEvent
|
||||
partial struct AnimancerEvent
|
||||
{
|
||||
/// <summary>Executes <see cref="Invoker.InvokeAllAndClear"/> after animations in the Fixed Update cycle.</summary>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer/InvokerFixed
|
||||
[AnimancerHelpUrl(typeof(InvokerFixed))]
|
||||
[AddComponentMenu("")]// Singleton creates itself.
|
||||
public class InvokerFixed : Invoker
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private static InvokerFixed _Instance;
|
||||
|
||||
/// <summary>Creates the singleton instance.</summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static InvokerFixed Initialize()
|
||||
=> AnimancerUtilities.InitializeSingleton(ref _Instance);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Should this system execute events?</summary>
|
||||
/// <remarks>If disabled, this system will not be re-enabled automatically.</remarks>
|
||||
public static bool Enabled
|
||||
{
|
||||
get => _Instance != null && _Instance.enabled;
|
||||
set
|
||||
{
|
||||
if (value)
|
||||
{
|
||||
Initialize();
|
||||
_Instance.enabled = true;
|
||||
}
|
||||
else if (_Instance != null)
|
||||
{
|
||||
_Instance.enabled = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>A cached instance of <see cref="UnityEngine.WaitForFixedUpdate"/>.</summary>
|
||||
public static readonly WaitForFixedUpdate
|
||||
WaitForFixedUpdate = new();
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Starts the <see cref="LateFixedUpdate"/> coroutine.</summary>
|
||||
protected virtual void OnEnable()
|
||||
=> StartCoroutine(LateFixedUpdate());
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>After animation update with fixed timestep.</summary>
|
||||
private IEnumerator LateFixedUpdate()
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
yield return WaitForFixedUpdate;
|
||||
InvokeAllAndClear();
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7e7ae0209e44c77469a807230bd36ab5
|
||||
timeCreated: 1515060256
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,446 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
#pragma warning disable CS0649 // Field is never assigned to, and will always have its default value.
|
||||
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Animancer
|
||||
{
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer/AnimancerEvent
|
||||
partial struct AnimancerEvent
|
||||
{
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer/Sequence
|
||||
partial class Sequence
|
||||
{
|
||||
/// <summary>
|
||||
/// Serializable data which can be used to construct an <see cref="Sequence"/> using
|
||||
/// <see cref="StringAsset"/>s and <see cref="IInvokable"/>s.
|
||||
/// </summary>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer/Serializable
|
||||
[Serializable]
|
||||
public class Serializable : ICloneable<Serializable>
|
||||
#if UNITY_EDITOR
|
||||
, ISerializationCallbackReceiver
|
||||
#endif
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
#region Fields and Properties
|
||||
/************************************************************************************************************************/
|
||||
|
||||
[SerializeField]
|
||||
private float[] _NormalizedTimes;
|
||||
|
||||
/// <summary>[<see cref="SerializeField"/>] The serialized <see cref="normalizedTime"/>s.</summary>
|
||||
/// <remarks>The last item is used for the <see cref="EndEvent"/>.</remarks>
|
||||
public ref float[] NormalizedTimes => ref _NormalizedTimes;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
[SerializeReference, Polymorphic]
|
||||
private IInvokable[] _Callbacks;
|
||||
|
||||
/// <summary>[<see cref="SerializeField"/>] The serialized <see cref="callback"/>s.</summary>
|
||||
/// <remarks>
|
||||
/// This array only needs to be large enough to hold the last item that isn't null.
|
||||
/// <para></para>
|
||||
/// If this array is larger than the <see cref="NormalizedTimes"/>, the first item
|
||||
/// with no corresponding time will be used as the <see cref="OnEnd"/> callback
|
||||
/// and any others after that will be ignored.
|
||||
/// </remarks>
|
||||
public ref IInvokable[] Callbacks => ref _Callbacks;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
[SerializeField]
|
||||
private StringAsset[] _Names;
|
||||
|
||||
/// <summary>[<see cref="SerializeField"/>] The serialized <see cref="Sequence.Names"/>.</summary>
|
||||
public ref StringAsset[] Names => ref _Names;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#if UNITY_EDITOR
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>[Editor-Only] [Internal]
|
||||
/// The name of the array field which stores the <see cref="normalizedTime"/>s.
|
||||
/// </summary>
|
||||
internal const string NormalizedTimesField = nameof(_NormalizedTimes);
|
||||
|
||||
/// <summary>[Editor-Only] [Internal]
|
||||
/// The name of the array field which stores the serialized <see cref="Callbacks"/>.
|
||||
/// </summary>
|
||||
internal const string CallbacksField = nameof(_Callbacks);
|
||||
|
||||
/// <summary>[Editor-Only] [Internal]
|
||||
/// The name of the array field which stores the serialized <see cref="Names"/>.
|
||||
/// </summary>
|
||||
internal const string NamesField = nameof(_Names);
|
||||
|
||||
/// <summary>[Editor-Only] Disable Inspector Gadgets Nested Object Drawers.</summary>
|
||||
private const bool NestedObjectDrawers = false;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endif
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private Sequence _Events;
|
||||
|
||||
/// <summary>Returns the <see cref="Events"/> or <c>null</c> if it wasn't yet initialized.</summary>
|
||||
public Sequence InitializedEvents
|
||||
=> _Events;
|
||||
|
||||
/// <summary>
|
||||
/// The runtime <see cref="Sequence"/> compiled from this <see cref="Serializable"/>.
|
||||
/// Each call after the first will return the same reference.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Unlike <see cref="GetEventsOptional"/>, this property will create an empty
|
||||
/// <see cref="Sequence"/> instead of returning null if there are no events.
|
||||
/// </remarks>
|
||||
public Sequence Events
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_Events == null)
|
||||
{
|
||||
GetEventsOptional();
|
||||
_Events ??= new();
|
||||
}
|
||||
|
||||
return _Events;
|
||||
}
|
||||
set => _Events = value;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endregion
|
||||
/************************************************************************************************************************/
|
||||
#region Initialization
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// Returns the runtime <see cref="Sequence"/> compiled from this <see cref="Serializable"/>.
|
||||
/// Each call after the first will return the same reference.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This method returns null if the sequence would be empty anyway and is used by the implicit
|
||||
/// conversion from <see cref="Serializable"/> to <see cref="Sequence"/>.
|
||||
/// </remarks>
|
||||
public Sequence GetEventsOptional()
|
||||
{
|
||||
if (_Events != null ||
|
||||
_NormalizedTimes == null)
|
||||
return _Events;
|
||||
|
||||
var timeCount = _NormalizedTimes.Length;
|
||||
if (timeCount == 0)
|
||||
return null;
|
||||
|
||||
var callbackCount = _Callbacks != null
|
||||
? _Callbacks.Length
|
||||
: 0;
|
||||
|
||||
var callback = callbackCount >= timeCount--
|
||||
? GetInvoke(_Callbacks[timeCount])
|
||||
: null;
|
||||
var endEvent = new AnimancerEvent(_NormalizedTimes[timeCount], callback);
|
||||
|
||||
_Events = new(timeCount)
|
||||
{
|
||||
EndEvent = endEvent,
|
||||
Count = timeCount,
|
||||
Names = StringAsset.ToStringReferences(_Names),
|
||||
};
|
||||
|
||||
var events = _Events._Events;
|
||||
for (int i = 0; i < timeCount; i++)
|
||||
{
|
||||
callback = i < callbackCount
|
||||
? GetInvoke(_Callbacks[i])
|
||||
: InvokeBoundCallback;
|
||||
|
||||
events[i] = new(_NormalizedTimes[i], callback);
|
||||
}
|
||||
|
||||
return _Events;
|
||||
}
|
||||
|
||||
/// <summary>Calls <see cref="GetEventsOptional"/>.</summary>
|
||||
public static implicit operator Sequence(Serializable serializable)
|
||||
=> serializable?.GetEventsOptional();
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// Returns the <see cref="IInvokable.Invoke"/> if the `invokable` isn't <c>null</c>.
|
||||
/// Otherwise, returns <c>null</c>.
|
||||
/// </summary>
|
||||
public static Action GetInvoke(IInvokable invokable)
|
||||
=> invokable != null
|
||||
? invokable.Invoke
|
||||
: InvokeBoundCallback;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endregion
|
||||
/************************************************************************************************************************/
|
||||
#region End Event
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Returns the <see cref="normalizedTime"/> of the <see cref="EndEvent"/>.</summary>
|
||||
/// <remarks>If the value is not set, the value is determined by <see cref="GetDefaultNormalizedEndTime"/>.</remarks>
|
||||
public float GetNormalizedEndTime(float speed = 1)
|
||||
{
|
||||
return _NormalizedTimes.IsNullOrEmpty()
|
||||
? GetDefaultNormalizedEndTime(speed)
|
||||
: _NormalizedTimes[^1];
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Sets the <see cref="normalizedTime"/> of the <see cref="EndEvent"/>.</summary>
|
||||
public void SetNormalizedEndTime(float normalizedTime)
|
||||
{
|
||||
if (_NormalizedTimes.IsNullOrEmpty())
|
||||
_NormalizedTimes = new float[] { normalizedTime };
|
||||
else
|
||||
_NormalizedTimes[^1] = normalizedTime;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Sets the <see cref="callback"/> of the <see cref="EndEvent"/>.</summary>
|
||||
public void SetEndCallback(IInvokable callback = null)
|
||||
{
|
||||
if (_NormalizedTimes.IsNullOrEmpty())
|
||||
_NormalizedTimes = new float[] { float.NaN };
|
||||
|
||||
InsertOptionalItem(ref _Callbacks, _NormalizedTimes.Length - 1, callback);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Sets the data of the <see cref="EndEvent"/>.</summary>
|
||||
public void SetEndEvent(float normalizedTime = float.NaN, IInvokable callback = null)
|
||||
{
|
||||
if (_NormalizedTimes.IsNullOrEmpty())
|
||||
_NormalizedTimes = new float[] { normalizedTime };
|
||||
else
|
||||
_NormalizedTimes[^1] = normalizedTime;
|
||||
|
||||
InsertOptionalItem(ref _Callbacks, _NormalizedTimes.Length - 1, callback);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endregion
|
||||
/************************************************************************************************************************/
|
||||
#region Other Events
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Adds an event to the serialized fields.</summary>
|
||||
public int AddEvent(float normalizedTime, IInvokable callback = null, StringAsset name = null)
|
||||
{
|
||||
int index;
|
||||
|
||||
if (_NormalizedTimes.IsNullOrEmpty())
|
||||
{
|
||||
_NormalizedTimes = new float[] { normalizedTime, float.NaN };
|
||||
index = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
index = _NormalizedTimes.Length - 1;
|
||||
|
||||
for (int i = 0; i < _NormalizedTimes.Length - 1; i++)
|
||||
{
|
||||
if (_NormalizedTimes[i] > normalizedTime)
|
||||
{
|
||||
index = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
AnimancerUtilities.InsertAt(ref _NormalizedTimes, index, normalizedTime);
|
||||
}
|
||||
|
||||
InsertOptionalItem(ref _Callbacks, index, callback);
|
||||
InsertOptionalItem(ref _Names, index, name);
|
||||
|
||||
return index;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Inserts an `item` at the specified `index` in an optional `array`.</summary>
|
||||
/// <remarks>
|
||||
/// If the `item` is <c>null</c> then the array only needs
|
||||
/// to be expanded if it was already larger than the `index`.
|
||||
/// </remarks>
|
||||
private static void InsertOptionalItem<T>(ref T[] array, int index, T item)
|
||||
where T : class
|
||||
{
|
||||
if (item == null &&
|
||||
(array == null || array.Length < index))
|
||||
return;
|
||||
|
||||
AnimancerUtilities.InsertAt(ref array, index, item);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Removes an event from the serialized fields.</summary>
|
||||
public void RemoveEvent(int index)
|
||||
{
|
||||
if (_NormalizedTimes.IsNullOrEmpty())
|
||||
return;
|
||||
|
||||
AnimancerUtilities.RemoveAt(ref _NormalizedTimes, index);
|
||||
|
||||
if (_Callbacks != null && _Callbacks.Length > index)
|
||||
AnimancerUtilities.RemoveAt(ref _Callbacks, index);
|
||||
|
||||
if (_Names != null && _Names.Length > index)
|
||||
AnimancerUtilities.RemoveAt(ref _Names, index);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Removes all events.</summary>
|
||||
public void Clear(bool keepEndEvent = false)
|
||||
{
|
||||
if (keepEndEvent)
|
||||
{
|
||||
if (_NormalizedTimes != null && _NormalizedTimes.Length > 0)
|
||||
_NormalizedTimes = new float[] { _NormalizedTimes[^1] };
|
||||
else
|
||||
_NormalizedTimes = null;
|
||||
|
||||
if (_Callbacks != null && _Callbacks.Length > 0)
|
||||
_Callbacks = new IInvokable[] { _Callbacks[^1] };
|
||||
else
|
||||
_Callbacks = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
_NormalizedTimes = null;
|
||||
_Callbacks = null;
|
||||
}
|
||||
|
||||
_Names = null;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endregion
|
||||
/************************************************************************************************************************/
|
||||
#region Copying
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Creates a new <see cref="Serializable"/> and copies the contents of <c>this</c> into it.</summary>
|
||||
/// <remarks>To copy into an existing sequence, use <see cref="CopyFrom"/> instead.</remarks>
|
||||
public Serializable Clone()
|
||||
{
|
||||
var clone = new Serializable();
|
||||
clone.CopyFrom(this);
|
||||
return clone;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Serializable Clone(CloneContext context)
|
||||
=> Clone();
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void CopyFrom(Serializable copyFrom)
|
||||
{
|
||||
AnimancerUtilities.CopyExactArray(copyFrom._NormalizedTimes, ref _NormalizedTimes);
|
||||
AnimancerUtilities.CopyExactArray(copyFrom._Callbacks, ref _Callbacks);
|
||||
AnimancerUtilities.CopyExactArray(copyFrom._Names, ref _Names);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endregion
|
||||
/************************************************************************************************************************/
|
||||
#region Serialization
|
||||
/************************************************************************************************************************/
|
||||
#if UNITY_EDITOR
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>[Editor-Only] Does nothing.</summary>
|
||||
void ISerializationCallbackReceiver.OnAfterDeserialize() { }
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>[Editor-Only] [Internal]
|
||||
/// Called by <see cref="ISerializationCallbackReceiver.OnBeforeSerialize"/>.
|
||||
/// </summary>
|
||||
internal static event Action<Serializable> OnBeforeSerialize;
|
||||
|
||||
/// <summary>[Editor-Only] Ensures that the events are sorted by time (excluding the end event).</summary>
|
||||
void ISerializationCallbackReceiver.OnBeforeSerialize()
|
||||
=> OnBeforeSerialize?.Invoke(this);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>[Editor-Only] [Internal]
|
||||
/// Should the arrays be prevented from reducing their size when their last elements are unused?
|
||||
/// </summary>
|
||||
internal static bool DisableCompactArrays { get; set; }
|
||||
|
||||
/// <summary>[Editor-Only] [Internal]
|
||||
/// Removes empty data from the ends of the arrays to reduce the serialized data size.
|
||||
/// </summary>
|
||||
internal void CompactArrays()
|
||||
{
|
||||
if (DisableCompactArrays)
|
||||
return;
|
||||
|
||||
// If there is only one time and it is NaN, we don't need to store anything.
|
||||
if (_NormalizedTimes == null ||
|
||||
(_NormalizedTimes.Length == 1 &&
|
||||
(_Callbacks == null || _Callbacks.Length == 0) &&
|
||||
(_Names == null || _Names.Length == 0) &&
|
||||
float.IsNaN(_NormalizedTimes[0])))
|
||||
{
|
||||
_NormalizedTimes = Array.Empty<float>();
|
||||
_Callbacks = Array.Empty<IInvokable>();
|
||||
_Names = Array.Empty<StringAsset>();
|
||||
return;
|
||||
}
|
||||
|
||||
Trim(ref _Callbacks, _NormalizedTimes.Length, callback => callback != null);
|
||||
Trim(ref _Names, _NormalizedTimes.Length, name => name != null);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>[Editor-Only] Removes unimportant values from the end of the `array`.</summary>
|
||||
private static void Trim<T>(ref T[] array, int maxLength, Func<T, bool> isImportant)
|
||||
{
|
||||
if (array == null)
|
||||
return;
|
||||
|
||||
var count = Math.Min(array.Length, maxLength);
|
||||
|
||||
while (count >= 1)
|
||||
{
|
||||
var item = array[count - 1];
|
||||
if (isImportant(item))
|
||||
break;
|
||||
else
|
||||
count--;
|
||||
}
|
||||
|
||||
Array.Resize(ref array, count);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endif
|
||||
/************************************************************************************************************************/
|
||||
#endregion
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3fdae2a3656b01946b64f620d7cff364
|
||||
timeCreated: 1515060256
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 32e43f9433f92a3428077ed007e0e287
|
||||
timeCreated: 1515060256
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,375 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Animancer
|
||||
{
|
||||
/// <summary>
|
||||
/// A <see cref="callback"/> delegate
|
||||
/// paired with a <see cref="normalizedTime"/> which determines when to invoke it.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <strong>Documentation:</strong>
|
||||
/// <see href="https://kybernetik.com.au/animancer/docs/manual/events/animancer">
|
||||
/// Animancer Events</see>
|
||||
/// </remarks>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer/AnimancerEvent
|
||||
///
|
||||
public partial struct AnimancerEvent : IEquatable<AnimancerEvent>
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
#region Event
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>The <see cref="AnimancerState.NormalizedTime"/> at which to invoke the <see cref="callback"/>.</summary>
|
||||
public float normalizedTime;
|
||||
|
||||
/// <summary>The delegate to invoke when the <see cref="normalizedTime"/> passes.</summary>
|
||||
public Action callback;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>The largest possible float value less than 1.</summary>
|
||||
/// <remarks>
|
||||
/// This value is useful for placing events at the end of a looping animation since they do not allow the
|
||||
/// <see cref="normalizedTime"/> to be greater than or equal to 1.
|
||||
/// </remarks>
|
||||
public const float
|
||||
AlmostOne = 0.99999994f;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>The event name used for <see cref="Sequence.EndEvent"/>s.</summary>
|
||||
/// <remarks>
|
||||
/// This is a <see cref="StringReference.Unique"/> so that even if the same name happens
|
||||
/// to be used elsewhere, it would be treated as a different name.
|
||||
/// The reason for this is explained in <see cref="NamedEventDictionary.AssertNotEndEvent"/>.
|
||||
/// </remarks>
|
||||
public static readonly StringReference
|
||||
EndEventName = StringReference.Unique("EndEvent");
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Does nothing.</summary>
|
||||
/// <remarks>This delegate can be used for events which would otherwise have a <c>null</c> <see cref="callback"/>.</remarks>
|
||||
public static readonly Action
|
||||
DummyCallback = Dummy;
|
||||
|
||||
/// <summary>Does nothing.</summary>
|
||||
/// <remarks>Used by <see cref="DummyCallback"/>.</remarks>
|
||||
private static void Dummy() { }
|
||||
|
||||
/// <summary>Is the `callback` <c>null</c> or the <see cref="DummyCallback"/>?</summary>
|
||||
public static bool IsNullOrDummy(Action callback)
|
||||
=> callback == null
|
||||
|| callback == DummyCallback;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Creates a new <see cref="AnimancerEvent"/>.</summary>
|
||||
public AnimancerEvent(float normalizedTime, Action callback)
|
||||
{
|
||||
this.normalizedTime = normalizedTime;
|
||||
this.callback = callback;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Returns a string describing the details of this event.</summary>
|
||||
public readonly override string ToString()
|
||||
{
|
||||
var text = StringBuilderPool.Instance.Acquire();
|
||||
text.Append($"{nameof(AnimancerEvent)}(");
|
||||
AppendDetails(text);
|
||||
text.Append(')');
|
||||
return text.ReleaseToString();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Appends the details of this event to the `text`.</summary>
|
||||
public readonly void AppendDetails(StringBuilder text)
|
||||
{
|
||||
text.Append("NormalizedTime: ")
|
||||
.Append(normalizedTime);
|
||||
|
||||
var callbacks = AnimancerReflection.GetInvocationList(callback);
|
||||
if (callbacks != null)
|
||||
{
|
||||
text.Append(", Callbacks: [")
|
||||
.Append(callbacks.Length)
|
||||
.Append("] { ");
|
||||
|
||||
for (int i = 0; i < callbacks.Length; i++)
|
||||
{
|
||||
if (i > 0)
|
||||
text.Append(", ");
|
||||
|
||||
text.AppendDelegate(callbacks[i]);
|
||||
}
|
||||
|
||||
text.Append(" }");
|
||||
}
|
||||
else
|
||||
{
|
||||
text.Append(", Callback: ")
|
||||
.AppendDelegate(callback);
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endregion
|
||||
/************************************************************************************************************************/
|
||||
#region Invocation
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>The details of the event currently being triggered.</summary>
|
||||
/// <remarks>Cleared after the event is invoked.</remarks>
|
||||
// Having the underlying field here can cause type initialization errors due to circular dependencies.
|
||||
public static Invocation Current
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get => Invocation.Current;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// A cached delegate which calls <see cref="Invocation.InvokeBoundCallback"/>
|
||||
/// on the <see cref="Current"/>.
|
||||
/// </summary>
|
||||
public static readonly Action
|
||||
InvokeBoundCallback = InvokeCurrentBoundCallback;
|
||||
|
||||
/// <summary>
|
||||
/// Calls <see cref="Invocation.InvokeBoundCallback"/> on the <see cref="Current"/>.
|
||||
/// </summary>
|
||||
private static void InvokeCurrentBoundCallback()
|
||||
=> Current.InvokeBoundCallback();
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>The custom parameter of the event currently being triggered.</summary>
|
||||
/// <remarks>Cleared after the event is finished.</remarks>
|
||||
public static object CurrentParameter { get; private set; }
|
||||
|
||||
/// <summary>Calls <see cref="ConvertableUtilities.ConvertOrThrow"/> on the <see cref="CurrentParameter"/>.</summary>
|
||||
public static T GetCurrentParameter<T>()
|
||||
=> ConvertableUtilities.ConvertOrThrow<T>(CurrentParameter);
|
||||
|
||||
/// <summary>Returns a new delegate which invokes the `callback` using <see cref="GetCurrentParameter{T}"/>.</summary>
|
||||
/// <remarks>
|
||||
/// If <typeparamref name="T"/> is <see cref="string"/>,
|
||||
/// consider using <see cref="Parametize(Action{string})"/> instead of this.
|
||||
/// </remarks>
|
||||
/// <exception cref="ArgumentNullException">The `callback` is <c>null</c>.</exception>
|
||||
public static Action Parametize<T>(Action<T> callback)
|
||||
{
|
||||
#if UNITY_ASSERTIONS
|
||||
if (callback == null)
|
||||
throw new ArgumentNullException(
|
||||
nameof(callback),
|
||||
$"Can't {nameof(Parametize)} a null callback.");
|
||||
#endif
|
||||
|
||||
return () => callback(GetCurrentParameter<T>());
|
||||
}
|
||||
|
||||
/// <summary>Returns a new delegate which invokes the `callback` using the <see cref="CurrentParameter"/>.</summary>
|
||||
/// <exception cref="ArgumentNullException">The `callback` is <c>null</c>.</exception>
|
||||
public static Action Parametize(Action<string> callback)
|
||||
{
|
||||
#if UNITY_ASSERTIONS
|
||||
if (callback == null)
|
||||
throw new ArgumentNullException(
|
||||
nameof(callback),
|
||||
$"Can't {nameof(Parametize)} a null callback.");
|
||||
#endif
|
||||
|
||||
return () => callback(CurrentParameter?.ToString());
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>[Assert-Only]
|
||||
/// Logs an error if the `callback` doesn't contain a <see cref="Parameter{T}.Invoke"/>
|
||||
/// so that adding to it with <see cref="Parametize{T}(Action{T})"/> can use that parameter.
|
||||
/// </summary>
|
||||
[System.Diagnostics.Conditional(Strings.Assertions)]
|
||||
public static void AssertContainsParameter<T>(Action callback)
|
||||
{
|
||||
if (!ContainsParameterInvoke<T>(callback))
|
||||
Debug.LogWarning(
|
||||
$"Adding parametized callback will do nothing because the existing callback" +
|
||||
$" doesn't contain a {typeof(T).GetNameCS()} parameter." +
|
||||
$"\n• Existing Callback: {callback.ToStringDetailed()}");
|
||||
}
|
||||
|
||||
/// <summary>Does the `callback` contain a <see cref="Parameter{T}.Invoke"/>?</summary>
|
||||
private static bool ContainsParameterInvoke<T>(Action callback)
|
||||
{
|
||||
if (callback == null)
|
||||
return false;
|
||||
|
||||
if (IsParameterInvoke<T>(callback))
|
||||
return true;
|
||||
|
||||
var invocations = AnimancerReflection.GetInvocationList(callback);
|
||||
|
||||
if (invocations.Length == 1 && ReferenceEquals(invocations[0], callback))
|
||||
return false;
|
||||
|
||||
for (int i = 0; i < invocations.Length; i++)
|
||||
{
|
||||
var invocation = invocations[i];
|
||||
if (IsParameterInvoke<T>(invocation))
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>Is the `callback` a call to <see cref="Parameter{T}.Invoke"/>?</summary>
|
||||
private static bool IsParameterInvoke<T>(Delegate callback)
|
||||
=> callback.Target is IParameter parameter
|
||||
&& callback.Method.Name == nameof(IInvokable.Invoke)
|
||||
&& typeof(T).IsAssignableFrom(parameter.Value.GetType());
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// Adds this event to the <see cref="Invoker"/>
|
||||
/// which will call <see cref="Invocation.Invoke"/> later in the current frame.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public readonly void DelayInvoke(
|
||||
StringReference eventName,
|
||||
AnimancerState state)
|
||||
=> Invoker.Add(new(this, eventName, state));
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>[Assert-Conditional]
|
||||
/// This method should be called when an animation is played.
|
||||
/// It asserts that either no event is currently being triggered
|
||||
/// or that the event is being triggered inside `playing`.
|
||||
/// Otherwise, it logs <see cref="OptionalWarning.EventPlayMismatch"/>.
|
||||
/// </summary>
|
||||
[System.Diagnostics.Conditional(Strings.Assertions)]
|
||||
public static void AssertEventPlayMismatch(AnimancerGraph playing)
|
||||
{
|
||||
#if UNITY_ASSERTIONS
|
||||
if (Current.State == null ||
|
||||
Current.State.Graph == playing ||
|
||||
OptionalWarning.EventPlayMismatch.IsDisabled())
|
||||
return;
|
||||
|
||||
OptionalWarning.EventPlayMismatch.Log(
|
||||
$"An Animancer Event triggered by '{Current.State}' on '{Current.State.Graph}'" +
|
||||
$" was used to play an animation on a different character ('{playing}')." +
|
||||
$"\n\nThis most commonly happens when a Transition is shared by multiple characters" +
|
||||
$" and they all register their own callbacks to its events which leads to" +
|
||||
$" those events being triggered by the wrong character." +
|
||||
$"\n\n{Current}",
|
||||
playing.Component);
|
||||
#endif
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// Returns either the <see cref="AnimancerGraph.DefaultFadeDuration"/>
|
||||
/// or the <see cref="AnimancerState.RemainingDuration"/>
|
||||
/// of the <see cref="Current"/> state (whichever is higher).
|
||||
/// </summary>
|
||||
public static float GetFadeOutDuration()
|
||||
=> GetFadeOutDuration(Current.State, AnimancerGraph.DefaultFadeDuration);
|
||||
|
||||
/// <summary>
|
||||
/// Returns either the `minDuration` or the <see cref="AnimancerState.RemainingDuration"/>
|
||||
/// of the <see cref="Current"/> state (whichever is higher).
|
||||
/// </summary>
|
||||
public static float GetFadeOutDuration(float minDuration)
|
||||
=> GetFadeOutDuration(Current.State, minDuration);
|
||||
|
||||
/// <summary>
|
||||
/// Returns either the `minDuration` or the <see cref="AnimancerState.RemainingDuration"/>
|
||||
/// of the `state` (whichever is higher).
|
||||
/// </summary>
|
||||
public static float GetFadeOutDuration(AnimancerState state, float minDuration)
|
||||
{
|
||||
if (state == null)
|
||||
return minDuration;
|
||||
|
||||
var time = state.Time;
|
||||
var speed = state.EffectiveSpeed;
|
||||
if (speed == 0)
|
||||
return minDuration;
|
||||
|
||||
float remainingDuration;
|
||||
if (state.IsLooping)
|
||||
{
|
||||
var previousTime = time - speed * Time.deltaTime;
|
||||
var inverseLength = 1f / state.Length;
|
||||
|
||||
// If we just passed the end of the animation, the remaining duration would technically be the full
|
||||
// duration of the animation, so we most likely want to use the minimum duration instead.
|
||||
if (Math.Floor(time * inverseLength) != Math.Floor(previousTime * inverseLength))
|
||||
return minDuration;
|
||||
}
|
||||
|
||||
if (speed > 0)
|
||||
{
|
||||
remainingDuration = (state.Length - time) / speed;
|
||||
}
|
||||
else
|
||||
{
|
||||
remainingDuration = time / -speed;
|
||||
}
|
||||
|
||||
return Math.Max(minDuration, remainingDuration);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endregion
|
||||
/************************************************************************************************************************/
|
||||
#region Operators
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Are the <see cref="normalizedTime"/> and <see cref="callback"/> equal?</summary>
|
||||
public static bool operator ==(AnimancerEvent a, AnimancerEvent b)
|
||||
=> a.Equals(b);
|
||||
|
||||
/// <summary>Are the <see cref="normalizedTime"/> and <see cref="callback"/> not equal?</summary>
|
||||
public static bool operator !=(AnimancerEvent a, AnimancerEvent b)
|
||||
=> !a.Equals(b);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>[<see cref="IEquatable{AnimancerEvent}"/>]
|
||||
/// Are the <see cref="normalizedTime"/> and <see cref="callback"/> of this event equal to `other`?
|
||||
/// </summary>
|
||||
public readonly bool Equals(AnimancerEvent other)
|
||||
=> callback == other.callback
|
||||
&& normalizedTime.IsEqualOrBothNaN(other.normalizedTime);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public readonly override bool Equals(object obj)
|
||||
=> obj is AnimancerEvent animancerEvent
|
||||
&& Equals(animancerEvent);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public readonly override int GetHashCode()
|
||||
=> AnimancerUtilities.Hash(-78069441,
|
||||
normalizedTime.GetHashCode(),
|
||||
callback.SafeGetHashCode());
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endregion
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6c5dbe324bf11624d95460fa335ae439
|
||||
timeCreated: 1515060256
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,406 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
using System.Runtime.CompilerServices;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Animancer
|
||||
{
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer/AnimancerState
|
||||
partial class AnimancerState
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>The system which manages the <see cref="SharedEvents"/>.</summary>
|
||||
private AnimancerEvent.Dispatcher _EventDispatcher;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// Events which will be triggered while this state plays
|
||||
/// based on its <see cref="NormalizedTime"/>.
|
||||
/// </summary>
|
||||
///
|
||||
/// <remarks>
|
||||
/// This property tries to ensure that the event sequence is only referenced by this state.
|
||||
/// <list type="bullet">
|
||||
/// <item>
|
||||
/// If the reference was <c>null</c>,
|
||||
/// a new sequence will be created.
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// If a reference was assigned to <see cref="SharedEvents"/>,
|
||||
/// it will be cloned so this state owns the clone.
|
||||
/// </item>
|
||||
/// </list>
|
||||
/// <para></para>
|
||||
/// Using <see cref="Events(object)"/> or <see cref="Events(object, out AnimancerEvent.Sequence)"/>
|
||||
/// is often safer than this property since they help detect if multiple scripts are using the same
|
||||
/// state which could lead to unexpected bugs if they each assign conflicting callbacks.
|
||||
/// <para></para>
|
||||
/// <strong>Documentation:</strong>
|
||||
/// <see href="https://kybernetik.com.au/animancer/docs/manual/events/animancer">
|
||||
/// Animancer Events</see>
|
||||
/// </remarks>
|
||||
public AnimancerEvent.Sequence OwnedEvents
|
||||
{
|
||||
get
|
||||
{
|
||||
_EventDispatcher ??= new(this);
|
||||
_EventDispatcher.InitializeEvents(out var events);
|
||||
return events;
|
||||
}
|
||||
set
|
||||
{
|
||||
if (value != null)
|
||||
(_EventDispatcher ??= new(this)).SetEvents(value, true);
|
||||
else
|
||||
_EventDispatcher = null;
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// Events which will be triggered while this state plays
|
||||
/// based on its <see cref="NormalizedTime"/>.
|
||||
/// </summary>
|
||||
///
|
||||
/// <remarks>
|
||||
/// This reference is <c>null</c> by default and once assigned it may be shared by multiple states.
|
||||
/// <para></para>
|
||||
/// <strong>Documentation:</strong>
|
||||
/// <see href="https://kybernetik.com.au/animancer/docs/manual/events/animancer">
|
||||
/// Animancer Events</see>
|
||||
/// </remarks>
|
||||
public AnimancerEvent.Sequence SharedEvents
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get => _EventDispatcher?.Events;
|
||||
set
|
||||
{
|
||||
if (value != null)
|
||||
(_EventDispatcher ??= new(this)).SetEvents(value, false);
|
||||
else
|
||||
_EventDispatcher = null;
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Have the <see cref="SharedEvents"/> or <see cref="OwnedEvents"/> been initialized?</summary>
|
||||
/// <remarks>
|
||||
/// <strong>Documentation:</strong>
|
||||
/// <see href="https://kybernetik.com.au/animancer/docs/manual/events/animancer">
|
||||
/// Animancer Events</see>
|
||||
/// </remarks>
|
||||
public bool HasEvents
|
||||
=> _EventDispatcher != null;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Have the <see cref="OwnedEvents"/> been initialized?</summary>
|
||||
/// <remarks>
|
||||
/// <strong>Documentation:</strong>
|
||||
/// <see href="https://kybernetik.com.au/animancer/docs/manual/events/animancer">
|
||||
/// Animancer Events</see>
|
||||
/// </remarks>
|
||||
public bool HasOwnedEvents
|
||||
=> _EventDispatcher != null
|
||||
&& _EventDispatcher.HasOwnEvents;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// If the <see cref="OwnedEvents"/> haven't been initialized yet,
|
||||
/// this method gets them and returns <c>true</c>.
|
||||
/// </summary>
|
||||
///
|
||||
/// <remarks>
|
||||
/// This method tries to ensure that the event sequence is only referenced by this state.
|
||||
/// <list type="bullet">
|
||||
/// <item>
|
||||
/// If the reference was <c>null</c>,
|
||||
/// a new sequence will be created.
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// If a reference was assigned to <see cref="SharedEvents"/>,
|
||||
/// it will be cloned so this state owns the clone.
|
||||
/// </item>
|
||||
/// </list>
|
||||
/// In both of those cases, this method returns <c>true</c>
|
||||
/// to indicate that the caller should initialize their event callbacks.
|
||||
/// <para></para>
|
||||
/// Also calls <see cref="AssertOwnership"/>.
|
||||
/// <para></para>
|
||||
/// <strong>Documentation:</strong>
|
||||
/// <see href="https://kybernetik.com.au/animancer/docs/manual/events/animancer">
|
||||
/// Animancer Events</see>
|
||||
/// <para></para>
|
||||
/// <strong>Example:</strong>
|
||||
/// <code>
|
||||
/// public static readonly StringReference EventName = "Event Name";
|
||||
///
|
||||
/// ...
|
||||
///
|
||||
/// AnimancerState state = animancerComponent.Play(animation);
|
||||
/// if (state.Events(this, out AnimancerEvent.Sequence events))
|
||||
/// {
|
||||
/// events.SetCallback(EventName, OnAnimationEvent);
|
||||
/// events.OnEnd = OnAnimationEnded;
|
||||
/// }
|
||||
/// </code>
|
||||
/// If multiple different owners need to take turns reusing the same state,
|
||||
/// use <see cref="Events(ref AnimancerEvent.Sequence)"/> instead.
|
||||
/// <para></para>
|
||||
/// If you only need to initialize the End Event,
|
||||
/// consider using <see cref="Events(object)"/> instead.
|
||||
/// </remarks>
|
||||
public bool Events(object owner, out AnimancerEvent.Sequence events)
|
||||
{
|
||||
AssertOwnership(owner);
|
||||
_EventDispatcher ??= new(this);
|
||||
return _EventDispatcher.InitializeEvents(out events);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// If the <see cref="OwnedEvents"/> haven't been initialized yet,
|
||||
/// this method gets them and returns <c>true</c>.
|
||||
/// </summary>
|
||||
///
|
||||
/// <remarks>
|
||||
/// This method tries to ensure that the event sequence is only referenced by this state.
|
||||
/// <list type="bullet">
|
||||
/// <item>
|
||||
/// If the reference was <c>null</c>,
|
||||
/// a new sequence will be created.
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// If a reference was assigned to <see cref="SharedEvents"/>,
|
||||
/// it will be cloned so this state owns the clone.
|
||||
/// </item>
|
||||
/// </list>
|
||||
/// <para></para>
|
||||
/// Also calls <see cref="AssertOwnership"/>.
|
||||
/// <para></para>
|
||||
/// <strong>Documentation:</strong>
|
||||
/// <see href="https://kybernetik.com.au/animancer/docs/manual/events/animancer">
|
||||
/// Animancer Events</see>
|
||||
/// <para></para>
|
||||
/// <strong>Example:</strong>
|
||||
/// <code>
|
||||
/// AnimancerState state = animancerComponent.Play(animation);
|
||||
/// state.Events(this).OnEnd ??= OnAnimationEnded;
|
||||
/// </code>
|
||||
/// If multiple different owners need to take turns reusing the same state,
|
||||
/// use <see cref="Events(ref AnimancerEvent.Sequence)"/> instead.
|
||||
/// <para></para>
|
||||
/// If you need to initialize more than just the End Event,
|
||||
/// use <see cref="Events(object, out AnimancerEvent.Sequence)"/> instead.
|
||||
/// </remarks>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public AnimancerEvent.Sequence Events(object owner)
|
||||
{
|
||||
Events(owner, out var events);
|
||||
return events;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// If the `events` are <c>null</c>, this method assigns a <c>new</c> <see cref="AnimancerEvent.Sequence"/>
|
||||
/// and returns <c>true</c> to indicate that the caller should now initialize their event callbacks.
|
||||
/// Otherwise, this method simply assigns the provided `events` to this state and returns <c>false</c>.
|
||||
/// </summary>
|
||||
///
|
||||
/// <remarks>
|
||||
/// If this state already had events, the <c>new</c> <see cref="AnimancerEvent.Sequence"/>
|
||||
/// will be a copy of those events for the caller to own.
|
||||
/// <para></para>
|
||||
/// This method allows multiple callers to safely take turns using the same state
|
||||
/// as long as they each call this method to assign their own events.
|
||||
/// <para></para>
|
||||
/// Also calls <see cref="AssertOwnership"/>.
|
||||
/// <para></para>
|
||||
/// <strong>Documentation:</strong>
|
||||
/// <see href="https://kybernetik.com.au/animancer/docs/manual/events/animancer">
|
||||
/// Animancer Events</see>
|
||||
/// <para></para>
|
||||
/// <strong>Example:</strong>
|
||||
/// <code>
|
||||
/// public static readonly StringReference EventName = "Event Name";
|
||||
///
|
||||
/// private AnimancerEvent.Sequence _Events;// Don't new() this.
|
||||
///
|
||||
/// ...
|
||||
///
|
||||
/// AnimancerState state = animancerComponent.Play(animation);
|
||||
///
|
||||
/// // The first time this is called it will assign a new event sequence
|
||||
/// // to the _Events and return true so you can initialize it.
|
||||
///
|
||||
/// // After that, it will just re-assign the _Events to the state.
|
||||
/// // and return false so you don't need to re-initialize the events.
|
||||
///
|
||||
/// if (state.Events(ref _Events))
|
||||
/// {
|
||||
/// _Events.SetCallback(EventName, OnAnimationEvent);
|
||||
/// _Events.OnEnd = OnAnimationEnded;
|
||||
/// }
|
||||
/// </code>
|
||||
/// </remarks>
|
||||
public bool Events(ref AnimancerEvent.Sequence events)
|
||||
{
|
||||
_EventDispatcher ??= new(this);
|
||||
|
||||
var justInitialized = events == null;
|
||||
if (justInitialized)
|
||||
events = new(_EventDispatcher.Events);
|
||||
|
||||
#if UNITY_ASSERTIONS
|
||||
// Normally swapping owners is an error,
|
||||
// but with this method it's fine to swap between event sequences since each caller is responsible for its own.
|
||||
if (Owner != null &&
|
||||
Owner != events &&
|
||||
Owner is not AnimancerEvent.Sequence)
|
||||
AssertOwnership(events);
|
||||
else
|
||||
Owner = events;
|
||||
#endif
|
||||
|
||||
_EventDispatcher.SetEvents(events, false);
|
||||
return justInitialized;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Copies the contents of the <see cref="_EventDispatcher"/>.</summary>
|
||||
private void CopyEvents(AnimancerState copyFrom, CloneContext context)
|
||||
{
|
||||
if (copyFrom._EventDispatcher != null)
|
||||
{
|
||||
var original = copyFrom._EventDispatcher.Events;
|
||||
var events = context.GetOrCreateCloneOrOriginal(original);
|
||||
if (events != null)
|
||||
{
|
||||
_EventDispatcher ??= new(this);
|
||||
_EventDispatcher.SetEvents(events, true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
_EventDispatcher = null;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Should events be raised on a state which is currently fading out?</summary>
|
||||
/// <remarks>
|
||||
/// Default <c>false</c>.
|
||||
/// <para></para>
|
||||
/// <strong>Documentation:</strong>
|
||||
/// <see href="https://kybernetik.com.au/animancer/docs/manual/events/animancer">
|
||||
/// Animancer Events</see>
|
||||
/// </remarks>
|
||||
public static bool RaiseEventsDuringFadeOut { get; set; }
|
||||
|
||||
/// <summary>Should this state check for events to invoke?</summary>
|
||||
private bool ShouldRaiseEvents
|
||||
=> TargetWeight > 0
|
||||
|| RaiseEventsDuringFadeOut;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// Checks if any events should be invoked based on the current time of this state.
|
||||
/// </summary>
|
||||
protected internal virtual void UpdateEvents()
|
||||
=> _EventDispatcher?.UpdateEvents(ShouldRaiseEvents);
|
||||
|
||||
/// <summary>
|
||||
/// Checks if any events should be invoked on the `parent` and its children recursively.
|
||||
/// </summary>
|
||||
public static void UpdateEventsRecursive(AnimancerState parent)
|
||||
=> UpdateEventsRecursive(
|
||||
parent,
|
||||
parent.ShouldRaiseEvents);
|
||||
|
||||
/// <summary>
|
||||
/// Checks if any events should be invoked on the `parent` and its children recursively.
|
||||
/// </summary>
|
||||
public static void UpdateEventsRecursive(AnimancerState parent, bool raiseEvents)
|
||||
{
|
||||
parent._EventDispatcher?.UpdateEvents(raiseEvents);
|
||||
|
||||
for (int i = parent.ChildCount - 1; i >= 0; i--)
|
||||
{
|
||||
var child = parent.GetChild(i);
|
||||
UpdateEventsRecursive(child, raiseEvents && child.Weight > 0);
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// Sets the <see cref="NormalizedTime"/> to the <see cref="NormalizedEndTime"/>
|
||||
/// and invokes any remaining <see cref="AnimancerEvent"/>s.
|
||||
/// </summary>
|
||||
public void FinishImmediately()
|
||||
{
|
||||
if (_EventDispatcher != null)
|
||||
_EventDispatcher.FinishImmediately();
|
||||
else
|
||||
NormalizedTime = AnimancerEvent.Sequence.GetDefaultNormalizedEndTime(EffectiveSpeed);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#if UNITY_ASSERTIONS
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>[Assert-Only]
|
||||
/// Returns <c>null</c> if Animancer Events will work properly on this type of state,
|
||||
/// or a message explaining why they might not work.
|
||||
/// </summary>
|
||||
protected internal virtual string UnsupportedEventsMessage
|
||||
=> null;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>[Assert-Only] An optional reference to the object that owns this state.</summary>
|
||||
public object Owner { get; private set; }
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endif
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>[Assert-Conditional]
|
||||
/// Sets the <see cref="Owner"/> and asserts that it wasn't already set to a different object.
|
||||
/// </summary>
|
||||
/// <remarks>This helps detect if multiple scripts attempt to manage the same state.</remarks>
|
||||
[System.Diagnostics.Conditional(Strings.Assertions)]
|
||||
public void AssertOwnership(object owner)
|
||||
{
|
||||
#if UNITY_ASSERTIONS
|
||||
if (Owner == owner)
|
||||
return;
|
||||
|
||||
if (Owner != null)
|
||||
{
|
||||
Debug.LogError(
|
||||
$"Multiple objects have asserted ownership over the state '{ToString()}':" +
|
||||
$"\n• Old Owner: {AnimancerUtilities.ToStringOrNull(Owner)}" +
|
||||
$"\n• New Owner: {AnimancerUtilities.ToStringOrNull(owner)}" +
|
||||
$"\n• State: {GetPath()}" +
|
||||
$"\n• Graph: {Graph?.GetDescription("\n• ")}",
|
||||
Graph?.Component as Object);
|
||||
}
|
||||
|
||||
Owner = owner;
|
||||
#endif
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8523302103ade774080bab8eb1f8a840
|
||||
timeCreated: 1515060256
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bfafec2a7eda5e149b200fa583c51e15
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,64 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
using UnityEngine;
|
||||
|
||||
namespace Animancer
|
||||
{
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer/AnimancerEvent
|
||||
partial struct AnimancerEvent
|
||||
{
|
||||
/// <summary>A non-generic interface for <see cref="Parameter{T}"/>.</summary>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer/IParameter
|
||||
public interface IParameter
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>The parameter value.</summary>
|
||||
object Value { get; set; }
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Base class for <see cref="IInvokable"/>s which assign the <see cref="CurrentParameter"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Inherit from <see cref="ParameterBoxed{T}"/>
|
||||
/// instead of this if <typeparamref name="T"/> is a value type to avoid repeated boxing costs.
|
||||
/// </remarks>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer/Parameter_1
|
||||
public abstract class Parameter<T> :
|
||||
IParameter,
|
||||
IInvokable
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
[SerializeField]
|
||||
private T _Value;
|
||||
|
||||
/// <summary>[<see cref="SerializeField"/>] The serialized <typeparamref name="T"/>.</summary>
|
||||
public virtual T Value
|
||||
{
|
||||
get => _Value;
|
||||
set => _Value = value;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
object IParameter.Value
|
||||
{
|
||||
get => _Value;
|
||||
set => _Value = (T)value;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual void Invoke()
|
||||
{
|
||||
CurrentParameter = _Value;
|
||||
Current.InvokeBoundCallback();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5927c425f3f0c0e4baeb0cd737707442
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,68 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
using UnityEngine;
|
||||
|
||||
namespace Animancer
|
||||
{
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer/AnimancerEvent
|
||||
partial struct AnimancerEvent
|
||||
{
|
||||
/// <summary>
|
||||
/// An <see cref="Parameter{T}"/>s which internally boxes value types
|
||||
/// to avoid re-boxing them every <see cref="Invoke"/>.
|
||||
/// </summary>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer/ParameterBoxed_1
|
||||
public abstract class ParameterBoxed<T> : Parameter<T>,
|
||||
IParameter
|
||||
#if UNITY_EDITOR
|
||||
, ISerializationCallbackReceiver
|
||||
#endif
|
||||
where T : struct
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private object _Boxed;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override T Value
|
||||
{
|
||||
get => base.Value;
|
||||
set
|
||||
{
|
||||
base.Value = value;
|
||||
_Boxed = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
object IParameter.Value
|
||||
{
|
||||
get => Value;
|
||||
set => Value = (T)value;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Invoke()
|
||||
{
|
||||
CurrentParameter = _Boxed ??= base.Value;
|
||||
Current.InvokeBoundCallback();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#if UNITY_EDITOR
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
void ISerializationCallbackReceiver.OnBeforeSerialize() { }
|
||||
|
||||
/// <inheritdoc/>
|
||||
void ISerializationCallbackReceiver.OnAfterDeserialize()
|
||||
=> _Boxed = null;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endif
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 30eb95e632f815a49a7ac0f1d68cb006
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,56 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
using System;
|
||||
|
||||
namespace Animancer
|
||||
{
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer/AnimancerEvent
|
||||
partial struct AnimancerEvent
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
// Reference Types.
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>An <see cref="Parameter{T}"/> for <see cref="UnityEngine.Object"/>.</summary>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer/ParameterObject
|
||||
[Serializable]
|
||||
public class ParameterObject : Parameter<UnityEngine.Object> { }
|
||||
|
||||
/// <summary>An <see cref="Parameter{T}"/> for <see cref="string"/>.</summary>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer/ParameterString
|
||||
[Serializable]
|
||||
public class ParameterString : Parameter<string> { }
|
||||
|
||||
/************************************************************************************************************************/
|
||||
// Value Types.
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>An <see cref="Parameter{T}"/> for <see cref="bool"/>.</summary>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer/ParameterBool
|
||||
[Serializable]
|
||||
public class ParameterBool : ParameterBoxed<bool> { }
|
||||
|
||||
/// <summary>An <see cref="Parameter{T}"/> for <see cref="double"/>.</summary>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer/ParameterDouble
|
||||
[Serializable]
|
||||
public class ParameterDouble : ParameterBoxed<double> { }
|
||||
|
||||
/// <summary>An <see cref="Parameter{T}"/> for <see cref="float"/>.</summary>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer/ParameterFloat
|
||||
[Serializable]
|
||||
public class ParameterFloat : ParameterBoxed<float> { }
|
||||
|
||||
/// <summary>An <see cref="Parameter{T}"/> for <see cref="int"/>.</summary>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer/ParameterInt
|
||||
[Serializable]
|
||||
public class ParameterInt : ParameterBoxed<int> { }
|
||||
|
||||
/// <summary>An <see cref="Parameter{T}"/> for <see cref="long"/>.</summary>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer/ParameterLong
|
||||
[Serializable]
|
||||
public class ParameterLong : ParameterBoxed<long> { }
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ddc1c2d634269b94891d46befabcb48b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,16 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
#if ULT_EVENTS
|
||||
|
||||
using System;
|
||||
|
||||
namespace Animancer
|
||||
{
|
||||
/// <summary>An <see cref="UltEvents.UltEvent"/> which implements <see cref="IInvokable"/>.</summary>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer/UltEvent
|
||||
[Serializable]
|
||||
public class UltEvent : UltEvents.UltEvent, IInvokable { }
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 03d1988336fb5dd4ab091ab50047a06f
|
||||
timeCreated: 1515060256
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,12 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
using System;
|
||||
|
||||
namespace Animancer
|
||||
{
|
||||
/// <summary>A <see cref="UnityEngine.Events.UnityEvent"/> which implements <see cref="IInvokable"/>.</summary>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer/UnityEvent
|
||||
[Serializable]
|
||||
public class UnityEvent : UnityEngine.Events.UnityEvent, IInvokable { }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9391a6fb545883c44a19bb0c1f8b2589
|
||||
timeCreated: 1515060256
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,334 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Animancer
|
||||
{
|
||||
/// <summary>A dictionary which maps event names to callbacks.</summary>
|
||||
/// <remarks>
|
||||
/// <strong>Documentation:</strong>
|
||||
/// <see href="https://kybernetik.com.au/animancer/docs/manual/events/animancer">
|
||||
/// Animancer Events</see>
|
||||
/// </remarks>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer/NamedEventDictionary
|
||||
public class NamedEventDictionary : IDictionary<StringReference, Action>
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private readonly Dictionary<StringReference, Action>
|
||||
Dictionary = new();
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>The number of items in this dictionary.</summary>
|
||||
public int Count
|
||||
=> Dictionary.Count;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#region Access
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Accesses a callback in this dictionary.</summary>
|
||||
public Action this[StringReference name]
|
||||
{
|
||||
get => Dictionary[name];
|
||||
set
|
||||
{
|
||||
AssertNotEndEvent(name);
|
||||
Dictionary[name] = value;
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Returns the callback registered using the `name`.</summary>
|
||||
/// <remarks>Returns <c>null</c> if nothing was registered.</remarks>
|
||||
public Action Get(StringReference name)
|
||||
=> Dictionary.Get(name);
|
||||
|
||||
/// <summary>Registers the callback using the `name`, replacing anything previously registered.</summary>
|
||||
public void Set(StringReference name, Action callback)
|
||||
{
|
||||
AssertNotEndEvent(name);
|
||||
Dictionary[name] = callback;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Are any callbacks registered for the `name`?</summary>
|
||||
/// <remarks>To get the registered callbacks at the same time, use <see cref="TryGetValue"/> instead.</remarks>
|
||||
public bool ContainsKey(StringReference name)
|
||||
=> Dictionary.ContainsKey(name);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Tries to get the `callback` registered with the `name` and returns <c>true</c> if successful.</summary>
|
||||
public bool TryGetValue(StringReference name, out Action callback)
|
||||
=> Dictionary.TryGetValue(name, out callback);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endregion
|
||||
/************************************************************************************************************************/
|
||||
#region Add
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Adds the `callback` to any existing ones registered with the `name`.</summary>
|
||||
/// <remarks>
|
||||
/// If you want an exception to be thrown if something is already registered with the `name`,
|
||||
/// use <see cref="AddNew(StringReference, Action)"/> instead.
|
||||
/// </remarks>
|
||||
public void AddTo(StringReference name, Action callback)
|
||||
{
|
||||
AssertNotEndEvent(name);
|
||||
|
||||
if (Dictionary.TryGetValue(name, out var existing))
|
||||
callback = existing + callback;
|
||||
|
||||
Dictionary[name] = callback;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// Registers the `callback` with the `name` but throws an <see cref="ArgumentException"/>
|
||||
/// if something was already registered with the same `name`.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This matches the standard <see cref="Dictionary{TKey, TValue}.Add(TKey, TValue)"/> behaviour,
|
||||
/// unlike <see cref="AddTo(StringReference, Action)"/>.
|
||||
/// </remarks>
|
||||
public void AddNew(StringReference name, Action callback)
|
||||
{
|
||||
AssertNotEndEvent(name);
|
||||
Dictionary.Add(name, callback);
|
||||
}
|
||||
|
||||
void IDictionary<StringReference, Action>.Add(StringReference name, Action callback)
|
||||
=> AddNew(name, callback);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// Adds the `callback` to any existing ones registered with the `name`.
|
||||
/// <para></para>
|
||||
/// It will be invoked using <see cref="AnimancerEvent.GetCurrentParameter{T}"/> to get its parameter.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// If you want an exception to be thrown if something is already registered with the `name`,
|
||||
/// use <see cref="AddNew{T}(StringReference, Action{T})"/> instead.
|
||||
/// <para></para>
|
||||
/// If <typeparamref name="T"/> is <see cref="string"/>,
|
||||
/// consider using <see cref="AddTo(StringReference, Action{string})"/> instead of this overload.
|
||||
/// <para></para>
|
||||
/// If you want to later remove the `callback`,
|
||||
/// you need to store and remove the returned <see cref="Action"/>.
|
||||
/// </remarks>
|
||||
public Action AddTo<T>(StringReference name, Action<T> callback)
|
||||
{
|
||||
AssertNotEndEvent(name);
|
||||
var parametized = AnimancerEvent.Parametize(callback);
|
||||
|
||||
if (Dictionary.TryGetValue(name, out var existing))
|
||||
parametized = existing + parametized;
|
||||
|
||||
Dictionary[name] = parametized;
|
||||
|
||||
return parametized;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers the `callback` with the `name` but throws an <see cref="ArgumentException"/>
|
||||
/// if something was already registered with the same `name`.
|
||||
/// <para></para>
|
||||
/// It will be invoked using <see cref="AnimancerEvent.GetCurrentParameter{T}"/> to get its parameter.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This matches the standard <see cref="Dictionary{TKey, TValue}.Add(TKey, TValue)"/> behaviour,
|
||||
/// unlike <see cref="AddTo{T}(StringReference, Action{T})"/>.
|
||||
/// If <typeparamref name="T"/> is <see cref="string"/>,
|
||||
/// consider using <see cref="AddTo(StringReference, Action{string})"/> instead of this overload.
|
||||
/// <para></para>
|
||||
/// If you want to later remove the `callback`,
|
||||
/// you need to store and remove the returned <see cref="Action"/>.
|
||||
/// </remarks>
|
||||
public Action AddNew<T>(StringReference name, Action<T> callback)
|
||||
{
|
||||
AssertNotEndEvent(name);
|
||||
var parametized = AnimancerEvent.Parametize(callback);
|
||||
Dictionary.Add(name, parametized);
|
||||
return parametized;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// Adds the `callback` to any existing ones registered with the `name`.
|
||||
/// <para></para>
|
||||
/// It will be invoked using <see cref="object.ToString"/> on the
|
||||
/// <see cref="AnimancerEvent.CurrentParameter"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// If you want an exception to be thrown if something is already registered with the `name`,
|
||||
/// use <see cref="AddNew{T}(StringReference, Action{T})"/> instead.
|
||||
/// <para></para>
|
||||
/// If you want to later remove the `callback`,
|
||||
/// you need to store and remove the returned <see cref="Action"/>.
|
||||
/// </remarks>
|
||||
public Action AddTo(StringReference name, Action<string> callback)
|
||||
{
|
||||
AssertNotEndEvent(name);
|
||||
var parametized = AnimancerEvent.Parametize(callback);
|
||||
|
||||
if (Dictionary.TryGetValue(name, out var existing))
|
||||
parametized = existing + parametized;
|
||||
|
||||
Dictionary[name] = parametized;
|
||||
return parametized;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers the `callback` with the `name` but throws an <see cref="ArgumentException"/>
|
||||
/// if something was already registered with the same `name`.
|
||||
/// <para></para>
|
||||
/// It will be invoked using <see cref="object.ToString"/> on the
|
||||
/// <see cref="AnimancerEvent.CurrentParameter"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This matches the standard <see cref="Dictionary{TKey, TValue}.Add(TKey, TValue)"/>
|
||||
/// behaviour, unlike <see cref="AddTo(StringReference, Action{string})"/>.
|
||||
/// <para></para>
|
||||
/// If you want to later remove the `callback`,
|
||||
/// you need to store and remove the returned <see cref="Action"/>.
|
||||
/// </remarks>
|
||||
public Action AddNew(StringReference name, Action<string> callback)
|
||||
{
|
||||
AssertNotEndEvent(name);
|
||||
var parametized = AnimancerEvent.Parametize(callback);
|
||||
Dictionary.Add(name, parametized);
|
||||
return parametized;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>[Assert-Conditional]
|
||||
/// Throws an <see cref="ArgumentException"/> if the `name` is the <see cref="AnimancerEvent.EndEventName"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// In order to minimise the performance cost of End Events when there isn't one,
|
||||
/// the <see cref="AnimancerEvent.Dispatcher"/> won't even check the end time
|
||||
/// when there is no <see cref="AnimancerEvent.Sequence.OnEnd"/> callback.
|
||||
/// <para></para>
|
||||
/// That means if a callback was bound to the <see cref="AnimancerEvent.EndEventName"/>
|
||||
/// it would be triggered by any state with an <see cref="AnimancerEvent.Sequence.OnEnd"/>
|
||||
/// callback, but not by states without one. That would be very counterintuitive so it isn't allowed.
|
||||
/// </remarks>
|
||||
/// <exception cref="ArgumentException"/>
|
||||
[System.Diagnostics.Conditional(Strings.Assertions)]
|
||||
public static void AssertNotEndEvent(StringReference name)
|
||||
{
|
||||
if (name == AnimancerEvent.EndEventName)
|
||||
throw new ArgumentException(
|
||||
$"Binding event callbacks to the " +
|
||||
$"{nameof(AnimancerEvent)}.{nameof(AnimancerEvent.EndEventName)}" +
|
||||
$" is not supported for performance optimization reasons. See the documentation of" +
|
||||
$" {nameof(NamedEventDictionary)}.{nameof(AssertNotEndEvent)} for more details.",
|
||||
nameof(name));
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endregion
|
||||
/************************************************************************************************************************/
|
||||
#region Remove
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Removes all callbacks registered with the `name`.</summary>
|
||||
public bool Remove(StringReference name)
|
||||
=> Dictionary.Remove(name);
|
||||
|
||||
/// <summary>Removes a specific `callback` registered with the `name`.</summary>
|
||||
public bool Remove(StringReference name, Action callback)
|
||||
{
|
||||
if (!Dictionary.TryGetValue(name, out var callbacks))
|
||||
return false;
|
||||
|
||||
if (callbacks == callback)
|
||||
Dictionary.Remove(name);
|
||||
else
|
||||
Dictionary[name] = callbacks - callback;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Removes everything from this dictionary.</summary>
|
||||
public void Clear()
|
||||
=> Dictionary.Clear();
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endregion
|
||||
/************************************************************************************************************************/
|
||||
#region Enumeration
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Returns an enumerator to go through every item in this dictionary.</summary>
|
||||
public Dictionary<StringReference, Action>.Enumerator GetEnumerator()
|
||||
=> Dictionary.GetEnumerator();
|
||||
|
||||
IEnumerator<KeyValuePair<StringReference, Action>>
|
||||
IEnumerable<KeyValuePair<StringReference, Action>>.GetEnumerator()
|
||||
=> Dictionary.GetEnumerator();
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator()
|
||||
=> Dictionary.GetEnumerator();
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>The names in this dictionary.</summary>
|
||||
public Dictionary<StringReference, Action>.KeyCollection Keys
|
||||
=> Dictionary.Keys;
|
||||
|
||||
/// <summary>The values in this dictionary.</summary>
|
||||
public Dictionary<StringReference, Action>.ValueCollection Values
|
||||
=> Dictionary.Values;
|
||||
|
||||
ICollection<StringReference> IDictionary<StringReference, Action>.Keys
|
||||
=> Dictionary.Keys;
|
||||
|
||||
ICollection<Action> IDictionary<StringReference, Action>.Values
|
||||
=> Dictionary.Values;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endregion
|
||||
/************************************************************************************************************************/
|
||||
#region Explicit Dictionary Wrappers
|
||||
/************************************************************************************************************************/
|
||||
|
||||
void ICollection<KeyValuePair<StringReference, Action>>
|
||||
.Add(KeyValuePair<StringReference, Action> item)
|
||||
=> AddTo(item.Key, item.Value);
|
||||
|
||||
bool ICollection<KeyValuePair<StringReference, Action>>
|
||||
.Contains(KeyValuePair<StringReference, Action> item)
|
||||
=> ((ICollection<KeyValuePair<StringReference, Action>>)Dictionary).Contains(item);
|
||||
|
||||
void ICollection<KeyValuePair<StringReference, Action>>
|
||||
.CopyTo(KeyValuePair<StringReference, Action>[] array,
|
||||
int arrayIndex)
|
||||
=> ((ICollection<KeyValuePair<StringReference, Action>>)Dictionary).CopyTo(array, arrayIndex);
|
||||
|
||||
bool ICollection<KeyValuePair<StringReference, Action>>
|
||||
.Remove(KeyValuePair<StringReference, Action> item)
|
||||
=> Dictionary.Remove(item.Key);
|
||||
|
||||
bool ICollection<KeyValuePair<StringReference, Action>>.IsReadOnly
|
||||
=> false;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endregion
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 48f17aa1b028ddf4aa253749b85299a7
|
||||
timeCreated: 1515060256
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0d10e76a0343a30418b3cbc71bf313e3
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,733 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
#if UNITY_ASSERTIONS
|
||||
//#define ANIMANCER_ASSERT_FADE_GRAPH
|
||||
#endif
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Playables;
|
||||
using Object = UnityEngine.Object;
|
||||
|
||||
namespace Animancer
|
||||
{
|
||||
/// <summary>A group of <see cref="AnimancerNode"/>s which are cross-fading.</summary>
|
||||
///
|
||||
/// <remarks>
|
||||
/// <strong>Documentation:</strong>
|
||||
/// <see href="https://kybernetik.com.au/animancer/docs/manual/blending/fading/custom">
|
||||
/// Custom Easing</see>
|
||||
/// </remarks>
|
||||
///
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer/FadeGroup
|
||||
///
|
||||
public partial class FadeGroup : Updatable,
|
||||
ICloneable<FadeGroup>,
|
||||
ICopyable<FadeGroup>,
|
||||
IHasDescription
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
#region Fields and Properties
|
||||
/************************************************************************************************************************/
|
||||
// Parameters.
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>The 0-1 progress of this fade.</summary>
|
||||
public float NormalizedTime { get; set; }
|
||||
|
||||
/// <summary>The <see cref="AnimancerNode.Weight"/> which the <see cref="FadeIn"/> is moving towards.</summary>
|
||||
public float TargetWeight { get; set; }
|
||||
|
||||
/// <summary>The speed at which the <see cref="NormalizedTime"/> increases.</summary>
|
||||
public float NormalizedFadeSpeed { get; set; }
|
||||
|
||||
/// <summary>The speed at which the <see cref="AnimancerNode.Weight"/>s change.</summary>
|
||||
public float FadeSpeed
|
||||
{
|
||||
get => FadeDistance * NormalizedFadeSpeed;
|
||||
set => NormalizedFadeSpeed = value / FadeDistance;
|
||||
}
|
||||
|
||||
/// <summary>The distance from the starting weight to the <see cref="TargetWeight"/>.</summary>
|
||||
public float FadeDistance
|
||||
=> Math.Abs(TargetWeight - FadeIn.StartingWeight);
|
||||
|
||||
/// <summary>The total amount of time this fade will take to complete (in seconds).</summary>
|
||||
public float FadeDuration
|
||||
{
|
||||
get => NormalizedFadeSpeed != 0
|
||||
? 1 / NormalizedFadeSpeed
|
||||
: float.PositiveInfinity;
|
||||
set => NormalizedFadeSpeed = value != 0
|
||||
? 1 / value
|
||||
: float.PositiveInfinity;
|
||||
}
|
||||
|
||||
/// <summary>The remaining amount of time this fade will take to complete (in seconds).</summary>
|
||||
public float RemainingFadeDuration
|
||||
{
|
||||
get => NormalizedFadeSpeed != 0
|
||||
? (1 - NormalizedTime) / NormalizedFadeSpeed
|
||||
: float.PositiveInfinity;
|
||||
set => NormalizedFadeSpeed = value != 0
|
||||
? (1 - NormalizedTime) / value
|
||||
: float.PositiveInfinity;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
// Parent.
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>The <see cref="AnimancerNodeBase.Graph"/>.</summary>
|
||||
public AnimancerGraph Graph { get; private set; }
|
||||
|
||||
/// <summary>The <see cref="AnimancerNodeBase.Graph"/>.</summary>
|
||||
public AnimancerNodeBase Parent { get; private set; }
|
||||
|
||||
/// <summary>The <see cref="AnimancerNodeBase.Playable"/> of the <see cref="Parent"/>.</summary>
|
||||
public Playable ParentPlayable { get; private set; }
|
||||
|
||||
/// <summary>Should the fading nodes always be connected to the <see cref="ParentPlayable"/>?</summary>
|
||||
public bool KeepChildrenConnected { get; private set; }
|
||||
|
||||
/************************************************************************************************************************/
|
||||
// Nodes.
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>The node which is fading towards the <see cref="TargetWeight"/>.</summary>
|
||||
public NodeWeight FadeIn { get; private set; }
|
||||
|
||||
internal readonly List<NodeWeight> FadeOutInternal = new();
|
||||
|
||||
/// <summary>The nodes which are fading out.</summary>
|
||||
public IReadOnlyList<NodeWeight> FadeOut => FadeOutInternal;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
// Custom Fade.
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private Func<float, float> _Easing;
|
||||
|
||||
/// <summary>[Pro-Only] An optional function for modifying the fade curve.</summary>
|
||||
/// <remarks>
|
||||
/// The <see cref="NormalizedTime"/> is passed in and the return value is multiplied by the
|
||||
/// <see cref="TargetWeight"/> to set the <see cref="AnimancerNode.Weight"/> of the <see cref="FadeIn"/>.
|
||||
/// <para></para>
|
||||
/// <see cref="Animancer.Easing"/> has various common functions that could be used here.
|
||||
/// <para></para>
|
||||
/// Note that the <see cref="AnimancerNode.FadeGroup"/> may be <c>null</c>
|
||||
/// right after playing something if it was already playing, so
|
||||
/// <see cref="FadeGroupExtensions.SetEasing(FadeGroup, Easing.Function)"/>
|
||||
/// <see cref="FadeGroupExtensions.SetEasing(FadeGroup, Func{float, float})"/>
|
||||
/// can be used to avoid needing to null-check it.
|
||||
/// <para></para>
|
||||
/// <em>Animancer Lite ignores this property in runtime builds.</em>
|
||||
/// </remarks>
|
||||
public Func<float, float> Easing
|
||||
{
|
||||
get => _Easing;
|
||||
set
|
||||
{
|
||||
_Easing = value;
|
||||
AssertNormalizedBounds(value, nameof(Easing));
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endregion
|
||||
/************************************************************************************************************************/
|
||||
#region Initialization
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Assigns the target nodes that will be faded.</summary>
|
||||
public void SetNodes(
|
||||
AnimancerNode parent,
|
||||
AnimancerNode fadeIn,
|
||||
IReadOnlyList<AnimancerNode> fadeOut,
|
||||
bool keepChildrenConnected)
|
||||
{
|
||||
Parent = parent;
|
||||
Graph = parent.Graph;
|
||||
ParentPlayable = parent.Playable;
|
||||
KeepChildrenConnected = keepChildrenConnected;
|
||||
|
||||
FadeIn = new(fadeIn);
|
||||
|
||||
if (fadeIn.FadeGroup != this)
|
||||
fadeIn.FadeGroup = this;
|
||||
|
||||
var count = fadeOut.Count;
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var node = fadeOut[i];
|
||||
if (node != fadeIn)
|
||||
{
|
||||
FadeOutInternal.Add(new(node));
|
||||
|
||||
if (node.FadeGroup != this)
|
||||
node.FadeGroup = this;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Assigns the <see cref="FadeIn"/> with no <see cref="FadeOut"/>.</summary>
|
||||
public void SetFadeIn(AnimancerNode fadeIn)
|
||||
{
|
||||
Parent = fadeIn.Parent;
|
||||
if (Parent != null)
|
||||
{
|
||||
Graph = fadeIn.Graph;
|
||||
ParentPlayable = Parent.Playable;
|
||||
KeepChildrenConnected = Parent.KeepChildrenConnected;
|
||||
}
|
||||
|
||||
FadeIn = new(fadeIn);
|
||||
fadeIn.FadeGroup = this;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Adds a node to the <see cref="FadeOut"/> list.</summary>
|
||||
public void AddFadeOut(AnimancerNode fadeOut)
|
||||
{
|
||||
FadeOutInternal.Add(new(fadeOut));
|
||||
fadeOut.FadeGroup = this;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Sets the starting values and registers this fade to be updated.</summary>
|
||||
public void StartFade(
|
||||
float targetWeight,
|
||||
float normalizedFadeSpeed)
|
||||
{
|
||||
NormalizedTime = 0;
|
||||
TargetWeight = targetWeight;
|
||||
NormalizedFadeSpeed = normalizedFadeSpeed;
|
||||
|
||||
StartFade();
|
||||
}
|
||||
|
||||
/// <summary>Registers this fade to be updated.</summary>
|
||||
public void StartFade()
|
||||
{
|
||||
Graph?.RequirePreUpdate(this);
|
||||
|
||||
FadeIn.Node?.OnStartFade();
|
||||
for (int i = FadeOutInternal.Count - 1; i >= 0; i--)
|
||||
FadeOutInternal[i].Node.OnStartFade();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endregion
|
||||
/************************************************************************************************************************/
|
||||
#region Queries
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Should this fade continue?</summary>
|
||||
public bool IsValid
|
||||
=> NormalizedFadeSpeed > 0;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Does this fade affect the `node`?</summary>
|
||||
public bool Contains(AnimancerNode node)
|
||||
{
|
||||
if (FadeIn.Node == node)
|
||||
return true;
|
||||
|
||||
for (int i = 0; i < FadeOutInternal.Count; i++)
|
||||
if (FadeOutInternal[i].Node == node)
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// Returns the <see cref="TargetWeight"/> if the `node` is the <see cref="FadeIn"/>.
|
||||
/// Otherwise, returns 0.
|
||||
/// </summary>
|
||||
public float GetTargetWeight(AnimancerNode node)
|
||||
{
|
||||
return FadeIn.Node == node
|
||||
? TargetWeight
|
||||
: 0;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endregion
|
||||
/************************************************************************************************************************/
|
||||
#region Methods
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Update()
|
||||
{
|
||||
if (!IsValid)
|
||||
{
|
||||
Cancel();
|
||||
return;
|
||||
}
|
||||
|
||||
AssertGraph();
|
||||
|
||||
NormalizedTime += Math.Abs(AnimancerGraph.DeltaTime * Parent.EffectiveSpeed * NormalizedFadeSpeed);
|
||||
|
||||
if (NormalizedTime < 1)// Fade.
|
||||
{
|
||||
ApplyWeights();
|
||||
}
|
||||
else// End.
|
||||
{
|
||||
Finish();
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Immediately finishes this fade.</summary>
|
||||
public void Finish()
|
||||
{
|
||||
NormalizedTime = 1;
|
||||
|
||||
if (KeepChildrenConnected)
|
||||
{
|
||||
ApplyWeights(1);
|
||||
|
||||
for (int i = FadeOutInternal.Count - 1; i >= 0; i--)
|
||||
FadeOutInternal[i].Node.StopWithoutWeight();
|
||||
|
||||
if (TargetWeight == 0)
|
||||
FadeIn.Node?.StopWithoutWeight();
|
||||
}
|
||||
else// Disconnect all faded out nodes and only apply the faded in weight.
|
||||
{
|
||||
for (int i = FadeOutInternal.Count - 1; i >= 0; i--)
|
||||
StopAndDisconnect(FadeOutInternal[i].Node);
|
||||
|
||||
FadeOutInternal.Clear();
|
||||
|
||||
if (FadeIn.Node != null)
|
||||
{
|
||||
if (TargetWeight > 0)
|
||||
FadeIn.Node.SetWeight(TargetWeight);
|
||||
else
|
||||
StopAndDisconnect(FadeIn.Node);
|
||||
}
|
||||
}
|
||||
|
||||
Cancel();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// Recalculates the node weights based on the <see cref="NormalizedTime"/>.
|
||||
/// </summary>
|
||||
public void ApplyWeights()
|
||||
{
|
||||
if (NormalizedTime < 1)// Fade.
|
||||
{
|
||||
var progress = NormalizedTime;
|
||||
if (_Easing != null)
|
||||
progress = _Easing(progress);
|
||||
|
||||
ApplyWeights(progress);
|
||||
}
|
||||
else// End.
|
||||
{
|
||||
Finish();
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyWeights(float progress)
|
||||
{
|
||||
// Move FadeIn towards target (usually 1 or 0).
|
||||
|
||||
FadeIn.Node?.SetWeight(Mathf.LerpUnclamped(FadeIn.StartingWeight, TargetWeight, progress));
|
||||
|
||||
// Move FadeOut towards 0.
|
||||
|
||||
progress = 1 - progress;
|
||||
|
||||
for (int i = FadeOutInternal.Count - 1; i >= 0; i--)
|
||||
{
|
||||
var node = FadeOutInternal[i];
|
||||
node.Node.SetWeight(node.StartingWeight * progress);
|
||||
}
|
||||
}
|
||||
|
||||
private void StopAndDisconnect(AnimancerNode node)
|
||||
{
|
||||
// Don't InternalClearFade because it's virtual.
|
||||
node._FadeGroup = null;
|
||||
node.Stop();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private void Release()
|
||||
{
|
||||
NormalizedFadeSpeed = 0;
|
||||
_Easing = null;
|
||||
Graph = null;
|
||||
Parent = null;
|
||||
|
||||
if (FadeIn.Node != null)
|
||||
{
|
||||
FadeIn.Node.InternalClearFade();
|
||||
FadeIn = default;
|
||||
}
|
||||
|
||||
for (int i = FadeOutInternal.Count - 1; i >= 0; i--)
|
||||
FadeOutInternal[i].Node.InternalClearFade();
|
||||
FadeOutInternal.Clear();
|
||||
|
||||
Pool.Instance.Release(this);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Interrupts this fade and releases it to the <see cref="ObjectPool{T}"/>.</summary>
|
||||
public void Cancel()
|
||||
{
|
||||
Graph?.CancelPreUpdate(this);
|
||||
Release();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Removes the `node` from this <see cref="FadeGroup"/> and returns true if successful.</summary>
|
||||
public bool Remove(AnimancerNode node)
|
||||
{
|
||||
if (FadeIn.Node == node)
|
||||
{
|
||||
FadeIn = new(null, FadeIn.StartingWeight);
|
||||
|
||||
if (FadeOutInternal.Count == 0)
|
||||
Cancel();
|
||||
|
||||
node.InternalClearFade();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
for (int i = FadeOutInternal.Count - 1; i >= 0; i--)
|
||||
{
|
||||
if (FadeOutInternal[i].Node == node)
|
||||
{
|
||||
FadeOutInternal.RemoveAt(i);
|
||||
|
||||
if (FadeIn.Node == null && FadeOutInternal.Count == 0)
|
||||
Cancel();
|
||||
|
||||
node.InternalClearFade();
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual void AppendDescription(StringBuilder text, string separator = "\n")
|
||||
{
|
||||
text.Append(GetType().FullName);
|
||||
|
||||
if (!IsValid)
|
||||
{
|
||||
text.Append("(Cancelled)");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!separator.StartsWithNewLine())
|
||||
separator = "\n" + separator;
|
||||
|
||||
text.AppendField(separator, nameof(NormalizedTime), NormalizedTime);
|
||||
text.AppendField(separator, nameof(NormalizedFadeSpeed), NormalizedFadeSpeed);
|
||||
text.AppendField(separator, nameof(Easing), _Easing?.ToStringDetailed());
|
||||
|
||||
text.Append(separator).Append($"{nameof(FadeIn)}: ");
|
||||
FadeIn.AppendDescription(text, TargetWeight);
|
||||
|
||||
text.AppendField(separator, nameof(FadeOut), FadeOutInternal.Count);
|
||||
for (int i = 0; i < FadeOutInternal.Count; i++)
|
||||
{
|
||||
text.Append(separator)
|
||||
.Append(Strings.Indent);
|
||||
FadeOutInternal[i].AppendDescription(text, 0);
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>[Assert-Conditional] Checks <see cref="OptionalWarning.FadeEasingBounds"/>.</summary>
|
||||
[System.Diagnostics.Conditional(Strings.Assertions)]
|
||||
public static void AssertNormalizedBounds(Func<float, float> easing, string name = "function")
|
||||
{
|
||||
#if UNITY_ASSERTIONS
|
||||
if (easing != null && OptionalWarning.FadeEasingBounds.IsEnabled())
|
||||
{
|
||||
if (easing(0) != 0)
|
||||
OptionalWarning.FadeEasingBounds.Log(name + "(0) != 0.");
|
||||
|
||||
if (easing(1) != 1)
|
||||
OptionalWarning.FadeEasingBounds.Log(name + "(1) != 1.");
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endregion
|
||||
/************************************************************************************************************************/
|
||||
#region Cloning
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual FadeGroup Clone(CloneContext context)
|
||||
{
|
||||
if (!IsValid)
|
||||
return null;
|
||||
|
||||
var clone = new FadeGroup();
|
||||
clone.CopyFrom(this, context);
|
||||
return clone;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual void CopyFrom(FadeGroup copyFrom, CloneContext context)
|
||||
{
|
||||
CopyNodesFrom(copyFrom, context);
|
||||
|
||||
var node = FadeIn.Node;
|
||||
if (node == null)
|
||||
{
|
||||
if (FadeOut.Count == 0)
|
||||
return;
|
||||
|
||||
node = FadeOut[0].Node;
|
||||
}
|
||||
|
||||
ChangeParent(node);
|
||||
CopyDetailsFrom(copyFrom);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private void CopyNodesFrom(FadeGroup copyFrom, CloneContext context)
|
||||
{
|
||||
FadeIn = new(copyFrom.FadeIn, context);
|
||||
FadeIn.Node.FadeGroup = this;
|
||||
|
||||
FadeOutInternal.Clear();
|
||||
|
||||
var count = copyFrom.FadeOutInternal.Count;
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var nodeWeight = new NodeWeight(copyFrom.FadeOutInternal[i], context);
|
||||
if (nodeWeight.Node != null)
|
||||
{
|
||||
FadeOutInternal.Add(nodeWeight);
|
||||
nodeWeight.Node.FadeGroup = this;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
internal void ChangeParent(AnimancerNode child)
|
||||
{
|
||||
var parent = child.Parent;
|
||||
if (Parent == parent)
|
||||
return;
|
||||
|
||||
Parent = parent;
|
||||
if (Parent != null)
|
||||
{
|
||||
ParentPlayable = Parent.Playable;
|
||||
KeepChildrenConnected = Parent.KeepChildrenConnected;
|
||||
|
||||
ChangeGraph(child.Graph);
|
||||
|
||||
_AssertGraphNextFrame = true;
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
internal void ChangeGraph(AnimancerGraph graph)
|
||||
{
|
||||
if (Graph == graph)
|
||||
return;
|
||||
|
||||
Graph?.CancelPreUpdate(this);
|
||||
Graph = graph;
|
||||
Graph?.RequirePreUpdate(this);
|
||||
|
||||
_AssertGraphNextFrame = true;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private bool _AssertGraphNextFrame;
|
||||
|
||||
private void AssertGraph()
|
||||
{
|
||||
if (!_AssertGraphNextFrame)
|
||||
return;
|
||||
|
||||
_AssertGraphNextFrame = false;
|
||||
|
||||
if (FadeIn.Node != null && !AssertNode(FadeIn.Node))
|
||||
return;
|
||||
|
||||
for (int i = 0; i < FadeOutInternal.Count; i++)
|
||||
if (!AssertNode(FadeOutInternal[i].Node))
|
||||
return;
|
||||
}
|
||||
|
||||
private bool AssertNode(AnimancerNode node)
|
||||
{
|
||||
string propertyName;
|
||||
string nodeValue, myValue;
|
||||
if (node.Graph == Graph)
|
||||
{
|
||||
if (node.Parent == Parent)
|
||||
return true;
|
||||
|
||||
propertyName = nameof(node.Parent);
|
||||
nodeValue = AnimancerUtilities.ToStringOrNull(node.Parent);
|
||||
myValue = AnimancerUtilities.ToStringOrNull(Parent);
|
||||
}
|
||||
else
|
||||
{
|
||||
propertyName = nameof(node.Graph);
|
||||
nodeValue = AnimancerUtilities.ToStringOrNull(node.Graph);
|
||||
myValue = AnimancerUtilities.ToStringOrNull(Graph);
|
||||
}
|
||||
|
||||
var graph = Graph ?? node.Graph;
|
||||
Debug.LogWarning(
|
||||
$"{nameof(AnimancerNode)}.{propertyName} doesn't match {nameof(FadeGroup)}.{propertyName}." +
|
||||
$"\n• Node: {node.GetPath()}" +
|
||||
$"\n• Node.{propertyName}: {nodeValue}" +
|
||||
$"\n• This.{propertyName}: {myValue}" +
|
||||
$"\n• Graph: {graph?.GetDescription("\n• ")}");
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private void CopyDetailsFrom(FadeGroup copyFrom)
|
||||
{
|
||||
NormalizedTime = copyFrom.NormalizedTime;
|
||||
NormalizedFadeSpeed = copyFrom.NormalizedFadeSpeed;
|
||||
TargetWeight = copyFrom.TargetWeight;
|
||||
_Easing = copyFrom._Easing;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Creates a clone of this <see cref="FadeGroup"/> for a single target node (`copyTo`).</summary>
|
||||
public FadeGroup CloneForSingleTarget(AnimancerNode copyFrom, AnimancerNode copyTo)
|
||||
{
|
||||
if (!IsValid)
|
||||
return null;
|
||||
|
||||
var clone = Pool.Instance.Acquire();
|
||||
|
||||
if (copyFrom == FadeIn.Node)
|
||||
{
|
||||
clone.FadeIn = new(copyTo, FadeIn.StartingWeight);
|
||||
}
|
||||
else
|
||||
{
|
||||
for (int i = 0; i < FadeOutInternal.Count; i++)
|
||||
{
|
||||
var fadeOut = FadeOutInternal[i];
|
||||
if (fadeOut.Node == copyFrom)
|
||||
{
|
||||
clone.FadeOutInternal.Add(new(copyTo, fadeOut.StartingWeight));
|
||||
goto CopyDetails;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
CopyDetails:
|
||||
clone.ChangeParent(copyTo);
|
||||
clone.CopyDetailsFrom(this);
|
||||
clone.StartFade();
|
||||
|
||||
return clone;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endregion
|
||||
/************************************************************************************************************************/
|
||||
#region Pooling
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>An <see cref="ObjectPool{T}"/> for <see cref="FadeGroup"/>.</summary>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer/Pool
|
||||
public class Pool : ObjectPool<FadeGroup>
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Singleton.</summary>
|
||||
public static Pool Instance = new();
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override FadeGroup New()
|
||||
=> new();
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#if UNITY_ASSERTIONS
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override FadeGroup Acquire()
|
||||
{
|
||||
var fade = base.Acquire();
|
||||
Debug.Assert(fade.FadeIn.Node == null, $"{nameof(fade.FadeIn)} is not null");
|
||||
Debug.Assert(fade.FadeOutInternal.Count == 0, $"{nameof(fade.FadeOutInternal)} is not empty");
|
||||
Debug.Assert(fade.Easing == null, $"{nameof(fade.Easing)} is not null");
|
||||
return fade;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Release(FadeGroup item)
|
||||
{
|
||||
Debug.Assert(((IUpdatable)item).UpdatableIndex < 0,
|
||||
$"Releasing {nameof(FadeGroup)} which is still registered for updates.",
|
||||
item.Graph?.Component as Object);
|
||||
|
||||
base.Release(item);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endif
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endregion
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 185b5c0875be482428a95dd83b291f9c
|
||||
timeCreated: 1515048758
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,73 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
using System.Text;
|
||||
|
||||
namespace Animancer
|
||||
{
|
||||
/// <summary>An <see cref="AnimancerNode"/> and its <see cref="StartingWeight"/>.</summary>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer/NodeWeight
|
||||
public readonly struct NodeWeight
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>The <see cref="AnimancerNode"/>.</summary>
|
||||
public readonly AnimancerNode Node;
|
||||
|
||||
/// <summary>The <see cref="AnimancerNode.Weight"/> from when this struct was captured.</summary>
|
||||
public readonly float StartingWeight;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Creates a new <see cref="NodeWeight"/>.</summary>
|
||||
public NodeWeight(AnimancerNode node)
|
||||
{
|
||||
Node = node;
|
||||
StartingWeight = node.Weight;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Creates a new <see cref="NodeWeight"/>.</summary>
|
||||
public NodeWeight(AnimancerNode node, float startingWeight)
|
||||
{
|
||||
Node = node;
|
||||
StartingWeight = startingWeight;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Creates a copy of `copyFrom`.</summary>
|
||||
public NodeWeight(NodeWeight copyFrom, CloneContext context)
|
||||
{
|
||||
Node = context.GetOrCreateCloneOrOriginal(copyFrom.Node);
|
||||
StartingWeight = copyFrom.StartingWeight;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Appends a detailed descrption of this object.</summary>
|
||||
public void AppendDescription(StringBuilder text, float targetWeight)
|
||||
{
|
||||
if (Node == null)
|
||||
{
|
||||
text.Append("Null: ")
|
||||
.Append(StartingWeight)
|
||||
.Append(" -> ")
|
||||
.Append(targetWeight);
|
||||
}
|
||||
else
|
||||
{
|
||||
text.Append(Node.GetPath())
|
||||
.Append(": ")
|
||||
.Append(StartingWeight)
|
||||
.Append(" -> ")
|
||||
.Append(Node.Weight)
|
||||
.Append(" -> ")
|
||||
.Append(targetWeight);
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 79f763055fde60741a96db747cde69cf
|
||||
timeCreated: 1515048758
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f7e3a2eb62aa248408f31a43ea2090bf
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cae3088bc435d2942827b0a25e688903
|
||||
timeCreated: 1515048758
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,819 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Playables;
|
||||
using Object = UnityEngine.Object;
|
||||
|
||||
namespace Animancer
|
||||
{
|
||||
/// <summary>Base class for <see cref="Playable"/> wrapper objects in an <see cref="AnimancerGraph"/>.</summary>
|
||||
/// <remarks>This is the base class of <see cref="AnimancerLayer"/> and <see cref="AnimancerState"/>.</remarks>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer/AnimancerNode
|
||||
public abstract class AnimancerNode : AnimancerNodeBase,
|
||||
ICopyable<AnimancerNode>,
|
||||
IEnumerable<AnimancerState>,
|
||||
IEnumerator,
|
||||
IHasDescription
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
#region Playable
|
||||
/************************************************************************************************************************/
|
||||
|
||||
#if UNITY_EDITOR
|
||||
/// <summary>[Editor-Only] [Internal] Indicates whether the Inspector details for this node are expanded.</summary>
|
||||
internal bool _IsInspectorExpanded;
|
||||
#endif
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Creates and assigns the <see cref="Playable"/> managed by this node.</summary>
|
||||
/// <remarks>This method also applies the <see cref="AnimancerNodeBase.Speed"/> if it was set beforehand.</remarks>
|
||||
protected virtual void CreatePlayable()
|
||||
{
|
||||
#if UNITY_ASSERTIONS
|
||||
if (Graph == null)
|
||||
{
|
||||
MarkAsUsed(this);
|
||||
throw new InvalidOperationException($"{nameof(AnimancerNode)}.{nameof(Graph)}" +
|
||||
$" is null when attempting to create its {nameof(Playable)}: {this}" +
|
||||
$"\nThe {nameof(Graph)} is generally set when you first play a state," +
|
||||
$" so you probably just need to play it before trying to access it.");
|
||||
}
|
||||
|
||||
if (_Playable.IsValid())
|
||||
Debug.LogWarning($"{nameof(AnimancerNode)}.{nameof(CreatePlayable)}" +
|
||||
$" was called before destroying the previous {nameof(Playable)}: {this}", Graph?.Component as Object);
|
||||
#endif
|
||||
|
||||
CreatePlayable(out _Playable);
|
||||
|
||||
#if UNITY_ASSERTIONS
|
||||
if (!_Playable.IsValid())
|
||||
throw new InvalidOperationException(
|
||||
$"{nameof(AnimancerNode)}.{nameof(CreatePlayable)}" +
|
||||
$" did not create a valid {nameof(Playable)} for {this}");
|
||||
#endif
|
||||
|
||||
if (Speed != 1)
|
||||
_Playable.SetSpeed(Speed);
|
||||
}
|
||||
|
||||
/// <summary>Creates and assigns the <see cref="Playable"/> managed by this node.</summary>
|
||||
protected abstract void CreatePlayable(out Playable playable);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Destroys the <see cref="Playable"/>.</summary>
|
||||
public void DestroyPlayable()
|
||||
{
|
||||
if (_Playable.IsValid())
|
||||
Graph._PlayableGraph.DestroyPlayable(_Playable);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Calls <see cref="DestroyPlayable"/> and <see cref="CreatePlayable()"/>.</summary>
|
||||
public virtual void RecreatePlayable()
|
||||
{
|
||||
DestroyPlayable();
|
||||
CreatePlayable();
|
||||
}
|
||||
|
||||
/// <summary>Calls <see cref="RecreatePlayable"/> on this node and all its children recursively.</summary>
|
||||
public void RecreatePlayableRecursive()
|
||||
{
|
||||
RecreatePlayable();
|
||||
|
||||
for (int i = ChildCount - 1; i >= 0; i--)
|
||||
GetChild(i)?.RecreatePlayableRecursive();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Copies the details of `copyFrom` into this node, replacing its previous contents.</summary>
|
||||
public virtual void CopyFrom(AnimancerNode copyFrom, CloneContext context)
|
||||
{
|
||||
SetWeight(copyFrom._Weight);
|
||||
|
||||
FadeGroup = context.WillCloneUpdatables
|
||||
? null
|
||||
: copyFrom.FadeGroup?.CloneForSingleTarget(copyFrom, this);
|
||||
|
||||
Speed = copyFrom.Speed;
|
||||
|
||||
CopyIKFlags(copyFrom);
|
||||
|
||||
#if UNITY_ASSERTIONS
|
||||
DebugName = context.GetCloneOrOriginal(copyFrom.DebugName);
|
||||
#endif
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endregion
|
||||
/************************************************************************************************************************/
|
||||
#region Graph
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>The index of the port this node is connected to on the parent's <see cref="Playable"/>.</summary>
|
||||
/// <remarks>
|
||||
/// A negative value indicates that it is not assigned to a port.
|
||||
/// <para></para>
|
||||
/// Indices are generally assigned starting from 0, ascending in the order they are connected to their layer.
|
||||
/// They will not usually change unless the <see cref="AnimancerNodeBase.Parent"/> changes or another state on
|
||||
/// the same layer is destroyed so the last state is swapped into its place to avoid shuffling everything down
|
||||
/// to cover the gap.
|
||||
/// <para></para>
|
||||
/// The setter is internal so user defined states cannot set it incorrectly. Ideally,
|
||||
/// <see cref="AnimancerLayer"/> should be able to set the port in its constructor and
|
||||
/// <see cref="AnimancerState.SetParent"/> should also be able to set it, but classes that further inherit from
|
||||
/// there should not be able to change it without properly calling that method.
|
||||
/// </remarks>
|
||||
public int Index { get; internal set; } = int.MinValue;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Creates a new <see cref="AnimancerNode"/>.</summary>
|
||||
protected AnimancerNode()
|
||||
{
|
||||
#if UNITY_ASSERTIONS
|
||||
if (TraceConstructor)
|
||||
_ConstructorStackTrace = new(true);
|
||||
#endif
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#if UNITY_ASSERTIONS
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>[Assert-Only]
|
||||
/// Should a <see cref="System.Diagnostics.StackTrace"/> be captured in the constructor of all new nodes so
|
||||
/// <see cref="OptionalWarning.UnusedNode"/> can include it in the warning if that node ends up being unused?
|
||||
/// </summary>
|
||||
/// <remarks>This has a notable performance cost so it should only be used when trying to identify a problem.</remarks>
|
||||
public static bool TraceConstructor { get; set; }
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>[Assert-Only]
|
||||
/// The stack trace of the constructor (or null if <see cref="TraceConstructor"/> was false).
|
||||
/// </summary>
|
||||
private System.Diagnostics.StackTrace _ConstructorStackTrace;
|
||||
|
||||
/// <summary>[Assert-Only]
|
||||
/// Returns the stack trace of the constructor (or null if <see cref="TraceConstructor"/> was false).
|
||||
/// </summary>
|
||||
public static System.Diagnostics.StackTrace GetConstructorStackTrace(AnimancerNode node)
|
||||
=> node._ConstructorStackTrace;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>[Assert-Only] Checks <see cref="OptionalWarning.UnusedNode"/>.</summary>
|
||||
~AnimancerNode()
|
||||
{
|
||||
if (Graph != null ||
|
||||
Parent != null ||
|
||||
OptionalWarning.UnusedNode.IsDisabled())
|
||||
return;
|
||||
|
||||
// ToString might throw an exception since finalizers arn't run on the main thread.
|
||||
string name = null;
|
||||
try { name = ToString(); }
|
||||
catch { name = GetType().FullName; }
|
||||
|
||||
var message = $"The {nameof(Graph)} of '{name}'" +
|
||||
$" is null during finalization (garbage collection)." +
|
||||
$" This may have been caused by earlier exceptions, but otherwise it probably means" +
|
||||
$" that this node was never used for anything and should not have been created.";
|
||||
|
||||
if (_ConstructorStackTrace != null)
|
||||
message += "\n\nThis node was created at:\n" + _ConstructorStackTrace;
|
||||
else
|
||||
message += $"\n\nEnable {nameof(AnimancerNode)}.{nameof(TraceConstructor)} on startup" +
|
||||
$" to allow this warning to include the {nameof(System.Diagnostics.StackTrace)}" +
|
||||
$" of when the node was constructed.";
|
||||
|
||||
OptionalWarning.UnusedNode.Log(message);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endif
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Connects the `child`'s <see cref="Playable"/> to this node.</summary>
|
||||
/// <remarks>This method is NOT safe to call if the child was already connected.</remarks>
|
||||
protected internal void ConnectChildUnsafe(int index, AnimancerNode child)
|
||||
{
|
||||
#if UNITY_ASSERTIONS
|
||||
if (index < 0)
|
||||
{
|
||||
MarkAsUsed(this);
|
||||
throw new InvalidOperationException(
|
||||
$"Invalid {nameof(index)} when attempting to connect to its parent:" +
|
||||
"\n• Child: " + child +
|
||||
"\n• Parent: " + this);
|
||||
}
|
||||
|
||||
Validate.AssertPlayable(child);
|
||||
#endif
|
||||
|
||||
Graph._PlayableGraph.Connect(_Playable, child._Playable, index, child._Weight);
|
||||
}
|
||||
|
||||
/// <summary>Disconnects the <see cref="Playable"/> of the child at the specified `index` from this node.</summary>
|
||||
/// <remarks>This method is safe to call if the child was already disconnected.</remarks>
|
||||
protected void DisconnectChildSafe(int index)
|
||||
{
|
||||
if (_Playable.GetInput(index).IsValid())
|
||||
Graph._PlayableGraph.Disconnect(_Playable, index);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
// IEnumerator for yielding in a coroutine to wait until animations have stopped.
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Is this node playing and not yet at its end?</summary>
|
||||
/// <remarks>
|
||||
/// This method is called by <see cref="IEnumerator.MoveNext"/> so this object can be used as a custom yield
|
||||
/// instruction to wait until it finishes.
|
||||
/// </remarks>
|
||||
public abstract bool IsPlayingAndNotEnding();
|
||||
|
||||
bool IEnumerator.MoveNext()
|
||||
=> IsPlayingAndNotEnding();
|
||||
|
||||
object IEnumerator.Current
|
||||
=> null;
|
||||
|
||||
void IEnumerator.Reset() { }
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endregion
|
||||
/************************************************************************************************************************/
|
||||
#region Children
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected internal override AnimancerNode GetChildNode(int index)
|
||||
=> GetChild(index);
|
||||
|
||||
/// <summary>Returns the state connected to the specified `index` as a child of this node.</summary>
|
||||
/// <remarks>When overriding, don't call this base method because it throws an exception.</remarks>
|
||||
/// <exception cref="NotSupportedException">This node can't have children.</exception>
|
||||
public virtual AnimancerState GetChild(int index)
|
||||
{
|
||||
MarkAsUsed(this);
|
||||
throw new NotSupportedException(this + " can't have children.");
|
||||
}
|
||||
|
||||
/// <summary>Called when a child is connected with this node as its <see cref="AnimancerNodeBase.Parent"/>.</summary>
|
||||
/// <remarks>When overriding, don't call this base method because it throws an exception.</remarks>
|
||||
/// <exception cref="NotSupportedException">This node can't have children.</exception>
|
||||
protected internal virtual void OnAddChild(AnimancerState child)
|
||||
{
|
||||
MarkAsUsed(this);
|
||||
child.SetParentInternal(null);
|
||||
throw new NotSupportedException(this + " can't have children.");
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
// IEnumerable for 'foreach' statements.
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Gets an enumerator for all of this node's child states.</summary>
|
||||
public virtual FastEnumerator<AnimancerState> GetEnumerator()
|
||||
=> default;
|
||||
|
||||
IEnumerator<AnimancerState> IEnumerable<AnimancerState>.GetEnumerator()
|
||||
=> GetEnumerator();
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator()
|
||||
=> GetEnumerator();
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endregion
|
||||
/************************************************************************************************************************/
|
||||
#region Weight
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>[Internal] The current blend weight of this node. Accessed via <see cref="Weight"/>.</summary>
|
||||
internal float _Weight;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>The current blend weight of this node which determines how much it affects the final output.</summary>
|
||||
///
|
||||
/// <remarks>
|
||||
/// 0 has no effect while 1 applies the full effect and values inbetween apply a proportional effect.
|
||||
/// <para></para>
|
||||
/// Setting this property cancels any fade currently in progress. If you don't wish to do that, you can use
|
||||
/// <see cref="SetWeight"/> instead.
|
||||
/// <para></para>
|
||||
/// <em>Animancer Lite only allows this value to be set to 0 or 1 in runtime builds.</em>
|
||||
/// </remarks>
|
||||
///
|
||||
/// <example>
|
||||
/// Calling <see cref="AnimancerLayer.Play(AnimationClip)"/> immediately sets the weight of all states to 0
|
||||
/// and the new state to 1. Note that this is separate from other values like
|
||||
/// <see cref="AnimancerState.IsPlaying"/> so a state can be paused at any point and still show its pose on the
|
||||
/// character or it could be still playing at 0 weight if you want it to still trigger events (though states
|
||||
/// are normally stopped when they reach 0 weight so you would need to explicitly set it to playing again).
|
||||
/// <para></para>
|
||||
/// Calling <see cref="AnimancerLayer.Play(AnimationClip, float, FadeMode)"/> doesn't immediately change
|
||||
/// the weights, but instead calls <see cref="StartFade(float, float)"/> on every state to set their
|
||||
/// <see cref="TargetWeight"/> and <see cref="FadeSpeed"/>. Then every update each state's weight will move
|
||||
/// towards that target value at that speed.
|
||||
/// </example>
|
||||
public float Weight
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get => _Weight;
|
||||
set
|
||||
{
|
||||
FadeGroup = null;
|
||||
SetWeight(value);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the current blend weight of this node which determines how much it affects the final output.
|
||||
/// 0 has no effect while 1 applies the full effect of this node.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This method allows any fade currently in progress to continue. If you don't wish to do that, you can set
|
||||
/// the <see cref="Weight"/> property instead.
|
||||
/// <para></para>
|
||||
/// <em>Animancer Lite only allows this value to be set to 0 or 1 in runtime builds.</em>
|
||||
/// </remarks>
|
||||
public virtual void SetWeight(float value)
|
||||
=> SetWeightInternal(value);
|
||||
|
||||
/// <summary>The internal non-<c>virtual</c> implementation of <see cref="SetWeight"/>.</summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private void SetWeightInternal(float value)
|
||||
{
|
||||
if (_Weight == value)
|
||||
return;
|
||||
|
||||
Validate.AssertSetWeight(this, value);
|
||||
|
||||
_Weight = value;
|
||||
|
||||
if (Graph != null)
|
||||
Parent?.Playable.ApplyChildWeight(this);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected internal override float BaseWeight
|
||||
=> Weight;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="Weight"/> of this state multiplied by the <see cref="Weight"/> of each of its parents down
|
||||
/// the hierarchy to determine how much this state affects the final output.
|
||||
/// </summary>
|
||||
public float EffectiveWeight
|
||||
{
|
||||
get
|
||||
{
|
||||
var weight = Weight;
|
||||
|
||||
var parent = Parent;
|
||||
while (parent != null)
|
||||
{
|
||||
weight *= parent.BaseWeight;
|
||||
parent = parent.Parent;
|
||||
}
|
||||
|
||||
return weight;
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endregion
|
||||
/************************************************************************************************************************/
|
||||
#region Fading
|
||||
/************************************************************************************************************************/
|
||||
|
||||
internal FadeGroup _FadeGroup;
|
||||
|
||||
/// <summary>The current fade being applied to this node (if any).</summary>
|
||||
public FadeGroup FadeGroup
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get => _FadeGroup;
|
||||
internal set
|
||||
{
|
||||
_FadeGroup?.Remove(this);
|
||||
_FadeGroup = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The desired <see cref="Weight"/> which this node is fading towards according to the
|
||||
/// <see cref="FadeSpeed"/>.
|
||||
/// </summary>
|
||||
public float TargetWeight
|
||||
=> FadeGroup != null
|
||||
? FadeGroup.GetTargetWeight(this)
|
||||
: Weight;
|
||||
|
||||
/// <summary>The speed at which this node is fading towards the <see cref="TargetWeight"/>.</summary>
|
||||
/// <remarks>
|
||||
/// This value isn't affected by this node's <see cref="AnimancerNodeBase.Speed"/>,
|
||||
/// but is affected by its parents.
|
||||
/// </remarks>
|
||||
public float FadeSpeed
|
||||
=> FadeGroup != null
|
||||
? FadeGroup.FadeSpeed
|
||||
: 0;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// Calls <see cref="OnStartFade"/> and starts fading the <see cref="Weight"/> over the course
|
||||
/// of the <see cref="AnimancerGraph.DefaultFadeDuration"/> (in seconds).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// If the `targetWeight` is 0 then <see cref="Stop"/> will be called when the fade is complete.
|
||||
/// <para></para>
|
||||
/// If the <see cref="Weight"/> is already equal to the `targetWeight` then the fade will end
|
||||
/// immediately.
|
||||
/// </remarks>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void StartFade(float targetWeight)
|
||||
=> StartFade(targetWeight, AnimancerGraph.DefaultFadeDuration);
|
||||
|
||||
/// <summary>
|
||||
/// Calls <see cref="OnStartFade"/> and starts fading the <see cref="Weight"/>
|
||||
/// over the course of the `fadeDuration` (in seconds).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// If the `targetWeight` is 0 then <see cref="Stop"/> will be called when the fade is complete.
|
||||
/// <para></para>
|
||||
/// If the <see cref="Weight"/> is already equal to the `targetWeight`
|
||||
/// then the fade will end immediately.
|
||||
/// <para></para>
|
||||
/// <em>Animancer Lite only allows a `targetWeight` of 0 or 1
|
||||
/// and the default `fadeDuration` (0.25 seconds) in runtime builds.</em>
|
||||
/// </remarks>
|
||||
public void StartFade(float targetWeight, float fadeDuration)
|
||||
{
|
||||
if (Weight == targetWeight && FadeGroup == null)
|
||||
{
|
||||
OnStartFade();
|
||||
}
|
||||
else if (fadeDuration > 0)
|
||||
{
|
||||
var fade = FadeGroup.Pool.Instance.Acquire();
|
||||
fade.SetFadeIn(this);
|
||||
fade.StartFade(targetWeight, 1 / fadeDuration);
|
||||
}
|
||||
else
|
||||
{
|
||||
Weight = targetWeight;
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Called by <see cref="StartFade(float, float)"/>.</summary>
|
||||
protected internal abstract void OnStartFade();
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Removes this node from the <see cref="FadeGroup"/>.</summary>
|
||||
public void CancelFade()
|
||||
=> _FadeGroup?.Remove(this);
|
||||
|
||||
/// <summary>[Internal] Called by <see cref="FadeGroup.Remove"/>.</summary>
|
||||
/// <remarks>Not called when a fade fully completes.</remarks>
|
||||
protected internal virtual void InternalClearFade()
|
||||
{
|
||||
_FadeGroup = null;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Stops the animation and makes it inactive immediately so it no longer affects the output.</summary>
|
||||
/// <remarks>
|
||||
/// Sets <see cref="Weight"/> = 0 by default unless overridden.
|
||||
/// <para></para>
|
||||
/// Note that playing something new will automatically stop the old animation.
|
||||
/// </remarks>
|
||||
public void Stop()
|
||||
{
|
||||
FadeGroup = null;
|
||||
SetWeightInternal(0);
|
||||
StopWithoutWeight();
|
||||
}
|
||||
|
||||
/// <summary>[Internal] Stops this node without setting its <see cref="Weight"/>.</summary>
|
||||
protected internal virtual void StopWithoutWeight() { }
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endregion
|
||||
/************************************************************************************************************************/
|
||||
#region Inverse Kinematics
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// Should setting the <see cref="AnimancerNodeBase.Parent"/>
|
||||
/// also set this node's <see cref="ApplyAnimatorIK"/> to match it?
|
||||
/// Default is true.
|
||||
/// </summary>
|
||||
public static bool ApplyParentAnimatorIK { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Should setting the <see cref="AnimancerNodeBase.Parent"/>
|
||||
/// also set this node's <see cref="ApplyFootIK"/> to match it?
|
||||
/// Default is true.
|
||||
/// </summary>
|
||||
public static bool ApplyParentFootIK { get; set; } = true;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// Copies the IK settings from `copyFrom` into this node:
|
||||
/// <list type="bullet">
|
||||
/// <item>If <see cref="ApplyParentAnimatorIK"/> is true, copy <see cref="ApplyAnimatorIK"/>.</item>
|
||||
/// <item>If <see cref="ApplyParentFootIK"/> is true, copy <see cref="ApplyFootIK"/>.</item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
public virtual void CopyIKFlags(AnimancerNodeBase copyFrom)
|
||||
{
|
||||
if (Graph == null)
|
||||
return;
|
||||
|
||||
if (ApplyParentAnimatorIK)
|
||||
ApplyAnimatorIK = copyFrom.ApplyAnimatorIK;
|
||||
|
||||
if (ApplyParentFootIK)
|
||||
ApplyFootIK = copyFrom.ApplyFootIK;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool ApplyAnimatorIK
|
||||
{
|
||||
get
|
||||
{
|
||||
for (int i = ChildCount - 1; i >= 0; i--)
|
||||
{
|
||||
var state = GetChild(i);
|
||||
if (state.ApplyAnimatorIK)
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
set
|
||||
{
|
||||
for (int i = ChildCount - 1; i >= 0; i--)
|
||||
{
|
||||
var state = GetChild(i);
|
||||
state.ApplyAnimatorIK = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool ApplyFootIK
|
||||
{
|
||||
get
|
||||
{
|
||||
for (int i = ChildCount - 1; i >= 0; i--)
|
||||
{
|
||||
var state = GetChild(i);
|
||||
if (state.ApplyFootIK)
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
set
|
||||
{
|
||||
for (int i = ChildCount - 1; i >= 0; i--)
|
||||
{
|
||||
var state = GetChild(i);
|
||||
state.ApplyFootIK = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endregion
|
||||
/************************************************************************************************************************/
|
||||
#region Descriptions
|
||||
/************************************************************************************************************************/
|
||||
|
||||
#if UNITY_ASSERTIONS
|
||||
/// <summary>[Assert-Only] The Inspector display name of this node.</summary>
|
||||
/// <remarks>Set using <see cref="SetDebugName"/>.</remarks>
|
||||
public object DebugName { get; private set; }
|
||||
#endif
|
||||
|
||||
/// <summary>[Assert-Conditional] Sets the <see cref="DebugName"/> to display in the Inspector.</summary>
|
||||
[System.Diagnostics.Conditional(Strings.Assertions)]
|
||||
public void SetDebugName(object name)
|
||||
{
|
||||
#if UNITY_ASSERTIONS
|
||||
DebugName = name;
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>The Inspector display name of this node.</summary>
|
||||
public override string ToString()
|
||||
{
|
||||
#if UNITY_ASSERTIONS
|
||||
if (NameCache.TryToString(DebugName, out var name))
|
||||
return name;
|
||||
#endif
|
||||
|
||||
return base.ToString();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void AppendDescription(StringBuilder text, string separator = "\n")
|
||||
{
|
||||
|
||||
text.Append(ToString());
|
||||
|
||||
AppendDetails(text, separator);
|
||||
|
||||
if (ChildCount > 0)
|
||||
{
|
||||
text.AppendField(separator, nameof(ChildCount), ChildCount);
|
||||
var indentedSeparator = separator + Strings.Indent;
|
||||
|
||||
var i = 0;
|
||||
foreach (var child in this)
|
||||
{
|
||||
text.Append(separator)
|
||||
.Append('[')
|
||||
.Append(i++)
|
||||
.Append("] ")
|
||||
.AppendDescription(child, indentedSeparator, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Called by <see cref="AppendDescription"/> to append the details of this node.</summary>
|
||||
protected virtual void AppendDetails(StringBuilder text, string separator)
|
||||
{
|
||||
text.AppendField(separator, "Playable", _Playable.IsValid()
|
||||
? _Playable.GetPlayableType().ToString()
|
||||
: "Invalid");
|
||||
|
||||
var parent = Parent;
|
||||
var isConnected =
|
||||
parent != null &&
|
||||
parent.Playable.GetInput(Index).IsValid();
|
||||
|
||||
text.AppendField(separator, "Connected", isConnected);
|
||||
|
||||
text.AppendField(separator, nameof(Index), Index);
|
||||
if (Index < 0)
|
||||
text.Append(" (No Parent)");
|
||||
|
||||
text.AppendField(separator, nameof(Speed), Speed);
|
||||
|
||||
var realSpeed = _Playable.IsValid()
|
||||
? _Playable.GetSpeed()
|
||||
: Speed;
|
||||
|
||||
if (realSpeed != Speed)
|
||||
text.Append(" (Real ").Append(realSpeed).Append(')');
|
||||
|
||||
text.AppendField(separator, nameof(Weight), Weight);
|
||||
|
||||
if (Weight != TargetWeight)
|
||||
{
|
||||
text.AppendField(separator, nameof(TargetWeight), TargetWeight);
|
||||
text.AppendField(separator, nameof(FadeSpeed), FadeSpeed);
|
||||
}
|
||||
|
||||
AppendIKDetails(text, separator, this);
|
||||
|
||||
#if UNITY_ASSERTIONS
|
||||
if (_ConstructorStackTrace != null)
|
||||
text.AppendField(separator, "ConstructorStackTrace", _ConstructorStackTrace);
|
||||
#endif
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// Appends the details of <see cref="AnimancerNodeBase.ApplyAnimatorIK"/> and
|
||||
/// <see cref="AnimancerNodeBase.ApplyFootIK"/>.
|
||||
/// </summary>
|
||||
public static void AppendIKDetails(StringBuilder text, string separator, AnimancerNodeBase node)
|
||||
{
|
||||
if (!node.Playable.IsValid())
|
||||
return;
|
||||
|
||||
text.Append(separator)
|
||||
.Append("InverseKinematics: ");
|
||||
|
||||
if (node.ApplyAnimatorIK)
|
||||
{
|
||||
text.Append("OnAnimatorIK");
|
||||
if (node.ApplyFootIK)
|
||||
text.Append(", FootIK");
|
||||
}
|
||||
else if (node.ApplyFootIK)
|
||||
{
|
||||
text.Append("FootIK");
|
||||
}
|
||||
else
|
||||
{
|
||||
text.Append("None");
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Returns the hierarchy path of this node through its <see cref="AnimancerNodeBase.Parent"/>s.</summary>
|
||||
public string GetPath()
|
||||
{
|
||||
var path = StringBuilderPool.Instance.Acquire();
|
||||
|
||||
if (Parent is AnimancerNode parent)
|
||||
{
|
||||
AppendPath(path, parent);
|
||||
AppendPortAndType(path);
|
||||
}
|
||||
else
|
||||
{
|
||||
AppendPortAndType(path);
|
||||
}
|
||||
|
||||
return path.ReleaseToString();
|
||||
}
|
||||
|
||||
/// <summary>Appends the hierarchy path of this state through its <see cref="AnimancerNodeBase.Parent"/>s.</summary>
|
||||
private static void AppendPath(StringBuilder path, AnimancerNode parent)
|
||||
{
|
||||
if (parent != null)
|
||||
{
|
||||
if (parent.Parent is AnimancerNode grandParent)
|
||||
{
|
||||
AppendPath(path, grandParent);
|
||||
}
|
||||
else
|
||||
{
|
||||
var layer = parent.Layer;
|
||||
if (layer != null)
|
||||
{
|
||||
path.Append("Layers[")
|
||||
.Append(parent.Layer.Index)
|
||||
.Append("].States");
|
||||
}
|
||||
else
|
||||
{
|
||||
path.Append("NoLayer -> ")
|
||||
.Append(parent.ToString());
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (parent is AnimancerState state)
|
||||
{
|
||||
state.AppendPortAndType(path);
|
||||
}
|
||||
else
|
||||
{
|
||||
path.Append(" -> ")
|
||||
.Append(parent.GetType());
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Appends "[Index] -> ToString()".</summary>
|
||||
private void AppendPortAndType(StringBuilder path)
|
||||
{
|
||||
path.Append('[')
|
||||
.Append(Index)
|
||||
.Append("] -> ")
|
||||
.Append(ToString());
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endregion
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 54f55b421ce0c584ab6bcb47947126e4
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,282 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
using System;
|
||||
using UnityEngine.Playables;
|
||||
|
||||
namespace Animancer
|
||||
{
|
||||
/// <summary>Base class for objects that manage a <see cref="UnityEngine.Playables.Playable"/>.</summary>
|
||||
/// <remarks>This is the base class of <see cref="AnimancerGraph"/> and <see cref="AnimancerNode"/>.</remarks>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer/AnimancerNodeBase
|
||||
public abstract class AnimancerNodeBase
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>The <see cref="AnimancerGraph"/> containing this node.</summary>
|
||||
public AnimancerGraph Graph { get; internal set; }
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>The object which receives the output of the <see cref="Playable"/>.</summary>
|
||||
/// <remarks>
|
||||
/// This leads from <see cref="AnimancerState"/> to <see cref="AnimancerLayer"/> to
|
||||
/// <see cref="AnimancerGraph"/> to <c>null</c>.
|
||||
/// </remarks>
|
||||
public AnimancerNodeBase Parent { get; protected set; }
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>The root <see cref="AnimancerLayer"/> which this node is connected to (if any).</summary>
|
||||
public virtual AnimancerLayer Layer
|
||||
=> Parent?.Layer;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>The number of nodes using this as their <see cref="Parent"/>.</summary>
|
||||
public virtual int ChildCount
|
||||
=> 0;
|
||||
|
||||
/// <summary>Returns the node connected to the specified `index` as a child of this node.</summary>
|
||||
/// <remarks>When overriding, don't call this base method because it throws an exception.</remarks>
|
||||
/// <exception cref="NotSupportedException">This node can't have children.</exception>
|
||||
protected internal virtual AnimancerNode GetChildNode(int index)
|
||||
{
|
||||
MarkAsUsed(this);
|
||||
throw new NotSupportedException(this + " can't have children.");
|
||||
}
|
||||
|
||||
/// <summary>Should child playables stay connected to the graph at all times?</summary>
|
||||
/// <remarks>
|
||||
/// If false, playables will be disconnected from the graph while they are inactive to stop it from
|
||||
/// evaluating them every frame which usually improves performance.
|
||||
/// </remarks>
|
||||
/// <seealso cref="AnimancerGraph.KeepChildrenConnected"/>
|
||||
public virtual bool KeepChildrenConnected
|
||||
=> true;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Called when a child's <see cref="AnimancerState.IsLooping"/> value changes.</summary>
|
||||
protected virtual void OnChildIsLoopingChanged(bool value) { }
|
||||
|
||||
/// <summary>[Internal] Calls <see cref="OnChildIsLoopingChanged"/> for each <see cref="Parent"/> recursively.</summary>
|
||||
protected internal void OnIsLoopingChangedRecursive(bool value)
|
||||
{
|
||||
var parent = Parent;
|
||||
|
||||
while (parent != null)
|
||||
{
|
||||
parent.OnChildIsLoopingChanged(value);
|
||||
|
||||
parent = parent.Parent;
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>[Internal] Called when a child's <see cref="Parent"/> is changed from this node.</summary>
|
||||
/// <remarks>When overriding, don't call this base method because it throws an exception.</remarks>
|
||||
/// <exception cref="NotSupportedException">This node can't have children.</exception>
|
||||
protected internal virtual void OnRemoveChild(AnimancerState child)
|
||||
{
|
||||
MarkAsUsed(this);
|
||||
child.SetParentInternal(null);
|
||||
throw new NotSupportedException(this + " can't have children.");
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>[Internal] The <see cref="Playable"/>.</summary>
|
||||
protected internal Playable _Playable;
|
||||
|
||||
/// <summary>The internal object this node manages in the <see cref="PlayableGraph"/>.</summary>
|
||||
/// <remarks>
|
||||
/// Must be set by <see cref="AnimancerNode.CreatePlayable()"/>. Failure to do so will throw the following
|
||||
/// exception throughout the system when using this node: "<see cref="ArgumentException"/>: The playable passed
|
||||
/// as an argument is invalid. To create a valid playable, please use the appropriate Create method".
|
||||
/// </remarks>
|
||||
public Playable Playable => _Playable;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>The current blend weight of this node which determines how much it affects the final output.</summary>
|
||||
protected internal virtual float BaseWeight => 1;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#region Speed
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private float _Speed = 1;
|
||||
|
||||
/// <summary>[Pro-Only] How fast the <see cref="AnimancerState.Time"/> is advancing every frame (default 1).</summary>
|
||||
///
|
||||
/// <remarks>
|
||||
/// A negative value will play the animation backwards.
|
||||
/// <para></para>
|
||||
/// To pause an animation, consider setting <see cref="AnimancerState.IsPlaying"/> to false instead of setting
|
||||
/// this value to 0.
|
||||
/// <para></para>
|
||||
/// <em>Animancer Lite doesn't allow this value to be changed in runtime builds.</em>
|
||||
/// <para></para>
|
||||
/// <strong>Example:</strong><code>
|
||||
/// void SpeedExample(AnimancerComponent animancer, AnimationClip clip)
|
||||
/// {
|
||||
/// var state = animancer.Play(clip);
|
||||
///
|
||||
/// state.Speed = 1;// Normal speed.
|
||||
/// state.Speed = 2;// Double speed.
|
||||
/// state.Speed = 0.5f;// Half speed.
|
||||
/// state.Speed = -1;// Normal speed playing backwards.
|
||||
/// state.NormalizedTime = 1;// Start at the end to play backwards from there.
|
||||
/// }
|
||||
/// </code></remarks>
|
||||
///
|
||||
/// <exception cref="ArgumentOutOfRangeException">The value is not finite.</exception>
|
||||
public virtual float Speed
|
||||
{
|
||||
get => _Speed;
|
||||
set
|
||||
{
|
||||
if (_Speed == value)
|
||||
return;
|
||||
|
||||
#if UNITY_ASSERTIONS
|
||||
if (!value.IsFinite())
|
||||
{
|
||||
MarkAsUsed(this);
|
||||
throw new ArgumentOutOfRangeException(
|
||||
nameof(value),
|
||||
value,
|
||||
$"{nameof(Speed)} {Strings.MustBeFinite}");
|
||||
}
|
||||
#endif
|
||||
_Speed = value;
|
||||
|
||||
if (_Playable.IsValid())
|
||||
_Playable.SetSpeed(value);
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="Speed"/> of this node multiplied by the <see cref="Speed"/> of each of its parents to
|
||||
/// determine the actual speed it's playing at.
|
||||
/// </summary>
|
||||
public float EffectiveSpeed
|
||||
{
|
||||
get => Speed * ParentEffectiveSpeed;
|
||||
set => Speed = value / ParentEffectiveSpeed;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// The multiplied <see cref="Speed"/> of each of the <see cref="Parent"/> down the hierarchy,
|
||||
/// excluding the root <see cref="Speed"/>.
|
||||
/// </summary>
|
||||
private float ParentEffectiveSpeed
|
||||
{
|
||||
get
|
||||
{
|
||||
var parent = Parent;
|
||||
if (parent == null)
|
||||
return 1;
|
||||
|
||||
var speed = parent.Speed;
|
||||
|
||||
while ((parent = parent.Parent) != null)
|
||||
{
|
||||
speed *= parent.Speed;
|
||||
}
|
||||
|
||||
return speed;
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endregion
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// Should Unity call <c>OnAnimatorIK</c> on the animated object while this object and its children have any
|
||||
/// <see cref="AnimancerNode.Weight"/>?
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is equivalent to the "IK Pass" toggle in Animator Controller layers, except that due to limitations in
|
||||
/// the Playables API the <c>layerIndex</c> will always be zero.
|
||||
/// <para></para>
|
||||
/// This value starts false by default, but can be automatically changed by
|
||||
/// <see cref="AnimancerNode.CopyIKFlags"/> when the <see cref="Parent"/> is set.
|
||||
/// <para></para>
|
||||
/// IK only takes effect while at least one <see cref="ClipState"/> has a <see cref="AnimancerNode.Weight"/>
|
||||
/// above zero. Other node types either store the value to apply to their children or don't support IK.
|
||||
/// <para></para>
|
||||
/// <strong>Documentation:</strong>
|
||||
/// <see href="https://kybernetik.com.au/animancer/docs/manual/ik#ik-pass">
|
||||
/// IK Pass</see>
|
||||
/// </remarks>
|
||||
public abstract bool ApplyAnimatorIK { get; set; }
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Should this object and its children apply IK to the character's feet?</summary>
|
||||
/// <remarks>
|
||||
/// This is equivalent to the "Foot IK" toggle in Animator Controller states.
|
||||
/// <para></para>
|
||||
/// This value starts true by default for <see cref="ClipState"/>s (false for others), but can be automatically
|
||||
/// changed by <see cref="AnimancerNode.CopyIKFlags"/> when the <see cref="Parent"/> is set.
|
||||
/// <para></para>
|
||||
/// IK only takes effect while at least one <see cref="ClipState"/> has a <see cref="AnimancerNode.Weight"/>
|
||||
/// above zero. Other node types either store the value to apply to their children or don't support IK.
|
||||
/// <para></para>
|
||||
/// <strong>Documentation:</strong>
|
||||
/// <see href="https://kybernetik.com.au/animancer/docs/manual/ik#foot-ik">
|
||||
/// Foot IK</see>
|
||||
/// </remarks>
|
||||
public abstract bool ApplyFootIK { get; set; }
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>[Internal] Applies a change to a child's <see cref="AnimancerState.IsActive"/>.</summary>
|
||||
protected internal virtual void ApplyChildActive(AnimancerState child, bool setActive)
|
||||
=> child.ShouldBeActive = setActive;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>[Assert-Conditional] Prevents the `node` from causing <see cref="OptionalWarning.UnusedNode"/>.</summary>
|
||||
[System.Diagnostics.Conditional(Strings.Assertions)]
|
||||
public static void MarkAsUsed(AnimancerNodeBase node)
|
||||
{
|
||||
#if UNITY_ASSERTIONS
|
||||
if (node.Graph == null)
|
||||
GC.SuppressFinalize(node);
|
||||
#endif
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#if UNITY_EDITOR
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>[Editor-Only]
|
||||
/// Adds functions to show and set <see cref="ApplyAnimatorIK"/> and
|
||||
/// <see cref="ApplyFootIK"/>.
|
||||
/// </summary>
|
||||
public static void AddContextMenuIK(UnityEditor.GenericMenu menu, AnimancerNodeBase ik)
|
||||
{
|
||||
#if UNITY_IMGUI
|
||||
menu.AddItem(new("Inverse Kinematics/Apply Animator IK ?"),
|
||||
ik.ApplyAnimatorIK,
|
||||
() => ik.ApplyAnimatorIK = !ik.ApplyAnimatorIK);
|
||||
menu.AddItem(new("Inverse Kinematics/Apply Foot IK ?"),
|
||||
ik.ApplyFootIK,
|
||||
() => ik.ApplyFootIK = !ik.ApplyFootIK);
|
||||
#endif
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endif
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8abfd735619067c40b7e6e10bcc0b559
|
||||
labels:
|
||||
- Interface
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,67 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
namespace Animancer
|
||||
{
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer/AnimancerState
|
||||
public abstract partial class AnimancerState
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
#if UNITY_ASSERTIONS
|
||||
private static bool _SkipNextExpectFade;
|
||||
|
||||
private bool _ExpectFade;
|
||||
#endif
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>[Internal] Sets a flag for <see cref="OptionalWarning.ExpectFade"/>.</summary>
|
||||
[System.Diagnostics.Conditional(Strings.Assertions)]
|
||||
public static void SetExpectFade(AnimancerState state, float fadeDuration)
|
||||
{
|
||||
#if UNITY_ASSERTIONS
|
||||
state._ExpectFade = fadeDuration > 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>[Internal] Sets the next <see cref="AssertNotExpectingFade"/> call to be skipped.</summary>
|
||||
[System.Diagnostics.Conditional(Strings.Assertions)]
|
||||
internal static void SkipNextExpectFade()
|
||||
{
|
||||
#if UNITY_ASSERTIONS
|
||||
_SkipNextExpectFade = true;
|
||||
#endif
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>[Internal] Call when playing a `state` without a fade to check <see cref="OptionalWarning.ExpectFade"/>.</summary>
|
||||
[System.Diagnostics.Conditional(Strings.Assertions)]
|
||||
internal static void AssertNotExpectingFade(AnimancerState state)
|
||||
{
|
||||
#if UNITY_ASSERTIONS
|
||||
if (_SkipNextExpectFade)
|
||||
{
|
||||
_SkipNextExpectFade = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (state._ExpectFade)
|
||||
{
|
||||
state._ExpectFade = false;// Don't log again for the same state.
|
||||
OptionalWarning.ExpectFade.Log(
|
||||
"A state was created by a transition with a non-zero Fade Duration" +
|
||||
" but is now being played without a fade, which may be unintentional." +
|
||||
" In most cases, the transition should be played so that it can properly" +
|
||||
" apply its settings, unlike if the state is played directly.",
|
||||
state.Graph?.Component);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 04e4439666601f0439582e16fc1572eb
|
||||
timeCreated: 1515060256
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 887b4c2f23914df4085099d24992df1c
|
||||
timeCreated: 1515060256
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,217 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
using System;
|
||||
using Unity.Collections;
|
||||
using UnityEngine.Animations;
|
||||
using UnityEngine.Playables;
|
||||
|
||||
namespace Animancer
|
||||
{
|
||||
/// <summary>[Pro-Only] A scripted animation for an <see cref="AnimationJobState{T}"/>.</summary>
|
||||
/// <remarks>
|
||||
/// <strong>Sample:</strong>
|
||||
/// <see href="https://kybernetik.com.au/animancer/docs/samples/jobs/job-states">
|
||||
/// Job States</see>
|
||||
/// </remarks>
|
||||
public interface IAnimancerStateJob : IDisposable
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>The total time this job would take to play in seconds at normal speed.</summary>
|
||||
float Length { get; }
|
||||
|
||||
/// <summary>Does this job loop back to the start when its time passes its <see cref="Length"/>?</summary>
|
||||
bool IsLooping { get; }
|
||||
|
||||
/// <summary>Defines what do to when processing the root motion.</summary>
|
||||
/// <remarks>
|
||||
/// This is called by <see cref="IAnimationJob.ProcessRootMotion"/>
|
||||
/// and receives the <see cref="AnimancerState.TimeD"/>.
|
||||
/// </remarks>
|
||||
void ProcessRootMotion(AnimationStream stream, double time);
|
||||
|
||||
/// <summary>Defines what do to when processing the animation.</summary>
|
||||
/// <remarks>
|
||||
/// This is called by <see cref="IAnimationJob.ProcessAnimation"/>
|
||||
/// and receives the <see cref="AnimancerState.TimeD"/>.
|
||||
/// </remarks>
|
||||
void ProcessAnimation(AnimationStream stream, double time);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
|
||||
/// <summary>[Pro-Only] An <see cref="AnimancerState"/> which plays an <see cref="IAnimancerStateJob"/>.</summary>
|
||||
/// <remarks>
|
||||
/// <strong>Sample:</strong>
|
||||
/// <see href="https://kybernetik.com.au/animancer/docs/samples/jobs/job-states">
|
||||
/// Job States</see>
|
||||
/// </remarks>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer/AnimationJobState_1
|
||||
///
|
||||
public class AnimationJobState<T> : AnimancerState, IUpdatable, IDisposable
|
||||
where T : struct, IAnimancerStateJob
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// An <see cref="IAnimationJob"/> which wraps an <see cref="IAnimancerStateJob"/>
|
||||
/// to provide its <see cref="Time"/> value.
|
||||
/// </summary>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer/TimedJob
|
||||
public struct TimedJob : IAnimationJob, IDisposable
|
||||
{
|
||||
/// <summary>The <see cref="IAnimancerStateJob"/> data.</summary>
|
||||
public T Job;
|
||||
|
||||
/// <summary>The <see cref="AnimancerState.TimeD"/> to be passed to the job.</summary>
|
||||
public NativeArray<double> Time;
|
||||
|
||||
/// <summary>Cleans up the unmanaged resources used by this job.</summary>
|
||||
public readonly void Dispose()
|
||||
{
|
||||
if (Time.IsCreated)
|
||||
Time.Dispose();
|
||||
|
||||
Job.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>Defines what do to when processing the root motion.</summary>
|
||||
public readonly void ProcessRootMotion(AnimationStream stream)
|
||||
=> Job.ProcessRootMotion(stream, Time[0]);
|
||||
|
||||
/// <summary>Defines what do to when processing the animation.</summary>
|
||||
public readonly void ProcessAnimation(AnimationStream stream)
|
||||
=> Job.ProcessAnimation(stream, Time[0]);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private new AnimationScriptPlayable _Playable;
|
||||
private TimedJob _Job;
|
||||
|
||||
/// <summary>The data of the job to be executed by this state.</summary>
|
||||
/// <remarks>
|
||||
/// Setting this value has a minor performance cost. If it needs to be changed frequently,
|
||||
/// consider using a single-item <c>NativeArray</c> in your job as demonstrated in the
|
||||
/// <see href="https://kybernetik.com.au/animancer/docs/samples/jobs/hit-impacts#angle">
|
||||
/// Hit Impacts</see> sample.
|
||||
/// </remarks>
|
||||
public T Job
|
||||
{
|
||||
get => _Job.Job;
|
||||
set
|
||||
{
|
||||
_Job.Job = value;
|
||||
_Playable.SetJobData(_Job);
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override float Length
|
||||
=> _Job.Job.Length;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool IsLooping
|
||||
=> _Job.Job.IsLooping;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
int IUpdatable.UpdatableIndex { get; set; } = IUpdatable.List.NotInList;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Creates a new <see cref="AnimationJobState{T}"/>.</summary>
|
||||
public AnimationJobState(T job)
|
||||
{
|
||||
_Job.Job = job;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void SetGraph(AnimancerGraph graph)
|
||||
{
|
||||
Graph?.Disposables.Remove(this);
|
||||
base.SetGraph(graph);
|
||||
Graph?.Disposables.Add(this);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void CreatePlayable(out Playable playable)
|
||||
{
|
||||
if (!_Job.Time.IsCreated)
|
||||
_Job.Time = AnimancerUtilities.CreateNativeReference<double>();
|
||||
|
||||
playable = _Playable = AnimationScriptPlayable.Create(Graph.PlayableGraph, _Job);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void OnSetIsPlaying()
|
||||
{
|
||||
base.OnSetIsPlaying();
|
||||
|
||||
if (IsPlaying)
|
||||
Graph.RequirePreUpdate(this);
|
||||
else
|
||||
Graph.CancelPreUpdate(this);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// Called every frame before the job is applied to send the
|
||||
/// <see cref="AnimancerState.Time"/> to the <see cref="Job"/>.
|
||||
/// </summary>
|
||||
public virtual void Update()
|
||||
{
|
||||
var time = _Job.Time;
|
||||
time[0] = TimeD;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
void IDisposable.Dispose()
|
||||
=> _Job.Dispose();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Destroy()
|
||||
{
|
||||
base.Destroy();
|
||||
|
||||
if (Graph != null)
|
||||
{
|
||||
Graph.CancelPreUpdate(this);
|
||||
Graph.Disposables.Remove(this);
|
||||
}
|
||||
|
||||
_Job.Dispose();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string ToString()
|
||||
=> _Job.Job.ToString();
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override AnimancerState Clone(CloneContext context)
|
||||
{
|
||||
var clone = new AnimationJobState<T>(_Job.Job);
|
||||
clone.CopyFrom(this, context);
|
||||
return clone;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8a383b34eda0fe0408c12bc3a77108b5
|
||||
timeCreated: 1515060256
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,190 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
using System;
|
||||
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>
|
||||
/// <remarks>
|
||||
/// <strong>Documentation:</strong>
|
||||
/// <see href="https://kybernetik.com.au/animancer/docs/manual/playing/states">
|
||||
/// States</see>
|
||||
/// </remarks>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer/ClipState
|
||||
///
|
||||
public class ClipState : AnimancerState
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
#region Fields and Properties
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private AnimationClip _Clip;
|
||||
|
||||
/// <summary>The <see cref="AnimationClip"/> which this state plays.</summary>
|
||||
public override AnimationClip Clip
|
||||
{
|
||||
get => _Clip;
|
||||
set
|
||||
{
|
||||
Validate.AssertAnimationClip(value, true, $"set {nameof(ClipState)}.{nameof(Clip)}");
|
||||
if (ChangeMainObject(ref _Clip, value))
|
||||
{
|
||||
_Length = value.length;
|
||||
|
||||
var isLooping = value.isLooping;
|
||||
if (_IsLooping != isLooping)
|
||||
{
|
||||
_IsLooping = isLooping;
|
||||
OnIsLoopingChangedRecursive(isLooping);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>The <see cref="AnimationClip"/> which this state plays.</summary>
|
||||
public override Object MainObject
|
||||
{
|
||||
get => _Clip;
|
||||
set => Clip = (AnimationClip)value;
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
/// <inheritdoc/>
|
||||
public override Type MainObjectType
|
||||
=> typeof(AnimationClip);
|
||||
#endif
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private float _Length;
|
||||
|
||||
/// <summary>The <see cref="AnimationClip.length"/>.</summary>
|
||||
public override float Length
|
||||
=> _Length;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private bool _IsLooping;
|
||||
|
||||
/// <summary>The <see cref="Motion.isLooping"/>.</summary>
|
||||
public override bool IsLooping
|
||||
=> _IsLooping;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override AnimancerEvent.DispatchInfo GetEventDispatchInfo()
|
||||
{
|
||||
var length = _Length;
|
||||
return new(
|
||||
length,
|
||||
length != 0 ? Time / length : 0,
|
||||
_IsLooping);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override Vector3 AverageVelocity
|
||||
=> _Clip.averageSpeed;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#region Inverse Kinematics
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool ApplyAnimatorIK
|
||||
{
|
||||
get => _Playable.IsValid() && ((AnimationClipPlayable)_Playable).GetApplyPlayableIK();
|
||||
set
|
||||
{
|
||||
Validate.AssertPlayable(this);
|
||||
((AnimationClipPlayable)_Playable).SetApplyPlayableIK(value);
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool ApplyFootIK
|
||||
{
|
||||
get => _Playable.IsValid() && ((AnimationClipPlayable)_Playable).GetApplyFootIK();
|
||||
set
|
||||
{
|
||||
Validate.AssertPlayable(this);
|
||||
((AnimationClipPlayable)_Playable).SetApplyFootIK(value);
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endregion
|
||||
/************************************************************************************************************************/
|
||||
#endregion
|
||||
/************************************************************************************************************************/
|
||||
#region Methods
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Creates a new <see cref="ClipState"/> and sets its <see cref="Clip"/>.</summary>
|
||||
/// <exception cref="ArgumentNullException">The `clip` is null.</exception>
|
||||
public ClipState(AnimationClip clip)
|
||||
{
|
||||
Validate.AssertAnimationClip(clip, true, $"create {nameof(ClipState)}");
|
||||
_Clip = clip;
|
||||
_Length = clip.length;
|
||||
_IsLooping = clip.isLooping;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Creates and assigns the <see cref="AnimationClipPlayable"/> managed by this node.</summary>
|
||||
protected override void CreatePlayable(out Playable playable)
|
||||
{
|
||||
playable = AnimationClipPlayable.Create(Graph._PlayableGraph, _Clip);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void RecreatePlayable()
|
||||
{
|
||||
var playable = (AnimationClipPlayable)_Playable;
|
||||
var footIK = playable.GetApplyFootIK();
|
||||
var playableIK = playable.GetApplyPlayableIK();
|
||||
|
||||
base.RecreatePlayable();
|
||||
|
||||
playable = (AnimationClipPlayable)_Playable;
|
||||
playable.SetApplyFootIK(footIK);
|
||||
playable.SetApplyPlayableIK(playableIK);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Destroy()
|
||||
{
|
||||
_Clip = null;
|
||||
base.Destroy();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override AnimancerState Clone(CloneContext context)
|
||||
{
|
||||
var clip = context.GetCloneOrOriginal(_Clip);
|
||||
var clone = new ClipState(clip);
|
||||
clone.CopyFrom(this, context);
|
||||
return clone;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endregion
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5cba070da561e2f4fa6d618d4701bce3
|
||||
timeCreated: 1515060256
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,626 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
#pragma warning disable CS0649 // Field is never assigned to, and will always have its default value.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Animations;
|
||||
using UnityEngine.Playables;
|
||||
using Object = UnityEngine.Object;
|
||||
|
||||
namespace Animancer
|
||||
{
|
||||
/// <summary>[Pro-Only]
|
||||
/// An <see cref="AnimancerState"/> which contains other states.
|
||||
/// </summary>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer/ParentState
|
||||
///
|
||||
public abstract partial class ParentState : AnimancerState,
|
||||
ICopyable<ParentState>
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
#region Properties
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>The children contained within this state.</summary>
|
||||
/// <remarks>Only states up to the <see cref="ChildCount"/> should be assigned.</remarks>
|
||||
protected AnimancerState[] ChildStates { get; private set; }
|
||||
= Array.Empty<AnimancerState>();
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private int _ChildCount;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public sealed override int ChildCount
|
||||
=> _ChildCount;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>The size of the internal array of <see cref="ChildStates"/>.</summary>
|
||||
/// <remarks>
|
||||
/// This value starts at 0 then expands to <see cref="ChildCapacity"/>
|
||||
/// when the first child is added.
|
||||
/// </remarks>
|
||||
public int ChildCapacity
|
||||
{
|
||||
get => ChildStates.Length;
|
||||
set
|
||||
{
|
||||
if (value == ChildStates.Length)
|
||||
return;
|
||||
|
||||
#if UNITY_ASSERTIONS
|
||||
if (value <= 1 && OptionalWarning.MixerMinChildren.IsEnabled())
|
||||
OptionalWarning.MixerMinChildren.Log(
|
||||
$"The {nameof(ChildCapacity)} of '{this}' is being set to {value}." +
|
||||
$" The purpose of a mixer is to mix multiple child states so this may be a mistake.",
|
||||
Graph?.Component);
|
||||
#endif
|
||||
|
||||
var newChildStates = new AnimancerState[value];
|
||||
if (value > _ChildCount)// Increase size.
|
||||
{
|
||||
Array.Copy(ChildStates, newChildStates, _ChildCount);
|
||||
}
|
||||
else// Decrease size.
|
||||
{
|
||||
for (int i = value; i < _ChildCount; i++)
|
||||
ChildStates[i].Destroy();
|
||||
|
||||
Array.Copy(ChildStates, newChildStates, value);
|
||||
_ChildCount = value;
|
||||
}
|
||||
|
||||
ChildStates = newChildStates;
|
||||
|
||||
if (_Playable.IsValid())
|
||||
{
|
||||
_Playable.SetInputCount(value);
|
||||
}
|
||||
else if (Graph != null)
|
||||
{
|
||||
CreatePlayable();
|
||||
}
|
||||
|
||||
OnChildCapacityChanged();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Called when the <see cref="ChildCapacity"/> is changed.</summary>
|
||||
protected virtual void OnChildCapacityChanged() { }
|
||||
|
||||
/// <summary><see cref="ChildCapacity"/> starts at 0 then expands to this value when the first child is added.</summary>
|
||||
/// <remarks>Default 8.</remarks>
|
||||
public static int DefaultChildCapacity { get; set; } = 8;
|
||||
|
||||
/// <summary>
|
||||
/// Ensures that the remaining unused <see cref="ChildCapacity"/>
|
||||
/// is greater than or equal to the specified `minimumCapacity`.
|
||||
/// </summary>
|
||||
public void EnsureRemainingChildCapacity(int minimumCapacity)
|
||||
{
|
||||
minimumCapacity += _ChildCount;
|
||||
if (ChildCapacity < minimumCapacity)
|
||||
{
|
||||
var capacity = Math.Max(ChildCapacity, DefaultChildCapacity);
|
||||
while (capacity < minimumCapacity)
|
||||
capacity *= 2;
|
||||
|
||||
ChildCapacity = capacity;
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public sealed override AnimancerState GetChild(int index)
|
||||
=> ChildStates[index];
|
||||
|
||||
/// <inheritdoc/>
|
||||
public sealed override FastEnumerator<AnimancerState> GetEnumerator()
|
||||
=> new(ChildStates, _ChildCount);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endregion
|
||||
/************************************************************************************************************************/
|
||||
#region Initialization
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Creates and assigns the <see cref="AnimationMixerPlayable"/> managed by this state.</summary>
|
||||
protected override void CreatePlayable(out Playable playable)
|
||||
{
|
||||
playable = AnimationMixerPlayable.Create(Graph._PlayableGraph, ChildCapacity);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Connects the `child` to this mixer at its <see cref="AnimancerNode.Index"/>.</summary>
|
||||
protected internal override void OnAddChild(AnimancerState child)
|
||||
{
|
||||
Validate.AssertGraph(child, Graph);
|
||||
|
||||
var capacity = ChildCapacity;
|
||||
if (_ChildCount >= capacity)
|
||||
ChildCapacity = Math.Max(DefaultChildCapacity, capacity * 2);
|
||||
|
||||
child.Index = _ChildCount;
|
||||
ChildStates[_ChildCount] = child;
|
||||
_ChildCount++;
|
||||
|
||||
child.IsPlaying = IsPlaying;
|
||||
|
||||
if (Graph != null)
|
||||
ConnectChildUnsafe(child.Index, child);
|
||||
|
||||
#if UNITY_ASSERTIONS
|
||||
_CachedToString = null;
|
||||
#endif
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Disconnects the `child` from this mixer at its <see cref="AnimancerNode.Index"/>.</summary>
|
||||
protected internal override void OnRemoveChild(AnimancerState child)
|
||||
{
|
||||
Validate.AssertCanRemoveChild(child, ChildStates, _ChildCount);
|
||||
|
||||
// Shuffle all subsequent children down one place.
|
||||
if (Graph == null || !Graph._PlayableGraph.IsValid())
|
||||
{
|
||||
Array.Copy(
|
||||
ChildStates, child.Index + 1,
|
||||
ChildStates, child.Index,
|
||||
_ChildCount - child.Index - 1);
|
||||
|
||||
for (int i = child.Index; i < _ChildCount - 1; i++)
|
||||
ChildStates[i].Index = i;
|
||||
}
|
||||
else
|
||||
{
|
||||
Graph._PlayableGraph.Disconnect(_Playable, child.Index);
|
||||
|
||||
for (int i = child.Index + 1; i < _ChildCount; i++)
|
||||
{
|
||||
var otherChild = ChildStates[i];
|
||||
Graph._PlayableGraph.Disconnect(_Playable, otherChild.Index);
|
||||
otherChild.Index = i - 1;
|
||||
ChildStates[i - 1] = otherChild;
|
||||
ConnectChildUnsafe(i - 1, otherChild);
|
||||
}
|
||||
}
|
||||
|
||||
_ChildCount--;
|
||||
ChildStates[_ChildCount] = null;
|
||||
|
||||
#if UNITY_ASSERTIONS
|
||||
_CachedToString = null;
|
||||
#endif
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Destroy()
|
||||
{
|
||||
DestroyChildren();
|
||||
base.Destroy();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public sealed override void CopyFrom(AnimancerState copyFrom, CloneContext context)
|
||||
=> this.CopyFromBase(copyFrom, context);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual void CopyFrom(ParentState copyFrom, CloneContext context)
|
||||
{
|
||||
base.CopyFrom(copyFrom, context);
|
||||
|
||||
DestroyChildren();
|
||||
|
||||
var childCount = copyFrom.ChildCount;
|
||||
EnsureRemainingChildCapacity(childCount);
|
||||
|
||||
for (int i = 0; i < childCount; i++)
|
||||
{
|
||||
var child = copyFrom.ChildStates[i];
|
||||
child = context.Clone(child);
|
||||
Add(child);
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endregion
|
||||
/************************************************************************************************************************/
|
||||
#region Child Configuration
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Assigns the `state` as a child of this mixer.</summary>
|
||||
/// <remarks>This is the same as calling <see cref="AnimancerState.SetParent"/>.</remarks>
|
||||
public virtual void Add(AnimancerState child)
|
||||
=> child.SetParent(this);
|
||||
|
||||
/// <summary>Creates and returns a new <see cref="ClipState"/> to play the `clip` as a child of this mixer.</summary>
|
||||
public virtual ClipState Add(AnimationClip clip)
|
||||
{
|
||||
var child = new ClipState(clip);
|
||||
Add(child);
|
||||
return child;
|
||||
}
|
||||
|
||||
/// <summary>Calls <see cref="AnimancerUtilities.CreateStateAndApply"/> then <see cref="Add(AnimancerState)"/>.</summary>
|
||||
public virtual AnimancerState Add(ITransition transition)
|
||||
{
|
||||
var child = transition.CreateStateAndApply(Graph);
|
||||
Add(child);
|
||||
return child;
|
||||
}
|
||||
|
||||
/// <summary>Calls one of the other <see cref="Add(object)"/> overloads as appropriate for the `child`.</summary>
|
||||
public virtual AnimancerState Add(object child)
|
||||
{
|
||||
if (child is AnimationClip clip)
|
||||
return Add(clip);
|
||||
|
||||
if (child is ITransition transition)
|
||||
return Add(transition);
|
||||
|
||||
if (child is AnimancerState state)
|
||||
{
|
||||
Add(state);
|
||||
return state;
|
||||
}
|
||||
|
||||
MarkAsUsed(this);
|
||||
throw new ArgumentException(
|
||||
$"Failed to {nameof(Add)} '{AnimancerUtilities.ToStringOrNull(child)}'" +
|
||||
$" as child of '{this}' because it isn't an" +
|
||||
$" {nameof(AnimationClip)}, {nameof(ITransition)}, or {nameof(AnimancerState)}.");
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Calls <see cref="Add(AnimationClip)"/> for each of the `clips`.</summary>
|
||||
public void AddRange(IList<AnimationClip> clips)
|
||||
{
|
||||
var count = clips.Count;
|
||||
EnsureRemainingChildCapacity(count);
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
Add(clips[i]);
|
||||
}
|
||||
|
||||
/// <summary>Calls <see cref="Add(AnimationClip)"/> for each of the `clips`.</summary>
|
||||
public void AddRange(params AnimationClip[] clips)
|
||||
=> AddRange((IList<AnimationClip>)clips);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Calls <see cref="Add(ITransition)"/> for each of the `transitions`.</summary>
|
||||
public void AddRange(IList<ITransition> transitions)
|
||||
{
|
||||
var count = transitions.Count;
|
||||
EnsureRemainingChildCapacity(count);
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
Add(transitions[i]);
|
||||
}
|
||||
|
||||
/// <summary>Calls <see cref="Add(ITransition)"/> for each of the `transitions`.</summary>
|
||||
public void AddRange(params ITransition[] transitions)
|
||||
=> AddRange((IList<ITransition>)transitions);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Calls <see cref="Add(object)"/> for each of the `children`.</summary>
|
||||
public void AddRange(IList<object> children)
|
||||
{
|
||||
var count = children.Count;
|
||||
EnsureRemainingChildCapacity(count);
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
Add(children[i]);
|
||||
}
|
||||
|
||||
/// <summary>Calls <see cref="Add(object)"/> for each of the `children`.</summary>
|
||||
public void AddRange(params object[] children)
|
||||
=> AddRange((IList<object>)children);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Removes the child at the specified `index`.</summary>
|
||||
public void Remove(int index, bool destroy)
|
||||
=> Remove(ChildStates[index], destroy);
|
||||
|
||||
/// <summary>Removes the specified `child`.</summary>
|
||||
public void Remove(AnimancerState child, bool destroy)
|
||||
{
|
||||
#if UNITY_ASSERTIONS
|
||||
if (child.Parent != this)
|
||||
Debug.LogWarning($"Attempting to remove a state which is not a child of this {GetType().Name}." +
|
||||
$" This will remove the child from its actual parent so you should directly call" +
|
||||
$" child.{nameof(child.Destroy)} or child.{nameof(child.SetParent)}(null, -1) instead." +
|
||||
$"\n• Child: {child}" +
|
||||
$"\n• Removing From: {this}" +
|
||||
$"\n• Actual Parent: {child.Parent}",
|
||||
Graph?.Component as Object);
|
||||
#endif
|
||||
|
||||
if (destroy)
|
||||
child.Destroy();
|
||||
else
|
||||
child.SetParent(null);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Replaces the `child` at the specified `index`.</summary>
|
||||
public virtual void Set(int index, AnimancerState child, bool destroyPrevious)
|
||||
{
|
||||
#if UNITY_ASSERTIONS
|
||||
if ((uint)index >= _ChildCount)
|
||||
{
|
||||
MarkAsUsed(this);
|
||||
MarkAsUsed(child);
|
||||
throw new IndexOutOfRangeException(
|
||||
$"Invalid child index. Must be 0 <= index < {nameof(ChildCount)} ({ChildCount}).");
|
||||
}
|
||||
#endif
|
||||
|
||||
if (child.Parent != null)
|
||||
child.SetParent(null);
|
||||
|
||||
var previousChild = ChildStates[index];
|
||||
previousChild.SetParentInternal(null);
|
||||
|
||||
child.SetGraph(Graph);
|
||||
ChildStates[index] = child;
|
||||
child.SetParentInternal(this, index);
|
||||
child.IsPlaying = IsPlaying;
|
||||
|
||||
if (Graph != null)
|
||||
{
|
||||
Graph._PlayableGraph.Disconnect(_Playable, index);
|
||||
ConnectChildUnsafe(index, child);
|
||||
}
|
||||
|
||||
child.CopyIKFlags(this);
|
||||
|
||||
if (destroyPrevious)
|
||||
previousChild.Destroy();
|
||||
|
||||
#if UNITY_ASSERTIONS
|
||||
_CachedToString = null;
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>Replaces the child at the specified `index` with a new <see cref="ClipState"/>.</summary>
|
||||
public ClipState Set(int index, AnimationClip clip, bool destroyPrevious)
|
||||
{
|
||||
var child = new ClipState(clip);
|
||||
Set(index, child, destroyPrevious);
|
||||
return child;
|
||||
}
|
||||
|
||||
/// <summary>Replaces the child at the specified `index` with a <see cref="ITransition.CreateState"/>.</summary>
|
||||
public AnimancerState Set(int index, ITransition transition, bool destroyPrevious)
|
||||
{
|
||||
var child = transition.CreateStateAndApply(Graph);
|
||||
Set(index, child, destroyPrevious);
|
||||
return child;
|
||||
}
|
||||
|
||||
/// <summary>Calls one of the other <see cref="Set(int, object, bool)"/> overloads as appropriate for the `child`.</summary>
|
||||
public AnimancerState Set(int index, object child, bool destroyPrevious)
|
||||
{
|
||||
if (child is AnimationClip clip)
|
||||
return Set(index, clip, destroyPrevious);
|
||||
|
||||
if (child is ITransition transition)
|
||||
return Set(index, transition, destroyPrevious);
|
||||
|
||||
if (child is AnimancerState state)
|
||||
{
|
||||
Set(index, state, destroyPrevious);
|
||||
return state;
|
||||
}
|
||||
|
||||
MarkAsUsed(this);
|
||||
throw new ArgumentException(
|
||||
$"Failed to {nameof(Set)} '{AnimancerUtilities.ToStringOrNull(child)}'" +
|
||||
$" as child of '{this}' because it isn't an" +
|
||||
$" {nameof(AnimationClip)}, {nameof(ITransition)}, or {nameof(AnimancerState)}.");
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Returns the index of the specified `child` state.</summary>
|
||||
public int IndexOf(AnimancerState child)
|
||||
=> Array.IndexOf(ChildStates, child, 0, _ChildCount);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// Destroys all <see cref="ChildStates"/> connected to this mixer.
|
||||
/// This operation cannot be undone.
|
||||
/// </summary>
|
||||
public void DestroyChildren()
|
||||
{
|
||||
for (int i = _ChildCount - 1; i >= 0; i--)
|
||||
ChildStates[i].Destroy();
|
||||
|
||||
Array.Clear(ChildStates, 0, _ChildCount);
|
||||
_ChildCount = 0;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endregion
|
||||
/************************************************************************************************************************/
|
||||
#region Updates
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected internal override void UpdateEvents()
|
||||
=> UpdateEventsRecursive(this);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endregion
|
||||
/************************************************************************************************************************/
|
||||
#region Inverse Kinematics
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private bool _ApplyAnimatorIK;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool ApplyAnimatorIK
|
||||
{
|
||||
get => _ApplyAnimatorIK;
|
||||
set => base.ApplyAnimatorIK = _ApplyAnimatorIK = value;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private bool _ApplyFootIK;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool ApplyFootIK
|
||||
{
|
||||
get => _ApplyFootIK;
|
||||
set => base.ApplyFootIK = _ApplyFootIK = value;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endregion
|
||||
/************************************************************************************************************************/
|
||||
#region Other Methods
|
||||
/************************************************************************************************************************/
|
||||
|
||||
#if UNITY_ASSERTIONS
|
||||
/// <summary>[Assert-Only] A string built by <see cref="ToString"/> to describe this mixer.</summary>
|
||||
private string _CachedToString;
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// Returns a string describing the type of this mixer and the name of states connected to it.
|
||||
/// </summary>
|
||||
public override string ToString()
|
||||
{
|
||||
#if UNITY_ASSERTIONS
|
||||
if (NameCache.TryToString(DebugName, out var name))
|
||||
return name;
|
||||
|
||||
if (_CachedToString != null)
|
||||
return _CachedToString;
|
||||
#endif
|
||||
|
||||
// Gather child names.
|
||||
var childNames = ListPool.Acquire<string>();
|
||||
var allSimple = true;
|
||||
for (int i = 0; i < _ChildCount; i++)
|
||||
{
|
||||
var state = ChildStates[i];
|
||||
if (state == null)
|
||||
continue;
|
||||
|
||||
if (state.MainObject != null)
|
||||
{
|
||||
childNames.Add(state.MainObject.name);
|
||||
}
|
||||
else
|
||||
{
|
||||
childNames.Add(state.ToString());
|
||||
allSimple = false;
|
||||
}
|
||||
}
|
||||
|
||||
// If they all have a main object, check if they all have the same prefix so it doesn't need to be repeated.
|
||||
int prefixLength = 0;
|
||||
var count = childNames.Count;
|
||||
if (count <= 1 || !allSimple)
|
||||
{
|
||||
prefixLength = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
var prefix = childNames[0];
|
||||
var shortest = prefixLength = prefix.Length;
|
||||
|
||||
for (int iName = 0; iName < count; iName++)
|
||||
{
|
||||
var childName = childNames[iName];
|
||||
|
||||
if (shortest > childName.Length)
|
||||
{
|
||||
shortest = prefixLength = childName.Length;
|
||||
}
|
||||
|
||||
for (int iCharacter = 0; iCharacter < prefixLength; iCharacter++)
|
||||
{
|
||||
if (childName[iCharacter] != prefix[iCharacter])
|
||||
{
|
||||
prefixLength = iCharacter;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (prefixLength < 3 ||// Less than 3 characters probably isn't an intentional prefix.
|
||||
prefixLength >= shortest)
|
||||
prefixLength = 0;
|
||||
}
|
||||
|
||||
// Build the parent name.
|
||||
var parentName = StringBuilderPool.Instance.Acquire();
|
||||
|
||||
var type = GetType().Name;
|
||||
if (type.EndsWith("State"))
|
||||
parentName.Append(type, 0, type.Length - 5);
|
||||
else
|
||||
parentName.Append(type);
|
||||
|
||||
parentName.Append('(');
|
||||
|
||||
if (count > 0)
|
||||
{
|
||||
if (prefixLength > 0)
|
||||
parentName.Append(childNames[0], 0, prefixLength).Append('[');
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
if (i > 0)
|
||||
parentName.Append(", ");
|
||||
|
||||
var childName = childNames[i];
|
||||
parentName.Append(childName, prefixLength, childName.Length - prefixLength);
|
||||
}
|
||||
|
||||
parentName.Append(']');
|
||||
}
|
||||
ListPool.Release(childNames);
|
||||
|
||||
parentName.Append(')');
|
||||
|
||||
var result = parentName.ReleaseToString();
|
||||
|
||||
#if UNITY_ASSERTIONS
|
||||
_CachedToString = result;
|
||||
#endif
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void GatherAnimationClips(ICollection<AnimationClip> clips)
|
||||
=> clips.GatherFromSource(ChildStates);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endregion
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 053b822d8f58e2d4e977ce274eb61e6f
|
||||
timeCreated: 1515060256
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,442 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Animations;
|
||||
using UnityEngine.Audio;
|
||||
using UnityEngine.Playables;
|
||||
using Object = UnityEngine.Object;
|
||||
|
||||
namespace Animancer
|
||||
{
|
||||
/// <summary>[Pro-Only] An <see cref="AnimancerState"/> which plays a <see cref="PlayableAsset"/>.</summary>
|
||||
/// <remarks>
|
||||
/// <strong>Documentation:</strong>
|
||||
/// <see href="https://kybernetik.com.au/animancer/docs/manual/timeline">
|
||||
/// Timeline</see>
|
||||
/// </remarks>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer/PlayableAssetState
|
||||
public class PlayableAssetState : AnimancerState
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
#region Fields and Properties
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>The <see cref="PlayableAsset"/> which this state plays.</summary>
|
||||
private PlayableAsset _Asset;
|
||||
|
||||
/// <summary>The <see cref="PlayableAsset"/> which this state plays.</summary>
|
||||
public PlayableAsset Asset
|
||||
{
|
||||
get => _Asset;
|
||||
set => ChangeMainObject(ref _Asset, value);
|
||||
}
|
||||
|
||||
/// <summary>The <see cref="PlayableAsset"/> which this state plays.</summary>
|
||||
public override Object MainObject
|
||||
{
|
||||
get => _Asset;
|
||||
set => _Asset = (PlayableAsset)value;
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
/// <inheritdoc/>
|
||||
public override Type MainObjectType
|
||||
=> typeof(PlayableAsset);
|
||||
#endif
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private float _Length;
|
||||
|
||||
/// <summary>The <see cref="PlayableAsset.duration"/> (cached on initialization).</summary>
|
||||
public override float Length
|
||||
=> _Length;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override float Speed
|
||||
{
|
||||
get => base.Speed;
|
||||
set
|
||||
{
|
||||
base.Speed = value;
|
||||
|
||||
for (int i = Outputs.Count - 1; i >= 0; i--)
|
||||
Outputs[i].GetSourcePlayable().SetSpeed(value);
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override AnimancerEvent.DispatchInfo GetEventDispatchInfo()
|
||||
{
|
||||
var length = _Length;
|
||||
return new(
|
||||
length,
|
||||
length != 0 ? Time / length : 0,
|
||||
false);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void OnSetIsPlaying()
|
||||
{
|
||||
if (!_Playable.IsValid())
|
||||
return;
|
||||
|
||||
var inputCount = _Playable.GetInputCount();
|
||||
for (int i = 0; i < inputCount; i++)
|
||||
{
|
||||
var playable = _Playable.GetInput(i);
|
||||
if (!playable.IsValid())
|
||||
continue;
|
||||
|
||||
if (IsPlaying)
|
||||
playable.Play();
|
||||
else
|
||||
playable.Pause();
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>IK cannot be dynamically enabled on a <see cref="PlayableAssetState"/>.</summary>
|
||||
public override void CopyIKFlags(AnimancerNodeBase copyFrom) { }
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>IK cannot be dynamically enabled on a <see cref="PlayableAssetState"/>.</summary>
|
||||
public override bool ApplyAnimatorIK
|
||||
{
|
||||
get => false;
|
||||
set
|
||||
{
|
||||
#if UNITY_ASSERTIONS
|
||||
if (value)
|
||||
OptionalWarning.UnsupportedIK.Log(
|
||||
$"IK cannot be dynamically enabled on a {nameof(PlayableAssetState)}.", Graph?.Component);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>IK cannot be dynamically enabled on a <see cref="PlayableAssetState"/>.</summary>
|
||||
public override bool ApplyFootIK
|
||||
{
|
||||
get => false;
|
||||
set
|
||||
{
|
||||
#if UNITY_ASSERTIONS
|
||||
if (value)
|
||||
OptionalWarning.UnsupportedIK.Log(
|
||||
$"IK cannot be dynamically enabled on a {nameof(PlayableAssetState)}.", Graph?.Component);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endregion
|
||||
/************************************************************************************************************************/
|
||||
#region Methods
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Creates a new <see cref="PlayableAssetState"/> to play the `asset`.</summary>
|
||||
/// <exception cref="ArgumentNullException">The `asset` is null.</exception>
|
||||
public PlayableAssetState(PlayableAsset asset)
|
||||
{
|
||||
if (asset == null)
|
||||
throw new ArgumentNullException(nameof(asset));
|
||||
|
||||
_Asset = asset;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void CreatePlayable(out Playable playable)
|
||||
{
|
||||
playable = _Asset.CreatePlayable(Graph._PlayableGraph, Graph.Component.gameObject);
|
||||
playable.SetDuration(9223372.03685477);// https://github.com/KybernetikGames/animancer/issues/111
|
||||
|
||||
_Length = (float)_Asset.duration;
|
||||
|
||||
if (!_HasInitializedBindings)
|
||||
InitializeBindings();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private readonly List<PlayableOutput>
|
||||
Outputs = new();
|
||||
|
||||
private IList<Object> _Bindings;
|
||||
private bool _HasInitializedBindings;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>The objects controlled by each track in the asset.</summary>
|
||||
public IList<Object> Bindings
|
||||
{
|
||||
get => _Bindings;
|
||||
set
|
||||
{
|
||||
DestroyBoundOutputs(false);
|
||||
_Bindings = value;
|
||||
InitializeBindings();
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Sets the <see cref="Bindings"/>.</summary>
|
||||
public void SetBindings(params Object[] bindings)
|
||||
{
|
||||
Bindings = bindings;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private void InitializeBindings()
|
||||
{
|
||||
if (Graph == null)
|
||||
return;
|
||||
|
||||
_HasInitializedBindings = true;
|
||||
|
||||
Validate.AssertPlayable(this);
|
||||
|
||||
var graph = Graph._PlayableGraph;
|
||||
|
||||
var bindableIndex = 0;
|
||||
var bindableCount = _Bindings != null
|
||||
? _Bindings.Count
|
||||
: 0;
|
||||
|
||||
var speed = Speed;
|
||||
|
||||
foreach (var binding in _Asset.outputs)
|
||||
{
|
||||
GetBindingDetails(binding, out var trackName, out var trackType, out var isMarkers);
|
||||
|
||||
var bindable = bindableIndex < bindableCount
|
||||
? _Bindings[bindableIndex]
|
||||
: null;
|
||||
|
||||
#if UNITY_ASSERTIONS
|
||||
if (!isMarkers &&
|
||||
trackType != null &&
|
||||
bindable != null &&
|
||||
!trackType.IsAssignableFrom(bindable.GetType()))
|
||||
{
|
||||
Debug.LogError(
|
||||
$"Binding Type Mismatch: bindings[{bindableIndex}] is '{bindable}'" +
|
||||
$" but should be a {trackType.FullName} for {trackName}",
|
||||
Graph.Component as Object);
|
||||
bindableIndex++;
|
||||
continue;
|
||||
}
|
||||
#endif
|
||||
|
||||
var playable = _Playable.GetInput(bindableIndex);
|
||||
|
||||
if (speed != 1)
|
||||
playable.SetSpeed(speed);
|
||||
|
||||
if (trackType == typeof(Animator))// AnimationTrack.
|
||||
{
|
||||
if (bindable != null)
|
||||
{
|
||||
#if UNITY_ASSERTIONS
|
||||
if (bindable == Graph.Component?.Animator)
|
||||
Debug.LogError(
|
||||
$"{nameof(PlayableAsset)} tracks should not be bound to the same {nameof(Animator)} as" +
|
||||
$" Animancer. Leaving the binding of the first Animation Track empty will automatically" +
|
||||
$" apply its animation to the object being controlled by Animancer.",
|
||||
Graph.Component as Object);
|
||||
#endif
|
||||
|
||||
var playableOutput = AnimationPlayableOutput.Create(graph, trackName, (Animator)bindable);
|
||||
playableOutput.SetReferenceObject(binding.sourceObject);
|
||||
playableOutput.SetSourcePlayable(playable);
|
||||
playableOutput.SetWeight(1);
|
||||
Outputs.Add(playableOutput);
|
||||
}
|
||||
}
|
||||
#if UNITY_AUDIO
|
||||
else if (trackType == typeof(AudioSource))// AudioTrack.
|
||||
{
|
||||
if (bindable != null)
|
||||
{
|
||||
var playableOutput = AudioPlayableOutput.Create(graph, trackName, (AudioSource)bindable);
|
||||
playableOutput.SetReferenceObject(binding.sourceObject);
|
||||
playableOutput.SetSourcePlayable(playable);
|
||||
playableOutput.SetWeight(1);
|
||||
Outputs.Add(playableOutput);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
else if (isMarkers)// Markers.
|
||||
{
|
||||
var animancer = Graph.Component as Component;
|
||||
var playableOutput = ScriptPlayableOutput.Create(graph, trackName);
|
||||
playableOutput.SetReferenceObject(binding.sourceObject);
|
||||
playableOutput.SetSourcePlayable(playable);
|
||||
playableOutput.SetWeight(1);
|
||||
playableOutput.SetUserData(animancer);
|
||||
Outputs.Add(playableOutput);
|
||||
|
||||
var receivers = ListPool.Acquire<INotificationReceiver>();
|
||||
animancer.GetComponents(receivers);
|
||||
for (int i = 0; i < receivers.Count; i++)
|
||||
playableOutput.AddNotificationReceiver(receivers[i]);
|
||||
ListPool.Release(receivers);
|
||||
|
||||
continue;// Don't increment the bindingIndex.
|
||||
}
|
||||
else// ActivationTrack, ControlTrack, PlayableTrack, SignalTrack.
|
||||
{
|
||||
var playableOutput = ScriptPlayableOutput.Create(graph, trackName);
|
||||
playableOutput.SetReferenceObject(binding.sourceObject);
|
||||
playableOutput.SetSourcePlayable(playable);
|
||||
playableOutput.SetWeight(1);
|
||||
playableOutput.SetUserData(bindable);
|
||||
if (bindable is INotificationReceiver receiver)
|
||||
playableOutput.AddNotificationReceiver(receiver);
|
||||
Outputs.Add(playableOutput);
|
||||
}
|
||||
|
||||
bindableIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Gathers details about the `binding`.</summary>
|
||||
public static void GetBindingDetails(
|
||||
PlayableBinding binding,
|
||||
out string name,
|
||||
out Type type,
|
||||
out bool isMarkers)
|
||||
{
|
||||
name = binding.streamName;
|
||||
type = binding.outputTargetType;
|
||||
isMarkers = type == typeof(GameObject) && name == "Markers";
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void SetWeight(float value)
|
||||
{
|
||||
base.SetWeight(value);
|
||||
|
||||
for (int i = Outputs.Count - 1; i >= 0; i--)
|
||||
Outputs[i].SetWeight(value);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Destroy()
|
||||
{
|
||||
_Asset = null;
|
||||
|
||||
DestroyBoundOutputs(true);
|
||||
|
||||
base.Destroy();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// Destroys all of the outputs created for the <see cref="Bindings"/>
|
||||
/// and optionally the state playable itself.
|
||||
/// </summary>
|
||||
public void DestroyBoundOutputs(bool destroyStatePlayable)
|
||||
{
|
||||
if (Graph == null)
|
||||
return;
|
||||
|
||||
var graph = Graph._PlayableGraph;
|
||||
if (!graph.IsValid())
|
||||
return;
|
||||
|
||||
for (int i = Outputs.Count - 1; i >= 0; i--)
|
||||
{
|
||||
var output = Outputs[i];
|
||||
|
||||
var playable = output.GetSourcePlayable();
|
||||
if (playable.IsValid())
|
||||
graph.DestroySubgraph(playable);
|
||||
|
||||
graph.DestroyOutput(output);
|
||||
}
|
||||
|
||||
Outputs.Clear();
|
||||
|
||||
if (destroyStatePlayable && _Playable.IsValid())
|
||||
graph.DestroySubgraph(_Playable);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override AnimancerState Clone(CloneContext context)
|
||||
{
|
||||
var asset = context.GetCloneOrOriginal(_Asset);
|
||||
var clone = new PlayableAssetState(asset);
|
||||
clone.CopyFrom(this, context);
|
||||
return clone;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void AppendDetails(StringBuilder text, string separator)
|
||||
{
|
||||
base.AppendDetails(text, separator);
|
||||
|
||||
text.Append(separator)
|
||||
.Append($"{nameof(Bindings)}: ");
|
||||
|
||||
int count;
|
||||
if (_Bindings == null)
|
||||
{
|
||||
text.Append("Null");
|
||||
count = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
count = _Bindings.Count;
|
||||
text.Append('[')
|
||||
.Append(count)
|
||||
.Append(']');
|
||||
}
|
||||
|
||||
text.Append(_HasInitializedBindings
|
||||
? " (Initialized)"
|
||||
: " (Not Initialized)");
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
text.Append(separator)
|
||||
.Append($"{nameof(Bindings)}[")
|
||||
.Append(i)
|
||||
.Append("] = ")
|
||||
.Append(AnimancerUtilities.ToStringOrNull(_Bindings[i]));
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endregion
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c8771e9c8ab67844b8e5cf90e14bf90e
|
||||
timeCreated: 1515060256
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,375 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
#pragma warning disable CS0649 // Field is never assigned to, and will always have its default value.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Animancer
|
||||
{
|
||||
/// <summary>[Pro-Only]
|
||||
/// An <see cref="AnimancerState"/> which plays a sequence of other states.
|
||||
/// </summary>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer/SequenceState
|
||||
///
|
||||
public partial class SequenceState : ParentState,
|
||||
ICopyable<SequenceState>,
|
||||
IUpdatable
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
#region Fields and Properties
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private double[] _TimeOffsets = Array.Empty<double>();
|
||||
private double[] _FadeEndTimes = Array.Empty<double>();
|
||||
private double[] _StateEndTimes = Array.Empty<double>();
|
||||
|
||||
/// <summary>The index of the child state which is active at the current time.</summary>
|
||||
private int _ActiveChildIndex;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override float Length
|
||||
=> ChildCount > 0
|
||||
? (float)_StateEndTimes[ChildCount - 1]
|
||||
: 0;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override double RawTime
|
||||
{
|
||||
get => base.RawTime;
|
||||
set
|
||||
{
|
||||
base.RawTime = value;
|
||||
|
||||
if (ChildCount == 0)
|
||||
return;
|
||||
|
||||
var activeChildIndex = GetActiveChildIndex(value);
|
||||
SetActiveChildIndex(activeChildIndex);
|
||||
|
||||
for (int i = 0; i < ChildCount; i++)
|
||||
{
|
||||
var child = ChildStates[i];
|
||||
|
||||
var childTime = value;
|
||||
if (i > 0)
|
||||
childTime -= _StateEndTimes[i - 1];
|
||||
|
||||
childTime *= child.Speed;
|
||||
|
||||
child.TimeD = childTime + _TimeOffsets[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void MoveTime(double time, bool normalized)
|
||||
{
|
||||
base.MoveTime(time, normalized);
|
||||
|
||||
for (int i = 0; i < ChildCount; i++)
|
||||
{
|
||||
var child = ChildStates[i];
|
||||
|
||||
var childTime = time;
|
||||
if (i > 0)
|
||||
childTime -= _StateEndTimes[i - 1];
|
||||
|
||||
childTime *= child.Speed;
|
||||
|
||||
child.MoveTime(childTime + _TimeOffsets[i], normalized);
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Sequences don't loop.</summary>
|
||||
/// <remarks>
|
||||
/// If the last state in the sequence is set to loop it will do so,
|
||||
/// but the rest of the sequence won't replay automatically.
|
||||
/// </remarks>
|
||||
public override bool IsLooping
|
||||
=> false;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endregion
|
||||
/************************************************************************************************************************/
|
||||
#region Initialisation
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Replaces the child states with new ones created from the `transitions`.</summary>
|
||||
public void Set(params ITransition[] transitions)
|
||||
=> Set((IList<ITransition>)transitions);
|
||||
|
||||
/// <summary>Replaces the child states with new ones created from the `transitions`.</summary>
|
||||
public void Set(IList<ITransition> transitions)
|
||||
{
|
||||
var oldChildCount = ChildCount;
|
||||
var newChildCount = transitions.Count;
|
||||
|
||||
ChildCapacity = newChildCount;
|
||||
|
||||
for (int i = 0; i < newChildCount; i++)
|
||||
{
|
||||
var transition = transitions[i];
|
||||
var state = transition.CreateStateAndApply(Graph);
|
||||
state.IsPlaying = IsPlaying;
|
||||
|
||||
if (i < oldChildCount)
|
||||
Set(i, state, true);
|
||||
else
|
||||
Add(state);
|
||||
|
||||
_FadeEndTimes[i] += transition.FadeDuration;
|
||||
}
|
||||
|
||||
while (oldChildCount > newChildCount)
|
||||
Remove(--oldChildCount, true);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected internal override void OnAddChild(AnimancerState child)
|
||||
{
|
||||
base.OnAddChild(child);
|
||||
GatherChildDetails(child);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Gathers the timing details of a newly added `child` state.</summary>
|
||||
private void GatherChildDetails(AnimancerState child)
|
||||
{
|
||||
var index = child.Index;
|
||||
|
||||
if (index == 0)
|
||||
child.Weight = 1;
|
||||
|
||||
_TimeOffsets[index] = child.TimeD;
|
||||
|
||||
var startTime = GetStartTime(index);
|
||||
|
||||
_FadeEndTimes[index] = startTime;
|
||||
|
||||
var length = child.RemainingDuration;
|
||||
if (length < 0)
|
||||
length = 0;
|
||||
|
||||
startTime += length;
|
||||
|
||||
_StateEndTimes[index] = startTime;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override AnimancerState Add(ITransition transition)
|
||||
{
|
||||
var child = base.Add(transition);
|
||||
|
||||
_FadeEndTimes[child.Index] += transition.FadeDuration;
|
||||
|
||||
return child;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Adds the `child` to the end of this sequence.</summary>
|
||||
public void Add(AnimancerState child, float fadeDuration)
|
||||
{
|
||||
Add(child);
|
||||
|
||||
_FadeEndTimes[child.Index] += fadeDuration;
|
||||
}
|
||||
|
||||
/// <summary>Adds the `clip` to the end of this sequence.</summary>
|
||||
public void Add(AnimationClip clip, float fadeDuration)
|
||||
{
|
||||
var child = Add(clip);
|
||||
|
||||
_FadeEndTimes[child.Index] += fadeDuration;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endregion
|
||||
/************************************************************************************************************************/
|
||||
#region Execution
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void OnChildCapacityChanged()
|
||||
{
|
||||
base.OnChildCapacityChanged();
|
||||
|
||||
var capacity = ChildCapacity;
|
||||
Array.Resize(ref _TimeOffsets, capacity);
|
||||
Array.Resize(ref _FadeEndTimes, capacity);
|
||||
Array.Resize(ref _StateEndTimes, capacity);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
int IUpdatable.UpdatableIndex { get; set; } = IUpdatable.List.NotInList;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void OnSetIsPlaying()
|
||||
{
|
||||
base.OnSetIsPlaying();
|
||||
|
||||
var isPlaying = IsPlaying;
|
||||
var childStates = ChildStates;
|
||||
for (int i = 0; i < ChildCount; i++)
|
||||
childStates[i].IsPlaying = isPlaying;
|
||||
|
||||
if (IsPlaying)
|
||||
Graph.RequirePreUpdate(this);
|
||||
else
|
||||
Graph.CancelPreUpdate(this);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Destroy()
|
||||
{
|
||||
base.Destroy();
|
||||
Graph.CancelPreUpdate(this);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// Called every frame while this state is playing to control its children.
|
||||
/// </summary>
|
||||
public virtual void Update()
|
||||
{
|
||||
var time = Time;
|
||||
|
||||
var activeChildIndex = _ActiveChildIndex;
|
||||
if (Speed >= 0)
|
||||
{
|
||||
while (activeChildIndex < ChildCount - 1 && time > _StateEndTimes[activeChildIndex])
|
||||
activeChildIndex++;
|
||||
}
|
||||
else
|
||||
{
|
||||
while (activeChildIndex > 0 && time > _StateEndTimes[activeChildIndex - 1])
|
||||
activeChildIndex--;
|
||||
}
|
||||
|
||||
SetActiveChildIndex(activeChildIndex);
|
||||
|
||||
var startTime = GetStartTime(activeChildIndex);
|
||||
|
||||
var endFadeTime = _FadeEndTimes[activeChildIndex];
|
||||
|
||||
if (activeChildIndex == 0 || time > endFadeTime)
|
||||
{
|
||||
ChildStates[activeChildIndex].Weight = 1;
|
||||
|
||||
if (activeChildIndex > 0)
|
||||
ChildStates[activeChildIndex - 1].Weight = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
var weight = Mathf.InverseLerp((float)startTime, (float)endFadeTime, time);
|
||||
|
||||
ChildStates[activeChildIndex].Weight = weight;
|
||||
ChildStates[activeChildIndex - 1].Weight = 1 - weight;
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the indices of the `first` and `last` child states
|
||||
/// which should be active at the specified `time`.
|
||||
/// </summary>
|
||||
public int GetActiveChildIndex(double time)
|
||||
{
|
||||
var index = Array.BinarySearch(_StateEndTimes, time);
|
||||
|
||||
if (index < 0)
|
||||
index = ~index;
|
||||
|
||||
if (index >= ChildCount)
|
||||
index = ChildCount - 1;
|
||||
|
||||
return index;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Clears the weights of any active chhildren and sets the newly active child.</summary>
|
||||
private void SetActiveChildIndex(int index)
|
||||
{
|
||||
if (_ActiveChildIndex == index)
|
||||
return;
|
||||
|
||||
ChildStates[_ActiveChildIndex].Weight = 0;
|
||||
|
||||
if (_ActiveChildIndex > 0)
|
||||
ChildStates[_ActiveChildIndex - 1].Weight = 0;
|
||||
|
||||
_ActiveChildIndex = index;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Gets the time when the specified child starts relative to the start of this sequence.</summary>
|
||||
public double GetStartTime(int childIndex)
|
||||
=> childIndex > 0
|
||||
? _StateEndTimes[childIndex - 1]
|
||||
: 0;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endregion
|
||||
/************************************************************************************************************************/
|
||||
#region Copying
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override AnimancerState Clone(CloneContext context)
|
||||
{
|
||||
var clone = new SequenceState();
|
||||
clone.CopyFrom(this, context);
|
||||
return clone;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public sealed override void CopyFrom(ParentState copyFrom, CloneContext context)
|
||||
=> this.CopyFromBase(copyFrom, context);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual void CopyFrom(SequenceState copyFrom, CloneContext context)
|
||||
{
|
||||
base.CopyFrom(copyFrom, context);
|
||||
|
||||
var childCount = Math.Min(copyFrom.ChildCount, ChildCount);
|
||||
|
||||
Array.Copy(copyFrom._TimeOffsets, 0, _TimeOffsets, 0, childCount);
|
||||
Array.Copy(copyFrom._FadeEndTimes, 0, _FadeEndTimes, 0, childCount);
|
||||
Array.Copy(copyFrom._StateEndTimes, 0, _StateEndTimes, 0, childCount);
|
||||
|
||||
_ActiveChildIndex = copyFrom._ActiveChildIndex;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endregion
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7c8e97dddf2a0d246b4cdfe6319d1255
|
||||
timeCreated: 1515060256
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,60 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
using UnityEngine.Playables;
|
||||
|
||||
namespace Animancer
|
||||
{
|
||||
/// <summary>
|
||||
/// A <see cref="PlayableBehaviour"/> which executes <see cref="IUpdatable.Update"/>
|
||||
/// on each item in an <see cref="IUpdatable.List"/> every frame.
|
||||
/// </summary>
|
||||
public class UpdatableListPlayable : PlayableBehaviour
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// Since <see cref="ScriptPlayable{T}.Create(PlayableGraph, int)"/> needs to clone an existing instance,
|
||||
/// we keep a static template to avoid allocating an extra garbage one every time.
|
||||
/// This also means the fields can't be readonly because field initializers don't run on the clone.
|
||||
/// </summary>
|
||||
private static readonly UpdatableListPlayable Template = new();
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>The <see cref="AnimancerGraph"/> this behaviour is connected to.</summary>
|
||||
private AnimancerGraph _Graph;
|
||||
|
||||
/// <summary>Objects to be updated before time advances.</summary>
|
||||
private IUpdatable.List _Updatables;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Creates a new <see cref="UpdatableListPlayable"/>.</summary>
|
||||
public static ScriptPlayable<UpdatableListPlayable> Create(
|
||||
AnimancerGraph graph,
|
||||
int inputCount,
|
||||
IUpdatable.List updatables)
|
||||
{
|
||||
var playable = ScriptPlayable<UpdatableListPlayable>.Create(graph._PlayableGraph, Template, inputCount);
|
||||
var instance = playable.GetBehaviour();
|
||||
instance._Graph = graph;
|
||||
instance._Updatables = updatables;
|
||||
return playable;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>[Internal] Calls <see cref="IUpdatable.Update"/> on everything added to this list.</summary>
|
||||
/// <remarks>
|
||||
/// Called by the <see cref="PlayableGraph"/> after the rest of the <see cref="Playable"/>s are evaluated.
|
||||
/// </remarks>
|
||||
public override void PrepareFrame(Playable playable, FrameData info)
|
||||
=> _Graph.UpdateAll(
|
||||
_Updatables,
|
||||
info.deltaTime * info.effectiveParentSpeed,
|
||||
info.frameId);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b5360a72062b471418bfa0e8567a7fc5
|
||||
timeCreated: 1515048758
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: db2c18cf91057604fba518cb84714b94
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,128 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
using System;
|
||||
using Unity.Collections;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Animations;
|
||||
|
||||
namespace Animancer
|
||||
{
|
||||
/// <summary>
|
||||
/// A replacement for the default <see cref="AnimationLayerMixerPlayable"/> which uses custom
|
||||
/// <see cref="BoneWeights"/> for each individual bone instead of just using an <see cref="AvatarMask"/>
|
||||
/// to include or exclude them entirely.
|
||||
/// </summary>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer/WeightedMaskLayerList
|
||||
public class WeightedMaskLayerList : AnimancerLayerList, IDisposable
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>The objects being masked.</summary>
|
||||
public readonly Transform[] Bones;
|
||||
|
||||
/// <summary>The job data.</summary>
|
||||
private readonly WeightedMaskMixerJob _Job;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>The root motion weight of each layer.</summary>
|
||||
public NativeArray<float> RootMotionWeights
|
||||
=> _Job.rootMotionWeights;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>The number of objects being masked (excluding the root bone).</summary>
|
||||
public int BoneCount
|
||||
=> Bones.Length - 1;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>The blend weight of each of the <see cref="Bones"/>.</summary>
|
||||
public NativeArray<float> BoneWeights
|
||||
=> _Job.boneWeights;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Returns the index of the value corresponding to the 'bone' in the <see cref="BoneWeights"/> array.</summary>
|
||||
public int IndexOf(Transform bone)
|
||||
=> Array.IndexOf(Bones, bone) - 1;// Index - 1 since the root is ignored.
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Creates a new <see cref="AnimancerGraph"/> and <see cref="WeightedMaskLayerList"/>.</summary>
|
||||
/// <remarks>
|
||||
/// This method can't be a constructor because it would need to
|
||||
/// assign itself to the graph before being fully constructed.
|
||||
/// </remarks>
|
||||
public static WeightedMaskLayerList Create(Animator animator, int layerCount)
|
||||
{
|
||||
var graph = new AnimancerGraph();
|
||||
var layers = new WeightedMaskLayerList(graph, animator, layerCount);
|
||||
graph.Layers = layers;
|
||||
return layers;
|
||||
}
|
||||
|
||||
/// <summary>Creates a new <see cref="WeightedMaskLayerList"/>.</summary>
|
||||
public WeightedMaskLayerList(AnimancerGraph graph, Animator animator, int layerCount)
|
||||
: base(graph, layerCount)
|
||||
{
|
||||
if (layerCount < 2)
|
||||
throw new ArgumentOutOfRangeException(
|
||||
nameof(layerCount),
|
||||
"Layer count must be at least 2 (Base + 1).");
|
||||
|
||||
graph.Layers = this;
|
||||
|
||||
Bones = animator.GetComponentsInChildren<Transform>(true);
|
||||
|
||||
var boneCount = BoneCount;
|
||||
|
||||
_Job = new WeightedMaskMixerJob()
|
||||
{
|
||||
boneTransforms = new(boneCount, Allocator.Persistent, NativeArrayOptions.UninitializedMemory),
|
||||
boneWeights = new(boneCount * (layerCount - 1), Allocator.Persistent, NativeArrayOptions.ClearMemory),
|
||||
layerCount = layerCount,
|
||||
rootMotionWeights = new(layerCount, Allocator.Persistent, NativeArrayOptions.UninitializedMemory),
|
||||
};
|
||||
|
||||
graph.Disposables.Add(this);
|
||||
|
||||
for (var i = 0; i < layerCount; i++)
|
||||
_Job.rootMotionWeights[i] = 1;
|
||||
|
||||
for (var i = 0; i < boneCount; i++)
|
||||
{
|
||||
_Job.boneTransforms[i] = animator.BindStreamTransform(Bones[i + 1]);
|
||||
_Job.boneWeights[i] = 1;
|
||||
}
|
||||
|
||||
var playable = AnimationScriptPlayable.Create(graph, _Job, Capacity);
|
||||
playable.SetProcessInputs(false);
|
||||
Playable = playable;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
void IDisposable.Dispose()
|
||||
{
|
||||
_Job.rootMotionWeights.Dispose();
|
||||
_Job.boneTransforms.Dispose();
|
||||
_Job.boneWeights.Dispose();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override AnimancerLayer Add()
|
||||
{
|
||||
if (Count >= Capacity)
|
||||
throw new InvalidOperationException(
|
||||
$"{nameof(WeightedMaskLayerList)} doesn't support dynamically changing its layer count.");
|
||||
|
||||
return base.Add();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 48650aff8ff11364a9c5044d1722636f
|
||||
timeCreated: 1515060256
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,264 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
using System;
|
||||
using Unity.Collections;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Animancer
|
||||
{
|
||||
/// <summary>
|
||||
/// Replaces the default <see cref="AnimancerLayerMixerList"/>
|
||||
/// with a <see cref="WeightedMaskLayerList"/>.
|
||||
/// </summary>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer/WeightedMaskLayers
|
||||
[AddComponentMenu(Strings.MenuPrefix + "Weighted Mask Layers")]
|
||||
[AnimancerHelpUrl(typeof(WeightedMaskLayers))]
|
||||
[DefaultExecutionOrder(-10000)]// Awake before anything else initializes Animancer.
|
||||
public class WeightedMaskLayers : MonoBehaviour
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
[SerializeField] private AnimancerComponent _Animancer;
|
||||
|
||||
/// <summary>[<see cref="SerializeField"/>] The component to apply the layers to.</summary>
|
||||
public AnimancerComponent Animancer
|
||||
=> _Animancer;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
[SerializeField] private WeightedMaskLayersDefinition _Definition;
|
||||
|
||||
/// <summary>[<see cref="SerializeField"/>]
|
||||
/// The definition of transforms to control and weights to apply to them.
|
||||
/// </summary>
|
||||
public ref WeightedMaskLayersDefinition Definition
|
||||
=> ref _Definition;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
[SerializeField] private int _LayerCount = 2;
|
||||
|
||||
/// <summary>[<see cref="SerializeField"/>] The number of layers (minimum 2).</summary>
|
||||
public ref int LayerCount
|
||||
=> ref _LayerCount;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>The layer list created at runtime and assigned to <see cref="AnimancerGraph.Layers"/>.</summary>
|
||||
public WeightedMaskLayerList Layers { get; protected set; }
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>The index of each of the <see cref="WeightedMaskLayersDefinition.Transforms"/>.</summary>
|
||||
public int[] Indices { get; protected set; }
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Finds the <see cref="Animancer"/> reference if it was missing.</summary>
|
||||
protected virtual void OnValidate()
|
||||
{
|
||||
gameObject.GetComponentInParentOrChildren(ref _Animancer);
|
||||
|
||||
if (LayerCount < 2)
|
||||
LayerCount = 2;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Initializes the <see cref="Layers"/> and applies the default group weights.</summary>
|
||||
protected virtual void Awake()
|
||||
{
|
||||
if (Definition == null ||
|
||||
!Definition.IsValid)
|
||||
return;
|
||||
|
||||
if (_Animancer == null)
|
||||
TryGetComponent(out _Animancer);
|
||||
|
||||
Layers = WeightedMaskLayerList.Create(_Animancer.Animator, LayerCount);
|
||||
_Animancer.InitializeGraph(Layers.Graph);
|
||||
|
||||
Indices = Definition.CalculateIndices(Layers);
|
||||
|
||||
for (int i = 1; i < LayerCount; i++)// Start at 1.
|
||||
SetWeights(i, 0);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Applies the weights of the specified group to the specified layer.</summary>
|
||||
public void SetWeights(int layerIndex, int groupIndex)
|
||||
{
|
||||
Definition.AssertGroupIndex(groupIndex);
|
||||
|
||||
var boneWeights = Layers.BoneWeights;
|
||||
var definitionWeights = Definition.Weights;
|
||||
|
||||
var layerIndexOffset = (layerIndex - 1) * Layers.BoneCount;
|
||||
var groupDefinitionStart = groupIndex * Indices.Length;
|
||||
|
||||
for (int i = 0; i < Indices.Length; i++)
|
||||
{
|
||||
var index = Indices[i];
|
||||
if (index < 0)
|
||||
continue;
|
||||
|
||||
var weight = definitionWeights[groupDefinitionStart + i];
|
||||
boneWeights[layerIndexOffset + index] = weight;
|
||||
}
|
||||
|
||||
var rootMotionWeights = Layers.RootMotionWeights;
|
||||
rootMotionWeights[layerIndex] = Definition.RootMotionWeights[groupIndex];
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private Fade _Fade;
|
||||
|
||||
/// <summary>Fades the weights towards the specified group.</summary>
|
||||
public void FadeWeights(
|
||||
int layerIndex,
|
||||
int groupIndex,
|
||||
float fadeDuration,
|
||||
Func<float, float> easing = null)
|
||||
{
|
||||
if (fadeDuration > 0)
|
||||
{
|
||||
_Fade ??= new();
|
||||
_Fade.Start(this, layerIndex, groupIndex, fadeDuration, easing);
|
||||
}
|
||||
else
|
||||
{
|
||||
SetWeights(layerIndex, groupIndex);
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>An <see cref="IUpdatable"/> which fades <see cref="WeightedMaskLayers"/> over time.</summary>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer/Fade
|
||||
public class Fade : Updatable
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private NativeArray<float> _CurrentWeights;
|
||||
private NativeArray<float> _CurrentRootMotionWeights;
|
||||
private float[] _OriginalWeights;
|
||||
private WeightedMaskLayers _Layers;
|
||||
private int _LayerIndex;
|
||||
private int _LayerIndexOffset;
|
||||
private int _TargetWeightIndex;
|
||||
private float _OriginalRootMotionWeight;
|
||||
private float _TargetRootMotionWeight;
|
||||
private Func<float, float> _Easing;
|
||||
|
||||
/// <summary>The amount of time that has passed since the start of this fade (in seconds).</summary>
|
||||
public float ElapsedTime;
|
||||
|
||||
/// <summary>The total amount of time this fade will take (in seconds).</summary>
|
||||
public float Duration;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Initializes this fade and registers it to receive updates.</summary>
|
||||
public void Start(
|
||||
WeightedMaskLayers layers,
|
||||
int layerIndex,
|
||||
int groupIndex,
|
||||
float duration,
|
||||
Func<float, float> easing = null)
|
||||
{
|
||||
layers.Definition.AssertGroupIndex(groupIndex);
|
||||
|
||||
_CurrentWeights = layers.Layers.BoneWeights;
|
||||
_CurrentRootMotionWeights = layers.Layers.RootMotionWeights;
|
||||
_OriginalRootMotionWeight = _CurrentRootMotionWeights[layerIndex];
|
||||
_TargetRootMotionWeight = layers.Definition.RootMotionWeights[groupIndex];
|
||||
_Easing = easing;
|
||||
_Layers = layers;
|
||||
_LayerIndex = layerIndex;
|
||||
_TargetWeightIndex = layers.Definition.IndexOf(groupIndex, 0);
|
||||
Duration = duration;
|
||||
|
||||
_LayerIndexOffset = (layerIndex - 1) * layers.Layers.BoneCount;
|
||||
|
||||
var indices = _Layers.Indices;
|
||||
AnimancerUtilities.SetLength(ref _OriginalWeights, indices.Length);
|
||||
for (int i = 0; i < indices.Length; i++)
|
||||
{
|
||||
var index = _LayerIndexOffset + indices[i];
|
||||
_OriginalWeights[i] = _CurrentWeights[index];
|
||||
}
|
||||
|
||||
ElapsedTime = 0;
|
||||
|
||||
layers.Layers.Graph.RequirePreUpdate(this);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Update()
|
||||
{
|
||||
ElapsedTime += AnimancerGraph.DeltaTime;
|
||||
if (ElapsedTime < Duration)
|
||||
{
|
||||
ApplyFade(ElapsedTime / Duration);
|
||||
}
|
||||
else
|
||||
{
|
||||
ApplyTargetWeights();
|
||||
|
||||
AnimancerGraph.Current.CancelPreUpdate(this);
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Recalculates the weights by interpolating based on `t`.</summary>
|
||||
private void ApplyFade(float t)
|
||||
{
|
||||
if (_Easing != null)
|
||||
t = _Easing(t);
|
||||
|
||||
var targetWeights = _Layers.Definition.Weights;
|
||||
var indices = _Layers.Indices;
|
||||
var boneWeights = _CurrentWeights;
|
||||
|
||||
for (int i = 0; i < indices.Length; i++)
|
||||
{
|
||||
var index = _LayerIndexOffset + indices[i];
|
||||
var from = _OriginalWeights[i];
|
||||
var to = targetWeights[_TargetWeightIndex + i];
|
||||
boneWeights[index] = Mathf.LerpUnclamped(from, to, t);
|
||||
}
|
||||
|
||||
_CurrentRootMotionWeights[_LayerIndex] = Mathf.LerpUnclamped(
|
||||
_OriginalRootMotionWeight,
|
||||
_TargetRootMotionWeight,
|
||||
t);
|
||||
}
|
||||
|
||||
/// <summary>Recalculates the target weights.</summary>
|
||||
private void ApplyTargetWeights()
|
||||
{
|
||||
var targetWeights = _Layers.Definition.Weights;
|
||||
var indices = _Layers.Indices;
|
||||
var boneWeights = _CurrentWeights;
|
||||
|
||||
for (int i = 0; i < indices.Length; i++)
|
||||
{
|
||||
var index = _LayerIndexOffset + indices[i];
|
||||
var to = targetWeights[_TargetWeightIndex + i];
|
||||
boneWeights[index] = to;
|
||||
}
|
||||
|
||||
_CurrentRootMotionWeights[_LayerIndex] = _TargetRootMotionWeight;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 005fb639ab942fa46adf4e9a009744d2
|
||||
timeCreated: 1515060256
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,386 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Animancer
|
||||
{
|
||||
/// <summary>Serializable data which defines how to control a <see cref="WeightedMaskLayerList"/>.</summary>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer/WeightedMaskLayersDefinition
|
||||
[Serializable]
|
||||
public class WeightedMaskLayersDefinition :
|
||||
ICopyable<WeightedMaskLayersDefinition>,
|
||||
IEquatable<WeightedMaskLayersDefinition>
|
||||
#if UNITY_EDITOR
|
||||
, ISerializationCallbackReceiver
|
||||
#endif
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>The name of the serialized backing field of <see cref="Transforms"/>.</summary>
|
||||
public const string
|
||||
TransformsField = nameof(_Transforms);
|
||||
|
||||
[SerializeField]
|
||||
private Transform[] _Transforms;
|
||||
|
||||
/// <summary><see cref="Transform"/>s being controlled by this definition.</summary>
|
||||
public ref Transform[] Transforms
|
||||
=> ref _Transforms;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>The name of the serialized backing field of <see cref="Weights"/>.</summary>
|
||||
public const string
|
||||
WeightsField = nameof(_Weights);
|
||||
|
||||
[SerializeField]
|
||||
private float[] _Weights;
|
||||
|
||||
/// <summary>Groups of weights which will be applied to the <see cref="Transforms"/>.</summary>
|
||||
/// <remarks>
|
||||
/// This is a flattened 2D array containing groups of target weights corresponding to the transforms.
|
||||
/// With n transforms, indices 0 to n-1 are Group 0, n to n*2-1 are Group 1, etc.
|
||||
/// </remarks>
|
||||
public ref float[] Weights
|
||||
=> ref _Weights;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
[SerializeField]
|
||||
private float[] _RootMotionWeights;
|
||||
|
||||
/// <summary>Each group has a multiplier for the Root Motion output of any layer the group is applied to.</summary>
|
||||
public ref float[] RootMotionWeights
|
||||
=> ref _RootMotionWeights;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>The number of weight groups in this definition.</summary>
|
||||
public int GroupCount
|
||||
{
|
||||
get => _Transforms == null || _Transforms.Length == 0 || _Weights == null
|
||||
? 0
|
||||
: _Weights.Length / _Transforms.Length;
|
||||
set
|
||||
{
|
||||
if (_Transforms != null && value > 0)
|
||||
{
|
||||
Array.Resize(ref _Weights, _Transforms.Length * value);
|
||||
Array.Resize(ref _RootMotionWeights, value);
|
||||
}
|
||||
else
|
||||
{
|
||||
_Weights = Array.Empty<float>();
|
||||
_RootMotionWeights = Array.Empty<float>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>[Assert-Conditional] Asserts that the `groupIndex` is valid.</summary>
|
||||
[System.Diagnostics.Conditional(Strings.Assertions)]
|
||||
public void AssertGroupIndex(int groupIndex)
|
||||
{
|
||||
if ((uint)groupIndex >= (uint)GroupCount)
|
||||
throw new ArgumentOutOfRangeException(
|
||||
nameof(groupIndex),
|
||||
groupIndex,
|
||||
$"Must be 0 <= {nameof(groupIndex)} < Group Count ({GroupCount})");
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Calculates the index of each of the <see cref="Transforms"/>.</summary>
|
||||
public int[] CalculateIndices(WeightedMaskLayerList layers)
|
||||
{
|
||||
var indices = new int[_Transforms.Length];
|
||||
|
||||
for (int i = 0; i < _Transforms.Length; i++)
|
||||
{
|
||||
indices[i] = layers.IndexOf(_Transforms[i]);
|
||||
#if UNITY_ASSERTIONS
|
||||
if (indices[i] < 0)
|
||||
Debug.LogWarning(
|
||||
$"Unable to find index of {_Transforms[i]} in {nameof(WeightedMaskLayerList)}",
|
||||
_Transforms[i]);
|
||||
#endif
|
||||
}
|
||||
|
||||
return indices;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// Adds the `transform` at the specified `index`
|
||||
/// along with any associated <see cref="_Weights"/>.
|
||||
/// </summary>
|
||||
public void AddTransform(Transform transform)
|
||||
{
|
||||
var index = _Transforms.Length;
|
||||
|
||||
AnimancerUtilities.InsertAt(ref _Transforms, index, transform);
|
||||
|
||||
if (_Transforms.Length == 1 && _Weights.IsNullOrEmpty())
|
||||
{
|
||||
_Weights = new float[1];
|
||||
return;
|
||||
}
|
||||
|
||||
while (index <= _Weights.Length)
|
||||
{
|
||||
AnimancerUtilities.InsertAt(ref _Weights, index, 0);
|
||||
|
||||
index += _Transforms.Length;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes the `index` from the <see cref="_Transforms"/>
|
||||
/// along with any associated <see cref="_Weights"/>.
|
||||
/// </summary>
|
||||
public void RemoveTransform(int index)
|
||||
{
|
||||
AnimancerUtilities.RemoveAt(ref _Transforms, index);
|
||||
|
||||
while (index < _Weights.Length)
|
||||
{
|
||||
AnimancerUtilities.RemoveAt(ref _Weights, index);
|
||||
|
||||
index += _Transforms.Length;
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Calculates the index in the <see cref="Weights"/> corresponding to the specified values.</summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public int IndexOfGroup(int groupIndex)
|
||||
=> groupIndex * _Transforms.Length;
|
||||
|
||||
/// <summary>Calculates the index in the <see cref="Weights"/> corresponding to the specified values.</summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public int IndexOf(int groupIndex, int transformIndex)
|
||||
=> groupIndex * _Transforms.Length + transformIndex;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Gets the specified weight.</summary>
|
||||
/// <remarks>Returns <see cref="float.NaN"/> if the indices are outside the <see cref="Weights"/>.</remarks>
|
||||
public float GetWeight(int groupIndex, int transformIndex)
|
||||
{
|
||||
if (Weights == null)
|
||||
return float.NaN;
|
||||
|
||||
var index = IndexOf(groupIndex, transformIndex);
|
||||
return (uint)index < (uint)Weights.Length
|
||||
? Weights[index]
|
||||
: float.NaN;
|
||||
}
|
||||
|
||||
/// <summary>Sets the specified weight.</summary>
|
||||
/// <remarks>Returns false if the indices are outside the <see cref="Weights"/>.</remarks>
|
||||
public bool SetWeight(int groupIndex, int transformIndex, float value)
|
||||
{
|
||||
if (Weights == null)
|
||||
return false;
|
||||
|
||||
var index = IndexOf(groupIndex, transformIndex);
|
||||
if ((uint)index < (uint)Weights.Length)
|
||||
{
|
||||
Weights[index] = value;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Gets the specified RootMotion weight.</summary>
|
||||
/// <remarks>Returns <see cref="float.NaN"/> if the indices are outside the <see cref="RootMotionWeights"/>.</remarks>
|
||||
public float GetRmWeight(int groupIndex)
|
||||
{
|
||||
if (RootMotionWeights == null)
|
||||
return float.NaN;
|
||||
|
||||
return (uint)groupIndex < (uint)RootMotionWeights.Length
|
||||
? RootMotionWeights[groupIndex]
|
||||
: float.NaN;
|
||||
}
|
||||
|
||||
/// <summary>Sets the specified RootMotion weight.</summary>
|
||||
/// <remarks>Returns false if the indices are outside the <see cref="RootMotionWeights"/>.</remarks>
|
||||
public bool SetRmWeight(int groupIndex, float value)
|
||||
{
|
||||
if (RootMotionWeights == null)
|
||||
return false;
|
||||
|
||||
if ((uint)groupIndex < (uint)RootMotionWeights.Length)
|
||||
{
|
||||
RootMotionWeights[groupIndex] = value;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void CopyFrom(WeightedMaskLayersDefinition copyFrom, CloneContext context)
|
||||
{
|
||||
AnimancerUtilities.CopyExactArray(copyFrom._Transforms, ref _Transforms);
|
||||
AnimancerUtilities.CopyExactArray(copyFrom._Weights, ref _Weights);
|
||||
AnimancerUtilities.CopyExactArray(copyFrom._RootMotionWeights, ref _RootMotionWeights);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Does this definition contain valid data?</summary>
|
||||
public bool IsValid
|
||||
=> !_Transforms.IsNullOrEmpty()
|
||||
&& _Weights != null && _Weights.Length >= _Transforms.Length
|
||||
&& _RootMotionWeights != null && _RootMotionWeights.Length == GroupCount;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Ensures that the data in this definition is value.</summary>
|
||||
public void Validate()
|
||||
{
|
||||
ValidateArraySizes();
|
||||
RemoveMissingAndDuplicate();
|
||||
}
|
||||
|
||||
/// <summary>Ensures that all the arrays have valid sizes.</summary>
|
||||
public void ValidateArraySizes()
|
||||
{
|
||||
if (_Transforms.IsNullOrEmpty())
|
||||
{
|
||||
_Transforms = Array.Empty<Transform>();
|
||||
_Weights = Array.Empty<float>();
|
||||
_RootMotionWeights = Array.Empty<float>();
|
||||
}
|
||||
|
||||
if (_Weights == null ||
|
||||
_Weights.Length < _Transforms.Length)
|
||||
{
|
||||
AnimancerUtilities.SetLength(ref _Weights, _Transforms.Length);
|
||||
}
|
||||
else
|
||||
{
|
||||
var expectedWeightCount = (int)Math.Ceiling(_Weights.Length / (double)_Transforms.Length);
|
||||
expectedWeightCount *= _Transforms.Length;
|
||||
AnimancerUtilities.SetLength(ref _Weights, expectedWeightCount);
|
||||
}
|
||||
|
||||
var rootMotionWeightCount = _RootMotionWeights == null
|
||||
? 0
|
||||
: _RootMotionWeights.Length;
|
||||
var groupCount = GroupCount;
|
||||
if (rootMotionWeightCount != groupCount)
|
||||
{
|
||||
AnimancerUtilities.SetLength(ref _RootMotionWeights, groupCount);
|
||||
for (int i = rootMotionWeightCount; i < groupCount; i++)
|
||||
_RootMotionWeights[i] = 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Removes any missing or identical <see cref="_Transforms"/>.</summary>
|
||||
public bool RemoveMissingAndDuplicate()
|
||||
{
|
||||
var removedAny = false;
|
||||
|
||||
for (int i = 0; i < _Transforms.Length; i++)
|
||||
{
|
||||
var transform = _Transforms[i];
|
||||
if (transform == null)
|
||||
{
|
||||
RemoveTransform(i);
|
||||
removedAny = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
var nextIndex = i + 1;
|
||||
|
||||
RemoveDuplicates:
|
||||
|
||||
nextIndex = Array.IndexOf(_Transforms, transform, nextIndex);
|
||||
if (nextIndex > i)
|
||||
{
|
||||
RemoveTransform(nextIndex);
|
||||
removedAny = true;
|
||||
goto RemoveDuplicates;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return removedAny;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#if UNITY_EDITOR
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
void ISerializationCallbackReceiver.OnBeforeSerialize()
|
||||
=> Validate();
|
||||
|
||||
/// <inheritdoc/>
|
||||
void ISerializationCallbackReceiver.OnAfterDeserialize()
|
||||
=> Validate();
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endif
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Returns a summary of this definition.</summary>
|
||||
public override string ToString()
|
||||
=> $"{nameof(WeightedMaskLayersDefinition)}(" +
|
||||
$"{nameof(Transforms)}={(Transforms != null ? Transforms.Length : 0)}, " +
|
||||
$"{nameof(Weights)}={(Weights != null ? Weights.Length : 0)}, " +
|
||||
$"{nameof(RootMotionWeights)}={(RootMotionWeights != null ? RootMotionWeights.Length : 0)})";
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#region Equality
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Are all fields in this object equal to the equivalent in `obj`?</summary>
|
||||
public override bool Equals(object obj)
|
||||
=> Equals(obj as WeightedMaskLayersDefinition);
|
||||
|
||||
/// <summary>Are all fields in this object equal to the equivalent fields in `other`?</summary>
|
||||
public bool Equals(WeightedMaskLayersDefinition other)
|
||||
=> other != null
|
||||
&& AnimancerUtilities.ContentsAreEqual(_Transforms, other._Transforms)
|
||||
&& AnimancerUtilities.ContentsAreEqual(_Weights, other._Weights)
|
||||
&& AnimancerUtilities.ContentsAreEqual(_RootMotionWeights, other._RootMotionWeights);
|
||||
|
||||
/// <summary>Are all fields in `a` equal to the equivalent fields in `b`?</summary>
|
||||
public static bool operator ==(WeightedMaskLayersDefinition a, WeightedMaskLayersDefinition b)
|
||||
=> a is null
|
||||
? b is null
|
||||
: a.Equals(b);
|
||||
|
||||
/// <summary>Are any fields in `a` not equal to the equivalent fields in `b`?</summary>
|
||||
public static bool operator !=(WeightedMaskLayersDefinition a, WeightedMaskLayersDefinition b)
|
||||
=> !(a == b);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Returns a hash code based on the values of this object's fields.</summary>
|
||||
public override int GetHashCode()
|
||||
=> AnimancerUtilities.Hash(-871379578,
|
||||
_Transforms.SafeGetHashCode(),
|
||||
_Weights.SafeGetHashCode(),
|
||||
_RootMotionWeights.SafeGetHashCode());
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endregion
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c9269482d6edd6349972960b7bc4b82f
|
||||
timeCreated: 1515060256
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,199 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
using Unity.Collections;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Animations;
|
||||
|
||||
#if UNITY_BURST
|
||||
using Unity.Burst;
|
||||
#endif
|
||||
|
||||
namespace Animancer
|
||||
{
|
||||
/// <summary>
|
||||
/// An <see cref="IAnimationJob"/> which mixes its inputs based on individual <see cref="boneWeights"/>.
|
||||
/// </summary>
|
||||
#if UNITY_BURST
|
||||
[BurstCompile(FloatPrecision.Low, FloatMode.Fast
|
||||
#if UNITY_BURST_1_6_0
|
||||
, OptimizeFor = OptimizeFor.Performance
|
||||
#endif
|
||||
)]
|
||||
#endif
|
||||
public struct WeightedMaskMixerJob : IAnimationJob
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>The number of layers being mixed.</summary>
|
||||
public int layerCount;
|
||||
|
||||
/// <summary>The root motion weight of each layer.</summary>
|
||||
public NativeArray<float> rootMotionWeights;
|
||||
|
||||
/// <summary>The handles for each bone being mixed.</summary>
|
||||
/// <remarks>
|
||||
/// All animated bones must be included,
|
||||
/// even if their individual weight isn't modified.
|
||||
/// </remarks>
|
||||
public NativeArray<TransformStreamHandle> boneTransforms;
|
||||
|
||||
/// <summary>The blend weight of each bone.</summary>
|
||||
/// <remarks>
|
||||
/// This array corresponds to the <see cref="boneTransforms"/>,
|
||||
/// repeated for each layer after the first and excluding the base layer.
|
||||
/// For example, if there are 3 layers and 10 bones, then this array will have 20 elements
|
||||
/// with the first 10 being for Layer 1 and the next 10 being for Layer 2.
|
||||
/// </remarks>
|
||||
public NativeArray<float> boneWeights;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
readonly void IAnimationJob.ProcessRootMotion(AnimationStream output)
|
||||
{
|
||||
var input = output.GetInputStream(0);
|
||||
|
||||
var velocity = input.velocity;
|
||||
var angularVelocity = input.angularVelocity;
|
||||
|
||||
var hasRootMotionWeights = rootMotionWeights.IsCreated;
|
||||
if (hasRootMotionWeights)
|
||||
{
|
||||
var baseLayerWeight = rootMotionWeights[0];
|
||||
velocity *= baseLayerWeight;
|
||||
angularVelocity *= baseLayerWeight;
|
||||
}
|
||||
|
||||
for (int i = 1; i < layerCount; i++)// Start at 1.
|
||||
{
|
||||
input = output.GetInputStream(i);
|
||||
if (!input.isValid)
|
||||
continue;
|
||||
|
||||
var layerWeight = output.GetInputWeight(i);
|
||||
if (hasRootMotionWeights)
|
||||
layerWeight *= rootMotionWeights[i];
|
||||
velocity = Vector3.LerpUnclamped(
|
||||
velocity,
|
||||
input.velocity,
|
||||
layerWeight);
|
||||
angularVelocity = Vector3.LerpUnclamped(
|
||||
angularVelocity,
|
||||
input.angularVelocity,
|
||||
layerWeight);
|
||||
}
|
||||
|
||||
output.velocity = velocity;
|
||||
output.angularVelocity = angularVelocity;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
readonly void IAnimationJob.ProcessAnimation(AnimationStream output)
|
||||
{
|
||||
if (layerCount == 2)
|
||||
{
|
||||
ProcessAnimation2Layers(output);
|
||||
return;
|
||||
}
|
||||
|
||||
// Blending more than 2 layers is less efficient because we need to use these temporary arrays.
|
||||
|
||||
var transformCount = boneTransforms.Length;
|
||||
var localPositions = new NativeArray<Vector3>(transformCount, Allocator.Temp, NativeArrayOptions.UninitializedMemory);
|
||||
var localRotations = new NativeArray<Quaternion>(transformCount, Allocator.Temp, NativeArrayOptions.UninitializedMemory);
|
||||
|
||||
var input = output.GetInputStream(0);
|
||||
|
||||
for (var i = 0; i < transformCount; i++)
|
||||
{
|
||||
var transform = boneTransforms[i];
|
||||
localPositions[i] = transform.GetLocalPosition(input);
|
||||
localRotations[i] = transform.GetLocalRotation(input);
|
||||
}
|
||||
|
||||
for (int iLayer = 1; iLayer < layerCount; iLayer++)// Start at 1.
|
||||
{
|
||||
input = output.GetInputStream(iLayer);
|
||||
if (!input.isValid)
|
||||
break;
|
||||
|
||||
var layerWeight = output.GetInputWeight(iLayer);
|
||||
if (layerWeight == 0)
|
||||
continue;
|
||||
|
||||
var weightOffset = (iLayer - 1) * transformCount;
|
||||
|
||||
for (var iTransform = 0; iTransform < transformCount; iTransform++)
|
||||
{
|
||||
var transform = boneTransforms[iTransform];
|
||||
var weight = layerWeight * boneWeights[weightOffset + iTransform];
|
||||
if (weight == 0)
|
||||
continue;
|
||||
|
||||
localPositions[iTransform] = Vector3.LerpUnclamped(
|
||||
localPositions[iTransform],
|
||||
transform.GetLocalPosition(input),
|
||||
weight);
|
||||
|
||||
localRotations[iTransform] = Quaternion.SlerpUnclamped(
|
||||
localRotations[iTransform],
|
||||
transform.GetLocalRotation(input),
|
||||
weight);
|
||||
}
|
||||
}
|
||||
|
||||
for (var i = 0; i < transformCount; i++)
|
||||
{
|
||||
var transform = boneTransforms[i];
|
||||
transform.SetLocalPosition(output, localPositions[i]);
|
||||
transform.SetLocalRotation(output, localRotations[i]);
|
||||
}
|
||||
|
||||
localPositions.Dispose();
|
||||
localRotations.Dispose();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Blends the layers in an optimized way when there are only 2.</summary>
|
||||
private readonly void ProcessAnimation2Layers(AnimationStream output)
|
||||
{
|
||||
var input0 = output.GetInputStream(0);
|
||||
var input1 = output.GetInputStream(1);
|
||||
|
||||
if (input1.isValid)
|
||||
{
|
||||
var layerWeight = output.GetInputWeight(1);
|
||||
var transformCount = boneTransforms.Length;
|
||||
for (var i = 0; i < transformCount; i++)
|
||||
{
|
||||
var transform = boneTransforms[i];
|
||||
var weight = layerWeight * boneWeights[i];
|
||||
|
||||
var position0 = transform.GetLocalPosition(input0);
|
||||
var position1 = transform.GetLocalPosition(input1);
|
||||
transform.SetLocalPosition(output, Vector3.LerpUnclamped(position0, position1, weight));
|
||||
|
||||
var rotation0 = transform.GetLocalRotation(input0);
|
||||
var rotation1 = transform.GetLocalRotation(input1);
|
||||
transform.SetLocalRotation(output, Quaternion.SlerpUnclamped(rotation0, rotation1, weight));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var transformCount = boneTransforms.Length;
|
||||
for (var i = 0; i < transformCount; i++)
|
||||
{
|
||||
var transform = boneTransforms[i];
|
||||
transform.SetLocalPosition(output, transform.GetLocalPosition(input0));
|
||||
transform.SetLocalRotation(output, transform.GetLocalRotation(input0));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bf71afa335d45144c8e1513206b7c473
|
||||
timeCreated: 1515060256
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3c7bab446ace0ae4bb75f4027e27d859
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
119
Packages/com.kybernetik.animancer/Runtime/Data Types/FadeMode.cs
Normal file
119
Packages/com.kybernetik.animancer/Runtime/Data Types/FadeMode.cs
Normal file
@@ -0,0 +1,119 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
using System;
|
||||
|
||||
namespace Animancer
|
||||
{
|
||||
/// <summary>Determines how <see cref="AnimancerLayer.Play(AnimancerState, float, FadeMode)"/> works.</summary>
|
||||
///
|
||||
/// <remarks>
|
||||
/// <strong>Documentation:</strong>
|
||||
/// <see href="https://kybernetik.com.au/animancer/docs/manual/blending/fading/modes">
|
||||
/// Fade Modes</see>
|
||||
/// <para></para>
|
||||
/// <strong>Sample:</strong>
|
||||
/// <see href="https://kybernetik.com.au/animancer/docs/samples/basics/transitions">
|
||||
/// Transitions</see>
|
||||
/// </remarks>
|
||||
///
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer/FadeMode
|
||||
///
|
||||
public enum FadeMode
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// Calculate the fade speed to bring the <see cref="AnimancerNode.Weight"/>
|
||||
/// from 0 to 1 over the specified fade duration (in seconds),
|
||||
/// regardless of the actual starting weight.
|
||||
/// </summary>
|
||||
///
|
||||
/// <remarks>
|
||||
/// <strong>Example:</strong>
|
||||
/// A fade duration of 0.5 would make the fade last for 0.5 seconds, regardless of how long the animation is.
|
||||
/// <para></para>
|
||||
/// This is generally the same as <see cref="FixedDuration"/> but differs when starting the fade from a
|
||||
/// non-zero <see cref="AnimancerNode.Weight"/>, for example:
|
||||
/// <list type="bullet">
|
||||
/// <item>Fade Duration: 0.25</item>
|
||||
/// <item>To fade from 0 to 1 with either mode would get a speed of 4 and take 0.25 seconds</item>
|
||||
/// <item>To fade from 0.5 to 1 with <see cref="FixedDuration"/> would get a speed of 2 and take 0.25 seconds.
|
||||
/// It has half the distance to cover so it goes half as fast to maintain the expected duration.</item>
|
||||
/// <item>To fade from 0.5 to 1 with <see cref="FixedSpeed"/> would get a speed of 4 and take 0.125 seconds.
|
||||
/// It gets the same speed regardless of the distance to cover, so with less distance it completes faster.</item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
///
|
||||
/// <exception cref="InvalidOperationException">The <see cref="AnimancerState.Clip"/> is null.</exception>
|
||||
///
|
||||
/// <exception cref="ArgumentOutOfRangeException">
|
||||
/// More states have been created for the <see cref="AnimancerState.Clip"/> than the
|
||||
/// <see cref="AnimancerLayer.MaxCloneCount"/> allows.
|
||||
/// </exception>
|
||||
FixedSpeed,
|
||||
|
||||
/// <summary>
|
||||
/// Calculate the fade speed to bring the <see cref="AnimancerNode.Weight"/> to the target value over the
|
||||
/// specified fade duration (in seconds).
|
||||
/// </summary>
|
||||
///
|
||||
/// <remarks>
|
||||
/// <strong>Example:</strong>
|
||||
/// A fade duration of 0.5 would make the fade last for 0.5 seconds, regardless of how long the animation is.
|
||||
/// <para></para>
|
||||
/// This is generally the same as <see cref="FixedSpeed"/>, but differs when starting the fade from a
|
||||
/// non-zero <see cref="AnimancerNode.Weight"/>:
|
||||
/// <list type="bullet">
|
||||
/// <item>Fade Duration: 0.25</item>
|
||||
/// <item>To fade from 0 to 1 with either mode would get a speed of 4 and take 0.25 seconds</item>
|
||||
/// <item>To fade from 0.5 to 1 with <see cref="FixedDuration"/> would get a speed of 2 and take 0.25 seconds.
|
||||
/// It has half the distance to cover so it goes half as fast to maintain the expected duration.</item>
|
||||
/// <item>To fade from 0.5 to 1 with <see cref="FixedSpeed"/> would get a speed of 4 and take 0.125 seconds.
|
||||
/// It gets the same speed regardless of the distance to cover, so with less distance it completes faster.</item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
FixedDuration,
|
||||
|
||||
/// <summary>
|
||||
/// If the <see cref="AnimancerNode.Weight"/> is above the <see cref="AnimancerLayer.WeightlessThreshold"/>,
|
||||
/// this mode will use <see cref="AnimancerLayer.GetOrCreateWeightlessState"/> to get a copy of it that is at 0
|
||||
/// weight so it can fade the copy in while the original fades out with all other states. This allows an
|
||||
/// animation to fade into itself.
|
||||
/// </summary>
|
||||
///
|
||||
/// <remarks>
|
||||
/// This mode can be useful when you want to repeat an action while the previous animation is still fading out.
|
||||
/// For example, if you play an 'Attack' animation, it ends and starts fading back to 'Idle', and while it is
|
||||
/// doing so you want to start another 'Attack' with the same animation. The previous 'Attack' can't simply
|
||||
/// snap back to the start, so you can use this mode to create a second 'Attack' state to fade in while the old
|
||||
/// one fades out.
|
||||
/// <para></para>
|
||||
/// Using this mode repeatedly on subsequent frames will probably have undesirable effects because it will
|
||||
/// create a new state each time. In such a situation you most likely want <see cref="FixedSpeed"/> instead.
|
||||
/// <para></para>
|
||||
/// This mode only works for <see cref="ClipState"/>s.
|
||||
/// <para></para>
|
||||
/// The <see href="https://kybernetik.com.au/animancer/docs/manual/blending/fading/modes">Fade Modes</see> page
|
||||
/// explains this mode in more detail.
|
||||
/// </remarks>
|
||||
FromStart,
|
||||
|
||||
/// <summary>
|
||||
/// Like <see cref="FixedSpeed"/>, except that the fade duration is multiplied by the animation length.
|
||||
/// </summary>
|
||||
NormalizedSpeed,
|
||||
|
||||
/// <summary>
|
||||
/// Like <see cref="FixedDuration"/>, except that the fade duration is multiplied by the animation length.
|
||||
/// </summary>
|
||||
NormalizedDuration,
|
||||
|
||||
/// <summary>
|
||||
/// Like <see cref="FromStart"/>, except that the fade duration is multiplied by the animation length.
|
||||
/// </summary>
|
||||
NormalizedFromStart,
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b5a0e7e1cf8f4e74b8b43a8841174cc9
|
||||
timeCreated: 1515060256
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,265 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Animancer
|
||||
{
|
||||
/// <summary>
|
||||
/// An <see cref="IEnumerator{T}"/> for any <see cref="IList{T}"/>
|
||||
/// which doesn't bother checking if the target has been modified.
|
||||
/// This gives it good performance but also makes it slightly less safe to use.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This struct also implements <see cref="IEnumerable{T}"/>
|
||||
/// so it can be used in <c>foreach</c> statements and <see cref="IList{T}"/>
|
||||
/// to allow the target collection to be modified without breaking the enumerator
|
||||
/// (though doing so is still somewhat dangerous so use with care).
|
||||
/// <para></para>
|
||||
/// <strong>Example:</strong><code>
|
||||
/// var numbers = new int[] { 9, 8, 7, 6, 5, 4, 3, 2, 1, 0, };
|
||||
/// var count = 4;
|
||||
/// foreach (var number in new FastEnumerator<int>(numbers, count))
|
||||
/// {
|
||||
/// Debug.Log(number);
|
||||
/// }
|
||||
///
|
||||
/// // Log Output:
|
||||
/// // 9
|
||||
/// // 8
|
||||
/// // 7
|
||||
/// // 6
|
||||
/// </code></remarks>
|
||||
public struct FastEnumerator<T> : IReadOnlyList<T>, IEnumerator<T>
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>The target <see cref="IList{T}"/>.</summary>
|
||||
private readonly IList<T> List;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private int _Count;
|
||||
|
||||
/// <summary>[<see cref="ICollection{T}"/>]
|
||||
/// The number of items in the <see cref="List"/> (which can be less than the
|
||||
/// <see cref="ICollection{T}.Count"/> of the <see cref="List"/>).
|
||||
/// </summary>
|
||||
public int Count
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
readonly get => _Count;
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
set
|
||||
{
|
||||
AssertCount(value);
|
||||
_Count = value;
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private int _Index;
|
||||
|
||||
/// <summary>The position of the <see cref="Current"/> item in the <see cref="List"/>.</summary>
|
||||
public int Index
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
readonly get => _Index;
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
set
|
||||
{
|
||||
AssertIndex(value);
|
||||
_Index = value;
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>The item at the current <see cref="Index"/> in the <see cref="List"/>.</summary>
|
||||
public readonly T Current
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get
|
||||
{
|
||||
AssertCount(_Count);
|
||||
AssertIndex(_Index);
|
||||
return List[_Index];
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>The item at the current <see cref="Index"/> in the <see cref="List"/>.</summary>
|
||||
readonly object IEnumerator.Current
|
||||
=> Current;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Creates a new <see cref="FastEnumerator{T}"/>.</summary>
|
||||
/// <exception cref="NullReferenceException">
|
||||
/// The `list` is null. Use the <c>default</c> <see cref="FastEnumerator{T}"/> instead.
|
||||
/// </exception>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public FastEnumerator(IList<T> list)
|
||||
: this(list, list.Count)
|
||||
{ }
|
||||
|
||||
/// <summary>Creates a new <see cref="FastEnumerator{T}"/>.</summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public FastEnumerator(IList<T> list, int count)
|
||||
{
|
||||
List = list;
|
||||
_Count = count;
|
||||
_Index = -1;
|
||||
AssertCount(count);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Moves to the next item in the <see cref="List"/> and returns true if there is one.</summary>
|
||||
/// <remarks>At the end of the <see cref="List"/> the <see cref="Index"/> is set to <see cref="int.MinValue"/>.</remarks>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool MoveNext()
|
||||
{
|
||||
_Index++;
|
||||
if ((uint)_Index < (uint)_Count)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
_Index = int.MinValue;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Moves to the previous item in the <see cref="List"/> and returns true if there is one.</summary>
|
||||
/// <remarks>At the end of the <see cref="List"/> the <see cref="Index"/> is set to <c>-1</c>.</remarks>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool MovePrevious()
|
||||
{
|
||||
if (_Index > 0)
|
||||
{
|
||||
_Index--;
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
_Index = -1;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>[<see cref="IEnumerator"/>] Reverts this enumerator to the start of the <see cref="List"/>.</summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void Reset()
|
||||
{
|
||||
_Index = -1;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
readonly void IDisposable.Dispose() { }
|
||||
|
||||
/************************************************************************************************************************/
|
||||
// IEnumerator.
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Returns <c>this</c>.</summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public readonly FastEnumerator<T> GetEnumerator() => this;
|
||||
|
||||
/// <inheritdoc/>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
readonly IEnumerator<T> IEnumerable<T>.GetEnumerator() => this;
|
||||
|
||||
/// <inheritdoc/>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
readonly IEnumerator IEnumerable.GetEnumerator() => this;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
// IList.
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>[<see cref="IList{T}"/>] Returns the first index of the `item` in the <see cref="List"/>.</summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public readonly int IndexOf(T item)
|
||||
=> List.IndexOf(item);
|
||||
|
||||
/// <summary>[<see cref="IList{T}"/>] The item at the specified `index` in the <see cref="List"/>.</summary>
|
||||
public readonly T this[int index]
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get
|
||||
{
|
||||
AssertIndex(index);
|
||||
return List[index];
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
// ICollection.
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>[<see cref="ICollection{T}"/>] Does the <see cref="List"/> contain the `item`?</summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public readonly bool Contains(T item) => List.Contains(item);
|
||||
|
||||
/// <summary>[<see cref="ICollection{T}"/>] Copies the contents of the <see cref="List"/> into the `array`.</summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public readonly void CopyTo(T[] array, int arrayIndex)
|
||||
{
|
||||
for (int i = 0; i < _Count; i++)
|
||||
array[arrayIndex + i] = List[i];
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>[Assert-Only] Throws an exception unless 0 <= `index` < <see cref="Count"/>.</summary>
|
||||
/// <exception cref="ArgumentOutOfRangeException"/>
|
||||
[System.Diagnostics.Conditional(Strings.Assertions)]
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private readonly void AssertIndex(int index)
|
||||
{
|
||||
#if UNITY_ASSERTIONS
|
||||
if ((uint)index > (uint)_Count)
|
||||
throw new ArgumentOutOfRangeException(nameof(index),
|
||||
$"{nameof(FastEnumerator<T>)}.{nameof(Index)}" +
|
||||
$" must be within 0 <= {nameof(Index)} ({index}) < {nameof(Count)} ({_Count}).");
|
||||
#endif
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>[Assert-Only] Throws an exception unless 0 < `count` <= <see cref="ICollection{T}.Count"/>.</summary>
|
||||
/// <exception cref="ArgumentOutOfRangeException"/>
|
||||
[System.Diagnostics.Conditional(Strings.Assertions)]
|
||||
private readonly void AssertCount(int count)
|
||||
{
|
||||
#if UNITY_ASSERTIONS
|
||||
if (List == null)
|
||||
{
|
||||
if (count != 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(count),
|
||||
$"Must be 0 since the {nameof(List)} is null.");
|
||||
}
|
||||
else
|
||||
{
|
||||
if ((uint)count > (uint)List.Count)
|
||||
throw new ArgumentOutOfRangeException(nameof(count),
|
||||
$"Must be within 0 <= {nameof(count)} ({count}) < {nameof(List)}.{nameof(List.Count)} ({List.Count}).");
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 89db15324d7dd04468e475568b94623e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,623 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
//#define DEBUG_INDEXED_LISTS
|
||||
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Animancer
|
||||
{
|
||||
/// <summary>
|
||||
/// An <see cref="IReadOnlyList{T}"/> which can remove items in <c>O(1)</c> time without searching and an inbuilt
|
||||
/// enumerator which supports modifications at any time (including during enumeration).
|
||||
/// </summary>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer/IReadOnlyIndexedList_1
|
||||
public interface IReadOnlyIndexedList<T> : IReadOnlyList<T>
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>The number of items this list can contain before resizing is required.</summary>
|
||||
public int Capacity { get; set; }// Can't reduce the Count so it's safe for a Read-Only interface.
|
||||
|
||||
/// <summary>Is the `item` currently in this list?</summary>
|
||||
bool Contains(T item);
|
||||
|
||||
/// <summary>Is the `item` currently in this list at the specified `index`?</summary>
|
||||
bool Contains(T item, int index);
|
||||
|
||||
/// <summary>Copies all the items from this list into the `array`, starting at the specified `index`.</summary>
|
||||
void CopyTo(T[] array, int index);
|
||||
|
||||
/// <summary>Returns the index of the `item` in this list or <c>-1</c> if it's not in this list.</summary>
|
||||
int IndexOf(T item);
|
||||
|
||||
/// <summary>Returns a string describing this list and its contents.</summary>
|
||||
string DeepToString(string separator = "\n• ");
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
|
||||
/// <summary>An object which accesses the index of the items in an <see cref="IndexedList{TItem, TIndexer}"/>.</summary>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer/IIndexer_1
|
||||
public interface IIndexer<T>
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Returns the index of the `item`.</summary>
|
||||
/// <remarks>
|
||||
/// The index used by this method should be initialized at -1 and should not be modified by anything outside
|
||||
/// this indexer.
|
||||
/// </remarks>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
int GetIndex(T item);
|
||||
|
||||
/// <summary>Sets the index of the `item`.</summary>
|
||||
/// <remarks>
|
||||
/// The index used by this method should be initialized at -1 and should not be modified by anything outside
|
||||
/// this indexer.
|
||||
/// </remarks>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
void SetIndex(T item, int index);
|
||||
|
||||
/// <summary>Resets the index of the `item` to -1.</summary>
|
||||
/// <remarks>
|
||||
/// The index used by this method should be initialized at -1 and should not be modified by anything outside
|
||||
/// this indexer.
|
||||
/// </remarks>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
void ClearIndex(T item);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A <see cref="List{T}"/> which can remove items in <c>O(1)</c> time without searching and an inbuilt
|
||||
/// enumerator which supports modifications at any time (including during enumeration).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This implementation has several restrictions compared to a regular <see cref="List{T}"/>:
|
||||
/// <list type="bullet">
|
||||
/// <item>Items cannot be <c>null</c>.</item>
|
||||
/// <item>
|
||||
/// Items can only be in one <see cref="IndexedList{TItem, TIndexer}"/>
|
||||
/// at a time and cannot appear multiple times in it.
|
||||
/// </item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer/IndexedList_2
|
||||
public class IndexedList<TItem, TIndexer> :
|
||||
IList<TItem>,
|
||||
IReadOnlyIndexedList<TItem>,
|
||||
ICollection
|
||||
where TItem : class
|
||||
where TIndexer : IIndexer<TItem>
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
#region Fields and Accessors
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private const string
|
||||
SingleUse = "Each item can only be used in one " + nameof(IndexedList<TItem, TIndexer>) + " at a time.",
|
||||
NotFound = "The specified item does not exist in this " + nameof(IndexedList<TItem, TIndexer>) + ".";
|
||||
|
||||
/// <summary>The index which indicates that an item isn't in a list.</summary>
|
||||
public const int NotInList = -1;
|
||||
|
||||
/// <summary>The default <see cref="Capacity"/> which lists will expand to when their first item is added.</summary>
|
||||
public static int DefaultCapacity = 16;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>The <see cref="IIndexer{T}"/> used to access the details of items.</summary>
|
||||
public TIndexer Indexer;
|
||||
|
||||
private TItem[] _Items;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Creates a new <see cref="IndexedList{TItem, TIndexer}"/> using the default <see cref="List{T}"/> constructor.</summary>
|
||||
public IndexedList(TIndexer indexer = default)
|
||||
{
|
||||
Indexer = indexer;
|
||||
_Items = Array.Empty<TItem>();
|
||||
}
|
||||
|
||||
/// <summary>Creates a new <see cref="IndexedList{TItem, TIndexer}"/> with the specified initial `capacity`.</summary>
|
||||
public IndexedList(int capacity, TIndexer indexer = default)
|
||||
{
|
||||
Indexer = indexer;
|
||||
_Items = new TItem[capacity];
|
||||
}
|
||||
|
||||
// No copy constructor because the indices will not work if they are used in multiple lists at once.
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>The number of items currently in the list.</summary>
|
||||
public int Count { get; private set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public int Capacity
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get => _Items.Length;
|
||||
set
|
||||
{
|
||||
if (value < Count)
|
||||
throw new ArgumentOutOfRangeException(nameof(Count),
|
||||
$"{nameof(Capacity)} can't be less than {nameof(Count)}." +
|
||||
$" Excess items must be removed before the {nameof(Capacity)} can be reduced.");
|
||||
|
||||
Array.Resize(ref _Items, value);
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>The item at the specified `index`.</summary>
|
||||
/// <remarks>This indexer has <c>O(1)</c> complexity</remarks>
|
||||
/// <exception cref="ArgumentException">The `value` was already in an <see cref="IndexedList{TItem, TIndexer}"/> (setter only).</exception>
|
||||
public TItem this[int index]
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get => _Items[index];
|
||||
set
|
||||
{
|
||||
// Make sure it isn't already in a list.
|
||||
if (Indexer.GetIndex(value) != NotInList)
|
||||
throw new ArgumentException(SingleUse);
|
||||
|
||||
// Remove the old item at that index.
|
||||
Indexer.ClearIndex(_Items[index]);
|
||||
|
||||
// Set the index of the new item and add it at that index.
|
||||
Indexer.SetIndex(value, index);
|
||||
_Items[index] = value;
|
||||
AssertContents();
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Is the `item` currently in this list?</summary>
|
||||
/// <remarks>This method has <c>O(1)</c> complexity.</remarks>
|
||||
public bool Contains(TItem item)
|
||||
=> item != null
|
||||
&& Contains(item, Indexer.GetIndex(item));
|
||||
|
||||
/// <summary>Is the `item` currently in this list at the specified `index`?</summary>
|
||||
/// <remarks>This method has <c>O(1)</c> complexity.</remarks>
|
||||
public bool Contains(TItem item, int index)
|
||||
=> (uint)index < (uint)Count
|
||||
&& _Items[index] == item;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Returns the index of the `item` in this list or <c>-1</c> if it's not in this list.</summary>
|
||||
/// <remarks>This method has <c>O(1)</c> complexity.</remarks>
|
||||
public int IndexOf(TItem item)
|
||||
{
|
||||
if (item == null)
|
||||
return NotInList;
|
||||
|
||||
var index = Indexer.GetIndex(item);
|
||||
if (Contains(item, index))
|
||||
return index;
|
||||
else
|
||||
return NotInList;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void CopyTo(TItem[] array, int index)
|
||||
=> _Items.CopyTo(array, index);
|
||||
|
||||
/// <summary>Copies all the items from this list into the `array`, starting at the specified `index`.</summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
void ICollection.CopyTo(Array array, int index)
|
||||
=> _Items.CopyTo(array, index);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Returns false.</summary>
|
||||
bool ICollection<TItem>.IsReadOnly
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get => false;
|
||||
}
|
||||
|
||||
/// <summary>Is this list thread safe?</summary>
|
||||
bool ICollection.IsSynchronized
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get => _Items.IsSynchronized;
|
||||
}
|
||||
|
||||
/// <summary>An object that can be used to synchronize access to this <see cref="ICollection"/>.</summary>
|
||||
object ICollection.SyncRoot
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get => _Items.SyncRoot;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endregion
|
||||
/************************************************************************************************************************/
|
||||
#region Add
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Adds the `item` to the end of this list.</summary>
|
||||
/// <remarks>
|
||||
/// This method has <c>O(1)</c> complexity if the <see cref="Capacity"/> doesn't need to be increased.
|
||||
/// Otherwise, it's <c>O(N)</c> since all existing items need to be copied into a new array.
|
||||
/// </remarks>
|
||||
/// <exception cref="ArgumentException">
|
||||
/// The `item` was already in an <see cref="IndexedList{TItem, TIndexer}"/>.
|
||||
/// </exception>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
void ICollection<TItem>.Add(TItem item)
|
||||
=> Add(item);
|
||||
|
||||
/// <summary>Adds the `item` to the end of this list if it wasn't already in it and returns true if successful.</summary>
|
||||
/// <remarks>
|
||||
/// This method has <c>O(1)</c> complexity if the <see cref="Capacity"/> doesn't need to be increased.
|
||||
/// Otherwise, it's <c>O(N)</c> since all existing items need to be copied into a new array.
|
||||
/// </remarks>
|
||||
/// <exception cref="ArgumentException">
|
||||
/// The `item` is already in a different list.
|
||||
/// </exception>
|
||||
/// <exception cref="IndexOutOfRangeException">
|
||||
/// The `item` is already in a different list at an index larger than this list.
|
||||
/// </exception>
|
||||
public bool Add(TItem item)
|
||||
{
|
||||
var index = Indexer.GetIndex(item);
|
||||
|
||||
// Make sure it isn't already in a list.
|
||||
if (index != NotInList)
|
||||
{
|
||||
if (_Items[index] == item)// If it's in this list, do nothing.
|
||||
return false;
|
||||
else// Otherwise, it's in another list so we can't add it to this one.
|
||||
throw new ArgumentException(SingleUse);
|
||||
}
|
||||
|
||||
// Set the index of the new item and add it to the list.
|
||||
Indexer.SetIndex(item, Count);
|
||||
InternalAdd(item);
|
||||
AssertContents();
|
||||
return true;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Adds the `item` to this list at the specified `index`.</summary>
|
||||
/// <remarks>
|
||||
/// This method has <c>O(1)</c> complexity.
|
||||
/// <para></para>
|
||||
/// This does not maintain the order of items, but is more efficient than <see cref="List{T}.Insert(int, T)"/>
|
||||
/// because it avoids the need to move every item after the target up one place.
|
||||
/// </remarks>
|
||||
public void Insert(int index, TItem item)
|
||||
{
|
||||
if (index >= Count)
|
||||
{
|
||||
Add(item);
|
||||
return;
|
||||
}
|
||||
|
||||
var oldItem = _Items[index];
|
||||
|
||||
Indexer.SetIndex(item, index);
|
||||
Indexer.SetIndex(oldItem, Count);
|
||||
|
||||
_Items[index] = item;
|
||||
InternalAdd(oldItem);
|
||||
|
||||
AssertContents();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private void InternalAdd(TItem item)
|
||||
{
|
||||
var count = Count;
|
||||
var capacity = Capacity;
|
||||
|
||||
if (count == capacity)
|
||||
{
|
||||
if (capacity == 0)
|
||||
{
|
||||
_Items = new TItem[DefaultCapacity];
|
||||
}
|
||||
else
|
||||
{
|
||||
capacity *= 2;
|
||||
if (capacity < DefaultCapacity)
|
||||
capacity = DefaultCapacity;
|
||||
|
||||
var events = new TItem[capacity];
|
||||
|
||||
Array.Copy(_Items, 0, events, 0, count);
|
||||
|
||||
_Items = events;
|
||||
}
|
||||
}
|
||||
|
||||
_Items[count] = item;
|
||||
|
||||
Count = count + 1;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endregion
|
||||
/************************************************************************************************************************/
|
||||
#region Remove
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Removes the item at the specified `index` by swapping the last item in this list into its place.</summary>
|
||||
/// <remarks>
|
||||
/// This method has <c>O(1)</c> complexity.
|
||||
/// <para></para>
|
||||
/// This does not maintain the order of items, but is more efficient than <see cref="List{T}.RemoveAt"/>
|
||||
/// because it avoids the need to move every item after the target down one place.
|
||||
/// </remarks>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void RemoveAt(int index)
|
||||
=> RemoveAt(index, _Items[index]);
|
||||
|
||||
/// <summary>Removes the item at the specified `index` by swapping the last item in this list into its place.</summary>
|
||||
/// <remarks>
|
||||
/// This method has <c>O(1)</c> complexity.
|
||||
/// <para></para>
|
||||
/// This does not maintain the order of items, but is more efficient than <see cref="List{T}.RemoveAt"/>
|
||||
/// because it avoids the need to move every item after the target down one place.
|
||||
/// </remarks>
|
||||
private void RemoveAt(int index, TItem item)
|
||||
{
|
||||
var lastIndex = Count - 1;
|
||||
|
||||
// Adjust the enumerator if necessary.
|
||||
if (CurrentIndex > index)
|
||||
{
|
||||
CurrentIndex--;
|
||||
|
||||
// If the removal index is ahead of the current enumeration,
|
||||
// swap the current item to that index and swap the last item to the current index.
|
||||
|
||||
// Otherwise simply swapping the last item into that slot would mean that it gets covered again
|
||||
// when the enumerator reaches it.
|
||||
|
||||
if (CurrentIndex > index)
|
||||
{
|
||||
var lastItem = _Items[lastIndex];
|
||||
var currentItem = _Items[CurrentIndex];
|
||||
|
||||
_Items[CurrentIndex] = lastItem;
|
||||
_Items[index] = currentItem;
|
||||
_Items[lastIndex] = null;
|
||||
|
||||
Count--;
|
||||
|
||||
Indexer.ClearIndex(item);
|
||||
Indexer.SetIndex(currentItem, index);
|
||||
Indexer.SetIndex(lastItem, CurrentIndex);
|
||||
|
||||
AssertContents();
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// If it wasn't the last item, move the last item over it.
|
||||
if (lastIndex > index)
|
||||
{
|
||||
var lastItem = _Items[lastIndex];
|
||||
_Items[index] = lastItem;
|
||||
_Items[lastIndex] = null;
|
||||
|
||||
Count--;
|
||||
|
||||
Indexer.ClearIndex(item);
|
||||
Indexer.SetIndex(lastItem, index);
|
||||
}
|
||||
else// If it was the last item, just remove it.
|
||||
{
|
||||
_Items[lastIndex] = null;
|
||||
|
||||
Count--;
|
||||
|
||||
Indexer.ClearIndex(item);
|
||||
}
|
||||
|
||||
AssertContents();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Removes the `item` by swapping the last item in this list into its place.</summary>
|
||||
/// <remarks>
|
||||
/// This method has <c>O(1)</c> complexity.
|
||||
/// <para></para>
|
||||
/// This method does not maintain the order of items, but is more efficient than <see cref="Remove"/> because
|
||||
/// it avoids the need to move every item after the target down one place.
|
||||
/// </remarks>
|
||||
public bool Remove(TItem item)
|
||||
{
|
||||
var index = Indexer.GetIndex(item);
|
||||
|
||||
// If it isn't in this list, do nothing.
|
||||
if (!Contains(item, index))
|
||||
return false;
|
||||
|
||||
// Remove the item.
|
||||
RemoveAt(index, item);
|
||||
return true;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Removes all items from this list.</summary>
|
||||
/// <remarks>This method has <c>O(N)</c> complexity.</remarks>
|
||||
public void Clear()
|
||||
{
|
||||
for (int i = Count - 1; i >= 0; i--)
|
||||
Indexer.ClearIndex(_Items[i]);
|
||||
|
||||
Array.Clear(_Items, 0, Count);
|
||||
Count = 0;
|
||||
|
||||
CurrentIndex = NotInList;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endregion
|
||||
/************************************************************************************************************************/
|
||||
#region Enumeration
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// If something is currently enumerating through this list, this value holds the index it's currently up
|
||||
/// to. Otherwise, this value will be negative.
|
||||
/// </summary>
|
||||
public int CurrentIndex { get; private set; } = NotInList;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>The item at the <see cref="CurrentIndex"/>.</summary>
|
||||
/// <exception cref="IndexOutOfRangeException">
|
||||
/// The <see cref="CurrentIndex"/> is negative so this list isn't currently being enumerated.
|
||||
/// </exception>
|
||||
public TItem Current
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get => _Items[CurrentIndex];
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// Has <see cref="BeginEnumeraton"/> been called and <see cref="TryEnumerateNext"/> not yet been called
|
||||
/// enough times to go through all items?
|
||||
/// </summary>
|
||||
public bool IsEnumerating
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get => CurrentIndex != NotInList;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// Sets the <see cref="CurrentIndex"/> to the end of this list so that <see cref="TryEnumerateNext"/> can
|
||||
/// iterate backwards to the start.
|
||||
/// </summary>
|
||||
/// <exception cref="InvalidOperationException">
|
||||
/// This method was called multiple times without <see cref="TryEnumerateNext"/> going over all items. This
|
||||
/// list can only be enumerated by one thing at a time and it must fully complete before the next can begin.
|
||||
/// This limitation is necessary to allow items to be safely added and removed at any time.
|
||||
/// </exception>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void BeginEnumeraton()
|
||||
{
|
||||
if (IsEnumerating)
|
||||
throw new InvalidOperationException(
|
||||
$"{GetType().Name}<{typeof(TItem).Name}> was already enumerating." +
|
||||
$" Recursive enumeration is not supported.");
|
||||
|
||||
CurrentIndex = Count;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// Moves the <see cref="CurrentIndex"/> so the <see cref="Current"/> property points to the next item in
|
||||
/// this list.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This method should only be called after <see cref="BeginEnumeraton"/> and it should be called
|
||||
/// repeatedly until it returns false. <see cref="CancelEnumeration"/> can be used to cancel the
|
||||
/// enumeration early.
|
||||
/// </remarks>
|
||||
/// <returns>False if there are no more items to move to. Otherwise true.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool TryEnumerateNext()
|
||||
=> --CurrentIndex >= 0;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// Clears the <see cref="CurrentIndex"/> so that <see cref="BeginEnumeraton"/> can be used again without
|
||||
/// needing to call <see cref="TryEnumerateNext"/> repeatedly until it returns false.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void CancelEnumeration()
|
||||
=> CurrentIndex = NotInList;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Returns an enumerator which iterates through this list.</summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public FastEnumerator<TItem> GetEnumerator()
|
||||
=> new(_Items, Count);
|
||||
|
||||
/// <inheritdoc/>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
IEnumerator<TItem> IEnumerable<TItem>.GetEnumerator()
|
||||
=> GetEnumerator();
|
||||
|
||||
/// <inheritdoc/>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
IEnumerator IEnumerable.GetEnumerator()
|
||||
=> GetEnumerator();
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endregion
|
||||
/************************************************************************************************************************/
|
||||
#region Debugging
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Asserts that the indices stored in all items actually match their index in this list.</summary>
|
||||
[System.Diagnostics.Conditional("DEBUG_INDEXED_LISTS")]
|
||||
private void AssertContents(string name = null)
|
||||
{
|
||||
for (int i = 0; i < Count; i++)
|
||||
if (i != Indexer.GetIndex(_Items[i]))
|
||||
throw new ArgumentException($"Index mismatch at {i} in {name} {DeepToString()}");
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string DeepToString(string separator = "\n• ")
|
||||
{
|
||||
var text = StringBuilderPool.Instance.Acquire();
|
||||
|
||||
text.Append(GetType().GetNameCS())
|
||||
.Append('[')
|
||||
.Append(Count)
|
||||
.Append(']');
|
||||
|
||||
for (int i = 0; i < Count; i++)
|
||||
{
|
||||
var item = _Items[i];
|
||||
text.Append(separator)
|
||||
.Append('[')
|
||||
.Append(Indexer.GetIndex(item))
|
||||
.Append("] ")
|
||||
.Append(item);
|
||||
}
|
||||
|
||||
return text.ReleaseToString();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endregion
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c4370ee24ad78f540b5840b7a5632c98
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2a8cb96100e1eed4a81e0fca6c9c4834
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,48 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Animancer
|
||||
{
|
||||
/// <summary>An <see cref="ObjectPool{T}"/> for <see cref="ICollection{T}"/>.</summary>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer/CollectionPool_2
|
||||
public abstract class CollectionPool<TItem, TCollection> : ObjectPool<TCollection>
|
||||
where TCollection : class, ICollection<TItem>// The non-generic ICollection doesn't have Count.
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override TCollection Acquire()
|
||||
{
|
||||
var collection = base.Acquire();
|
||||
AssertEmpty(collection);
|
||||
return collection;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Release(TCollection collection)
|
||||
{
|
||||
collection.Clear();
|
||||
base.Release(collection);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>[Assert-Conditional] Asserts that the `collection` is empty.</summary>
|
||||
[System.Diagnostics.Conditional(Strings.Assertions)]
|
||||
public static void AssertEmpty(TCollection collection)
|
||||
{
|
||||
#if UNITY_ASSERTIONS
|
||||
if (collection.Count != 0)
|
||||
throw new UnityEngine.Assertions.AssertionException(
|
||||
$"A pooled {collection.GetType().GetNameCS()} is not empty.{NotResetError}",
|
||||
null);
|
||||
#endif
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e8da9ca455eec1c46b8d94a0029009b8
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,59 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Animancer
|
||||
{
|
||||
/// <summary>Convenience methods for accessing <see cref="DictionaryPool{TKey,TValue}"/>.</summary>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer/DictionaryPool
|
||||
public static class DictionaryPool
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Returns a spare <see cref="Dictionary{TKey,TValue}"/> if there are any, or creates a new one.</summary>
|
||||
/// <remarks>Remember to <see cref="Release{TKey,TValue}(Dictionary{TKey,TValue})"/> it when you are done.</remarks>
|
||||
public static Dictionary<TKey, TValue> Acquire<TKey, TValue>()
|
||||
=> DictionaryPool<TKey, TValue>.Instance.Acquire();
|
||||
|
||||
/// <summary>Returns a spare <see cref="Dictionary{TKey,TValue}"/> if there are any, or creates a new one.</summary>
|
||||
/// <remarks>Remember to <see cref="Release{TKey,TValue}(Dictionary{TKey,TValue})"/> it when you are done.</remarks>
|
||||
public static void Acquire<TKey, TValue>(out Dictionary<TKey, TValue> dictionary)
|
||||
=> dictionary = Acquire<TKey, TValue>();
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Clears the `dictionary` and adds it to the list of spares so it can be reused.</summary>
|
||||
public static void Release<TKey, TValue>(this Dictionary<TKey, TValue> dictionary)
|
||||
=> DictionaryPool<TKey, TValue>.Instance.Release(dictionary);
|
||||
|
||||
/// <summary>Clears the `dictionary`, adds it to the list of spares so it can be reused, and sets it to <c>null</c>.</summary>
|
||||
public static void Release<TKey, TValue>(ref Dictionary<TKey, TValue> dictionary)
|
||||
{
|
||||
Release(dictionary);
|
||||
dictionary = null;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>An <see cref="ObjectPool{T}"/> for <see cref="Dictionary{TKey,TValue}"/>.</summary>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer/DictionaryPool_2
|
||||
public class DictionaryPool<TKey, TValue> : CollectionPool<KeyValuePair<TKey, TValue>, Dictionary<TKey, TValue>>
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Singleton.</summary>
|
||||
public static DictionaryPool<TKey, TValue> Instance = new();
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override Dictionary<TKey, TValue> New()
|
||||
=> new();
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 812d2888d17283b45a11286c8a1f2c53
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,59 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Animancer
|
||||
{
|
||||
/// <summary>Convenience methods for accessing <see cref="ListPool{T}"/>.</summary>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer/ListPool
|
||||
public static class ListPool
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Returns a spare <see cref="List{T}"/> if there are any, or creates a new one.</summary>
|
||||
/// <remarks>Remember to <see cref="Release{T}(List{T})"/> it when you are done.</remarks>
|
||||
public static List<T> Acquire<T>()
|
||||
=> ListPool<T>.Instance.Acquire();
|
||||
|
||||
/// <summary>Returns a spare <see cref="List{T}"/> if there are any, or creates a new one.</summary>
|
||||
/// <remarks>Remember to <see cref="Release{T}(List{T})"/> it when you are done.</remarks>
|
||||
public static void Acquire<T>(out List<T> list)
|
||||
=> list = Acquire<T>();
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Clears the `list` and adds it to the list of spares so it can be reused.</summary>
|
||||
public static void Release<T>(this List<T> list)
|
||||
=> ListPool<T>.Instance.Release(list);
|
||||
|
||||
/// <summary>Clears the `list`, adds it to the list of spares so it can be reused, and sets it to <c>null</c>.</summary>
|
||||
public static void Release<T>(ref List<T> list)
|
||||
{
|
||||
Release(list);
|
||||
list = null;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>An <see cref="ObjectPool{T}"/> for <see cref="List{T}"/>.</summary>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer/ListPool_1
|
||||
public class ListPool<T> : CollectionPool<T, List<T>>
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Singleton.</summary>
|
||||
public static ListPool<T> Instance = new();
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override List<T> New()
|
||||
=> new();
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user