init
This commit is contained in:
@@ -0,0 +1,873 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 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.CreateOutput(value, 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. This property is mainly a wrapper
|
||||
/// around the <see cref="Animator.updateMode"/>.
|
||||
/// </summary>
|
||||
/// <remarks>Note that changing to or from <see cref="AnimatorUpdateMode.AnimatePhysics"/> at runtime has no effect.</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.CreateOutput(_Animator, 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 InitializePlayable(AnimancerGraph graph)
|
||||
{
|
||||
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.CreateOutput(_Animator, this);
|
||||
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}",
|
||||
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();
|
||||
clips.AddRange(set);
|
||||
SetPool.Release(set);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>[<see cref="IAnimationClipCollection"/>]
|
||||
/// Gathers all the animations in the <see cref="Graph"/>.
|
||||
/// <para></para>
|
||||
/// In the Unity Editor this method also gathers animations from other components on parent and child objects.
|
||||
/// </summary>
|
||||
public virtual void GatherAnimationClips(ICollection<AnimationClip> 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:
|
||||
+472
@@ -0,0 +1,472 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 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;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
+13
@@ -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,863 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 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();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <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 parameterCount = _Playable.GetParameterCount();
|
||||
var values = new object[parameterCount];
|
||||
for (int i = 0; i < parameterCount; i++)
|
||||
{
|
||||
values[i] = AnimancerUtilities.GetParameterValue(_Playable, _Playable.GetParameter(i));
|
||||
}
|
||||
|
||||
base.RecreatePlayable();
|
||||
|
||||
for (int i = 0; i < parameterCount; i++)
|
||||
{
|
||||
AnimancerUtilities.SetParameterValue(_Playable, _Playable.GetParameter(i), values[i]);
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <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 void GetEventDispatchInfo(
|
||||
out float length,
|
||||
out float normalizedTime,
|
||||
out bool isLooping)
|
||||
{
|
||||
var state = GetStateInfo(0);
|
||||
length = state.length;
|
||||
normalizedTime = state.normalizedTime;
|
||||
isLooping = 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:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f933bcd99582bff4b908b541fc73d8c7
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
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-2024 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)
|
||||
{
|
||||
Graph = graph;
|
||||
_Layers = new AnimancerLayer[DefaultCapacity];
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#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 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-2024 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)
|
||||
{
|
||||
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,516 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 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)
|
||||
{
|
||||
if (hasKey == null)
|
||||
{
|
||||
state = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
return 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>
|
||||
/// 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:
|
||||
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,478 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 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>
|
||||
/// 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;
|
||||
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)
|
||||
{
|
||||
State.GetEventDispatchInfo(out var length, out var normalizedTime, out var isLooping);
|
||||
|
||||
// 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 = 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 (length > 0)
|
||||
{
|
||||
if (_PreviousNormalizedTime == normalizedTime)
|
||||
return;
|
||||
|
||||
CheckGeneralEvents(normalizedTime, isLooping);
|
||||
|
||||
CheckEndEvent(normalizedTime);
|
||||
|
||||
_PreviousNormalizedTime = 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 currentTime, bool isLooping)
|
||||
{
|
||||
var count = Events.Count;
|
||||
if (count == 0)
|
||||
{
|
||||
_NextEventIndex = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
ValidateNextEventIndex(
|
||||
isLooping,
|
||||
ref currentTime,
|
||||
out var playDirectionFloat,
|
||||
out var playDirectionInt);
|
||||
|
||||
if (isLooping)// Looping.
|
||||
{
|
||||
var animancerEvent = Events[_NextEventIndex];
|
||||
var eventTime = animancerEvent.normalizedTime * playDirectionFloat;
|
||||
|
||||
var loopDelta = GetLoopDelta(_PreviousNormalizedTime, currentTime, 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, currentTime, eventTime))
|
||||
goto Invoke;
|
||||
}
|
||||
else// Non-Looping.
|
||||
{
|
||||
while ((uint)_NextEventIndex < (uint)count)
|
||||
{
|
||||
var animancerEvent = Events[_NextEventIndex];
|
||||
var eventTime = animancerEvent.normalizedTime * playDirectionFloat;
|
||||
|
||||
if (currentTime <= eventTime)
|
||||
return;
|
||||
|
||||
animancerEvent.DelayInvoke(Events.GetName(_NextEventIndex), State);
|
||||
|
||||
_NextEventIndex += playDirectionInt;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private void ValidateNextEventIndex(
|
||||
bool isLooping,
|
||||
ref float currentTime,
|
||||
out float playDirectionFloat,
|
||||
out int playDirectionInt)
|
||||
{
|
||||
if (currentTime < _PreviousNormalizedTime)// Playing Backwards.
|
||||
{
|
||||
var previousTime = _PreviousNormalizedTime;
|
||||
_PreviousNormalizedTime = -previousTime;
|
||||
currentTime = -currentTime;
|
||||
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>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,202 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 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
|
||||
else if (OptionalWarning.UselessEvent.IsEnabled())
|
||||
{
|
||||
OptionalWarning.UselessEvent.Log(
|
||||
$"An {nameof(AnimancerEvent)} which does nothing was invoked." +
|
||||
$" Most likely it wasn't configured correctly." +
|
||||
$" Unused events should be removed to avoid wasting performance checking them." +
|
||||
$"\n• Name: {AnimancerUtilities.ToStringOrNull(Name)}" +
|
||||
$"\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}" +
|
||||
$"\n• To avoid this behaviour, use a regular Animancer Event instead: {Strings.DocsURLs.AnimancerEvents}",
|
||||
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-2024 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-2024 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-2024 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,456 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 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>
|
||||
/// <remarks>
|
||||
/// <strong>Documentation:</strong>
|
||||
/// <see href="https://kybernetik.com.au/animancer/docs/manual/events/animancer/serialization">
|
||||
/// Serialized Events</see>
|
||||
/// </remarks>
|
||||
/// 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);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#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)
|
||||
{
|
||||
if (copyFrom == null)
|
||||
{
|
||||
_NormalizedTimes = default;
|
||||
_Callbacks = default;
|
||||
_Names = default;
|
||||
return;
|
||||
}
|
||||
|
||||
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
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+13
@@ -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,355 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 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"/> to determine 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)
|
||||
.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." +
|
||||
$" See the Shared Events page for more information: " +
|
||||
Strings.DocsURLs.SharedEventSequences +
|
||||
$"\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,320 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 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 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 you need to initialize more than just the End Event,
|
||||
/// consider using <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>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.GetCloneOrOriginal(original);
|
||||
if (events != null)
|
||||
{
|
||||
_EventDispatcher ??= new(this);
|
||||
_EventDispatcher.SetEvents(events, false);
|
||||
|
||||
if (events == original)
|
||||
copyFrom._EventDispatcher.DismissEventOwnership();
|
||||
|
||||
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>[Internal] Should this state check for events to invoke?</summary>
|
||||
internal 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--)
|
||||
UpdateEventsRecursive(parent.GetChild(i), raiseEvents);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#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-2024 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();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5927c425f3f0c0e4baeb0cd737707442
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 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
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 30eb95e632f815a49a7ac0f1d68cb006
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 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> { }
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
+11
@@ -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-2024 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-2024 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-2024 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,724 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 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 FadeSpeed { get; set; }
|
||||
|
||||
/// <summary>The total amount of time this fade will take to complete (in seconds).</summary>
|
||||
public float FadeDuration
|
||||
{
|
||||
get => FadeSpeed != 0
|
||||
? 1 / FadeSpeed
|
||||
: float.PositiveInfinity;
|
||||
set => FadeSpeed = 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 => FadeSpeed != 0
|
||||
? (1 - NormalizedTime) / FadeSpeed
|
||||
: float.PositiveInfinity;
|
||||
set => FadeSpeed = 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 fadeSpeed)
|
||||
{
|
||||
NormalizedTime = 0;
|
||||
TargetWeight = targetWeight;
|
||||
FadeSpeed = fadeSpeed;
|
||||
|
||||
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
|
||||
=> FadeSpeed > 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 * FadeSpeed);
|
||||
|
||||
if (NormalizedTime < 1)// Fade.
|
||||
{
|
||||
ApplyWeights();
|
||||
}
|
||||
else// End.
|
||||
{
|
||||
Finish();
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private 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 (TargetWeight > 0)
|
||||
ApplyWeight(FadeIn.Node, 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).
|
||||
|
||||
ApplyWeight(FadeIn.Node, 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];
|
||||
ApplyWeight(node.Node, node.StartingWeight * progress);
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyWeight(AnimancerNode node, float weight)
|
||||
{
|
||||
node._Weight = weight;
|
||||
ParentPlayable.ApplyChildWeight(node);
|
||||
}
|
||||
|
||||
private void StopAndDisconnect(AnimancerNode node)
|
||||
{
|
||||
// Don't InternalClearFade because it's virtual.
|
||||
node._FadeGroup = null;
|
||||
node.Stop();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private void Release()
|
||||
{
|
||||
FadeSpeed = 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 = default;
|
||||
|
||||
if (FadeOutInternal.Count == 0)
|
||||
FadeSpeed = 0;
|
||||
|
||||
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)
|
||||
FadeSpeed = 0;
|
||||
|
||||
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(FadeSpeed), FadeSpeed);
|
||||
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;
|
||||
FadeSpeed = copyFrom.FadeSpeed;
|
||||
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-2024 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,822 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 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 state)
|
||||
{
|
||||
MarkAsUsed(this);
|
||||
state.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 fadeSpeed = Math.Abs(targetWeight - Weight) / fadeDuration;
|
||||
|
||||
var fade = FadeGroup.Pool.Instance.Acquire();
|
||||
fade.SetFadeIn(this);
|
||||
fade.StartFade(targetWeight, fadeSpeed);
|
||||
}
|
||||
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,279 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 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 state)
|
||||
{
|
||||
MarkAsUsed(this);
|
||||
state.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 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-2024 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,195 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 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 void GetEventDispatchInfo(
|
||||
out float length,
|
||||
out float normalizedTime,
|
||||
out bool isLooping)
|
||||
{
|
||||
length = _Length;
|
||||
|
||||
normalizedTime = length != 0
|
||||
? Time / length
|
||||
: 0;
|
||||
|
||||
isLooping = _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,372 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 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"/>.</summary>
|
||||
public override float Length => _Length;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void GetEventDispatchInfo(
|
||||
out float length,
|
||||
out float normalizedTime,
|
||||
out bool isLooping)
|
||||
{
|
||||
length = _Length;
|
||||
|
||||
normalizedTime = length != 0
|
||||
? Time / length
|
||||
: 0;
|
||||
|
||||
isLooping = 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 IList<Object> _Bindings;
|
||||
private bool _HasInitializedBindings;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>The objects controlled by each track in the asset.</summary>
|
||||
public IList<Object> Bindings
|
||||
{
|
||||
get => _Bindings;
|
||||
set
|
||||
{
|
||||
_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;
|
||||
|
||||
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 (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);
|
||||
}
|
||||
}
|
||||
#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);
|
||||
}
|
||||
}
|
||||
#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);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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 Destroy()
|
||||
{
|
||||
_Asset = null;
|
||||
base.Destroy();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <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,60 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 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,99 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 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>
|
||||
/// <remarks>
|
||||
/// This system currently only supports 2 layers (Base + 1). Adding support for more would require additional
|
||||
/// <see cref="BoneWeights"/> for each additional layer and modifications to <see cref="WeightedMaskMixerJob"/>
|
||||
/// to iterate through each layer instead of just using the first two.
|
||||
/// </remarks>
|
||||
/// 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 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)
|
||||
{
|
||||
var graph = new AnimancerGraph();
|
||||
var layers = new WeightedMaskLayerList(graph, animator);
|
||||
graph.Layers = layers;
|
||||
return layers;
|
||||
}
|
||||
|
||||
/// <summary>Creates a new <see cref="WeightedMaskLayerList"/>.</summary>
|
||||
public WeightedMaskLayerList(AnimancerGraph graph, Animator animator)
|
||||
: base(graph)
|
||||
{
|
||||
graph.Layers = this;
|
||||
|
||||
Bones = animator.GetComponentsInChildren<Transform>();
|
||||
|
||||
var boneCount = Bones.Length - 1;// Ignore the root bone.
|
||||
|
||||
_Job = new WeightedMaskMixerJob()
|
||||
{
|
||||
boneTransforms = new(boneCount, Allocator.Persistent, NativeArrayOptions.UninitializedMemory),
|
||||
boneWeights = new(boneCount, Allocator.Persistent, NativeArrayOptions.UninitializedMemory),
|
||||
};
|
||||
|
||||
graph.Disposables.Add(this);
|
||||
|
||||
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.boneTransforms.Dispose();
|
||||
_Job.boneWeights.Dispose();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
+13
@@ -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,225 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 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;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <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);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <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);
|
||||
_Animancer.InitializePlayable(Layers.Graph);
|
||||
|
||||
Indices = Definition.CalculateIndices(Layers);
|
||||
|
||||
SetWeights(0);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Applies the weights of the specified group.</summary>
|
||||
public void SetWeights(int groupIndex)
|
||||
{
|
||||
Definition.AssertGroupIndex(groupIndex);
|
||||
|
||||
var boneWeights = Layers.BoneWeights;
|
||||
var definitionWeights = Definition.Weights;
|
||||
|
||||
var start = groupIndex * Indices.Length;
|
||||
|
||||
for (int i = 0; i < Indices.Length; i++)
|
||||
{
|
||||
var index = Indices[i];
|
||||
var weight = definitionWeights[start + i];
|
||||
boneWeights[index] = weight;
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private Fade _Fade;
|
||||
|
||||
/// <summary>Fades the weights towards the specified group.</summary>
|
||||
public void FadeWeights(
|
||||
int groupIndex,
|
||||
float fadeDuration,
|
||||
Func<float, float> easing = null)
|
||||
{
|
||||
if (fadeDuration > 0)
|
||||
{
|
||||
_Fade ??= new();
|
||||
_Fade.Start(this, groupIndex, fadeDuration, easing);
|
||||
}
|
||||
else
|
||||
{
|
||||
SetWeights(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 float[] _OriginalWeights;
|
||||
private WeightedMaskLayers _Layers;
|
||||
private int _TargetWeightIndex;
|
||||
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 { get; set; }
|
||||
|
||||
/// <summary>The total amount of time this fade will take (in seconds).</summary>
|
||||
public float Duration { get; set; }
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Initializes this fade and registers it to receive updates.</summary>
|
||||
public void Start(
|
||||
WeightedMaskLayers layers,
|
||||
int groupIndex,
|
||||
float duration,
|
||||
Func<float, float> easing = null)
|
||||
{
|
||||
layers.Definition.AssertGroupIndex(groupIndex);
|
||||
|
||||
_CurrentWeights = layers.Layers.BoneWeights;
|
||||
_Easing = easing;
|
||||
_Layers = layers;
|
||||
_TargetWeightIndex = layers.Definition.IndexOf(groupIndex, 0);
|
||||
Duration = duration;
|
||||
|
||||
var indices = _Layers.Indices;
|
||||
AnimancerUtilities.SetLength(ref _OriginalWeights, indices.Length);
|
||||
for (int i = 0; i < _OriginalWeights.Length; i++)
|
||||
{
|
||||
var index = 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 = indices[i];
|
||||
var from = _OriginalWeights[i];
|
||||
var to = targetWeights[_TargetWeightIndex + i];
|
||||
boneWeights[index] = Mathf.LerpUnclamped(from, to, 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 = indices[i];
|
||||
var to = targetWeights[_TargetWeightIndex + i];
|
||||
boneWeights[index] = to;
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
@@ -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:
|
||||
+322
@@ -0,0 +1,322 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 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;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <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);
|
||||
else
|
||||
_Weights = 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;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void CopyFrom(WeightedMaskLayersDefinition copyFrom, CloneContext context)
|
||||
{
|
||||
AnimancerUtilities.CopyExactArray(copyFrom._Transforms, ref _Transforms);
|
||||
AnimancerUtilities.CopyExactArray(copyFrom._Weights, ref _Weights);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Does this definition contain valid data?</summary>
|
||||
public bool IsValid
|
||||
=> !_Transforms.IsNullOrEmpty()
|
||||
&& _Weights != null && _Weights.Length > _Transforms.Length;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool OnValidate()
|
||||
=> ValidateArraySizes()
|
||||
|| RemoveMissingAndDuplicate();
|
||||
|
||||
/// <summary>Ensures that all the arrays have valid sizes.</summary>
|
||||
public bool ValidateArraySizes()
|
||||
{
|
||||
if (_Transforms.IsNullOrEmpty())
|
||||
{
|
||||
_Transforms = Array.Empty<Transform>();
|
||||
_Weights = Array.Empty<float>();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (_Weights == null ||
|
||||
_Weights.Length < _Transforms.Length)
|
||||
{
|
||||
AnimancerUtilities.SetLength(ref _Weights, _Transforms.Length);
|
||||
return true;
|
||||
}
|
||||
|
||||
var expectedWeightCount = (int)Math.Ceiling(_Weights.Length / (double)_Transforms.Length);
|
||||
expectedWeightCount *= _Transforms.Length;
|
||||
return AnimancerUtilities.SetLength(ref _Weights, expectedWeightCount);
|
||||
}
|
||||
|
||||
/// <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()
|
||||
=> OnValidate();
|
||||
|
||||
/// <inheritdoc/>
|
||||
void ISerializationCallbackReceiver.OnAfterDeserialize()
|
||||
=> OnValidate();
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#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)})";
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#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);
|
||||
|
||||
/// <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());
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endregion
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
+13
@@ -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,87 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 Kybernetik //
|
||||
|
||||
using Unity.Collections;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Animations;
|
||||
|
||||
namespace Animancer
|
||||
{
|
||||
/// <summary>
|
||||
/// An <see cref="IAnimationJob"/> which mixes its inputs based on individual <see cref="boneWeights"/>.
|
||||
/// </summary>
|
||||
public struct WeightedMaskMixerJob : IAnimationJob
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <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. This array corresponds to the <see cref="boneTransforms"/>.</summary>
|
||||
public NativeArray<float> boneWeights;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
readonly void IAnimationJob.ProcessRootMotion(AnimationStream stream)
|
||||
{
|
||||
var stream0 = stream.GetInputStream(0);
|
||||
var stream1 = stream.GetInputStream(1);
|
||||
|
||||
if (stream1.isValid)
|
||||
{
|
||||
var layerWeight = stream.GetInputWeight(1);
|
||||
var velocity = Vector3.LerpUnclamped(stream0.velocity, stream1.velocity, layerWeight);
|
||||
var angularVelocity = Vector3.LerpUnclamped(stream0.angularVelocity, stream1.angularVelocity, layerWeight);
|
||||
stream.velocity = velocity;
|
||||
stream.angularVelocity = angularVelocity;
|
||||
}
|
||||
else
|
||||
{
|
||||
stream.velocity = stream0.velocity;
|
||||
stream.angularVelocity = stream0.angularVelocity;
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
readonly void IAnimationJob.ProcessAnimation(AnimationStream stream)
|
||||
{
|
||||
var stream0 = stream.GetInputStream(0);
|
||||
var stream1 = stream.GetInputStream(1);
|
||||
|
||||
if (stream1.isValid)
|
||||
{
|
||||
var layerWeight = stream.GetInputWeight(1);
|
||||
var handleCount = boneTransforms.Length;
|
||||
for (var i = 0; i < handleCount; i++)
|
||||
{
|
||||
var handle = boneTransforms[i];
|
||||
var weight = layerWeight * boneWeights[i];
|
||||
|
||||
var position0 = handle.GetLocalPosition(stream0);
|
||||
var position1 = handle.GetLocalPosition(stream1);
|
||||
handle.SetLocalPosition(stream, Vector3.LerpUnclamped(position0, position1, weight));
|
||||
|
||||
var rotation0 = handle.GetLocalRotation(stream0);
|
||||
var rotation1 = handle.GetLocalRotation(stream1);
|
||||
handle.SetLocalRotation(stream, Quaternion.SlerpUnclamped(rotation0, rotation1, weight));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var handleCount = boneTransforms.Length;
|
||||
for (var i = 0; i < handleCount; i++)
|
||||
{
|
||||
var handle = boneTransforms[i];
|
||||
handle.SetLocalPosition(stream, handle.GetLocalPosition(stream0));
|
||||
handle.SetLocalRotation(stream, handle.GetLocalRotation(stream0));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
+13
@@ -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:
|
||||
@@ -0,0 +1,119 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 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-2024 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-2024 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-2024 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-2024 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>(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();
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7f15d016147a79a469f2c7a983c30ef2
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,361 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 Kybernetik //
|
||||
|
||||
//#define ANIMANCER_LOG_OBJECT_POOLING
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Animancer
|
||||
{
|
||||
/// <summary>A simple object pooling system.</summary>
|
||||
/// <remarks><typeparamref name="T"/> must not inherit from <see cref="Component"/> or <see cref="ScriptableObject"/>.</remarks>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer/ObjectPool_1
|
||||
public class ObjectPool<T>
|
||||
where T : class
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>An error message for when something has been modified after being released to the pool.</summary>
|
||||
/// <remarks>
|
||||
/// <strong>Example:</strong>
|
||||
/// <c>AnimancerUtilities.Assert($"A pooled list is not empty. {NotClearError}");</c>
|
||||
/// </remarks>
|
||||
public const string
|
||||
NotResetError = " It must be reset to its default values" +
|
||||
" before being released to the pool and not modified after that.";
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private readonly List<T>
|
||||
Items = new();
|
||||
|
||||
#if ANIMANCER_LOG_OBJECT_POOLING
|
||||
private int _TotalItemsCreated;
|
||||
#endif
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
#if UNITY_EDITOR
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="ObjectPool{T}"/>
|
||||
/// and asserts that there isn't a more derived type which should be used instead.
|
||||
/// </summary>
|
||||
public ObjectPool()
|
||||
{
|
||||
AnimancerUtilities.AssertDerivedPoolType<T>(GetType());
|
||||
#if ANIMANCER_LOG_OBJECT_POOLING
|
||||
AllPools.Add(this);
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Returns the <see cref="Static{T}.Instance"/>.</summary>
|
||||
public static ObjectPool<T> DefaultInstance
|
||||
=> Static<ObjectPool<T>>.Instance;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>The number of spare items currently in this pool.</summary>
|
||||
public int Count
|
||||
{
|
||||
get => Items.Count;
|
||||
set
|
||||
{
|
||||
var count = Items.Count;
|
||||
if (count < value)
|
||||
{
|
||||
if (Items.Capacity < value)
|
||||
Items.Capacity = Mathf.NextPowerOfTwo(value);
|
||||
|
||||
do
|
||||
{
|
||||
Items.Add(New());
|
||||
count++;
|
||||
#if ANIMANCER_LOG_OBJECT_POOLING
|
||||
_TotalItemsCreated++;
|
||||
#endif
|
||||
}
|
||||
while (count < value);
|
||||
|
||||
#if ANIMANCER_LOG_OBJECT_POOLING
|
||||
Debug.Log("Created multiple new items from " + ToString());
|
||||
#endif
|
||||
}
|
||||
else if (count > value)
|
||||
{
|
||||
Items.RemoveRange(value, count - value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Increases the <see cref="Count"/> to equal the `count` if it was lower.</summary>
|
||||
public void IncreaseCountTo(int count)
|
||||
{
|
||||
if (Count < count)
|
||||
Count = count;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>The <see cref="List{T}.Capacity"/> of the internal list of spare items.</summary>
|
||||
public int Capacity
|
||||
{
|
||||
get => Items.Capacity;
|
||||
set
|
||||
{
|
||||
if (Items.Count > value)
|
||||
Items.RemoveRange(value, Items.Count - value);
|
||||
Items.Capacity = value;
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Increases the <see cref="Capacity"/> to equal the `capacity` if it was lower.</summary>
|
||||
public void IncreaseCapacityTo(int capacity)
|
||||
{
|
||||
if (Capacity < capacity)
|
||||
Capacity = capacity;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Creates a <c>new()</c> instance of <typeparamref name="T"/>.</summary>
|
||||
protected virtual T New()
|
||||
=> Activator.CreateInstance<T>();
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Returns a spare item if there are any, or creates a new one.</summary>
|
||||
/// <remarks>Remember to <see cref="Release(T)"/> it when you are done.</remarks>
|
||||
public virtual T Acquire()
|
||||
{
|
||||
var count = Items.Count;
|
||||
if (count == 0)
|
||||
{
|
||||
#if ANIMANCER_LOG_OBJECT_POOLING
|
||||
_TotalItemsCreated++;
|
||||
var item = New();
|
||||
Debug.Log($"Created new item {item.GetHashCode()} from {ToString()}");
|
||||
return item;
|
||||
#else
|
||||
return New();
|
||||
#endif
|
||||
}
|
||||
else
|
||||
{
|
||||
count--;
|
||||
var item = Items[count];
|
||||
Items.RemoveAt(count);
|
||||
|
||||
#if ANIMANCER_LOG_OBJECT_POOLING
|
||||
Debug.Log($"Acquired spare item {item.GetHashCode()} from {ToString()}");
|
||||
#endif
|
||||
|
||||
return item;
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Adds the `item` to the list of spares so it can be reused.</summary>
|
||||
public virtual void Release(T item)
|
||||
{
|
||||
#if UNITY_ASSERTIONS
|
||||
AnimancerUtilities.Assert(item != null,
|
||||
$"Null objects must not be released into an {nameof(ObjectPool<T>)}.");
|
||||
|
||||
// Don't want to check the whole list, but just checking the last item adds a bit of safety.
|
||||
if (Items.Count > 0 && Items[^1] == item)
|
||||
throw new InvalidOperationException(
|
||||
$"Attempted to {nameof(Release)} an item which was already in the {nameof(ObjectPool<T>)}: {item}");
|
||||
#endif
|
||||
#if ANIMANCER_LOG_OBJECT_POOLING
|
||||
if (Items.Count + 1 >= Items.Capacity)
|
||||
Debug.LogWarning("Expanding " + ToString());
|
||||
#endif
|
||||
|
||||
Items.Add(item);
|
||||
|
||||
#if ANIMANCER_LOG_OBJECT_POOLING
|
||||
Debug.Log($"Released item {item.GetHashCode()} to {ToString()}");
|
||||
#endif
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Adds the `item` to the list of spares so it can be reused and sets it to <c>null</c>.</summary>
|
||||
public void Release(ref T item)
|
||||
{
|
||||
Release(item);
|
||||
item = null;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Returns a description of the state of this pool.</summary>
|
||||
public override string ToString()
|
||||
=> $"{nameof(ObjectPool<T>)}<{typeof(T).FullName}>" +
|
||||
$"({nameof(Count)} = {Items.Count}" +
|
||||
$", {nameof(Capacity)} = {Items.Capacity}" +
|
||||
#if ANIMANCER_LOG_OBJECT_POOLING
|
||||
$", Created = {_TotalItemsCreated}" +
|
||||
#endif
|
||||
")";
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
#if ANIMANCER_LOG_OBJECT_POOLING
|
||||
private static readonly List<ObjectPool<T>>
|
||||
AllPools = new();
|
||||
|
||||
static ObjectPool()
|
||||
{
|
||||
UnityEditor.EditorApplication.playModeStateChanged += change =>
|
||||
{
|
||||
for (int i = 0; i < AllPools.Count; i++)
|
||||
Debug.Log($"{change}: {AllPools[i]}");
|
||||
};
|
||||
}
|
||||
#endif
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#region Disposables
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="ObjectPool{T}.Disposable"/> and calls <see cref="Acquire()"/>
|
||||
/// to set the <see cref="ObjectPool{T}.Disposable.Item"/> and `item`.
|
||||
/// </summary>
|
||||
public Disposable Acquire(out T item)
|
||||
=> new(this, out item);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// An <see cref="IDisposable"/> to allow pooled objects to be acquired and released within <c>using</c>
|
||||
/// statements instead of needing to manually release everything.
|
||||
/// </summary>
|
||||
public readonly struct Disposable : IDisposable
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>The <see cref="ObjectPool{T}"/> which the <see cref="Item"/> is acquired from.</summary>
|
||||
public readonly ObjectPool<T> Pool;
|
||||
|
||||
/// <summary>The object acquired from the <see cref="Pool"/>.</summary>
|
||||
public readonly T Item;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="Disposable"/> and calls <see cref="Acquire()"/>
|
||||
/// to set the <see cref="Item"/> and `item`.
|
||||
/// </summary>
|
||||
public Disposable(ObjectPool<T> pool, out T item)
|
||||
{
|
||||
Pool = pool;
|
||||
Item = item = pool.Acquire();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary><see cref="Release(T)"/> the <see cref="Item"/>.</summary>
|
||||
public void Dispose()
|
||||
=> Pool.Release(Item);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endregion
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#if UNITY_EDITOR
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer/AnimancerUtilities
|
||||
public static partial class AnimancerUtilities
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>[Editor-Only]
|
||||
/// Asserts that the `type` isn't a base <see cref="ObjectPool{T}"/> with a more derived type available.
|
||||
/// </summary>
|
||||
internal static void AssertDerivedPoolType<T>(Type type)
|
||||
where T : class
|
||||
{
|
||||
if (type != typeof(ObjectPool<T>))
|
||||
return;
|
||||
|
||||
if (BaseToDerivedPoolType.TryGetValue(type, out var derived))
|
||||
Debug.LogWarning(
|
||||
$"A base {type.GetNameCS()} was created even though a more derived type exists" +
|
||||
$" and should be used instead: {derived.GetNameCS()}");
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private static Dictionary<Type, Type> _BaseToDerivedPoolType;
|
||||
|
||||
/// <summary>
|
||||
/// An automatically gathered dictionary which maps base <see cref="ObjectPool{T}"/>
|
||||
/// types to any other types which inherit from them.
|
||||
/// </summary>
|
||||
private static Dictionary<Type, Type> BaseToDerivedPoolType
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_BaseToDerivedPoolType != null)
|
||||
return _BaseToDerivedPoolType;
|
||||
|
||||
_BaseToDerivedPoolType = new();
|
||||
|
||||
var assemblies = AppDomain.CurrentDomain.GetAssemblies();
|
||||
for (int iAssembly = 0; iAssembly < assemblies.Length; iAssembly++)
|
||||
{
|
||||
var types = assemblies[iAssembly].GetTypes();
|
||||
for (int iType = 0; iType < types.Length; iType++)
|
||||
{
|
||||
var type = types[iType];
|
||||
if (TryGetBasePoolType(type, out var pool))
|
||||
_BaseToDerivedPoolType[pool] = type;
|
||||
}
|
||||
}
|
||||
|
||||
return _BaseToDerivedPoolType;
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private static bool TryGetBasePoolType(Type derived, out Type pool)
|
||||
{
|
||||
pool = null;
|
||||
if (!derived.IsClass ||
|
||||
derived.IsAbstract)
|
||||
return false;
|
||||
|
||||
pool = derived;
|
||||
while ((pool = pool.BaseType) != null)
|
||||
if (pool.IsGenericType && pool.GetGenericTypeDefinition() == typeof(ObjectPool<>))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endif
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e5fec2b1398f8c344b24f685dc58ecf0
|
||||
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-2024 Kybernetik //
|
||||
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Animancer
|
||||
{
|
||||
/// <summary>Convenience methods for accessing <see cref="SetPool{T}"/>.</summary>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer/SetPool
|
||||
public static class SetPool
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Returns a spare <see cref="HashSet{T}"/> if there are any, or creates a new one.</summary>
|
||||
/// <remarks>Remember to <see cref="Release{T}(HashSet{T})"/> it when you are done.</remarks>
|
||||
public static HashSet<T> Acquire<T>()
|
||||
=> SetPool<T>.Instance.Acquire();
|
||||
|
||||
/// <summary>Returns a spare <see cref="HashSet{T}"/> if there are any, or creates a new one.</summary>
|
||||
/// <remarks>Remember to <see cref="Release{T}(HashSet{T})"/> it when you are done.</remarks>
|
||||
public static void Acquire<T>(out HashSet<T> set)
|
||||
=> set = Acquire<T>();
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Clears the `set` and adds it to the list of spares so it can be reused.</summary>
|
||||
public static void Release<T>(HashSet<T> set)
|
||||
=> SetPool<T>.Instance.Release(set);
|
||||
|
||||
/// <summary>Clears the `set`, 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 HashSet<T> set)
|
||||
{
|
||||
Release(set);
|
||||
set = null;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>An <see cref="ObjectPool{T}"/> for <see cref="HashSet{T}"/>.</summary>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer/SetPool_1
|
||||
public class SetPool<T> : CollectionPool<T, HashSet<T>>
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Singleton.</summary>
|
||||
public static SetPool<T> Instance = new();
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override HashSet<T> New()
|
||||
=> new();
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3b87cc55d9220e94cb7c5cb85f38110e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,64 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 Kybernetik //
|
||||
|
||||
using System.Text;
|
||||
|
||||
namespace Animancer
|
||||
{
|
||||
/// <summary>An <see cref="ObjectPool{T}"/> for <see cref="StringBuilder"/>.</summary>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer/StringBuilderPool
|
||||
public class StringBuilderPool : ObjectPool<StringBuilder>
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Singleton.</summary>
|
||||
public static StringBuilderPool Instance = new();
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override StringBuilder New()
|
||||
=> new();
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override StringBuilder Acquire()
|
||||
{
|
||||
var text = base.Acquire();
|
||||
AnimancerUtilities.Assert(
|
||||
text.Length == 0,
|
||||
$"A pooled {nameof(StringBuilder)} is not empty." + NotResetError);
|
||||
return text;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Release(StringBuilder text)
|
||||
{
|
||||
text.Length = 0;
|
||||
base.Release(text);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer/AnimancerUtilities
|
||||
public static partial class AnimancerUtilities
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>[Animancer Extension]
|
||||
/// Calls <see cref="StringBuilder.ToString()"/> and <see cref="StringBuilderPool.Release(StringBuilder)"/>.
|
||||
/// </summary>
|
||||
public static string ReleaseToString(this StringBuilder text)
|
||||
{
|
||||
var result = text.ToString();
|
||||
StringBuilderPool.Instance.Release(text);
|
||||
return result;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9835fd8c111423949bf2ffbfd94d4c7c
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 91d69842453950140859230fcde8e49f
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user