chore: initial commit

This commit is contained in:
2026-05-08 11:04:00 +08:00
commit f55d2a57c3
6278 changed files with 866081 additions and 0 deletions

View File

@@ -0,0 +1,881 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
using Animancer.TransitionLibraries;
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Playables;
namespace Animancer
{
/// <summary>
/// The main component through which other scripts can interact with <see cref="Animancer"/>. It allows you to play
/// animations on an <see cref="UnityEngine.Animator"/> without using a <see cref="RuntimeAnimatorController"/>.
/// </summary>
/// <remarks>
/// This class can be used as a custom yield instruction to wait until all animations finish playing.
/// <para></para>
/// This class is mostly just a wrapper that connects an <see cref="AnimancerGraph"/> to an
/// <see cref="UnityEngine.Animator"/>.
/// <para></para>
/// <strong>Documentation:</strong>
/// <see href="https://kybernetik.com.au/animancer/docs/manual/playing/component-types">
/// Component Types</see>
/// </remarks>
/// https://kybernetik.com.au/animancer/api/Animancer/AnimancerComponent
///
[AddComponentMenu(Strings.MenuPrefix + "Animancer Component")]
[AnimancerHelpUrl(typeof(AnimancerComponent))]
[DefaultExecutionOrder(DefaultExecutionOrder)]
public class AnimancerComponent : MonoBehaviour,
IAnimancerComponent,
IEnumerator,
IAnimationClipSource,
IAnimationClipCollection
{
/************************************************************************************************************************/
#region Fields and Properties
/************************************************************************************************************************/
/// <summary>Initialize before anything else tries to use this component.</summary>
public const int DefaultExecutionOrder = -5000;
/************************************************************************************************************************/
[SerializeField]
[Tooltip("Animancer works by using Unity's Playables API to control an Animator component." +
"\n\nThe Animator's Controller field should be empty unless you intend to use it.")]
private Animator _Animator;
/// <summary>[<see cref="SerializeField"/>]
/// Animancer works by using Unity's Playables API to control an <see cref="UnityEngine.Animator"/> component.
/// </summary>
/// <remarks>
/// The <see cref="Animator.runtimeAnimatorController"/> should be empty unless you intend to use it.
/// </remarks>
public Animator Animator
{
get => _Animator;
set
{
_Animator = value;
if (IsGraphInitialized)
{
_Graph.DestroyOutput();
_Graph.Initialize(this);
}
}
}
#if UNITY_EDITOR
/// <inheritdoc/>
string IAnimancerComponent.AnimatorFieldName
=> nameof(_Animator);
#endif
/************************************************************************************************************************/
[SerializeField]
[Tooltip(Strings.ProOnlyTag + "An optional Transition Library" +
" which can modify the way Animancer transitions between animations.")]
private TransitionLibraryAsset _Transitions;
/// <summary>[<see cref="SerializeField"/>] [Pro-Only]
/// An optional <see cref="TransitionLibraryAsset"/>
/// which can modify the way Animancer transitions between animations.
/// </summary>
public TransitionLibraryAsset Transitions
{
get => _Transitions;
set
{
_Transitions = value;
if (IsGraphInitialized)
_Graph.Transitions = value?.Library;
}
}
/************************************************************************************************************************/
private AnimancerGraph _Graph;
/// <summary>
/// The internal system which manages the playing animations.
/// Accessing this property will automatically initialize it.
/// </summary>
public AnimancerGraph Graph
{
get
{
InitializeGraph();
return _Graph;
}
}
/// <summary>Has the <see cref="Graph"/> been initialized?</summary>
public bool IsGraphInitialized
=> _Graph != null
&& _Graph.IsValidOrDispose();
/************************************************************************************************************************/
/// <summary>The layers which each manage their own set of animations.</summary>
public AnimancerLayerList Layers
=> Graph.Layers;
/// <summary>The states managed by this component.</summary>
public AnimancerStateDictionary States
=> Graph.States;
/// <summary>Dynamic parameters which anything can get or set.</summary>
public ParameterDictionary Parameters
=> Graph.Parameters;
/// <summary>A dictionary of callbacks to be triggered by any event with a matching name.</summary>
public NamedEventDictionary Events
=> Graph.Events;
/************************************************************************************************************************/
/// <summary>Returns the <see cref="Graph"/>.</summary>
public static implicit operator AnimancerGraph(AnimancerComponent animancer)
=> animancer.Graph;
/// <summary>Returns layer 0.</summary>
public static implicit operator AnimancerLayer(AnimancerComponent animancer)
=> animancer.Graph.Layers[0];
/************************************************************************************************************************/
[SerializeField, Tooltip("Determines what happens when this component is disabled" +
" or its " + nameof(GameObject) + " becomes inactive (i.e. in OnDisable):" +
"\n• [" + nameof(DisableAction.Stop) + "] and reset all animations" +
"\n• [" + nameof(DisableAction.Pause) + "] to later resume from the current state" +
"\n• [" + nameof(DisableAction.Continue) + "] playing while inactive" +
"\n• [" + nameof(DisableAction.Reset) + "] to the original values" +
"\n• [" + nameof(DisableAction.Destroy) + "] all layers and states" +
"\n• If you're only destroying objects and not disabling them," +
" using " + nameof(DisableAction.Continue) + " is the most efficient" +
" because it avoids wasting performance stopping things that will be destroyed anyway.")]
private DisableAction _ActionOnDisable;
#if UNITY_EDITOR
/// <summary>[Editor-Only]
/// The name of the serialized backing field for the <see cref="ActionOnDisable"/> property.
/// </summary>
string IAnimancerComponent.ActionOnDisableFieldName
=> nameof(_ActionOnDisable);
#endif
/// <summary>[<see cref="SerializeField"/>]
/// Determines what happens when this component is disabled
/// or its <see cref="GameObject"/> becomes inactive
/// (i.e. in <see cref="OnDisable"/>).
/// </summary>
/// <remarks>
/// The default value is <see cref="DisableAction.Stop"/>.
/// <para></para>
/// If you're only destroying objects and not disabling them,
/// using <see cref="DisableAction.Continue"/> is the most efficient
/// because it avoids wasting performance stopping things that will be destroyed anyway.
/// </remarks>
public ref DisableAction ActionOnDisable
=> ref _ActionOnDisable;
/// <inheritdoc/>
bool IAnimancerComponent.ResetOnDisable
=> _ActionOnDisable == DisableAction.Reset;
/// <summary>
/// An action to perform when disabling an <see cref="AnimancerComponent"/>.
/// See <see cref="ActionOnDisable"/>.
/// </summary>
public enum DisableAction
{
/// <summary>
/// Stop and reset all animations, but leave all animated values as they are (unlike <see cref="Reset"/>).
/// </summary>
/// <remarks>Calls <see cref="Stop()"/> and <see cref="AnimancerGraph.PauseGraph"/>.</remarks>
Stop,
/// <summary>Pause to later resume from the current state.</summary>
/// <remarks>Calls <see cref="AnimancerGraph.PauseGraph"/>.</remarks>
Pause,
/// <summary>Keep playing while inactive.</summary>
Continue,
/// <summary>
/// Stop all animations, rewind them, and force the object back into its original state
/// (often called the bind pose).
/// </summary>
/// <remarks>
/// The <see cref="AnimancerComponent"/> must be either above the <see cref="UnityEngine.Animator"/> in
/// the Inspector or on a child object so that so that this <see cref="OnDisable"/> gets called first.
/// <para></para>
/// Calls <see cref="Stop()"/>, <see cref="Animator.Rebind"/>, and <see cref="AnimancerGraph.PauseGraph"/>.
/// </remarks>
Reset,
/// <summary>
/// Destroy the <see cref="PlayableGraph"/> and all its layers and states. This means that any layers or
/// states referenced by other scripts will no longer be valid so they will need to be recreated if you
/// want to use this object again.
/// </summary>
/// <remarks>Calls <see cref="AnimancerGraph.Destroy()"/>.</remarks>
Destroy,
}
/************************************************************************************************************************/
#region Update Mode
/************************************************************************************************************************/
/// <summary>Determines when animations are updated and which time source is used.</summary>
/// <remarks>
/// Note that changing to or from <see cref="AnimatorUpdateMode.AnimatePhysics"/>
/// at runtime has no effect due to limitations in the Playables API.
/// </remarks>
/// <exception cref="NullReferenceException">No <see cref="Animator"/> is assigned.</exception>
public AnimatorUpdateMode UpdateMode
{
get => _Animator.updateMode;
set
{
_Animator.updateMode = value;
if (!IsGraphInitialized)
return;
// UnscaledTime on the Animator is actually identical to Normal when using the Playables API so we need
// to set the graph's DirectorUpdateMode to determine how it gets its delta time.
_Graph.UpdateMode = value == AnimatorUpdateMode.UnscaledTime ?
DirectorUpdateMode.UnscaledGameTime :
DirectorUpdateMode.GameTime;
#if UNITY_EDITOR
if (InitialUpdateMode == null)
{
InitialUpdateMode = value;
}
else if (UnityEditor.EditorApplication.isPlaying)
{
if (Editor.AnimancerGraphCleanup.HasChangedToOrFromAnimatePhysics(InitialUpdateMode, value))
Debug.LogWarning(
$"Changing the {nameof(Animator)}.{nameof(Animator.updateMode)} to or from " +
#if UNITY_2023_1_OR_NEWER
nameof(AnimatorUpdateMode.Fixed) +
#else
nameof(AnimatorUpdateMode.AnimatePhysics) +
#endif
" at runtime will have no effect." +
" You must set it in the Unity Editor or on startup.", this);
}
#endif
}
}
/************************************************************************************************************************/
#if UNITY_EDITOR
/// <inheritdoc/>
public AnimatorUpdateMode? InitialUpdateMode { get; private set; }
#endif
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Initialization
/************************************************************************************************************************/
#if UNITY_EDITOR
/// <summary>[Editor-Only]
/// Destroys the <see cref="Graph"/> if it was initialized and searches for an <see cref="Animator"/> on
/// this object, or it's children or parents.
/// </summary>
protected virtual void Reset()
{
OnDestroy();
gameObject.GetComponentInParentOrChildren(ref _Animator);
}
#endif
/************************************************************************************************************************/
/// <summary>Ensures that the <see cref="PlayableGraph"/> is playing.</summary>
protected virtual void OnEnable()
{
if (IsGraphInitialized)
{
_Graph.UnpauseGraph();
#if UNITY_EDITOR
AnimancerGraph.ClearInactiveInitializationStackTrace(this);
#endif
}
}
/// <summary>Acts according to the <see cref="ActionOnDisable"/>.</summary>
protected virtual void OnDisable()
{
if (!IsGraphInitialized)
return;
switch (_ActionOnDisable)
{
case DisableAction.Stop:
_Graph.Stop();
_Graph.PauseGraph();
break;
case DisableAction.Pause:
_Graph.PauseGraph();
break;
case DisableAction.Continue:
break;
case DisableAction.Reset:
Debug.Assert(_Animator.isActiveAndEnabled,
$"{nameof(DisableAction)}.{nameof(DisableAction.Reset)} failed because the {nameof(Animator)}" +
$" is not enabled. This most likely means you are disabling the {nameof(GameObject)} and the" +
$" {nameof(Animator)} is above the {nameof(AnimancerComponent)} in the Inspector so it got" +
$" disabled right before this method was called." +
$" See the Inspector of {this} to fix the issue" +
$" or use {nameof(DisableAction)}.{nameof(DisableAction.Stop)}" +
$" and call {nameof(Animator)}.{nameof(Animator.Rebind)} manually" +
$" before disabling the {nameof(GameObject)}.",
this);
_Graph.Stop();
_Animator.Rebind();
_Graph.PauseGraph();
break;
case DisableAction.Destroy:
_Graph.Destroy();
_Graph = null;
break;
default:
throw new ArgumentOutOfRangeException(nameof(ActionOnDisable));
}
}
/************************************************************************************************************************/
/// <summary>Creates and initializes the <see cref="Graph"/> if it wasn't already initialized.</summary>
public void InitializeGraph()
{
if (IsGraphInitialized)
return;
TryGetAnimator();
AnimancerGraph.SetNextGraphName(name + " (Animancer)");
_Graph = new(_Transitions?.Library);
_Graph.Initialize(this);
OnInitializeGraph();
}
/************************************************************************************************************************/
/// <summary>Sets the <see cref="Graph"/> and connects it to the <see cref="Animator"/>.</summary>
/// <exception cref="InvalidOperationException">
/// The <see cref="AnimancerGraph"/> is already initialized.
/// You must call <see cref="AnimancerGraph.Destroy"/> before re-initializing it.
/// </exception>
public void InitializeGraph(AnimancerGraph graph, bool createOutput = true)
{
if (IsGraphInitialized)
throw new InvalidOperationException(
$"The {nameof(AnimancerGraph)} is already initialized." +
$" Either call this method before anything else uses it or call" +
$" animancerComponent.{nameof(Graph)}.{nameof(AnimancerGraph.Destroy)}" +
$" before re-initializing it.");
TryGetAnimator();
_Graph = graph;
_Graph.Transitions = _Transitions?.Library;
_Graph.Initialize(this, createOutput);
OnInitializeGraph();
}
/************************************************************************************************************************/
/// <summary>Called right after the <see cref="Graph"/> is initialized.</summary>
protected virtual void OnInitializeGraph()
{
#if UNITY_ASSERTIONS
ValidateGraphInitialization();
#endif
}
/************************************************************************************************************************/
/// <summary>
/// Tries to ensure that an <see cref="Animator"/> is present using
/// <see cref="Component.TryGetComponent{T}(out T)"/> if necessary.
/// </summary>
public bool TryGetAnimator()
=> _Animator != null
|| TryGetComponent(out _Animator);
/************************************************************************************************************************/
#if UNITY_ASSERTIONS
/// <summary>[Assert-Only]
/// Validates various conditions relating to <see cref="AnimancerGraph"/> initialization.
/// </summary>
private void ValidateGraphInitialization()
{
#if UNITY_EDITOR
if (_Animator != null)
InitialUpdateMode = UpdateMode;
#if UNITY_IMGUI
if (OptionalWarning.CreateGraphDuringGuiEvent.IsEnabled())
{
var currentEvent = Event.current;
if (currentEvent != null)
{
var eventType = currentEvent.type;
if (eventType == EventType.Layout ||
eventType == EventType.Repaint)
{
OptionalWarning.CreateGraphDuringGuiEvent.Log(
$"An {nameof(AnimancerGraph)} is being initialized" +
$" during a {eventType} event which is likely undesirable.",
this);
}
}
}
#endif
#endif
if (_Animator != null)
{
if (!_Animator.enabled)
OptionalWarning.AnimatorDisabled.Log(Strings.AnimatorDisabledMessage, this);
if (_Animator.isHuman &&
_Animator.runtimeAnimatorController != null)
OptionalWarning.NativeControllerHumanoid.Log(
$"An Animator Controller is assigned to the {nameof(Animator)} component" +
$" but the Rig is Humanoid so it can't be blended with Animancer." +
$" See the documentation for more information:" +
$" {Strings.DocsURLs.AnimatorControllersNative.AsHtmlLink()}",
this);
}
}
#endif
/************************************************************************************************************************/
/// <summary>Ensures that the <see cref="Graph"/> is properly cleaned up.</summary>
protected virtual void OnDestroy()
{
if (IsGraphInitialized)
{
_Graph.Destroy();
_Graph = null;
}
}
/************************************************************************************************************************/
#if UNITY_EDITOR
/// <summary>[Editor-Only]
/// Ensures that the <see cref="AnimancerGraph"/> is destroyed in Edit Mode, but not in Play Mode since we want
/// to let Unity complain if that happens.
/// </summary>
~AnimancerComponent()
{
if (_Graph != null)
{
UnityEditor.EditorApplication.delayCall += () =>
{
if (!UnityEditor.EditorApplication.isPlayingOrWillChangePlaymode)
OnDestroy();
};
}
}
#endif
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Play Management
/************************************************************************************************************************/
/// <summary>Returns the `clip` itself.</summary>
/// <remarks>
/// This method is used to determine the dictionary key to use for an animation when none is specified by the
/// caller, such as in <see cref="Play(AnimationClip)"/>.
/// </remarks>
public virtual object GetKey(AnimationClip clip)
=> clip;
/************************************************************************************************************************/
// Play Immediately.
/************************************************************************************************************************/
/// <summary>Stops all other animations on the same layer, plays the `clip`, and returns its state.</summary>
/// <remarks>
/// The animation will continue playing from its current <see cref="AnimancerState.Time"/>.
/// To restart it from the beginning you can use <c>...Play(clip).Time = 0;</c>.
/// <para></para>
/// This method is safe to call repeatedly without checking whether the `clip` was already playing.
/// </remarks>
public AnimancerState Play(AnimationClip clip)
=> Graph.Layers[0].Play(States.GetOrCreate(clip));
/// <summary>Stops all other animations on the same layer, plays the `state`, and returns it.</summary>
/// <remarks>
/// The animation will continue playing from its current <see cref="AnimancerState.Time"/>.
/// To restart it from the beginning you can use <c>...Play(state).Time = 0;</c>.
/// <para></para>
/// This method is safe to call repeatedly without checking whether the `state` was already playing.
/// </remarks>
public AnimancerState Play(AnimancerState state)
=> Graph.Layers[0].Play(state);
/************************************************************************************************************************/
// Cross Fade.
/************************************************************************************************************************/
/// <summary>
/// Starts fading in the `clip` while fading out all other states in the same layer over the course of the
/// `fadeDuration`. Returns its state.
/// </summary>
/// <remarks>
/// If the `state` was already playing and fading in with less time remaining than the `fadeDuration`, this
/// method will allow it to complete the existing fade rather than starting a slower one.
/// <para></para>
/// If the layer currently has 0 <see cref="AnimancerNode.Weight"/>, this method will fade in the layer itself
/// and simply <see cref="AnimancerState.Play"/> the `state`.
/// <para></para>
/// This method is safe to call repeatedly without checking whether the `clip` was already playing.
/// <para></para>
/// <em>Animancer Lite only allows the default `fadeDuration` (0.25 seconds) in runtime builds.</em>
/// </remarks>
public AnimancerState Play(AnimationClip clip, float fadeDuration, FadeMode mode = default)
=> Graph.Layers[0].Play(States.GetOrCreate(clip), fadeDuration, mode);
/// <summary>
/// Starts fading in the `state` while fading out all others in the same layer over the course of the
/// `fadeDuration`. Returns the `state`.
/// </summary>
/// <remarks>
/// If the `state` was already playing and fading in with less time remaining than the `fadeDuration`, this
/// method will allow it to complete the existing fade rather than starting a slower one.
/// <para></para>
/// If the layer currently has 0 <see cref="AnimancerNode.Weight"/>, this method will fade in the layer itself
/// and simply <see cref="AnimancerState.Play"/> the `state`.
/// <para></para>
/// This method is safe to call repeatedly without checking whether the `state` was already playing.
/// <para></para>
/// <em>Animancer Lite only allows the default `fadeDuration` (0.25 seconds) in runtime builds.</em>
/// </remarks>
public AnimancerState Play(AnimancerState state, float fadeDuration, FadeMode mode = default)
=> Graph.Layers[0].Play(state, fadeDuration, mode);
/************************************************************************************************************************/
// Transition.
/************************************************************************************************************************/
/// <summary>
/// Creates a state for the `transition` if it didn't already exist, then calls
/// <see cref="Play(AnimancerState)"/> or <see cref="Play(AnimancerState, float, FadeMode)"/>
/// depending on <see cref="ITransition.CrossFadeFromStart"/>.
/// </summary>
/// <remarks>
/// This method is safe to call repeatedly without checking whether the `transition` was already playing.
/// </remarks>
public AnimancerState Play(ITransition transition)
=> Graph.Layers[0].Play(transition);
/// <summary>
/// Creates a state for the `transition` if it didn't already exist, then calls
/// <see cref="Play(AnimancerState)"/> or <see cref="Play(AnimancerState, float, FadeMode)"/>
/// depending on <see cref="ITransition.CrossFadeFromStart"/>.
/// </summary>
/// <remarks>
/// This method is safe to call repeatedly without checking whether the `transition` was already playing.
/// </remarks>
public AnimancerState Play(ITransition transition, float fadeDuration, FadeMode mode = default)
=> Graph.Layers[0].Play(transition, fadeDuration, mode);
/************************************************************************************************************************/
// Try Play.
/************************************************************************************************************************/
/// <summary>
/// Stops all other animations on the base layer,
/// plays the animation registered with the `key`,
/// and returns the animation's state.
/// </summary>
/// <remarks>
/// If no state is registered with the `key`, this method does nothing and returns null.
/// <para></para>
/// The animation will continue playing from its current <see cref="AnimancerState.Time"/>.
/// To restart it from the beginning you can simply set the returned state's time to 0.
/// <para></para>
/// This method is safe to call repeatedly without checking whether the animation was already playing.
/// </remarks>
/// <exception cref="ArgumentNullException">The `key` is null.</exception>
public AnimancerState TryPlay(object key)
=> Graph.Layers[0].TryPlay(key);
/// <summary>
/// Stops all other animations on the base layer,
/// plays the animation registered with the `key`,
/// and returns the animation's state.
/// </summary>
/// <remarks>
/// If no state is registered with the `key`, this method does nothing and returns null.
/// <para></para>
/// The animation will continue playing from its current <see cref="AnimancerState.Time"/>.
/// To restart it from the beginning you can simply set the returned state's time to 0.
/// <para></para>
/// This method is safe to call repeatedly without checking whether the animation was already playing.
/// </remarks>
public AnimancerState TryPlay(IHasKey hasKey)
=> TryPlay(hasKey.Key);
/// <summary>
/// Starts fading in the animation registered with the `key` while fading out all others in the same layer
/// over the course of the `fadeDuration`. Or if no state is registered with that `key`, this method does
/// nothing and returns null.
/// </summary>
/// <remarks>
/// If the `state` was already playing and fading in with less time remaining than the `fadeDuration`, this
/// method will allow it to complete the existing fade rather than starting a slower one.
/// <para></para>
/// If the layer currently has 0 <see cref="AnimancerNode.Weight"/>, this method will fade in the layer itself
/// and simply <see cref="AnimancerState.Play"/> the `state`.
/// <para></para>
/// This method is safe to call repeatedly without checking whether the animation was already playing.
/// <para></para>
/// <em>Animancer Lite only allows the default `fadeDuration` (0.25 seconds) in runtime builds.</em>
/// </remarks>
/// <exception cref="ArgumentNullException">The `key` is null.</exception>
public AnimancerState TryPlay(object key, float fadeDuration, FadeMode mode = default)
=> Graph.Layers[0].TryPlay(key, fadeDuration, mode);
/// <summary>
/// Starts fading in the animation registered with the `key`
/// while fading out all others in the same layer over the course of the `fadeDuration`
/// and returns the animation's state.
/// </summary>
/// <remarks>
/// If no state is registered with the `key`, this method does nothing and returns null.
/// <para></para>
/// If the `state` was already playing and fading in with less time remaining than the `fadeDuration`,
/// this method allows it to continue the existing fade rather than starting a slower one.
/// <para></para>
/// If the layer currently has 0 <see cref="AnimancerNode.Weight"/>, this method will
/// fade in the layer itself and simply <see cref="AnimancerState.Play"/> the `state`.
/// <para></para>
/// This method is safe to call repeatedly without checking whether the animation was already playing.
/// <para></para>
/// <em>Animancer Lite only allows the default `fadeDuration` (0.25 seconds) in runtime builds.</em>
/// </remarks>
public AnimancerState TryPlay(
IHasKey hasKey,
float fadeDuration,
FadeMode mode = default)
=> TryPlay(hasKey.Key, fadeDuration, mode);
/************************************************************************************************************************/
/// <summary>
/// Gets the state associated with the `clip`, stops and rewinds it to the start, then returns it.
/// </summary>
public AnimancerState Stop(AnimationClip clip)
=> Stop(GetKey(clip));
/// <summary>
/// Gets the state registered with the <see cref="IHasKey.Key"/>, stops and rewinds it to the start, then
/// returns it.
/// </summary>
public AnimancerState Stop(IHasKey hasKey)
=> _Graph?.Stop(hasKey);
/// <summary>
/// Gets the state associated with the `key`, stops and rewinds it to the start, then returns it.
/// </summary>
public AnimancerState Stop(object key)
=> _Graph?.Stop(key);
/// <summary>Stops all animations and rewinds them to the start.</summary>
public void Stop()
{
if (IsGraphInitialized)
_Graph.Stop();
}
/************************************************************************************************************************/
/// <summary>
/// Returns true if a state is registered for the `clip` and it is currently playing.
/// <para></para>
/// The actual dictionary key is determined using <see cref="GetKey"/>.
/// </summary>
public bool IsPlaying(AnimationClip clip)
=> IsPlaying(GetKey(clip));
/// <summary>
/// Returns true if a state is registered with the <see cref="IHasKey.Key"/> and it is currently playing.
/// </summary>
public bool IsPlaying(IHasKey hasKey)
=> IsGraphInitialized
&& _Graph.IsPlaying(hasKey);
/// <summary>
/// Returns true if a state is registered with the `key` and it is currently playing.
/// </summary>
public bool IsPlaying(object key)
=> IsGraphInitialized
&& _Graph.IsPlaying(key);
/// <summary>
/// Returns true if at least one animation is being played.
/// </summary>
public bool IsPlaying()
=> IsGraphInitialized
&& _Graph.IsPlaying();
/************************************************************************************************************************/
/// <summary>
/// Returns true if the `clip` is currently being played by at least one state.
/// <para></para>
/// This method is inefficient because it searches through every state to find any that are playing the `clip`,
/// unlike <see cref="IsPlaying(AnimationClip)"/> which only checks the state registered using the `clip`s key.
/// </summary>
public bool IsPlayingClip(AnimationClip clip)
=> IsGraphInitialized
&& _Graph.IsPlayingClip(clip);
/************************************************************************************************************************/
/// <summary>
/// Immediately applies the current states of all animations to the animated objects.
/// </summary>
public void Evaluate()
=> Graph.Evaluate();
/// <summary>
/// Advances time by the specified value (in seconds)
/// and immediately applies the current states of all animations to the animated objects.
/// </summary>
public void Evaluate(float deltaTime)
=> Graph.Evaluate(deltaTime);
/************************************************************************************************************************/
#region Key Error Methods
#if UNITY_EDITOR
/************************************************************************************************************************/
// These are overloads of other methods that take a System.Object key to ensure the user doesn't try to use an
// AnimancerState as a key, since the whole point of a key is to identify a state in the first place.
/************************************************************************************************************************/
/// <summary>[Warning]
/// You should not use an <see cref="AnimancerState"/> as a key.
/// Just call <see cref="AnimancerState.Stop"/>.
/// </summary>
[Obsolete("You should not use an AnimancerState as a key. Just call AnimancerState.Stop().", true)]
public AnimancerState Stop(AnimancerState key)
{
key.Stop();
return key;
}
/// <summary>[Warning]
/// You should not use an <see cref="AnimancerState"/> as a key.
/// Just check <see cref="AnimancerState.IsPlaying"/>.
/// </summary>
[Obsolete("You should not use an AnimancerState as a key. Just check AnimancerState.IsPlaying.", true)]
public bool IsPlaying(AnimancerState key)
=> key.IsPlaying;
/************************************************************************************************************************/
#endif
#endregion
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Enumeration
/************************************************************************************************************************/
// IEnumerator for yielding in a coroutine to wait until all animations have stopped.
/************************************************************************************************************************/
/// <summary>Are any animations are still playing?</summary>
/// <remarks>This allows this object to be used as a custom yield instruction.</remarks>
bool IEnumerator.MoveNext()
=> IsGraphInitialized
&& ((IEnumerator)_Graph).MoveNext();
/// <summary>Returns null.</summary>
object IEnumerator.Current
=> null;
/// <summary>Does nothing.</summary>
void IEnumerator.Reset()
{ }
/************************************************************************************************************************/
/// <summary>[<see cref="IAnimationClipSource"/>]
/// Calls <see cref="GatherAnimationClips(ICollection{AnimationClip})"/>.
/// </summary>
public void GetAnimationClips(List<AnimationClip> clips)
{
var set = SetPool.Acquire<AnimationClip>();
set.UnionWith(clips);
GatherAnimationClips(set);
clips.Clear();
foreach (var clip in set)
if (clip != null)
clips.Add(clip);
SetPool.Release(set);
}
/************************************************************************************************************************/
/// <summary>[<see cref="IAnimationClipCollection"/>]
/// Gathers all the animations in the <see cref="Transitions"/> and <see cref="Graph"/>.
/// </summary>
/// <remarks>
/// In the Unity Editor this method also gathers animations from other components on parent and child objects.
/// </remarks>
public virtual void GatherAnimationClips(ICollection<AnimationClip> clips)
{
if (_Transitions != null)
_Transitions.GatherAnimationClips(clips);
if (IsGraphInitialized)
_Graph.GatherAnimationClips(clips);
#if UNITY_EDITOR
Editor.AnimationGatherer.GatherFromGameObject(gameObject, clips);
if (_Animator != null && _Animator.gameObject != gameObject)
Editor.AnimationGatherer.GatherFromGameObject(_Animator.gameObject, clips);
#endif
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
}
}

View File

@@ -0,0 +1,13 @@
fileFormatVersion: 2
guid: 0ad50f81b1d25c441943c37a89ba23f6
labels:
- Component
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 0024209230bdd0d46a82810456402e2c
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,472 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
using System;
using System.Collections.Generic;
using UnityEngine;
namespace Animancer
{
/// https://kybernetik.com.au/animancer/api/Animancer/ControllerState
partial class ControllerState
{
/************************************************************************************************************************/
private SerializableParameterBindings _SerializedParameterBindings;
/// <summary>Serialized data used to create <see cref="ParameterBinding{T}"/>s at runtime.</summary>
public SerializableParameterBindings SerializedParameterBindings
{
get => _SerializedParameterBindings;
set
{
_SerializedParameterBindings = value;
DeserializeParameterBindings();
}
}
/// <summary>Deserializes the <see cref="SerializedParameterBindings"/>.</summary>
private void DeserializeParameterBindings()
{
if (Graph == null)
return;
DisposeParameterBindings();
_SerializedParameterBindings?.Deserialize(this);
}
/************************************************************************************************************************/
private List<IDisposable> _ParameterBindings;
/// <summary>
/// Adds an object to a list for <see cref="IDisposable.Dispose"/>
/// to be called in <see cref="Destroy"/>.
/// </summary>
private void AddParameterBinding(IDisposable disposable)
{
_ParameterBindings ??= new();
_ParameterBindings.Add(disposable);
}
/// <summary>Disposes everything added by <see cref="AddParameterBinding"/>.</summary>
private void DisposeParameterBindings()
{
if (_ParameterBindings == null)
return;
for (int i = _ParameterBindings.Count - 1; i >= 0; i--)
_ParameterBindings[i].Dispose();
_ParameterBindings.Clear();
}
/************************************************************************************************************************/
/// <summary>
/// Configures all parameters in the <see cref="Controller"/>
/// to follow the value of a parameter with the same name in the <see cref="AnimancerGraph.Parameters"/>.
/// </summary>
public void BindAllParameters()
{
var count = Playable.GetParameterCount();
for (int i = 0; i < count; i++)
{
var parameter = Playable.GetParameter(i);
BindParameter(parameter.name, parameter);
}
}
/************************************************************************************************************************/
/// <summary>
/// Configures a parameter in the <see cref="Controller"/>
/// to follow the value of a parameter with the same name in the <see cref="AnimancerGraph.Parameters"/>.
/// </summary>
public void BindParameter(StringReference name)
=> BindParameter(name, name);
/// <summary>
/// Configures a parameter in the <see cref="Controller"/>
/// to follow the value of a parameter in the <see cref="AnimancerGraph.Parameters"/>.
/// </summary>
public void BindParameter(StringReference animancerParameter, string controllerParameterName)
=> BindParameter(animancerParameter, Animator.StringToHash(controllerParameterName));
/// <summary>
/// Configures a parameter in the <see cref="Controller"/>
/// to follow the value of a parameter in the <see cref="AnimancerGraph.Parameters"/>.
/// </summary>
public void BindParameter(StringReference animancerParameter, int controllerParameterHash)
{
var count = Playable.GetParameterCount();
for (int i = 0; i < count; i++)
{
var parameter = Playable.GetParameter(i);
if (parameter.nameHash == controllerParameterHash)
{
BindParameter(animancerParameter, parameter);
break;
}
}
}
/************************************************************************************************************************/
/// <summary>
/// Configures all parameters in the <see cref="Controller"/>
/// to follow the value of a parameter with the same name in the <see cref="AnimancerGraph.Parameters"/>.
/// </summary>
public void BindParameter(
StringReference animancerParameter,
AnimatorControllerParameter controllerParameter)
{
switch (controllerParameter.type)
{
case AnimatorControllerParameterType.Float:
BindFloat(animancerParameter, controllerParameter.nameHash);
break;
case AnimatorControllerParameterType.Int:
BindInt(animancerParameter, controllerParameter.nameHash);
break;
case AnimatorControllerParameterType.Bool:
case AnimatorControllerParameterType.Trigger:
BindBool(animancerParameter, controllerParameter.nameHash);
break;
}
}
/************************************************************************************************************************/
/// <summary>
/// Configures a parameter in the <see cref="Controller"/>
/// to follow the value of a parameter with the same name in the <see cref="AnimancerGraph.Parameters"/>.
/// </summary>
public ParameterBinding<bool> BindBool(StringReference name)
=> BindBool(name, name);
/// <summary>
/// Configures a parameter in the <see cref="Controller"/>
/// to follow the value of a parameter in the <see cref="AnimancerGraph.Parameters"/>.
/// </summary>
public ParameterBinding<bool> BindBool(StringReference animancerParameter, string controllerParameterName)
=> BindBool(animancerParameter, Animator.StringToHash(controllerParameterName));
/// <summary>
/// Configures a parameter in the <see cref="Controller"/>
/// to follow the value of a parameter in the <see cref="AnimancerGraph.Parameters"/>.
/// </summary>
public ParameterBinding<bool> BindBool(StringReference animancerParameter, int controllerParameterHash)
{
var parameter = Graph.Parameters.GetOrCreate<bool>(animancerParameter);
var binding = new ParameterBinding<bool>(
parameter,
value => Playable.SetBool(controllerParameterHash, value));
AddParameterBinding(binding);
return binding;
}
/************************************************************************************************************************/
/// <summary>
/// Configures a parameter in the <see cref="Controller"/>
/// to follow the value of a parameter with the same name in the <see cref="AnimancerGraph.Parameters"/>.
/// </summary>
public ParameterBinding<float> BindFloat(StringReference name)
=> BindFloat(name, name);
/// <summary>
/// Configures a parameter in the <see cref="Controller"/>
/// to follow the value of a parameter in the <see cref="AnimancerGraph.Parameters"/>.
/// </summary>
public ParameterBinding<float> BindFloat(StringReference animancerParameter, string controllerParameterName)
=> BindFloat(animancerParameter, Animator.StringToHash(controllerParameterName));
/// <summary>
/// Configures a parameter in the <see cref="Controller"/>
/// to follow the value of a parameter in the <see cref="AnimancerGraph.Parameters"/>.
/// </summary>
public ParameterBinding<float> BindFloat(StringReference animancerParameter, int controllerParameterHash)
{
var parameter = Graph.Parameters.GetOrCreate<float>(animancerParameter);
var binding = new ParameterBinding<float>(
parameter,
value => Playable.SetFloat(controllerParameterHash, value));
AddParameterBinding(binding);
return binding;
}
/************************************************************************************************************************/
/// <summary>
/// Configures a parameter in the <see cref="Controller"/>
/// to follow the value of a parameter with the same name in the <see cref="AnimancerGraph.Parameters"/>.
/// </summary>
public ParameterBinding<int> BindInt(StringReference name)
=> BindInt(name, name);
/// <summary>
/// Configures a parameter in the <see cref="Controller"/>
/// to follow the value of a parameter in the <see cref="AnimancerGraph.Parameters"/>.
/// </summary>
public ParameterBinding<int> BindInt(StringReference animancerParameter, string controllerParameterName)
=> BindInt(animancerParameter, Animator.StringToHash(controllerParameterName));
/// <summary>
/// Configures a parameter in the <see cref="Controller"/>
/// to follow the value of a parameter in the <see cref="AnimancerGraph.Parameters"/>.
/// </summary>
public ParameterBinding<int> BindInt(StringReference animancerParameter, int controllerParameterHash)
{
var parameter = Graph.Parameters.GetOrCreate<int>(animancerParameter);
var binding = new ParameterBinding<int>(
parameter,
value => Playable.SetInteger(controllerParameterHash, value));
AddParameterBinding(binding);
return binding;
}
/************************************************************************************************************************/
/// <summary>An <see cref="IDisposable"/> binding to <see cref="Parameter{T}.OnValueChanged"/>.</summary>
/// https://kybernetik.com.au/animancer/api/Animancer/ParameterBinding_1
public class ParameterBinding<T> : IDisposable
{
/************************************************************************************************************************/
/// <summary>The parameter being watched.</summary>
public readonly Parameter<T> Parameter;
/// <summary>The callback to invoke when the parameter changes.</summary>
public readonly Action<T> OnParameterChanged;
/************************************************************************************************************************/
/// <summary>
/// Invokes `onParameterChanged` and adds it to the <see cref="Parameter{T}.OnValueChanged"/>
/// to be removed by <see cref="Dispose"/>.
/// </summary>
public ParameterBinding(
Parameter<T> parameter,
Action<T> onParameterChanged)
{
Parameter = parameter;
OnParameterChanged = onParameterChanged;
OnParameterChanged(Parameter.Value);
Parameter.OnValueChanged += OnParameterChanged;
}
/************************************************************************************************************************/
/// <summary>
/// Removes <see cref="OnParameterChanged"/> from the <see cref="Parameter{T}.OnValueChanged"/>.
/// </summary>
public void Dispose()
{
Parameter.OnValueChanged -= OnParameterChanged;
}
/************************************************************************************************************************/
}
/************************************************************************************************************************/
/// <summary>
/// A serializable array of data which can create <see cref="ParameterBinding{T}"/>s at runtime.
/// </summary>
/// <remarks>
/// This data contains a <see cref="Bindings"/> array and <see cref="Mode"/> flag:
/// <list type="bullet">
///
/// <item>
/// If the array is empty,
/// <c>true</c> will bind all parameters by name
/// and <c>false</c> will bind nothing.
/// </item>
///
/// <item>
/// Otherwise, <c>true</c> will bind <c>[i * 2]</c> in the <see cref="RuntimeAnimatorController"/>
/// to <c>[i * 2 + 1]</c> in the <see cref="AnimancerGraph.Parameters"/>.
/// </item>
///
/// <item>
/// And <c>false</c> will bind each of its parameters to the same name in both systems.
/// </item>
///
/// </list>
/// </remarks>
/// https://kybernetik.com.au/animancer/api/Animancer/SerializableParameterBindings
[Serializable]
public class SerializableParameterBindings :
ICloneable<SerializableParameterBindings>
{
/************************************************************************************************************************/
[SerializeField]
private bool _Mode;
/// <summary>[<see cref="SerializeField"/>]
/// Modifies the way the <see cref="Bindings"/> array is interpreted.
/// </summary>
/// <remarks>See the <see cref="SerializableParameterBindings"/> class for details.</remarks>
public ref bool Mode
=> ref _Mode;
#if UNITY_EDITOR
/// <summary>[Editor-Only] The name of the serialized backing field of <see cref="Mode"/>.</summary>
public const string ModeFieldName = nameof(_Mode);
#endif
/************************************************************************************************************************/
/// <summary>[<see cref="SerializeField"/>]
/// Should all parameters in the <see cref="RuntimeAnimatorController"/> be bound by name?
/// </summary>
/// <remarks>See the <see cref="SerializableParameterBindings"/> class for details.</remarks>
public bool BindAllParameters
{
get => _Mode && _Bindings.Length == 0;
set
{
_Mode = value;
if (value)
{
_Bindings = Array.Empty<StringAsset>();
}
else
{
Debug.Assert(
_Bindings.Length == 0,
$"{nameof(BindAllParameters)} can't be disabled unless the {nameof(Bindings)}" +
$" array is empty because it changes the meaning of that array.");
}
}
}
/************************************************************************************************************************/
/// <summary>[<see cref="SerializeField"/>]
/// Should the <see cref="Bindings"/> be grouped into pairs
/// to bind each <see cref="RuntimeAnimatorController"/> parameter
/// to the subsequent parameter in <see cref="AnimancerGraph.Parameters"/>?
/// </summary>
/// <remarks>See the <see cref="SerializableParameterBindings"/> class for details.</remarks>
public bool RebindNames
{
get => _Mode && _Bindings.Length > 0;
set
{
_Mode = value;
if (value)
{
if (_Bindings.Length % 2 != 0)
Array.Resize(ref _Bindings, _Bindings.Length + 1);
}
else
{
Debug.Assert(
_Bindings.Length == 0,
$"{nameof(RebindNames)} can't be disabled unless the {nameof(Bindings)}" +
$" array is empty because it changes the meaning of that array.");
}
}
}
/************************************************************************************************************************/
[SerializeField]
private StringAsset[] _Bindings = Array.Empty<StringAsset>();
/// <summary>[<see cref="SerializeField"/>]
/// Parameter names used to have parameters in the <see cref="RuntimeAnimatorController"/>
/// follow the value of parameters in the <see cref="AnimancerGraph.Parameters"/>.
/// </summary>
/// <remarks>See the <see cref="SerializableParameterBindings"/> class for details.</remarks>
public StringAsset[] Bindings
{
get => _Bindings;
set
{
Debug.Assert(
value != null,
$"{nameof(Bindings)} can't be null. Use Array.Empty<StringAsset>() instead.");
_Bindings = value;
}
}
#if UNITY_EDITOR
/// <summary>[Editor-Only] The name of the serialized backing field of <see cref="Bindings"/>.</summary>
public const string BindingsFieldName = nameof(_Bindings);
#endif
/************************************************************************************************************************/
/// <summary>Creates runtime bindings for the `state`.</summary>
/// <remarks>See the <see cref="SerializableParameterBindings"/> class for details.</remarks>
public void Deserialize(ControllerState state)
{
if (_Bindings.Length == 0)
{
if (_Mode)
state.BindAllParameters();
// Else do nothing.
}
else
{
if (_Mode)
{
for (int i = 0; i < _Bindings.Length - 1; i += 2)
{
var controller = _Bindings[i];
var animancer = _Bindings[i + 1];
if (controller == null ||
animancer == null)
continue;
state.BindParameter(animancer, controller);
}
}
else
{
for (int i = 0; i < _Bindings.Length; i++)
{
var name = _Bindings[i];
if (name == null)
continue;
state.BindParameter(name);
}
}
}
}
/************************************************************************************************************************/
/// <inheritdoc/>
public SerializableParameterBindings Clone(CloneContext context)
{
var bindingCount = Bindings != null ? Bindings.Length : 0;
var clone = new SerializableParameterBindings()
{
BindAllParameters = BindAllParameters,
Bindings = new StringAsset[bindingCount],
};
for (int i = 0; i < bindingCount; i++)
clone.Bindings[i] = context.GetCloneOrOriginal(Bindings[i]);
return clone;
}
/************************************************************************************************************************/
}
/************************************************************************************************************************/
}
}

View File

@@ -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:

View File

@@ -0,0 +1,887 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using UnityEngine;
using UnityEngine.Animations;
using UnityEngine.Playables;
using Object = UnityEngine.Object;
namespace Animancer
{
/// <summary>[Pro-Only]
/// An <see cref="AnimancerState"/> which plays a <see cref="RuntimeAnimatorController"/>.
/// </summary>
/// <remarks>
/// This state can be controlled very similarly to an <see cref="Animator"/>
/// via its <see cref="Playable"/> property.
/// <para></para>
/// <strong>Documentation:</strong>
/// <see href="https://kybernetik.com.au/animancer/docs/manual/animator-controllers">
/// Animator Controllers</see>
/// </remarks>
/// https://kybernetik.com.au/animancer/api/Animancer/ControllerState
///
public partial class ControllerState : AnimancerState,
ICopyable<ControllerState>,
IParametizedState,
IUpdatable
{
/************************************************************************************************************************/
#region Fields and Properties
/************************************************************************************************************************/
private RuntimeAnimatorController _Controller;
/// <summary>The <see cref="RuntimeAnimatorController"/> which this state plays.</summary>
public RuntimeAnimatorController Controller
{
get => _Controller;
set => ChangeMainObject(ref _Controller, value);
}
/// <summary>The <see cref="RuntimeAnimatorController"/> which this state plays.</summary>
public override Object MainObject
{
get => Controller;
set => Controller = (RuntimeAnimatorController)value;
}
#if UNITY_EDITOR
/// <inheritdoc/>
public override Type MainObjectType
=> typeof(RuntimeAnimatorController);
#endif
/************************************************************************************************************************/
private new AnimatorControllerPlayable _Playable;
/// <summary>The internal system which plays the <see cref="RuntimeAnimatorController"/>.</summary>
public new AnimatorControllerPlayable Playable
{
get
{
Validate.AssertPlayable(this);
return _Playable;
}
}
/************************************************************************************************************************/
/// <summary>Determines what a layer does when <see cref="AnimancerNode.Stop"/> is called.</summary>
public enum ActionOnStop
{
/// <summary>Reset the layer to the first state it was in.</summary>
DefaultState,
/// <summary>Rewind the current state's time to 0.</summary>
RewindTime,
/// <summary>Allow the current state to stay at its current time.</summary>
Continue,
}
/// <summary>Determines what each layer does when <see cref="AnimancerNode.Stop"/> is called.</summary>
/// <remarks>
/// If empty, all layers will reset to their <see cref="ActionOnStop.DefaultState"/>.
/// <para></para>
/// If this array is smaller than the <see cref="AnimatorControllerPlayable.GetLayerCount"/>,
/// any additional layers will use the last value in this array.
/// </remarks>
public ActionOnStop[] ActionsOnStop { get; set; }
/// <summary>
/// The <see cref="AnimatorStateInfo.shortNameHash"/> of the default state on each layer,
/// used to reset to those states when <see cref="ApplyActionsOnStop"/>
/// is called for layers using <see cref="ActionOnStop.DefaultState"/>.
/// </summary>
/// <remarks>Gathered automatically by <see cref="GatherDefaultStates"/>.</remarks>
public int[] DefaultStateHashes { get; set; }
/************************************************************************************************************************/
#if UNITY_ASSERTIONS
/************************************************************************************************************************/
/// <summary>[Assert-Only] Animancer Events work badly on <see cref="ControllerState"/>s.</summary>
protected internal override string UnsupportedEventsMessage =>
"Animancer Events on " + nameof(ControllerState) + "s will probably not work as expected." +
" The events will be associated with the entire Animator Controller and be triggered by any of the" +
" states inside it. If you want to use events in an Animator Controller you will likely need to use" +
" Unity's regular Animation Event system.";
/************************************************************************************************************************/
#endif
/************************************************************************************************************************/
/// <summary>[Assert-Conditional] Asserts that the `value` is valid for a parameter.</summary>
/// <exception cref="ArgumentOutOfRangeException">The `value` is NaN or Infinity.</exception>
[System.Diagnostics.Conditional(Strings.Assertions)]
public void AssertParameterValue(float value, [CallerMemberName] string parameterName = null)
{
if (!value.IsFinite())
{
MarkAsUsed(this);
throw new ArgumentOutOfRangeException(parameterName, Strings.MustBeFinite);
}
}
/************************************************************************************************************************/
/// <summary>IK cannot be dynamically enabled on a <see cref="ControllerState"/>.</summary>
public override void CopyIKFlags(AnimancerNodeBase copyFrom) { }
/************************************************************************************************************************/
/// <summary>IK cannot be dynamically enabled on a <see cref="ControllerState"/>.</summary>
public override bool ApplyAnimatorIK
{
get => false;
set
{
#if UNITY_ASSERTIONS
if (value)
OptionalWarning.UnsupportedIK.Log(
$"IK cannot be dynamically enabled on a {nameof(ControllerState)}." +
" You must instead enable it on the desired layer inside the Animator Controller.",
_Controller);
#endif
}
}
/************************************************************************************************************************/
/// <summary>IK cannot be dynamically enabled on a <see cref="ControllerState"/>.</summary>
public override bool ApplyFootIK
{
get => false;
set
{
#if UNITY_ASSERTIONS
if (value)
OptionalWarning.UnsupportedIK.Log(
$"IK cannot be dynamically enabled on a {nameof(ControllerState)}." +
" You must instead enable it on the desired state inside the Animator Controller.",
_Controller);
#endif
}
}
/************************************************************************************************************************/
/// <summary>Returns the hash of a parameter being wrapped by this state.</summary>
/// <exception cref="NotSupportedException">This state doesn't wrap any parameters.</exception>
public virtual int GetParameterHash(int index)
{
MarkAsUsed(this);
throw new NotSupportedException();
}
/************************************************************************************************************************/
/// <inheritdoc/>
public virtual void GetParameters(List<StateParameterDetails> parameters) { }
/// <inheritdoc/>
public virtual void SetParameters(List<StateParameterDetails> parameters) { }
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Public API
/************************************************************************************************************************/
/// <summary>Creates a new <see cref="ControllerState"/> to play the `controller`.</summary>
public ControllerState(RuntimeAnimatorController controller)
{
_Controller = controller != null
? controller
: throw new ArgumentNullException(nameof(controller));
}
/// <summary>Creates a new <see cref="ControllerState"/> to play the `controller`.</summary>
public ControllerState(RuntimeAnimatorController controller, params ActionOnStop[] actionsOnStop)
: this(controller)
{
ActionsOnStop = actionsOnStop;
}
/************************************************************************************************************************/
/// <summary>Creates and assigns the <see cref="AnimatorControllerPlayable"/> managed by this state.</summary>
protected override void CreatePlayable(out Playable playable)
{
playable = _Playable = AnimatorControllerPlayable.Create(Graph._PlayableGraph, _Controller);
GatherDefaultStates();
DeserializeParameterBindings();
#if UNITY_ASSERTIONS
var animator = Graph.Component?.Animator;
if (animator != null && animator.runtimeAnimatorController != null)
{
var usingType = Graph.Component is HybridAnimancerComponent
? Graph.Component.GetType()
: GetType();
OptionalWarning.NativeControllerState.Log(
$"An Animator Controller is assigned to the {nameof(Animator)} component" +
$" while also using a {usingType.Name}." +
$" Most likely only one of them is being used so the other should be removed." +
$" See the documentation for more information:" +
$" {Strings.DocsURLs.AnimatorControllers.AsHtmlLink()}",
this);
}
#endif
}
/************************************************************************************************************************/
/// <summary>
/// Stores the values of all parameters and calls <see cref="AnimancerNode.RecreatePlayable"/>,
/// then restores the parameter values.
/// </summary>
public override void RecreatePlayable()
{
if (!_Playable.IsValid())
{
CreatePlayable();
return;
}
var parameterNameToValue = DictionaryPool.Acquire<string, object>();
var parameterCount = _Playable.GetParameterCount();
for (int i = 0; i < parameterCount; i++)
{
var parameter = _Playable.GetParameter(i);
var value = AnimancerUtilities.GetParameterValue(_Playable, parameter);
parameterNameToValue[parameter.name] = value;
}
base.RecreatePlayable();
parameterCount = _Playable.GetParameterCount();
for (int i = 0; i < parameterCount; i++)
{
var parameter = _Playable.GetParameter(i);
if (parameterNameToValue.TryGetValue(parameter.name, out var value))
AnimancerUtilities.TrySetParameterValue(_Playable, parameter, value);
}
DictionaryPool.Release(parameterNameToValue);
}
/************************************************************************************************************************/
/// <summary>
/// Returns the current state on the specified `layer`,
/// or the next state if it is currently in a transition.
/// </summary>
public AnimatorStateInfo GetStateInfo(int layerIndex)
{
if (!_Playable.IsValid())
return default;
Validate.AssertPlayable(this);
return _Playable.IsInTransition(layerIndex)
? _Playable.GetNextAnimatorStateInfo(layerIndex)
: _Playable.GetCurrentAnimatorStateInfo(layerIndex);
}
/************************************************************************************************************************/
/// <summary>
/// The <see cref="AnimatorStateInfo.normalizedTime"/> * <see cref="AnimatorStateInfo.length"/> of layer 0.
/// </summary>
public override double RawTime
{
get
{
var info = GetStateInfo(0);
return info.normalizedTime * info.length;
}
set
{
Validate.AssertPlayable(this);
_Playable.PlayInFixedTime(0, 0, (float)value);
// Setting the time requires it to be playing.
// This will leave it at the specified time.
if (!IsPlaying)
{
_Playable.Play();
Graph.RequirePostUpdate(this);
}
}
}
/************************************************************************************************************************/
/// <inheritdoc/>
int IUpdatable.UpdatableIndex { get; set; } = IUpdatable.List.NotInList;
/************************************************************************************************************************/
/// <summary>Pauses the <see cref="Playable"/> if necessary after <see cref="RawTime"/> was set.</summary>
void IUpdatable.Update()
{
if (!IsPlaying)
_Playable.Pause();
AnimancerGraph.Current.CancelPostUpdate(this);
}
/************************************************************************************************************************/
/// <inheritdoc/>
public override void SetGraph(AnimancerGraph graph)
{
if (Graph == graph)
return;
Graph?.CancelPostUpdate(this);
base.SetGraph(graph);
}
/************************************************************************************************************************/
/// <summary>The current <see cref="AnimatorStateInfo.length"/> of layer 0.</summary>
public override float Length => GetStateInfo(0).length;
/************************************************************************************************************************/
/// <summary>The current <see cref="AnimatorStateInfo.loop"/> of layer 0.</summary>
public override bool IsLooping => GetStateInfo(0).loop;
/************************************************************************************************************************/
/// <inheritdoc/>
public override AnimancerEvent.DispatchInfo GetEventDispatchInfo()
{
var state = GetStateInfo(0);
return new(
state.length,
state.normalizedTime,
state.loop);
}
/************************************************************************************************************************/
/// <summary>Gathers the <see cref="DefaultStateHashes"/> from the current states on each layer.</summary>
/// <remarks>This is called by <see cref="CreatePlayable(out UnityEngine.Playables.Playable)"/>.</remarks>
public void GatherDefaultStates()
{
Validate.AssertPlayable(this);
var layerCount = _Playable.GetLayerCount();
if (DefaultStateHashes == null || DefaultStateHashes.Length != layerCount)
DefaultStateHashes = new int[layerCount];
while (--layerCount >= 0)
DefaultStateHashes[layerCount] = _Playable.GetCurrentAnimatorStateInfo(layerCount).shortNameHash;
}
/************************************************************************************************************************/
/// <summary>
/// Stops the animation and makes it inactive immediately so it no longer affects the output.
/// Also calls <see cref="ApplyActionsOnStop"/>.
/// </summary>
protected internal override void StopWithoutWeight()
{
// Don't call base.StopWithoutWeight(); because it sets Time = 0;
// which uses PlayInFixedTime and interferes with resetting to the default states.
SetIsPlaying(false);
UpdateIsActive();
ApplyActionsOnStop();
_SmoothingVelocities?.Clear();
}
/// <summary>Applies the <see cref="ActionsOnStop"/> to their corresponding layers.</summary>
/// <exception cref="NullReferenceException"><see cref="DefaultStateHashes"/> is null.</exception>
public void ApplyActionsOnStop()
{
Validate.AssertPlayable(this);
var layerCount = Math.Min(DefaultStateHashes.Length, _Playable.GetLayerCount());
if (ActionsOnStop == null || ActionsOnStop.Length == 0)
{
for (int i = layerCount - 1; i >= 0; i--)
_Playable.Play(DefaultStateHashes[i], i, 0);
}
else
{
for (int i = layerCount - 1; i >= 0; i--)
{
var index = i < ActionsOnStop.Length
? i
: ActionsOnStop.Length - 1;
switch (ActionsOnStop[index])
{
case ActionOnStop.DefaultState:
_Playable.Play(DefaultStateHashes[i], i, 0);
break;
case ActionOnStop.RewindTime:
_Playable.Play(0, i, 0);
break;
case ActionOnStop.Continue:
break;
}
}
}
}
/************************************************************************************************************************/
/// <inheritdoc/>
public override void GatherAnimationClips(ICollection<AnimationClip> clips)
{
if (_Controller != null)
clips.Gather(_Controller.animationClips);
}
/************************************************************************************************************************/
/// <inheritdoc/>
public override void Destroy()
{
_Controller = null;
DisposeParameterBindings();
base.Destroy();
}
/************************************************************************************************************************/
/// <inheritdoc/>
public override AnimancerState Clone(CloneContext context)
{
var clone = new ControllerState(_Controller);
clone.CopyFrom(this, context);
return clone;
}
/************************************************************************************************************************/
/// <inheritdoc/>
public sealed override void CopyFrom(AnimancerState copyFrom, CloneContext context)
=> this.CopyFromBase(copyFrom, context);
/// <inheritdoc/>
public virtual void CopyFrom(ControllerState copyFrom, CloneContext context)
{
ActionsOnStop = copyFrom.ActionsOnStop;
if (copyFrom.Graph != null &&
Graph != null)
{
var layerCount = copyFrom._Playable.GetLayerCount();
for (int i = 0; i < layerCount; i++)
{
var info = copyFrom._Playable.GetCurrentAnimatorStateInfo(i);
_Playable.Play(info.shortNameHash, i, info.normalizedTime);
}
var parameterCount = copyFrom._Playable.GetParameterCount();
for (int i = 0; i < parameterCount; i++)
{
AnimancerUtilities.CopyParameterValue(
copyFrom._Playable,
_Playable,
copyFrom._Playable.GetParameter(i));
}
}
CopySmoothingVelocitiesFrom(copyFrom);
base.CopyFrom(copyFrom, context);
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Animator Controller Wrappers
/************************************************************************************************************************/
#region Cross Fade
/************************************************************************************************************************/
/// <summary>
/// The default constant for fade duration parameters which causes it to use the
/// <see cref="AnimancerGraph.DefaultFadeDuration"/> instead.
/// </summary>
public const float DefaultFadeDuration = -1;
/************************************************************************************************************************/
/// <summary>
/// Returns the `fadeDuration` if it is zero or positive.
/// Otherwise returns the <see cref="AnimancerGraph.DefaultFadeDuration"/>.
/// </summary>
public static float GetFadeDuration(float fadeDuration)
=> fadeDuration >= 0
? fadeDuration
: AnimancerGraph.DefaultFadeDuration;
/************************************************************************************************************************/
/// <summary>Starts a transition from the current state to the specified state using normalized times.</summary>
/// <remarks>If `fadeDuration` is negative, it uses the <see cref="AnimancerGraph.DefaultFadeDuration"/>.</remarks>
public void CrossFade(
int stateNameHash,
float fadeDuration = DefaultFadeDuration,
int layer = -1,
float normalizedTime = float.NegativeInfinity)
=> Playable.CrossFade(stateNameHash, GetFadeDuration(fadeDuration), layer, normalizedTime);
/************************************************************************************************************************/
/// <summary>Starts a transition from the current state to the specified state using normalized times.</summary>
/// <remarks>If `fadeDuration` is negative, it uses the <see cref="AnimancerGraph.DefaultFadeDuration"/>.</remarks>
public void CrossFade(
string stateName,
float fadeDuration = DefaultFadeDuration,
int layer = -1,
float normalizedTime = float.NegativeInfinity)
=> Playable.CrossFade(stateName, GetFadeDuration(fadeDuration), layer, normalizedTime);
/************************************************************************************************************************/
/// <summary>Starts a transition from the current state to the specified state using times in seconds.</summary>
/// <remarks>If `fadeDuration` is negative, it uses the <see cref="AnimancerGraph.DefaultFadeDuration"/>.</remarks>
public void CrossFadeInFixedTime(
int stateNameHash,
float fadeDuration = DefaultFadeDuration,
int layer = -1,
float fixedTime = 0)
=> Playable.CrossFadeInFixedTime(stateNameHash, GetFadeDuration(fadeDuration), layer, fixedTime);
/************************************************************************************************************************/
/// <summary>Starts a transition from the current state to the specified state using times in seconds.</summary>
/// <remarks>If `fadeDuration` is negative, it uses the <see cref="AnimancerGraph.DefaultFadeDuration"/>.</remarks>
public void CrossFadeInFixedTime(
string stateName,
float fadeDuration = DefaultFadeDuration,
int layer = -1,
float fixedTime = 0)
=> Playable.CrossFadeInFixedTime(stateName, GetFadeDuration(fadeDuration), layer, fixedTime);
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Play
/************************************************************************************************************************/
/// <summary>Plays the specified state immediately, starting from a particular normalized time.</summary>
public void Play(
int stateNameHash,
int layer = -1,
float normalizedTime = float.NegativeInfinity)
=> Playable.Play(stateNameHash, layer, normalizedTime);
/************************************************************************************************************************/
/// <summary>Plays the specified state immediately, starting from a particular normalized time.</summary>
public void Play(
string stateName,
int layer = -1,
float normalizedTime = float.NegativeInfinity)
=> Playable.Play(stateName, layer, normalizedTime);
/************************************************************************************************************************/
/// <summary>Plays the specified state immediately, starting from a particular time (in seconds).</summary>
public void PlayInFixedTime(
int stateNameHash,
int layer = -1,
float fixedTime = 0)
=> Playable.PlayInFixedTime(stateNameHash, layer, fixedTime);
/************************************************************************************************************************/
/// <summary>Plays the specified state immediately, starting from a particular time (in seconds).</summary>
public void PlayInFixedTime(
string stateName,
int layer = -1,
float fixedTime = 0)
=> Playable.PlayInFixedTime(stateName, layer, fixedTime);
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Parameters
/************************************************************************************************************************/
/// <summary>Gets the value of the specified boolean parameter.</summary>
public bool GetBool(int id)
=> Playable.GetBool(id);
/// <summary>Gets the value of the specified boolean parameter.</summary>
public bool GetBool(string name)
=> Playable.GetBool(name);
/// <summary>Sets the value of the specified boolean parameter.</summary>
public void SetBool(int id, bool value)
=> Playable.SetBool(id, value);
/// <summary>Sets the value of the specified boolean parameter.</summary>
public void SetBool(string name, bool value)
=> Playable.SetBool(name, value);
/// <summary>Gets the value of the specified float parameter.</summary>
public float GetFloat(int id)
=> Playable.GetFloat(id);
/// <summary>Gets the value of the specified float parameter.</summary>
public float GetFloat(string name)
=> Playable.GetFloat(name);
/// <summary>Sets the value of the specified float parameter.</summary>
public void SetFloat(int id, float value)
=> Playable.SetFloat(id, value);
/// <summary>Sets the value of the specified float parameter.</summary>
public void SetFloat(string name, float value)
=> Playable.SetFloat(name, value);
/// <summary>Gets the value of the specified integer parameter.</summary>
public int GetInteger(int id)
=> Playable.GetInteger(id);
/// <summary>Gets the value of the specified integer parameter.</summary>
public int GetInteger(string name)
=> Playable.GetInteger(name);
/// <summary>Sets the value of the specified integer parameter.</summary>
public void SetInteger(int id, int value)
=> Playable.SetInteger(id, value);
/// <summary>Sets the value of the specified integer parameter.</summary>
public void SetInteger(string name, int value)
=> Playable.SetInteger(name, value);
/// <summary>Sets the specified trigger parameter to true.</summary>
public void SetTrigger(int id)
=> Playable.SetTrigger(id);
/// <summary>Sets the specified trigger parameter to true.</summary>
///
public void SetTrigger(string name)
=> Playable.SetTrigger(name);
/// <summary>Resets the specified trigger parameter to false.</summary>
///
public void ResetTrigger(int id)
=> Playable.ResetTrigger(id);
/// <summary>Resets the specified trigger parameter to false.</summary>
///
public void ResetTrigger(string name)
=> Playable.ResetTrigger(name);
/// <summary>Indicates whether the specified parameter is controlled by an <see cref="AnimationClip"/>.</summary>
public bool IsParameterControlledByCurve(int id)
=> Playable.IsParameterControlledByCurve(id);
/// <summary>Indicates whether the specified parameter is controlled by an <see cref="AnimationClip"/>.</summary>
public bool IsParameterControlledByCurve(string name)
=> Playable.IsParameterControlledByCurve(name);
/// <summary>Gets the details of one of the <see cref="Controller"/>'s parameters.</summary>
public AnimatorControllerParameter GetParameter(int index)
=> Playable.GetParameter(index);
/// <summary>Gets the number of parameters in the <see cref="Controller"/>.</summary>
public int GetParameterCount()
=> Playable.GetParameterCount();
/************************************************************************************************************************/
/// <summary>The number of parameters in the <see cref="Controller"/>.</summary>
public int parameterCount => Playable.GetParameterCount();
/************************************************************************************************************************/
private AnimatorControllerParameter[] _Parameters;
/// <summary>The parameters in the <see cref="Controller"/>.</summary>
/// <remarks>
/// This property allocates a new array when first accessed. To avoid that, you can use
/// <see cref="GetParameterCount"/> and <see cref="GetParameter"/> instead.
/// </remarks>
public AnimatorControllerParameter[] parameters
{
get
{
if (_Parameters == null)
{
var count = GetParameterCount();
_Parameters = new AnimatorControllerParameter[count];
for (int i = 0; i < count; i++)
_Parameters[i] = GetParameter(i);
}
return _Parameters;
}
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Smoothed Set Float
/************************************************************************************************************************/
private Dictionary<int, float> _SmoothingVelocities;
/************************************************************************************************************************/
/// <summary>Sets the value of the specified float parameter with smoothing.</summary>
/// <remarks>Consider using a <see cref="SmoothedFloatParameter"/> instead.</remarks>
public float SetFloat(
string name,
float value,
float dampTime,
float deltaTime,
float maxSpeed = float.PositiveInfinity)
=> SetFloat(Animator.StringToHash(name), value, dampTime, deltaTime, maxSpeed);
/// <summary>Sets the value of the specified float parameter with smoothing.</summary>
/// <remarks>Consider using a <see cref="SmoothedFloatParameter"/> instead.</remarks>
public float SetFloat(
int id,
float value,
float dampTime,
float deltaTime,
float maxSpeed = float.PositiveInfinity)
{
_SmoothingVelocities ??= new();
_SmoothingVelocities.TryGetValue(id, out var velocity);
value = Mathf.SmoothDamp(GetFloat(id), value, ref velocity, dampTime, maxSpeed, deltaTime);
SetFloat(id, value);
_SmoothingVelocities[id] = velocity;
return value;
}
/************************************************************************************************************************/
/// <summary>Copies the smoothing velocities.</summary>
private void CopySmoothingVelocitiesFrom(ControllerState copyFrom)
{
if (copyFrom._SmoothingVelocities != null)
{
if (_SmoothingVelocities == null)
_SmoothingVelocities = new();
else
_SmoothingVelocities.Clear();
foreach (var item in copyFrom._SmoothingVelocities)
_SmoothingVelocities[item.Key] = item.Value;
}
else
{
_SmoothingVelocities?.Clear();
}
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Misc
/************************************************************************************************************************/
// Layers.
/************************************************************************************************************************/
/// <summary>Gets the weight of the layer at the specified index.</summary>
public float GetLayerWeight(int layerIndex)
=> Playable.GetLayerWeight(layerIndex);
/// <summary>Sets the weight of the layer at the specified index.</summary>
public void SetLayerWeight(int layerIndex, float weight)
=> Playable.SetLayerWeight(layerIndex, weight);
/// <summary>Gets the number of layers in the <see cref="Controller"/>.</summary>
public int GetLayerCount()
=> Playable.GetLayerCount();
/// <summary>The number of layers in the <see cref="Controller"/>.</summary>
public int layerCount
=> Playable.GetLayerCount();
/// <summary>Gets the index of the layer with the specified name.</summary>
public int GetLayerIndex(string layerName)
=> Playable.GetLayerIndex(layerName);
/// <summary>Gets the name of the layer with the specified index.</summary>
public string GetLayerName(int layerIndex)
=> Playable.GetLayerName(layerIndex);
/************************************************************************************************************************/
// States.
/************************************************************************************************************************/
/// <summary>Returns information about the current state.</summary>
public AnimatorStateInfo GetCurrentAnimatorStateInfo(int layerIndex = 0)
=> Playable.GetCurrentAnimatorStateInfo(layerIndex);
/// <summary>Returns information about the next state being transitioned towards.</summary>
public AnimatorStateInfo GetNextAnimatorStateInfo(int layerIndex = 0)
=> Playable.GetNextAnimatorStateInfo(layerIndex);
/// <summary>Indicates whether the specified layer contains the specified state.</summary>
public bool HasState(int layerIndex, int stateID)
=> Playable.HasState(layerIndex, stateID);
/************************************************************************************************************************/
// Transitions.
/************************************************************************************************************************/
/// <summary>Indicates whether the specified layer is currently executing a transition.</summary>
public bool IsInTransition(int layerIndex = 0)
=> Playable.IsInTransition(layerIndex);
/// <summary>Gets information about the current transition.</summary>
public AnimatorTransitionInfo GetAnimatorTransitionInfo(int layerIndex = 0)
=> Playable.GetAnimatorTransitionInfo(layerIndex);
/************************************************************************************************************************/
// Clips.
/************************************************************************************************************************/
/// <summary>Gets information about the <see cref="AnimationClip"/>s currently being played.</summary>
public AnimatorClipInfo[] GetCurrentAnimatorClipInfo(int layerIndex = 0)
=> Playable.GetCurrentAnimatorClipInfo(layerIndex);
/// <summary>Gets information about the <see cref="AnimationClip"/>s currently being played.</summary>
public void GetCurrentAnimatorClipInfo(int layerIndex, List<AnimatorClipInfo> clips)
=> Playable.GetCurrentAnimatorClipInfo(layerIndex, clips);
/// <summary>Gets the number of <see cref="AnimationClip"/>s currently being played.</summary>
///
public int GetCurrentAnimatorClipInfoCount(int layerIndex = 0)
=> Playable.GetCurrentAnimatorClipInfoCount(layerIndex);
/// <summary>Gets information about the <see cref="AnimationClip"/>s currently being transitioned towards.</summary>
public AnimatorClipInfo[] GetNextAnimatorClipInfo(int layerIndex = 0)
=> Playable.GetNextAnimatorClipInfo(layerIndex);
/// <summary>Gets information about the <see cref="AnimationClip"/>s currently being transitioned towards.</summary>
public void GetNextAnimatorClipInfo(int layerIndex, List<AnimatorClipInfo> clips)
=> Playable.GetNextAnimatorClipInfo(layerIndex, clips);
/// <summary>Gets the number of <see cref="AnimationClip"/>s currently being transitioned towards.</summary>
public int GetNextAnimatorClipInfoCount(int layerIndex = 0)
=> Playable.GetNextAnimatorClipInfoCount(layerIndex);
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
}
}

View File

@@ -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:

View File

@@ -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

View File

@@ -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:

View File

@@ -0,0 +1,305 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Playables;
namespace Animancer
{
/// <summary>A list of <see cref="AnimancerLayer"/>s with methods to control their mixing and masking.</summary>
/// <remarks>
/// The default implementation of this class is <see cref="AnimancerLayerMixerList"/>.
/// <para></para>
/// <strong>Documentation:</strong>
/// <see href="https://kybernetik.com.au/animancer/docs/manual/blending/layers">
/// Layers</see>
/// </remarks>
/// https://kybernetik.com.au/animancer/api/Animancer/AnimancerLayerList
public abstract class AnimancerLayerList :
IEnumerable<AnimancerLayer>,
IAnimationClipCollection
{
/************************************************************************************************************************/
#region Fields
/************************************************************************************************************************/
/// <summary>The <see cref="AnimancerGraph"/> containing this list.</summary>
public readonly AnimancerGraph Graph;
/// <summary>The layers which each manage their own set of animations.</summary>
/// <remarks>This field should never be null so it shouldn't need null-checking.</remarks>
private AnimancerLayer[] _Layers;
/// <summary>The number of layers that have actually been created.</summary>
private int _Count;
/// <summary>The <see cref="UnityEngine.Playables.Playable"/> which blends the layers.</summary>
public Playable Playable { get; protected set; }
/************************************************************************************************************************/
/// <summary>Creates a new <see cref="AnimancerLayerList"/>.</summary>
/// <remarks>The <see cref="Playable"/> must be assigned by the end of the derived constructor.</remarks>
protected AnimancerLayerList(AnimancerGraph graph, int capacity)
{
Graph = graph;
_Layers = new AnimancerLayer[capacity];
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region List Operations
/************************************************************************************************************************/
/// <summary>[Pro-Only] The number of layers in this list.</summary>
/// <exception cref="ArgumentOutOfRangeException">
/// The value is set higher than the <see cref="DefaultCapacity"/>. This is simply a safety measure,
/// so if you do actually need more layers you can just increase the limit.
/// </exception>
/// <exception cref="IndexOutOfRangeException">The value is set to a negative number.</exception>
public int Count
{
get => _Count;
set
{
var count = _Count;
if (value == count)
return;
CheckAgain:
if (value > count)// Increasing.
{
Add();
count++;
goto CheckAgain;
}
else// Decreasing.
{
while (value < count--)
{
var layer = _Layers[count];
if (layer._Playable.IsValid())
Graph._PlayableGraph.DestroySubgraph(layer._Playable);
layer.DestroyStates();
}
Array.Clear(_Layers, value, _Count - value);
_Count = value;
Playable.SetInputCount(value);
}
}
}
/************************************************************************************************************************/
/// <summary>[Pro-Only]
/// If the <see cref="Count"/> is below the specified `min`, this method increases it to that value.
/// </summary>
public void SetMinCount(int min)
{
if (Count < min)
Count = min;
}
/************************************************************************************************************************/
/// <summary>[Pro-Only]
/// The maximum number of layers that can be created before an <see cref="ArgumentOutOfRangeException"/> will
/// be thrown (default 4).
/// <para></para>
/// Lowering this value will not affect layers that have already been created.
/// </summary>
/// <remarks>
/// <strong>Example:</strong>
/// To set this value automatically when the application starts, place a method like this in any class:
/// <para></para><code>
/// [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
/// private static void SetMaxLayerCount()
/// {
/// Animancer.AnimancerLayerList.DefaultCapacity = 8;
/// }
/// </code>
/// Otherwise you can set the <see cref="Capacity"/> of each individual list:
/// <para></para><code>
/// AnimancerComponent animancer;
/// animancer.Layers.Capacity = 8;
/// </code></remarks>
public static int DefaultCapacity { get; set; } = 4;
/// <summary>[Pro-Only]
/// If the <see cref="DefaultCapacity"/> is below the specified `min`, this method increases it to that value.
/// </summary>
public static void SetMinDefaultCapacity(int min)
{
if (DefaultCapacity < min)
DefaultCapacity = min;
}
/************************************************************************************************************************/
/// <summary>[Pro-Only]
/// The maximum number of layers that can be created before an <see cref="ArgumentOutOfRangeException"/> will
/// be thrown. The initial capacity is determined by <see cref="DefaultCapacity"/>.
/// </summary>
///
/// <remarks>
/// Lowering this value will destroy any layers beyond the specified value.
/// <para></para>
/// Changing this value will cause the allocation of a new array and garbage collection of the old one,
/// so you should generally set the <see cref="DefaultCapacity"/> before initializing this list.
/// </remarks>
///
/// <exception cref="ArgumentOutOfRangeException">The value is not greater than 0.</exception>
public int Capacity
{
get => _Layers.Length;
set
{
if (value <= 0)
throw new ArgumentOutOfRangeException(nameof(Capacity), $"must be greater than 0 ({value} <= 0)");
if (_Count > value)
Count = value;
Array.Resize(ref _Layers, value);
}
}
/************************************************************************************************************************/
/// <summary>[Pro-Only] Creates and returns a new <see cref="AnimancerLayer"/> at the end of this list.</summary>
/// <remarks>If the <see cref="Capacity"/> would be exceeded, it will be doubled.</remarks>
public virtual AnimancerLayer Add()
{
var index = _Count;
if (index >= _Layers.Length)
Capacity *= 2;
var layer = new AnimancerLayer(Graph, index);
_Count = index + 1;
Playable.SetInputCount(_Count);
Graph._PlayableGraph.Connect(Playable, layer._Playable, index, 0);
_Layers[index] = layer;
return layer;
}
/************************************************************************************************************************/
/// <summary>Returns the layer at the specified index. If it didn't already exist, this method creates it.</summary>
/// <remarks>To only get an existing layer without creating new ones, use <see cref="GetLayer"/> instead.</remarks>
public AnimancerLayer this[int index]
{
get
{
SetMinCount(index + 1);
return _Layers[index];
}
}
/************************************************************************************************************************/
/// <summary>Returns the layer at the specified index.</summary>
/// <remarks>To create a new layer if the target doesn't exist, use <see cref="this[int]"/> instead.</remarks>
public AnimancerLayer GetLayer(int index)
=> _Layers[index];
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Enumeration
/************************************************************************************************************************/
/// <summary>Returns an enumerator that will iterate through all layers.</summary>
public FastEnumerator<AnimancerLayer> GetEnumerator()
=> new(_Layers, _Count);
/// <inheritdoc/>
IEnumerator<AnimancerLayer> IEnumerable<AnimancerLayer>.GetEnumerator()
=> GetEnumerator();
/// <inheritdoc/>
IEnumerator IEnumerable.GetEnumerator()
=> GetEnumerator();
/************************************************************************************************************************/
/// <summary>[<see cref="IAnimationClipCollection"/>] Gathers all the animations in all layers.</summary>
public void GatherAnimationClips(ICollection<AnimationClip> clips)
=> clips.GatherFromSource(_Layers);
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Layer Details
/************************************************************************************************************************/
/// <summary>[Pro-Only]
/// Is the layer at the specified index is set to additive blending?
/// Otherwise it will override lower layers.
/// </summary>
public virtual bool IsAdditive(int index)
=> false;
/// <summary>[Pro-Only]
/// Sets the layer at the specified index to blend additively with earlier layers (if true)
/// or to override them (if false). Newly created layers will override by default.
/// </summary>
public virtual void SetAdditive(int index, bool value) { }
/************************************************************************************************************************/
/// <summary>[Pro-Only]
/// Sets an <see cref="AvatarMask"/> to determine which bones the layer at the specified index will affect.
/// </summary>
/// <remarks>
/// Don't assign the same mask repeatedly unless you have modified it.
/// This property doesn't check if the mask is the same
/// so repeatedly assigning the same thing will simply waste performance.
/// </remarks>
public virtual void SetMask(int index, AvatarMask mask) { }
/************************************************************************************************************************/
/// <summary>[Editor-Conditional] Sets the Inspector display name of the layer at the specified index.</summary>
[System.Diagnostics.Conditional(Strings.UnityEditor)]
public void SetDebugName(int index, string name)
=> this[index].SetDebugName(name);
/************************************************************************************************************************/
/// <summary>
/// The average velocity of the root motion of all currently playing animations,
/// taking their current <see cref="AnimancerNode.Weight"/> into account.
/// </summary>
public Vector3 AverageVelocity
{
get
{
var velocity = default(Vector3);
for (int i = 0; i < _Count; i++)
{
var layer = _Layers[i];
velocity += layer.AverageVelocity * layer.Weight;
}
return velocity;
}
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
}
}

View File

@@ -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:

View File

@@ -0,0 +1,75 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
using UnityEngine;
using UnityEngine.Animations;
namespace Animancer
{
/// <summary>An <see cref="AnimancerLayerList"/> which uses an <see cref="AnimationLayerMixerPlayable"/>.</summary>
/// <remarks>
/// <strong>Documentation:</strong>
/// <see href="https://kybernetik.com.au/animancer/docs/manual/blending/layers">
/// Layers</see>
/// </remarks>
/// https://kybernetik.com.au/animancer/api/Animancer/AnimancerLayerMixerList
public class AnimancerLayerMixerList : AnimancerLayerList
{
/************************************************************************************************************************/
/// <summary>Creates a new <see cref="AnimancerLayerMixerList"/>.</summary>
public AnimancerLayerMixerList(AnimancerGraph graph)
: base(graph, DefaultCapacity)
{
LayerMixer = AnimationLayerMixerPlayable.Create(graph._PlayableGraph, 1);
Playable = LayerMixer;
}
/************************************************************************************************************************/
/// <summary>The <see cref="AnimationLayerMixerPlayable"/> which blends the layers.</summary>
public AnimationLayerMixerPlayable LayerMixer { get; protected set; }
/************************************************************************************************************************/
/// <inheritdoc/>
public override bool IsAdditive(int index)
=> LayerMixer.IsLayerAdditive((uint)index);
/// <inheritdoc/>
public override void SetAdditive(int index, bool value)
{
SetMinCount(index + 1);
LayerMixer.SetLayerAdditive((uint)index, value);
}
/************************************************************************************************************************/
private static AvatarMask _DefaultMask;
/// <inheritdoc/>
public override void SetMask(int index, AvatarMask mask)
{
var layer = this[index];
if (mask == null)
{
// If the existing mask was already null, do nothing.
// If it was destroyed, we still need to continue and set it to the default.
if (layer._Mask is null)
return;
_DefaultMask ??= new();
mask = _DefaultMask;
}
// Don't check if the same mask was already assigned because it might have been modified.
layer._Mask = mask;
LayerMixer.SetLayerMaskFromAvatarMask((uint)index, mask);
}
/************************************************************************************************************************/
}
}

View File

@@ -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:

View File

@@ -0,0 +1,537 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;
using UnityEngine;
namespace Animancer
{
/// <summary>A dictionary of <see cref="AnimancerState"/>s mapped to their <see cref="AnimancerState.Key"/>.</summary>
/// <remarks>
/// <strong>Documentation:</strong>
/// <see href="https://kybernetik.com.au/animancer/docs/manual/playing/states">
/// States</see>
/// </remarks>
/// https://kybernetik.com.au/animancer/api/Animancer/AnimancerStateDictionary
public class AnimancerStateDictionary :
IAnimationClipCollection,
IEnumerable<AnimancerState>
{
/************************************************************************************************************************/
/// <summary>The <see cref="AnimancerGraph"/> at the root of the graph.</summary>
private readonly AnimancerGraph Graph;
/************************************************************************************************************************/
/// <summary><see cref="AnimancerState.Key"/> mapped to <see cref="AnimancerState"/>.</summary>
private readonly Dictionary<object, AnimancerState>
States = new();
/************************************************************************************************************************/
/// <summary>[Internal] Creates a new <see cref="AnimancerStateDictionary"/>.</summary>
internal AnimancerStateDictionary(AnimancerGraph graph)
=> Graph = graph;
/************************************************************************************************************************/
/// <summary>The number of states that have been registered with a <see cref="AnimancerState.Key"/>.</summary>
public int Count
=> States.Count;
/************************************************************************************************************************/
/// <inheritdoc/>
public void AppendDescriptionOrOrphans(
StringBuilder text,
string separator = "\n")
{
string stateSeparator = null;
foreach (var state in States.Values)
{
if (state.Parent != null)
continue;
if (stateSeparator is null)
{
text.Append(separator)
.Append("Orphan States:");
separator += Strings.Indent;
stateSeparator = separator + Strings.Indent;
}
text.Append(separator);
state.AppendDescription(text, stateSeparator);
}
}
/************************************************************************************************************************/
#region Create
/************************************************************************************************************************/
/// <summary>Creates and returns a new <see cref="ClipState"/> to play the `clip`.</summary>
/// <remarks>
/// To create a state on a specific layer, use <c>animancer.Layers[x].CreateState(clip)</c> instead.
/// <para></para>
/// <see cref="AnimancerGraph.GetKey"/> is used to determine the <see cref="AnimancerState.Key"/>.
/// </remarks>
public ClipState Create(AnimationClip clip)
=> Create(Graph.GetKey(clip), clip);
/// <summary>
/// Creates and returns a new <see cref="ClipState"/> to play the `clip` and registers it with the `key`.
/// </summary>
/// <remarks>
/// To create a state on a specific layer, use <c>animancer.Layers[x].CreateState(key, clip)</c> instead.
/// </remarks>
public ClipState Create(object key, AnimationClip clip)
{
var state = new ClipState(clip);
state.SetGraph(Graph);
state._Key = key;
Register(state);
return state;
}
/************************************************************************************************************************/
/// <summary>Calls <see cref="GetOrCreate(AnimationClip, bool)"/> for each of the specified clips.</summary>
public void CreateIfNew(AnimationClip clip0, AnimationClip clip1)
{
GetOrCreate(clip0);
GetOrCreate(clip1);
}
/// <summary>Calls <see cref="GetOrCreate(AnimationClip, bool)"/> for each of the specified clips.</summary>
public void CreateIfNew(AnimationClip clip0, AnimationClip clip1, AnimationClip clip2)
{
GetOrCreate(clip0);
GetOrCreate(clip1);
GetOrCreate(clip2);
}
/// <summary>Calls <see cref="GetOrCreate(AnimationClip, bool)"/> for each of the specified clips.</summary>
public void CreateIfNew(AnimationClip clip0, AnimationClip clip1, AnimationClip clip2, AnimationClip clip3)
{
GetOrCreate(clip0);
GetOrCreate(clip1);
GetOrCreate(clip2);
GetOrCreate(clip3);
}
/// <summary>Calls <see cref="GetOrCreate(AnimationClip, bool)"/> for each of the specified `clips`.</summary>
public void CreateIfNew(params AnimationClip[] clips)
{
if (clips == null)
return;
var count = clips.Length;
for (int i = 0; i < count; i++)
{
var clip = clips[i];
if (clip != null)
GetOrCreate(clip);
}
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Access
/************************************************************************************************************************/
/// <summary>
/// The <see cref="AnimancerLayer.CurrentState"/> on layer 0.
/// <para></para>
/// Specifically, this is the state that was most recently started using any of the Play methods on that layer.
/// States controlled individually via methods in the <see cref="AnimancerState"/> itself will not register in
/// this property.
/// </summary>
public AnimancerState Current
=> Graph.Layers[0].CurrentState;
/************************************************************************************************************************/
/// <summary>Calls <see cref="AnimancerGraph.GetKey"/> then returns the state registered with that key.</summary>
/// <exception cref="ArgumentNullException">The key is null.</exception>
/// <exception cref="KeyNotFoundException">No state is registered with the key.</exception>
public AnimancerState this[AnimationClip clip]
=> States[Graph.GetKey(clip)];
/// <summary>Returns the state registered with the <see cref="IHasKey.Key"/>.</summary>
/// <exception cref="ArgumentNullException">The `key` is null.</exception>
/// <exception cref="KeyNotFoundException">No state is registered with the `key`.</exception>
public AnimancerState this[IHasKey hasKey]
=> States[hasKey.Key];
/// <summary>Returns the state registered with the `key`.</summary>
/// <exception cref="ArgumentNullException">The `key` is null.</exception>
/// <exception cref="KeyNotFoundException">No state is registered with the `key`.</exception>
public AnimancerState this[object key]
=> States[key];
/************************************************************************************************************************/
/// <summary>
/// Calls <see cref="AnimancerGraph.GetKey"/> then passes the key to
/// <see cref="TryGet(object, out AnimancerState)"/> and returns the result.
/// </summary>
public bool TryGet(AnimationClip clip, out AnimancerState state)
{
if (clip == null)
{
state = null;
return false;
}
return TryGet(Graph.GetKey(clip), out state);
}
/// <summary>
/// Passes the <see cref="IHasKey.Key"/> into <see cref="TryGet(object, out AnimancerState)"/>
/// and returns the result.
/// </summary>
public bool TryGet(IHasKey hasKey, out AnimancerState state)
=> TryGet(hasKey?.Key, out state);
/// <summary>
/// If a `state` is registered with the `key`, this method outputs it and returns true.
/// Otherwise the `state` is set to null and this method returns false.
/// </summary>
public bool TryGet(object key, out AnimancerState state)
{
if (key == null)
{
state = null;
return false;
}
return States.TryGetValue(key, out state);
}
/************************************************************************************************************************/
/// <summary>
/// Passes the <see cref="IHasKey.Key"/> into <see cref="TryGetAlias(object, out AnimancerState)"/>
/// and returns the result.
/// </summary>
public bool TryGetAlias(IHasKey hasKey, out AnimancerState state)
=> TryGetAlias(hasKey?.Key, out state);
/// <summary>
/// If a `state` is registered with the `key` or the <see cref="TransitionLibraries.TransitionLibrary"/>,
/// is using it as an alias, this method outputs it and returns true.
/// Otherwise the `state` is set to null and this method returns false.
/// </summary>
public bool TryGetAlias(object key, out AnimancerState state)
{
if (key == null)
{
state = null;
return false;
}
if (Graph.Transitions != null &&
Graph.Transitions.TryGetTransition(key, out var group))
key = group.Transition.Key;
return States.TryGetValue(key, out state);
}
/************************************************************************************************************************/
/// <summary>
/// Calls <see cref="AnimancerGraph.GetKey"/> and returns the state registered with that key
/// or creates one if it doesn't exist.
/// </summary>
/// <remarks>
/// If the state already exists but has the wrong <see cref="AnimancerState.Clip"/>, the `allowSetClip`
/// parameter determines what will happen. False causes it to throw an <see cref="ArgumentException"/> while
/// true allows it to change the <see cref="AnimancerState.Clip"/>. Note that the change is somewhat costly to
/// performance so use with caution.
/// </remarks>
/// <exception cref="ArgumentException"/>
public AnimancerState GetOrCreate(AnimationClip clip, bool allowSetClip = false)
=> GetOrCreate(Graph.GetKey(clip), clip, allowSetClip);
/// <summary>
/// Returns the state registered with the `transition`s <see cref="IHasKey.Key"/> if there is one.
/// Otherwise this method uses <see cref="ITransition.CreateState"/> to create a new one
/// and registers it with that key before returning it.
/// </summary>
public AnimancerState GetOrCreate(ITransition transition)
{
var key = transition.Key;
if (!TryGet(key, out var state))
{
state = transition.CreateState();
state._Key = key;
state.SetGraph(Graph);
}
return state;
}
/// <summary>
/// Returns the state which registered with the `key` or creates one if it doesn't exist.
/// <para></para>
/// If the state already exists but has the wrong <see cref="AnimancerState.Clip"/>, the `allowSetClip`
/// parameter determines what will happen. False causes it to throw an <see cref="ArgumentException"/>
/// while true allows it to change the <see cref="AnimancerState.Clip"/>.
/// Note that the change is somewhat costly to performance so use with caution.
/// </summary>
/// <exception cref="ArgumentException"/>
/// <remarks>See also: <see cref="AnimancerLayer.GetOrCreateState(object, AnimationClip, bool)"/></remarks>
public AnimancerState GetOrCreate(object key, AnimationClip clip, bool allowSetClip = false)
{
if (TryGet(key, out var state))
{
// If a state exists with the 'key' but has the wrong clip, either change it or complain.
if (!ReferenceEquals(state.Clip, clip))
{
if (allowSetClip)
{
state.Clip = clip;
}
else
{
throw new ArgumentException(GetClipMismatchError(key, state.Clip, clip));
}
}
}
else
{
state = Create(key, clip);
}
return state;
}
/************************************************************************************************************************/
/// <summary>Returns an error message explaining that a state already exists with the specified `key`.</summary>
public static string GetClipMismatchError(object key, AnimationClip oldClip, AnimationClip newClip)
=> $"A state already exists using the specified '{nameof(key)}', but has a different {nameof(AnimationClip)}:" +
$"\n• Key: {key}" +
$"\n• Old Clip: {oldClip}" +
$"\n• New Clip: {newClip}";
/************************************************************************************************************************/
/// <summary>[Internal]
/// Registers the `state` in this dictionary so the <see cref="AnimancerState.Key"/> can be used to get it
/// later on using any of the lookup methods such as <see cref="this[object]"/> or
/// <see cref="TryGet(object, out AnimancerState)"/>.
/// </summary>
/// <remarks>Does nothing if the <see cref="AnimancerState.Key"/> is <c>null</c>.</remarks>
internal void Register(AnimancerState state)
{
var key = state._Key;
if (key != null)
{
#if UNITY_ASSERTIONS
if (state.Graph != Graph)
throw new ArgumentException(
$"{nameof(AnimancerStateDictionary)} cannot register a state with a different {nameof(Graph)}: " + state);
#endif
States.Add(key, state);
}
}
/// <summary>[Internal] Removes the `state` from this dictionary (the opposite of <see cref="Register"/>).</summary>
internal void Unregister(AnimancerState state)
{
var key = state._Key;
if (key != null)
States.Remove(key);
}
/************************************************************************************************************************/
#region Enumeration
/************************************************************************************************************************/
// IEnumerable for 'foreach' statements.
/************************************************************************************************************************/
/// <summary>Returns an enumerator that will iterate through all registered states.</summary>
public Dictionary<object, AnimancerState>.ValueCollection.Enumerator GetEnumerator()
=> States.Values.GetEnumerator();
/// <inheritdoc/>
IEnumerator<AnimancerState> IEnumerable<AnimancerState>.GetEnumerator()
=> GetEnumerator();
/// <inheritdoc/>
IEnumerator IEnumerable.GetEnumerator()
=> GetEnumerator();
/************************************************************************************************************************/
/// <summary>[<see cref="IAnimationClipCollection"/>]
/// Adds all the animations of states with a <see cref="AnimancerState.Key"/> to the `clips`.
/// </summary>
public void GatherAnimationClips(ICollection<AnimationClip> clips)
{
foreach (var state in States.Values)
clips.GatherFromSource(state);
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Destroy
/************************************************************************************************************************/
/// <summary>
/// Calls <see cref="AnimancerState.Destroy"/> on the state associated with the `clip` (if any).
/// Returns true if the state existed.
/// </summary>
public bool Destroy(AnimationClip clip)
{
if (clip == null)
return false;
return Destroy(Graph.GetKey(clip));
}
/// <summary>
/// Calls <see cref="AnimancerState.Destroy"/> on the state associated with the <see cref="IHasKey.Key"/>
/// (if any). Returns true if the state existed.
/// </summary>
public bool Destroy(IHasKey hasKey)
{
if (hasKey == null)
return false;
return Destroy(hasKey.Key);
}
/// <summary>
/// Calls <see cref="AnimancerState.Destroy"/> on the state associated with the `key` (if any).
/// Returns true if the state existed.
/// </summary>
public bool Destroy(object key)
{
if (!TryGet(key, out var state))
return false;
state.Destroy();
return true;
}
/************************************************************************************************************************/
/// <summary>Calls <see cref="Destroy(AnimationClip)"/> on each of the `clips`.</summary>
public void DestroyAll(IList<AnimationClip> clips)
{
if (clips == null)
return;
for (int i = clips.Count - 1; i >= 0; i--)
Destroy(clips[i]);
}
/// <summary>Calls <see cref="Destroy(AnimationClip)"/> on each of the `clips`.</summary>
public void DestroyAll(IEnumerable<AnimationClip> clips)
{
if (clips == null)
return;
foreach (var clip in clips)
Destroy(clip);
}
/************************************************************************************************************************/
/// <summary>
/// Calls <see cref="Destroy(AnimationClip)"/> on all states gathered by
/// <see cref="IAnimationClipSource.GetAnimationClips"/>.
/// </summary>
public void DestroyAll(IAnimationClipSource source)
{
if (source == null)
return;
var clips = ListPool.Acquire<AnimationClip>();
source.GetAnimationClips(clips);
DestroyAll(clips);
ListPool.Release(clips);
}
/// <summary>
/// Calls <see cref="Destroy(AnimationClip)"/> on all states gathered by
/// <see cref="IAnimationClipCollection.GatherAnimationClips"/>.
/// </summary>
public void DestroyAll(IAnimationClipCollection source)
{
if (source == null)
return;
var clips = SetPool.Acquire<AnimationClip>();
source.GatherAnimationClips(clips);
DestroyAll(clips);
SetPool.Release(clips);
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Key Error Methods
#if UNITY_EDITOR
/************************************************************************************************************************/
// These are overloads of other methods that take a System.Object key to ensure the user doesn't try to use an
// AnimancerState as a key, since the whole point of a key is to identify a state in the first place.
/************************************************************************************************************************/
/// <summary>[Warning]
/// You should not use an <see cref="AnimancerState"/> as a key.
/// The whole point of a key is to identify a state in the first place.
/// </summary>
[Obsolete("You should not use an AnimancerState as a key. The whole point of a key is to identify a state in the first place.", true)]
public AnimancerState this[AnimancerState key]
=> key;
/// <summary>[Warning]
/// You should not use an <see cref="AnimancerState"/> as a key.
/// The whole point of a key is to identify a state in the first place.
/// </summary>
[Obsolete("You should not use an AnimancerState as a key. The whole point of a key is to identify a state in the first place.", true)]
public bool TryGet(AnimancerState key, out AnimancerState state)
{
state = key;
return true;
}
/// <summary>[Warning]
/// You should not use an <see cref="AnimancerState"/> as a key.
/// The whole point of a key is to identify a state in the first place.
/// </summary>
[Obsolete("You should not use an AnimancerState as a key. The whole point of a key is to identify a state in the first place.", true)]
public AnimancerState GetOrCreate(AnimancerState key, AnimationClip clip)
=> key;
/// <summary>[Warning]
/// You should not use an <see cref="AnimancerState"/> as a key.
/// Just call <see cref="AnimancerState.Destroy"/>.
/// </summary>
[Obsolete("You should not use an AnimancerState as a key. Just call AnimancerState.Destroy.", true)]
public bool Destroy(AnimancerState key)
{
key.Destroy();
return true;
}
/************************************************************************************************************************/
#endif
#endregion
/************************************************************************************************************************/
}
}

View File

@@ -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

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 5951f73829d4fe5478c69d982a08e77e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 89969125b8d531b45b2e18a548ac6b92
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,551 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
using System;
using System.Runtime.CompilerServices;
using System.Text;
using UnityEngine;
namespace Animancer
{
/// https://kybernetik.com.au/animancer/api/Animancer/AnimancerEvent
partial struct AnimancerEvent
{
/// <summary>Details about a state which determine how it triggers events.</summary>
/// https://kybernetik.com.au/animancer/api/Animancer/DispatchInfo
public readonly struct DispatchInfo
{
/************************************************************************************************************************/
/// <summary><see cref="AnimancerState.Length"/></summary>
public readonly float Length;
/// <summary><see cref="AnimancerState.NormalizedTime"/></summary>
public readonly float NormalizedTime;
/// <summary><see cref="AnimancerState.IsLooping"/></summary>
public readonly bool IsLooping;
/************************************************************************************************************************/
/// <summary>Creates a new <see cref="DispatchInfo"/>.</summary>
public DispatchInfo(float length, float normalizedTime, bool isLooping)
{
Length = length;
NormalizedTime = normalizedTime;
IsLooping = isLooping;
}
/************************************************************************************************************************/
}
/// <summary>
/// A system which triggers events in an <see cref="Sequence"/>
/// based on a target <see cref="State"/>.
/// </summary>
/// https://kybernetik.com.au/animancer/api/Animancer/Dispatcher
public class Dispatcher : IHasDescription
{
/************************************************************************************************************************/
/// <summary>The target state.</summary>
public readonly AnimancerState State;
/// <summary>
/// <see cref="AnimancerState.OwnedEvents"/> and
/// <see cref="AnimancerState.SharedEvents"/>.
/// </summary>
/// <remarks>Should never be null.</remarks>
public Sequence Events { get; private set; }
/// <summary><see cref="AnimancerState.HasOwnedEvents"/></summary>
public bool HasOwnEvents { get; private set; }
private float _PreviousNormalizedTime;
private int _NextEventIndex = RecalculateEventIndex;
private int _SequenceVersion = -1;// When version changes, next event index is invalid.
private bool _WasPlayingForwards;// When direction changes, next event index is invalid.
/// <summary>
/// A special value for the <see cref="_NextEventIndex"/>
/// which indicates that it needs to be recalculated.
/// </summary>
private const int RecalculateEventIndex = int.MinValue;
/************************************************************************************************************************/
/// <summary>Creates a new <see cref="Dispatcher"/>.</summary>
public Dispatcher(AnimancerState state)
{
State = state;
_PreviousNormalizedTime = state.NormalizedTime;
#if UNITY_ASSERTIONS
OptionalWarning.UnsupportedEvents.Log(state.UnsupportedEventsMessage, state.Graph?.Component);
#endif
}
/************************************************************************************************************************/
/// <summary>
/// Setters for <see cref="AnimancerState.OwnedEvents"/>
/// and <see cref="AnimancerState.SharedEvents"/>.
/// </summary>
public void SetEvents(Sequence events, bool isOwned)
{
Events = events;
_NextEventIndex = RecalculateEventIndex;
_SequenceVersion = Events.Version;
HasOwnEvents = isOwned;
}
/************************************************************************************************************************/
/// <summary>Sets <see cref="HasOwnEvents"/> to <c>false</c>.</summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void DismissEventOwnership()
=> HasOwnEvents = false;
/************************************************************************************************************************/
/// <summary><see cref="AnimancerState.Events(object, out Sequence)"/>.</summary>
public bool InitializeEvents(out Sequence events)
{
if (HasOwnEvents)
{
events = Events;
return false;
}
Events = events = new(Events);
_NextEventIndex = RecalculateEventIndex;
_SequenceVersion = Events.Version;
HasOwnEvents = true;
return true;
}
/************************************************************************************************************************/
/// <summary>[Internal]
/// Notifies this dispatcher that the target's <see cref="Time"/> has changed.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal void OnSetTime()
{
// The Playable's time won't move in the same frame it was set,
// so we'll just let the next frame grab its time.
_PreviousNormalizedTime = float.NaN;
}
/************************************************************************************************************************/
/// <inheritdoc/>
public void UpdateEvents(bool raiseEvents)
{
var info = State.GetEventDispatchInfo();
// If we aren't raising events or don't have a previous time, just keep track of the time.
if (!raiseEvents || float.IsNaN(_PreviousNormalizedTime))
{
_PreviousNormalizedTime = info.NormalizedTime;
// Since we aren't paying attention to the events,
// we also aren't paying attention to which index the time corresponds to.
_NextEventIndex = RecalculateEventIndex;
return;
}
// If the sequence is modified, we need to recalculate the next event index.
var sequenceVersion = Events.Version;
if (_SequenceVersion != sequenceVersion)
{
_SequenceVersion = sequenceVersion;
_NextEventIndex = RecalculateEventIndex;
}
if (info.Length > 0)
{
if (_PreviousNormalizedTime == info.NormalizedTime)
return;
CheckGeneralEvents(info.NormalizedTime, info.IsLooping);
CheckEndEvent(info.NormalizedTime);
_PreviousNormalizedTime = info.NormalizedTime;
}
else// Length zero, negative, or NaN.
{
UpdateZeroLength();
}
}
/************************************************************************************************************************/
/// <summary>If the state has zero length, trigger its events every frame.</summary>
private void UpdateZeroLength()
{
var speed = State.EffectiveSpeed;
if (speed == 0)
return;
if (Events.Count > 0)
{
int playDirectionInt;
if (speed < 0)
{
playDirectionInt = -1;
if (_NextEventIndex == RecalculateEventIndex ||
_WasPlayingForwards)
{
_NextEventIndex = Events.Count - 1;
_WasPlayingForwards = false;
}
}
else
{
playDirectionInt = 1;
if (_NextEventIndex == RecalculateEventIndex ||
!_WasPlayingForwards)
{
_NextEventIndex = 0;
_WasPlayingForwards = true;
}
}
if (!InvokeAllEvents(Events, 1, playDirectionInt))
return;
}
var endEvent = Events.EndEvent;
if (endEvent.callback != null)
endEvent.DelayInvoke(EndEventName, State);
}
/************************************************************************************************************************/
/// <summary>General events are triggered on the frame when their time passes.</summary>
/// <remarks>Looping animations trigger their events every loop.</remarks>
private void CheckGeneralEvents(float normalizedTime, bool isLooping)
{
var count = Events.Count;
if (count == 0)
{
_NextEventIndex = 0;
return;
}
ValidateNextEventIndex(
isLooping,
ref normalizedTime,
out var playDirectionFloat,
out var playDirectionInt);
if (isLooping)// Looping.
{
var animancerEvent = Events[_NextEventIndex];
var eventTime = animancerEvent.normalizedTime * playDirectionFloat;
var loopDelta = GetLoopDelta(_PreviousNormalizedTime, normalizedTime, eventTime);
if (loopDelta == 0)
return;
// For each additional loop, invoke all events without needing to check their times.
if (!InvokeAllEvents(Events, loopDelta - 1, playDirectionInt))
return;
var loopStartIndex = _NextEventIndex;
Invoke:
animancerEvent.DelayInvoke(Events.GetName(_NextEventIndex), State);
if (!NextEventLooped(Events, playDirectionInt) ||
_NextEventIndex == loopStartIndex)
return;
animancerEvent = Events[_NextEventIndex];
eventTime = animancerEvent.normalizedTime * playDirectionFloat;
if (loopDelta == GetLoopDelta(_PreviousNormalizedTime, normalizedTime, eventTime))
goto Invoke;
}
else// Non-Looping.
{
while ((uint)_NextEventIndex < (uint)count)
{
var animancerEvent = Events[_NextEventIndex];
var eventTime = animancerEvent.normalizedTime * playDirectionFloat;
if (normalizedTime <= eventTime)
return;
animancerEvent.DelayInvoke(Events.GetName(_NextEventIndex), State);
_NextEventIndex += playDirectionInt;
}
}
}
/************************************************************************************************************************/
private void ValidateNextEventIndex(
bool isLooping,
ref float normalizedTime,
out float playDirectionFloat,
out int playDirectionInt)
{
if (normalizedTime < _PreviousNormalizedTime)// Playing Backwards.
{
var previousTime = _PreviousNormalizedTime;
_PreviousNormalizedTime = -previousTime;
normalizedTime = -normalizedTime;
playDirectionFloat = -1;
playDirectionInt = -1;
if (_NextEventIndex == RecalculateEventIndex ||
_WasPlayingForwards)
{
_NextEventIndex = Events.Count - 1;
_WasPlayingForwards = false;
if (isLooping)
previousTime = AnimancerUtilities.Wrap01(previousTime);
while (Events[_NextEventIndex].normalizedTime > previousTime)
{
_NextEventIndex--;
if (_NextEventIndex < 0)
{
if (isLooping)
_NextEventIndex = Events.Count - 1;
break;
}
}
Events.AssertNormalizedTimes(State, isLooping);
}
}
else// Playing Forwards.
{
playDirectionFloat = 1;
playDirectionInt = 1;
if (_NextEventIndex == RecalculateEventIndex ||
!_WasPlayingForwards)
{
_NextEventIndex = 0;
_WasPlayingForwards = true;
var previousTime = _PreviousNormalizedTime;
if (isLooping)
previousTime = AnimancerUtilities.Wrap01(previousTime);
var max = Events.Count - 1;
while (Events[_NextEventIndex].normalizedTime < previousTime)
{
_NextEventIndex++;
if (_NextEventIndex > max)
{
if (isLooping)
_NextEventIndex = 0;
break;
}
}
Events.AssertNormalizedTimes(State, isLooping);
}
}
// This method could be slightly optimised for playback direction changes by using the current index
// as the starting point instead of iterating from the edge of the sequence, but that would make it
// significantly more complex for something that shouldn't happen very often and would only matter if
// there are lots of events (in which case the optimisation would be tiny compared to the cost of
// actually invoking all those events and running the rest of the application).
}
/************************************************************************************************************************/
/// <summary>
/// Calculates the number of times an event at `eventTime` should be invoked when the
/// <see cref="AnimancerState.NormalizedTime"/> goes from `previousTime` to `nextTime` on a looping animation.
/// </summary>
private static int GetLoopDelta(float previousTime, float nextTime, float eventTime)
{
previousTime -= eventTime;
nextTime -= eventTime;
var previousLoopCount = Mathf.FloorToInt(previousTime);
var nextLoopCount = Mathf.FloorToInt(nextTime);
var loopCount = nextLoopCount - previousLoopCount;
// Previous time must be inclusive.
// And next time must be exclusive.
// So if the previous time is exactly on a looped increment of the event time, count one more.
// And if the next time is exactly on a looped increment of the event time, count one less.
if (previousTime == previousLoopCount)
loopCount++;
if (nextTime == nextLoopCount)
loopCount--;
return loopCount;
}
/************************************************************************************************************************/
private static int _MaximumFullLoopCount = 3;
/// <summary>
/// The maximum number of times a looping animation can trigger all of its events in a single frame.
/// Default 3, Minimum 1.
/// </summary>
/// <remarks>
/// This limit should only ever be reached when a state has a very short length and high speed.
/// </remarks>
public static int MaximumFullLoopCount
{
get => _MaximumFullLoopCount;
set => _MaximumFullLoopCount = Math.Max(value, 1);
}
private bool InvokeAllEvents(Sequence events, int count, int playDirectionInt)
{
if (count > _MaximumFullLoopCount)
count = _MaximumFullLoopCount;
var loopStartIndex = _NextEventIndex;
while (count-- > 0)
{
do
{
events[_NextEventIndex].DelayInvoke(events.GetName(_NextEventIndex), State);
if (!NextEventLooped(events, playDirectionInt))
return false;
}
while (_NextEventIndex != loopStartIndex);
}
return true;
}
/************************************************************************************************************************/
private bool NextEventLooped(Sequence events, int playDirectionInt)
{
_NextEventIndex += playDirectionInt;
var count = events.Count;
if (_NextEventIndex >= count)
_NextEventIndex = 0;
else if (_NextEventIndex < 0)
_NextEventIndex = count - 1;
return true;
}
/************************************************************************************************************************/
/// <summary>End events are triggered every frame after their time passes.</summary>
/// <remarks>
/// This ensures that assigning the event after the time has passed
/// will still trigger it rather than leaving it playing indefinitely.
/// </remarks>
private void CheckEndEvent(float normalizedTime)
{
var endEvent = Events.EndEvent;
if (endEvent.callback == null)
return;
if (normalizedTime > _PreviousNormalizedTime)// Playing Forwards.
{
var eventTime = float.IsNaN(endEvent.normalizedTime)
? 1
: endEvent.normalizedTime;
if (normalizedTime > eventTime)
endEvent.DelayInvoke(EndEventName, State);
}
else// Playing Backwards.
{
var eventTime = float.IsNaN(endEvent.normalizedTime)
? 0
: endEvent.normalizedTime;
if (normalizedTime < eventTime)
endEvent.DelayInvoke(EndEventName, State);
}
}
/************************************************************************************************************************/
/// <summary>
/// Sets the <see cref="AnimancerState.NormalizedTime"/>
/// to the <see cref="AnimancerState.NormalizedEndTime"/>
/// and invokes any remaining <see cref="AnimancerEvent"/>s.
/// </summary>
public void FinishImmediately()
{
var index = _NextEventIndex;
if (index == RecalculateEventIndex && Events.Count > 0)
{
var normalizedTime = State.NormalizedTime;
ValidateNextEventIndex(
State.IsLooping,
ref normalizedTime,
out _,
out _);
index = _NextEventIndex;
}
State.NormalizedTime = State.NormalizedEndTime;
if (Events.Count > 0)
{
if (State.EffectiveSpeed < 0)// Backwards.
{
for (int i = index; i >= 0; i--)
Events[i].DelayInvoke(Events.GetName(i), State);
}
else// Forwards, 0, or NaN.
{
for (int i = index; i < Events.Count; i++)
Events[i].DelayInvoke(Events.GetName(i), State);
}
}
var endEvent = Events.EndEvent;
if (endEvent.callback != null)
endEvent.DelayInvoke(EndEventName, State);
Invoker.InvokeAllAndClear();
}
/************************************************************************************************************************/
/// <summary>Returns "<see cref="Dispatcher"/> (Target State)".</summary>
public override string ToString()
=> State != null
? $"{nameof(Dispatcher)} ({State})"
: $"{nameof(Dispatcher)} (No Target State)";
/************************************************************************************************************************/
/// <inheritdoc/>
public void AppendDescription(StringBuilder text, string separator = "\n")
{
text.AppendField(separator, "State", State.GetPath());
text.AppendField(separator, "IsLooping", State.IsLooping);
text.AppendField(separator, "PreviousNormalizedTime", _PreviousNormalizedTime);
text.AppendField(separator, "NextEventIndex", _NextEventIndex);
text.AppendField(separator, "SequenceVersion", _SequenceVersion);
text.AppendField(separator, "WasPlayingForwards", _WasPlayingForwards);
}
/************************************************************************************************************************/
}
}
}

View File

@@ -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:

View File

@@ -0,0 +1,206 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
using System;
using System.Runtime.CompilerServices;
using UnityEngine;
using Object = UnityEngine.Object;
namespace Animancer
{
/// https://kybernetik.com.au/animancer/api/Animancer/AnimancerEvent
partial struct AnimancerEvent
{
/// <summary>An <see cref="AnimancerEvent"/> and other associated details used to invoke it.</summary>
/// https://kybernetik.com.au/animancer/api/Animancer/Invocation
public readonly struct Invocation
{
/************************************************************************************************************************/
/// <summary>The details of the event currently being triggered.</summary>
/// <remarks>Cleared after the event is invoked.</remarks>
public static Invocation Current { get; private set; }
/************************************************************************************************************************/
/// <summary>The <see cref="AnimancerEvent"/>.</summary>
public readonly AnimancerEvent Event;
/// <summary>The name of the <see cref="Event"/>.</summary>
public readonly StringReference Name;
/// <summary>The <see cref="AnimancerState"/> triggering the <see cref="Event"/>.</summary>
public readonly AnimancerState State;
/************************************************************************************************************************/
/// <summary>Creates a new <see cref="Invocation"/>.</summary>
public Invocation(
AnimancerEvent animancerEvent,
StringReference eventName,
AnimancerState state)
{
Event = animancerEvent;
State = state;
Name = eventName;
}
/************************************************************************************************************************/
/// <summary>
/// Sets the <see cref="Current"/>, invokes the <see cref="callback"/>,
/// then reverts the <see cref="Current"/>.
/// </summary>
/// <remarks>This method catches and logs any exception thrown by the <see cref="callback"/>.</remarks>
/// <exception cref="NullReferenceException">The <see cref="callback"/> is null.</exception>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly void Invoke()
{
#if UNITY_ASSERTIONS
var oldLayer = State.Layer;
var oldCommandCount = oldLayer.CommandCount;
#endif
var previous = Current;
var parameter = CurrentParameter;
Current = this;
CurrentParameter = null;
try
{
Event.callback();
}
catch (Exception exception)
{
Debug.LogException(exception, State?.Graph?.Component as Object);
}
Current = previous;
CurrentParameter = parameter;
#if UNITY_ASSERTIONS
if (Name == EndEventName)
AssertEndEventInvoked(oldLayer, oldCommandCount);
#endif
}
/************************************************************************************************************************/
/// <summary>
/// Returns the callback registered in the <see cref="AnimancerGraph.Events"/>
/// with the <see cref="Name"/> (or null if there isn't one).
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly Action GetBoundCallback()
=> Name.IsNullOrEmpty()
? null
: State.Graph._Events?.Get(Name);
/************************************************************************************************************************/
/// <summary>Returns a string describing the contents of this invocation.</summary>
public override string ToString()
=> $"{nameof(AnimancerEvent)}.{nameof(Invocation)}(" +
$"{nameof(Name)}={AnimancerUtilities.ToStringOrNull(Name)}, " +
$"NormalizedTime={Event.normalizedTime:0.##}, " +
$"Callback=({AnimancerReflection.ToStringDetailed(Event.callback)}), " +
$"{nameof(State)}={AnimancerUtilities.ToStringOrNull(State)})";
/************************************************************************************************************************/
/// <summary>
/// Invokes the callback bound to the <see cref="Name"/>
/// in the <see cref="AnimancerGraph.Events"/>.
/// </summary>
/// <remarks>
/// Logs <see cref="OptionalWarning.UselessEvent"/> if no callback is bound.
/// </remarks>
public void InvokeBoundCallback()
{
if (Name != null &&
State.Graph._Events != null &&
State.Graph._Events.TryGetValue(Name, out var callback))
{
callback();
}
#if UNITY_ASSERTIONS
// If the callback doesn't do anything else, then this is a useless event.
else if (Event.callback == AnimancerEvent.InvokeBoundCallback &&
OptionalWarning.UselessEvent.IsEnabled())
{
OptionalWarning.UselessEvent.Log(
$"An {nameof(AnimancerEvent)} attempted to invoke the callback bound to the name" +
$" '{AnimancerUtilities.ToStringOrNull(Name)}' but there is no callback" +
$" bound to that name so the event may not be configured correctly." +
$"\n• Normalized Time: {Event.normalizedTime}" +
$"\n• State: {State}" +
$"\n• Object: {AnimancerUtilities.ToStringOrNull(State.Graph?.Component)}",
State.Graph?.Component);
}
#endif
}
/************************************************************************************************************************/
#if UNITY_ASSERTIONS
/************************************************************************************************************************/
/// <summary>[Assert-Only]
/// Call after invoking an end event to assert <see cref="OptionalWarning.EndEventInterrupt"/>.
/// </summary>
private readonly void AssertEndEventInvoked(AnimancerLayer oldLayer, int oldCommandCount)
{
if (ShouldLogEndEventInterrupt(oldLayer, oldCommandCount))
{
OptionalWarning.EndEventInterrupt.Log(
$"An End Event callback didn't stop the animation." +
$" Animancer doesn't handle End Events automatically," +
$" so the controlling script is responsible for stopping the animation," +
$" often by playing a different one." +
$"\n• State: {State}" +
$"\n• Callback: {Event.callback.ToStringDetailed()}" +
$"\n• End Events are triggered every frame after their time has passed:" +
$" {Strings.DocsURLs.EndEvents.AsHtmlLink()}" +
$"\n• To avoid this behaviour, use a regular Animancer Event instead:" +
$" {Strings.DocsURLs.AnimancerEvents.AsHtmlLink()}",
State.Graph?.Component);
OptionalWarning.EndEventInterrupt.Disable();
}
}
/************************************************************************************************************************/
/// <summary>[Assert-Only] Should <see cref="OptionalWarning.EndEventInterrupt"/> be logged?</summary>
private readonly bool ShouldLogEndEventInterrupt(AnimancerLayer oldLayer, int oldCommandCount)
{
if (!OptionalWarning.EndEventInterrupt.IsEnabled())
return false;
var events = State.SharedEvents;
if (events == null ||
events.OnEnd != Event.callback)
return false;
var newLayer = State.Layer;
if (oldLayer != newLayer ||
oldCommandCount != newLayer.CommandCount ||
!State.Graph.IsGraphPlaying ||
!State.IsPlaying)
return false;
var speed = State.EffectiveSpeed;
if (speed > 0)
return State.NormalizedTime > State.NormalizedEndTime;
else if (speed < 0)
return State.NormalizedTime < State.NormalizedEndTime;
else// Speed 0.
return false;
}
/************************************************************************************************************************/
#endif
/************************************************************************************************************************/
}
}
}

View File

@@ -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:

View File

@@ -0,0 +1,136 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using UnityEngine;
namespace Animancer
{
/// https://kybernetik.com.au/animancer/api/Animancer/AnimancerEvent
partial struct AnimancerEvent
{
/************************************************************************************************************************/
/// <summary>Events ready to be invoked by the next <see cref="Invoker.InvokeAllAndClear"/>.</summary>
/// <remarks>
/// This field should be inside the Invoker class.
/// But that can potentially cause a TypeLoadException if Invoker initializes before AnimancerEvent.
/// Having it out in AnimancerEvent avoids that possibility.
/// </remarks>
private static readonly List<Invocation>
InvocationQueue = new();
/************************************************************************************************************************/
/// <summary>Gathers delegates in a static list to be invoked at a later time by any child class.</summary>
/// https://kybernetik.com.au/animancer/api/Animancer/Invoker
[DefaultExecutionOrder(-30000)]// Run as soon as possible in whatever update cycle is being executed.
[ExecuteAlways]
public abstract class Invoker : MonoBehaviour
{
/************************************************************************************************************************/
/// <summary>Ensures that an appropriate <see cref="Invoker"/> has been created.</summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Invoker Initialize(bool fixedUpdate)
=> fixedUpdate
? InvokerFixed.Initialize()
: InvokerDynamic.Initialize();
/// <summary>Ensures that an appropriate <see cref="Invoker"/> has been created.</summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Invoker Initialize(AnimatorUpdateMode updateMode)
{
const AnimatorUpdateMode FixedUpdateMode =
#if UNITY_2023_1_OR_NEWER
AnimatorUpdateMode.Fixed;
#else
AnimatorUpdateMode.AnimatePhysics;
#endif
return Initialize(updateMode == FixedUpdateMode);
}
/************************************************************************************************************************/
/// <summary>[Internal] Adds an event to the queue to be invoked by the next update.</summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static void Add(Invocation invocation)
{
#if UNITY_ASSERTIONS
if (!HasEnabledInstance)
Debug.LogWarning(
$"There is no currently enabled {nameof(AnimancerEvent)}.{nameof(Invoker)}" +
$" so events will not be invoked.");
#endif
InvocationQueue.Add(invocation);
}
/************************************************************************************************************************/
/// <summary>
/// In case <see cref="InvokeAllAndClear"/> gets called recursively,
/// we need to avoid invoking the same event multiple times
/// without the performance cost of immediately removing them each from the queue.
/// </summary>
private static int _CurrentInvocation;
/// <summary>Invokes all queued events and clears the queue.</summary>
public static void InvokeAllAndClear()
{
while (_CurrentInvocation < InvocationQueue.Count)
InvocationQueue[_CurrentInvocation++].Invoke();
InvocationQueue.Clear();
_CurrentInvocation = 0;
}
/************************************************************************************************************************/
/// <summary>Returns an enumerator for all invocations currently in the queue.</summary>
public static List<Invocation>.Enumerator EnumerateInvocationQueue()
=> InvocationQueue.GetEnumerator();
/************************************************************************************************************************/
#if UNITY_ASSERTIONS
/************************************************************************************************************************/
private static readonly List<Invoker>
Instances = new();
/************************************************************************************************************************/
/// <summary>[Assert-Only] Registers this instance.</summary>
protected virtual void Awake()
=> Instances.Add(this);
/************************************************************************************************************************/
/// <summary>[Assert-Only] Un-registers this instance.</summary>
protected virtual void OnDestroy()
=> Instances.Remove(this);
/************************************************************************************************************************/
/// <summary>[Assert-Only] Is there any <see cref="Behaviour.enabled"/> instance?</summary>
private static bool HasEnabledInstance
{
get
{
for (int i = 0; i < Instances.Count; i++)
if (Instances[i].enabled)
return true;
return false;
}
}
/************************************************************************************************************************/
#endif
/************************************************************************************************************************/
}
}
}

View File

@@ -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:

View File

@@ -0,0 +1,59 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
using System.Runtime.CompilerServices;
using UnityEngine;
namespace Animancer
{
/// https://kybernetik.com.au/animancer/api/Animancer/AnimancerEvent
partial struct AnimancerEvent
{
/// <summary>Executes <see cref="Invoker.InvokeAllAndClear"/> after animations in the Dynamic Update cycle.</summary>
/// https://kybernetik.com.au/animancer/api/Animancer/InvokerDynamic
[AnimancerHelpUrl(typeof(InvokerDynamic))]
[AddComponentMenu("")]// Singleton creates itself.
public class InvokerDynamic : Invoker
{
/************************************************************************************************************************/
private static InvokerDynamic _Instance;
/// <summary>Creates the singleton instance.</summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static InvokerDynamic Initialize()
=> AnimancerUtilities.InitializeSingleton(ref _Instance);
/************************************************************************************************************************/
/// <summary>Should this system execute events?</summary>
/// <remarks>If disabled, this system will not be re-enabled automatically.</remarks>
public static bool Enabled
{
get => _Instance != null && _Instance.enabled;
set
{
if (value)
{
Initialize();
_Instance.enabled = true;
}
else if (_Instance != null)
{
_Instance.enabled = false;
}
}
}
/************************************************************************************************************************/
/// <summary>After animation update with dynamic timestep.</summary>
protected virtual void LateUpdate()
{
InvokeAllAndClear();
}
/************************************************************************************************************************/
}
}
}

View File

@@ -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:

View File

@@ -0,0 +1,76 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
using System.Collections;
using System.Runtime.CompilerServices;
using UnityEngine;
namespace Animancer
{
/// https://kybernetik.com.au/animancer/api/Animancer/AnimancerEvent
partial struct AnimancerEvent
{
/// <summary>Executes <see cref="Invoker.InvokeAllAndClear"/> after animations in the Fixed Update cycle.</summary>
/// https://kybernetik.com.au/animancer/api/Animancer/InvokerFixed
[AnimancerHelpUrl(typeof(InvokerFixed))]
[AddComponentMenu("")]// Singleton creates itself.
public class InvokerFixed : Invoker
{
/************************************************************************************************************************/
private static InvokerFixed _Instance;
/// <summary>Creates the singleton instance.</summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static InvokerFixed Initialize()
=> AnimancerUtilities.InitializeSingleton(ref _Instance);
/************************************************************************************************************************/
/// <summary>Should this system execute events?</summary>
/// <remarks>If disabled, this system will not be re-enabled automatically.</remarks>
public static bool Enabled
{
get => _Instance != null && _Instance.enabled;
set
{
if (value)
{
Initialize();
_Instance.enabled = true;
}
else if (_Instance != null)
{
_Instance.enabled = false;
}
}
}
/************************************************************************************************************************/
/// <summary>A cached instance of <see cref="UnityEngine.WaitForFixedUpdate"/>.</summary>
public static readonly WaitForFixedUpdate
WaitForFixedUpdate = new();
/************************************************************************************************************************/
/// <summary>Starts the <see cref="LateFixedUpdate"/> coroutine.</summary>
protected virtual void OnEnable()
=> StartCoroutine(LateFixedUpdate());
/************************************************************************************************************************/
/// <summary>After animation update with fixed timestep.</summary>
private IEnumerator LateFixedUpdate()
{
while (true)
{
yield return WaitForFixedUpdate;
InvokeAllAndClear();
}
}
/************************************************************************************************************************/
}
}
}

View File

@@ -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:

View File

@@ -0,0 +1,446 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
#pragma warning disable CS0649 // Field is never assigned to, and will always have its default value.
using System;
using UnityEngine;
namespace Animancer
{
/// https://kybernetik.com.au/animancer/api/Animancer/AnimancerEvent
partial struct AnimancerEvent
{
/// https://kybernetik.com.au/animancer/api/Animancer/Sequence
partial class Sequence
{
/// <summary>
/// Serializable data which can be used to construct an <see cref="Sequence"/> using
/// <see cref="StringAsset"/>s and <see cref="IInvokable"/>s.
/// </summary>
/// https://kybernetik.com.au/animancer/api/Animancer/Serializable
[Serializable]
public class Serializable : ICloneable<Serializable>
#if UNITY_EDITOR
, ISerializationCallbackReceiver
#endif
{
/************************************************************************************************************************/
#region Fields and Properties
/************************************************************************************************************************/
[SerializeField]
private float[] _NormalizedTimes;
/// <summary>[<see cref="SerializeField"/>] The serialized <see cref="normalizedTime"/>s.</summary>
/// <remarks>The last item is used for the <see cref="EndEvent"/>.</remarks>
public ref float[] NormalizedTimes => ref _NormalizedTimes;
/************************************************************************************************************************/
[SerializeReference, Polymorphic]
private IInvokable[] _Callbacks;
/// <summary>[<see cref="SerializeField"/>] The serialized <see cref="callback"/>s.</summary>
/// <remarks>
/// This array only needs to be large enough to hold the last item that isn't null.
/// <para></para>
/// If this array is larger than the <see cref="NormalizedTimes"/>, the first item
/// with no corresponding time will be used as the <see cref="OnEnd"/> callback
/// and any others after that will be ignored.
/// </remarks>
public ref IInvokable[] Callbacks => ref _Callbacks;
/************************************************************************************************************************/
[SerializeField]
private StringAsset[] _Names;
/// <summary>[<see cref="SerializeField"/>] The serialized <see cref="Sequence.Names"/>.</summary>
public ref StringAsset[] Names => ref _Names;
/************************************************************************************************************************/
#if UNITY_EDITOR
/************************************************************************************************************************/
/// <summary>[Editor-Only] [Internal]
/// The name of the array field which stores the <see cref="normalizedTime"/>s.
/// </summary>
internal const string NormalizedTimesField = nameof(_NormalizedTimes);
/// <summary>[Editor-Only] [Internal]
/// The name of the array field which stores the serialized <see cref="Callbacks"/>.
/// </summary>
internal const string CallbacksField = nameof(_Callbacks);
/// <summary>[Editor-Only] [Internal]
/// The name of the array field which stores the serialized <see cref="Names"/>.
/// </summary>
internal const string NamesField = nameof(_Names);
/// <summary>[Editor-Only] Disable Inspector Gadgets Nested Object Drawers.</summary>
private const bool NestedObjectDrawers = false;
/************************************************************************************************************************/
#endif
/************************************************************************************************************************/
private Sequence _Events;
/// <summary>Returns the <see cref="Events"/> or <c>null</c> if it wasn't yet initialized.</summary>
public Sequence InitializedEvents
=> _Events;
/// <summary>
/// The runtime <see cref="Sequence"/> compiled from this <see cref="Serializable"/>.
/// Each call after the first will return the same reference.
/// </summary>
/// <remarks>
/// Unlike <see cref="GetEventsOptional"/>, this property will create an empty
/// <see cref="Sequence"/> instead of returning null if there are no events.
/// </remarks>
public Sequence Events
{
get
{
if (_Events == null)
{
GetEventsOptional();
_Events ??= new();
}
return _Events;
}
set => _Events = value;
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Initialization
/************************************************************************************************************************/
/// <summary>
/// Returns the runtime <see cref="Sequence"/> compiled from this <see cref="Serializable"/>.
/// Each call after the first will return the same reference.
/// </summary>
/// <remarks>
/// This method returns null if the sequence would be empty anyway and is used by the implicit
/// conversion from <see cref="Serializable"/> to <see cref="Sequence"/>.
/// </remarks>
public Sequence GetEventsOptional()
{
if (_Events != null ||
_NormalizedTimes == null)
return _Events;
var timeCount = _NormalizedTimes.Length;
if (timeCount == 0)
return null;
var callbackCount = _Callbacks != null
? _Callbacks.Length
: 0;
var callback = callbackCount >= timeCount--
? GetInvoke(_Callbacks[timeCount])
: null;
var endEvent = new AnimancerEvent(_NormalizedTimes[timeCount], callback);
_Events = new(timeCount)
{
EndEvent = endEvent,
Count = timeCount,
Names = StringAsset.ToStringReferences(_Names),
};
var events = _Events._Events;
for (int i = 0; i < timeCount; i++)
{
callback = i < callbackCount
? GetInvoke(_Callbacks[i])
: InvokeBoundCallback;
events[i] = new(_NormalizedTimes[i], callback);
}
return _Events;
}
/// <summary>Calls <see cref="GetEventsOptional"/>.</summary>
public static implicit operator Sequence(Serializable serializable)
=> serializable?.GetEventsOptional();
/************************************************************************************************************************/
/// <summary>
/// Returns the <see cref="IInvokable.Invoke"/> if the `invokable` isn't <c>null</c>.
/// Otherwise, returns <c>null</c>.
/// </summary>
public static Action GetInvoke(IInvokable invokable)
=> invokable != null
? invokable.Invoke
: InvokeBoundCallback;
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region End Event
/************************************************************************************************************************/
/// <summary>Returns the <see cref="normalizedTime"/> of the <see cref="EndEvent"/>.</summary>
/// <remarks>If the value is not set, the value is determined by <see cref="GetDefaultNormalizedEndTime"/>.</remarks>
public float GetNormalizedEndTime(float speed = 1)
{
return _NormalizedTimes.IsNullOrEmpty()
? GetDefaultNormalizedEndTime(speed)
: _NormalizedTimes[^1];
}
/************************************************************************************************************************/
/// <summary>Sets the <see cref="normalizedTime"/> of the <see cref="EndEvent"/>.</summary>
public void SetNormalizedEndTime(float normalizedTime)
{
if (_NormalizedTimes.IsNullOrEmpty())
_NormalizedTimes = new float[] { normalizedTime };
else
_NormalizedTimes[^1] = normalizedTime;
}
/************************************************************************************************************************/
/// <summary>Sets the <see cref="callback"/> of the <see cref="EndEvent"/>.</summary>
public void SetEndCallback(IInvokable callback = null)
{
if (_NormalizedTimes.IsNullOrEmpty())
_NormalizedTimes = new float[] { float.NaN };
InsertOptionalItem(ref _Callbacks, _NormalizedTimes.Length - 1, callback);
}
/************************************************************************************************************************/
/// <summary>Sets the data of the <see cref="EndEvent"/>.</summary>
public void SetEndEvent(float normalizedTime = float.NaN, IInvokable callback = null)
{
if (_NormalizedTimes.IsNullOrEmpty())
_NormalizedTimes = new float[] { normalizedTime };
else
_NormalizedTimes[^1] = normalizedTime;
InsertOptionalItem(ref _Callbacks, _NormalizedTimes.Length - 1, callback);
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Other Events
/************************************************************************************************************************/
/// <summary>Adds an event to the serialized fields.</summary>
public int AddEvent(float normalizedTime, IInvokable callback = null, StringAsset name = null)
{
int index;
if (_NormalizedTimes.IsNullOrEmpty())
{
_NormalizedTimes = new float[] { normalizedTime, float.NaN };
index = 0;
}
else
{
index = _NormalizedTimes.Length - 1;
for (int i = 0; i < _NormalizedTimes.Length - 1; i++)
{
if (_NormalizedTimes[i] > normalizedTime)
{
index = i;
break;
}
}
AnimancerUtilities.InsertAt(ref _NormalizedTimes, index, normalizedTime);
}
InsertOptionalItem(ref _Callbacks, index, callback);
InsertOptionalItem(ref _Names, index, name);
return index;
}
/************************************************************************************************************************/
/// <summary>Inserts an `item` at the specified `index` in an optional `array`.</summary>
/// <remarks>
/// If the `item` is <c>null</c> then the array only needs
/// to be expanded if it was already larger than the `index`.
/// </remarks>
private static void InsertOptionalItem<T>(ref T[] array, int index, T item)
where T : class
{
if (item == null &&
(array == null || array.Length < index))
return;
AnimancerUtilities.InsertAt(ref array, index, item);
}
/************************************************************************************************************************/
/// <summary>Removes an event from the serialized fields.</summary>
public void RemoveEvent(int index)
{
if (_NormalizedTimes.IsNullOrEmpty())
return;
AnimancerUtilities.RemoveAt(ref _NormalizedTimes, index);
if (_Callbacks != null && _Callbacks.Length > index)
AnimancerUtilities.RemoveAt(ref _Callbacks, index);
if (_Names != null && _Names.Length > index)
AnimancerUtilities.RemoveAt(ref _Names, index);
}
/************************************************************************************************************************/
/// <summary>Removes all events.</summary>
public void Clear(bool keepEndEvent = false)
{
if (keepEndEvent)
{
if (_NormalizedTimes != null && _NormalizedTimes.Length > 0)
_NormalizedTimes = new float[] { _NormalizedTimes[^1] };
else
_NormalizedTimes = null;
if (_Callbacks != null && _Callbacks.Length > 0)
_Callbacks = new IInvokable[] { _Callbacks[^1] };
else
_Callbacks = null;
}
else
{
_NormalizedTimes = null;
_Callbacks = null;
}
_Names = null;
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Copying
/************************************************************************************************************************/
/// <summary>Creates a new <see cref="Serializable"/> and copies the contents of <c>this</c> into it.</summary>
/// <remarks>To copy into an existing sequence, use <see cref="CopyFrom"/> instead.</remarks>
public Serializable Clone()
{
var clone = new Serializable();
clone.CopyFrom(this);
return clone;
}
/// <inheritdoc/>
public Serializable Clone(CloneContext context)
=> Clone();
/************************************************************************************************************************/
/// <inheritdoc/>
public void CopyFrom(Serializable copyFrom)
{
AnimancerUtilities.CopyExactArray(copyFrom._NormalizedTimes, ref _NormalizedTimes);
AnimancerUtilities.CopyExactArray(copyFrom._Callbacks, ref _Callbacks);
AnimancerUtilities.CopyExactArray(copyFrom._Names, ref _Names);
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Serialization
/************************************************************************************************************************/
#if UNITY_EDITOR
/************************************************************************************************************************/
/// <summary>[Editor-Only] Does nothing.</summary>
void ISerializationCallbackReceiver.OnAfterDeserialize() { }
/************************************************************************************************************************/
/// <summary>[Editor-Only] [Internal]
/// Called by <see cref="ISerializationCallbackReceiver.OnBeforeSerialize"/>.
/// </summary>
internal static event Action<Serializable> OnBeforeSerialize;
/// <summary>[Editor-Only] Ensures that the events are sorted by time (excluding the end event).</summary>
void ISerializationCallbackReceiver.OnBeforeSerialize()
=> OnBeforeSerialize?.Invoke(this);
/************************************************************************************************************************/
/// <summary>[Editor-Only] [Internal]
/// Should the arrays be prevented from reducing their size when their last elements are unused?
/// </summary>
internal static bool DisableCompactArrays { get; set; }
/// <summary>[Editor-Only] [Internal]
/// Removes empty data from the ends of the arrays to reduce the serialized data size.
/// </summary>
internal void CompactArrays()
{
if (DisableCompactArrays)
return;
// If there is only one time and it is NaN, we don't need to store anything.
if (_NormalizedTimes == null ||
(_NormalizedTimes.Length == 1 &&
(_Callbacks == null || _Callbacks.Length == 0) &&
(_Names == null || _Names.Length == 0) &&
float.IsNaN(_NormalizedTimes[0])))
{
_NormalizedTimes = Array.Empty<float>();
_Callbacks = Array.Empty<IInvokable>();
_Names = Array.Empty<StringAsset>();
return;
}
Trim(ref _Callbacks, _NormalizedTimes.Length, callback => callback != null);
Trim(ref _Names, _NormalizedTimes.Length, name => name != null);
}
/************************************************************************************************************************/
/// <summary>[Editor-Only] Removes unimportant values from the end of the `array`.</summary>
private static void Trim<T>(ref T[] array, int maxLength, Func<T, bool> isImportant)
{
if (array == null)
return;
var count = Math.Min(array.Length, maxLength);
while (count >= 1)
{
var item = array[count - 1];
if (isImportant(item))
break;
else
count--;
}
Array.Resize(ref array, count);
}
/************************************************************************************************************************/
#endif
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
}
}
}
}

View File

@@ -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

View File

@@ -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:

View File

@@ -0,0 +1,375 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
using System;
using System.Runtime.CompilerServices;
using System.Text;
using UnityEngine;
namespace Animancer
{
/// <summary>
/// A <see cref="callback"/> delegate
/// paired with a <see cref="normalizedTime"/> which determines when to invoke it.
/// </summary>
/// <remarks>
/// <strong>Documentation:</strong>
/// <see href="https://kybernetik.com.au/animancer/docs/manual/events/animancer">
/// Animancer Events</see>
/// </remarks>
/// https://kybernetik.com.au/animancer/api/Animancer/AnimancerEvent
///
public partial struct AnimancerEvent : IEquatable<AnimancerEvent>
{
/************************************************************************************************************************/
#region Event
/************************************************************************************************************************/
/// <summary>The <see cref="AnimancerState.NormalizedTime"/> at which to invoke the <see cref="callback"/>.</summary>
public float normalizedTime;
/// <summary>The delegate to invoke when the <see cref="normalizedTime"/> passes.</summary>
public Action callback;
/************************************************************************************************************************/
/// <summary>The largest possible float value less than 1.</summary>
/// <remarks>
/// This value is useful for placing events at the end of a looping animation since they do not allow the
/// <see cref="normalizedTime"/> to be greater than or equal to 1.
/// </remarks>
public const float
AlmostOne = 0.99999994f;
/************************************************************************************************************************/
/// <summary>The event name used for <see cref="Sequence.EndEvent"/>s.</summary>
/// <remarks>
/// This is a <see cref="StringReference.Unique"/> so that even if the same name happens
/// to be used elsewhere, it would be treated as a different name.
/// The reason for this is explained in <see cref="NamedEventDictionary.AssertNotEndEvent"/>.
/// </remarks>
public static readonly StringReference
EndEventName = StringReference.Unique("EndEvent");
/************************************************************************************************************************/
/// <summary>Does nothing.</summary>
/// <remarks>This delegate can be used for events which would otherwise have a <c>null</c> <see cref="callback"/>.</remarks>
public static readonly Action
DummyCallback = Dummy;
/// <summary>Does nothing.</summary>
/// <remarks>Used by <see cref="DummyCallback"/>.</remarks>
private static void Dummy() { }
/// <summary>Is the `callback` <c>null</c> or the <see cref="DummyCallback"/>?</summary>
public static bool IsNullOrDummy(Action callback)
=> callback == null
|| callback == DummyCallback;
/************************************************************************************************************************/
/// <summary>Creates a new <see cref="AnimancerEvent"/>.</summary>
public AnimancerEvent(float normalizedTime, Action callback)
{
this.normalizedTime = normalizedTime;
this.callback = callback;
}
/************************************************************************************************************************/
/// <summary>Returns a string describing the details of this event.</summary>
public readonly override string ToString()
{
var text = StringBuilderPool.Instance.Acquire();
text.Append($"{nameof(AnimancerEvent)}(");
AppendDetails(text);
text.Append(')');
return text.ReleaseToString();
}
/************************************************************************************************************************/
/// <summary>Appends the details of this event to the `text`.</summary>
public readonly void AppendDetails(StringBuilder text)
{
text.Append("NormalizedTime: ")
.Append(normalizedTime);
var callbacks = AnimancerReflection.GetInvocationList(callback);
if (callbacks != null)
{
text.Append(", Callbacks: [")
.Append(callbacks.Length)
.Append("] { ");
for (int i = 0; i < callbacks.Length; i++)
{
if (i > 0)
text.Append(", ");
text.AppendDelegate(callbacks[i]);
}
text.Append(" }");
}
else
{
text.Append(", Callback: ")
.AppendDelegate(callback);
}
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Invocation
/************************************************************************************************************************/
/// <summary>The details of the event currently being triggered.</summary>
/// <remarks>Cleared after the event is invoked.</remarks>
// Having the underlying field here can cause type initialization errors due to circular dependencies.
public static Invocation Current
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => Invocation.Current;
}
/************************************************************************************************************************/
/// <summary>
/// A cached delegate which calls <see cref="Invocation.InvokeBoundCallback"/>
/// on the <see cref="Current"/>.
/// </summary>
public static readonly Action
InvokeBoundCallback = InvokeCurrentBoundCallback;
/// <summary>
/// Calls <see cref="Invocation.InvokeBoundCallback"/> on the <see cref="Current"/>.
/// </summary>
private static void InvokeCurrentBoundCallback()
=> Current.InvokeBoundCallback();
/************************************************************************************************************************/
/// <summary>The custom parameter of the event currently being triggered.</summary>
/// <remarks>Cleared after the event is finished.</remarks>
public static object CurrentParameter { get; private set; }
/// <summary>Calls <see cref="ConvertableUtilities.ConvertOrThrow"/> on the <see cref="CurrentParameter"/>.</summary>
public static T GetCurrentParameter<T>()
=> ConvertableUtilities.ConvertOrThrow<T>(CurrentParameter);
/// <summary>Returns a new delegate which invokes the `callback` using <see cref="GetCurrentParameter{T}"/>.</summary>
/// <remarks>
/// If <typeparamref name="T"/> is <see cref="string"/>,
/// consider using <see cref="Parametize(Action{string})"/> instead of this.
/// </remarks>
/// <exception cref="ArgumentNullException">The `callback` is <c>null</c>.</exception>
public static Action Parametize<T>(Action<T> callback)
{
#if UNITY_ASSERTIONS
if (callback == null)
throw new ArgumentNullException(
nameof(callback),
$"Can't {nameof(Parametize)} a null callback.");
#endif
return () => callback(GetCurrentParameter<T>());
}
/// <summary>Returns a new delegate which invokes the `callback` using the <see cref="CurrentParameter"/>.</summary>
/// <exception cref="ArgumentNullException">The `callback` is <c>null</c>.</exception>
public static Action Parametize(Action<string> callback)
{
#if UNITY_ASSERTIONS
if (callback == null)
throw new ArgumentNullException(
nameof(callback),
$"Can't {nameof(Parametize)} a null callback.");
#endif
return () => callback(CurrentParameter?.ToString());
}
/************************************************************************************************************************/
/// <summary>[Assert-Only]
/// Logs an error if the `callback` doesn't contain a <see cref="Parameter{T}.Invoke"/>
/// so that adding to it with <see cref="Parametize{T}(Action{T})"/> can use that parameter.
/// </summary>
[System.Diagnostics.Conditional(Strings.Assertions)]
public static void AssertContainsParameter<T>(Action callback)
{
if (!ContainsParameterInvoke<T>(callback))
Debug.LogWarning(
$"Adding parametized callback will do nothing because the existing callback" +
$" doesn't contain a {typeof(T).GetNameCS()} parameter." +
$"\n• Existing Callback: {callback.ToStringDetailed()}");
}
/// <summary>Does the `callback` contain a <see cref="Parameter{T}.Invoke"/>?</summary>
private static bool ContainsParameterInvoke<T>(Action callback)
{
if (callback == null)
return false;
if (IsParameterInvoke<T>(callback))
return true;
var invocations = AnimancerReflection.GetInvocationList(callback);
if (invocations.Length == 1 && ReferenceEquals(invocations[0], callback))
return false;
for (int i = 0; i < invocations.Length; i++)
{
var invocation = invocations[i];
if (IsParameterInvoke<T>(invocation))
return true;
}
return false;
}
/// <summary>Is the `callback` a call to <see cref="Parameter{T}.Invoke"/>?</summary>
private static bool IsParameterInvoke<T>(Delegate callback)
=> callback.Target is IParameter parameter
&& callback.Method.Name == nameof(IInvokable.Invoke)
&& typeof(T).IsAssignableFrom(parameter.Value.GetType());
/************************************************************************************************************************/
/// <summary>
/// Adds this event to the <see cref="Invoker"/>
/// which will call <see cref="Invocation.Invoke"/> later in the current frame.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly void DelayInvoke(
StringReference eventName,
AnimancerState state)
=> Invoker.Add(new(this, eventName, state));
/************************************************************************************************************************/
/// <summary>[Assert-Conditional]
/// This method should be called when an animation is played.
/// It asserts that either no event is currently being triggered
/// or that the event is being triggered inside `playing`.
/// Otherwise, it logs <see cref="OptionalWarning.EventPlayMismatch"/>.
/// </summary>
[System.Diagnostics.Conditional(Strings.Assertions)]
public static void AssertEventPlayMismatch(AnimancerGraph playing)
{
#if UNITY_ASSERTIONS
if (Current.State == null ||
Current.State.Graph == playing ||
OptionalWarning.EventPlayMismatch.IsDisabled())
return;
OptionalWarning.EventPlayMismatch.Log(
$"An Animancer Event triggered by '{Current.State}' on '{Current.State.Graph}'" +
$" was used to play an animation on a different character ('{playing}')." +
$"\n\nThis most commonly happens when a Transition is shared by multiple characters" +
$" and they all register their own callbacks to its events which leads to" +
$" those events being triggered by the wrong character." +
$"\n\n{Current}",
playing.Component);
#endif
}
/************************************************************************************************************************/
/// <summary>
/// Returns either the <see cref="AnimancerGraph.DefaultFadeDuration"/>
/// or the <see cref="AnimancerState.RemainingDuration"/>
/// of the <see cref="Current"/> state (whichever is higher).
/// </summary>
public static float GetFadeOutDuration()
=> GetFadeOutDuration(Current.State, AnimancerGraph.DefaultFadeDuration);
/// <summary>
/// Returns either the `minDuration` or the <see cref="AnimancerState.RemainingDuration"/>
/// of the <see cref="Current"/> state (whichever is higher).
/// </summary>
public static float GetFadeOutDuration(float minDuration)
=> GetFadeOutDuration(Current.State, minDuration);
/// <summary>
/// Returns either the `minDuration` or the <see cref="AnimancerState.RemainingDuration"/>
/// of the `state` (whichever is higher).
/// </summary>
public static float GetFadeOutDuration(AnimancerState state, float minDuration)
{
if (state == null)
return minDuration;
var time = state.Time;
var speed = state.EffectiveSpeed;
if (speed == 0)
return minDuration;
float remainingDuration;
if (state.IsLooping)
{
var previousTime = time - speed * Time.deltaTime;
var inverseLength = 1f / state.Length;
// If we just passed the end of the animation, the remaining duration would technically be the full
// duration of the animation, so we most likely want to use the minimum duration instead.
if (Math.Floor(time * inverseLength) != Math.Floor(previousTime * inverseLength))
return minDuration;
}
if (speed > 0)
{
remainingDuration = (state.Length - time) / speed;
}
else
{
remainingDuration = time / -speed;
}
return Math.Max(minDuration, remainingDuration);
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Operators
/************************************************************************************************************************/
/// <summary>Are the <see cref="normalizedTime"/> and <see cref="callback"/> equal?</summary>
public static bool operator ==(AnimancerEvent a, AnimancerEvent b)
=> a.Equals(b);
/// <summary>Are the <see cref="normalizedTime"/> and <see cref="callback"/> not equal?</summary>
public static bool operator !=(AnimancerEvent a, AnimancerEvent b)
=> !a.Equals(b);
/************************************************************************************************************************/
/// <summary>[<see cref="IEquatable{AnimancerEvent}"/>]
/// Are the <see cref="normalizedTime"/> and <see cref="callback"/> of this event equal to `other`?
/// </summary>
public readonly bool Equals(AnimancerEvent other)
=> callback == other.callback
&& normalizedTime.IsEqualOrBothNaN(other.normalizedTime);
/// <inheritdoc/>
public readonly override bool Equals(object obj)
=> obj is AnimancerEvent animancerEvent
&& Equals(animancerEvent);
/// <inheritdoc/>
public readonly override int GetHashCode()
=> AnimancerUtilities.Hash(-78069441,
normalizedTime.GetHashCode(),
callback.SafeGetHashCode());
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
}
}

View File

@@ -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:

View File

@@ -0,0 +1,406 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
using System.Runtime.CompilerServices;
using UnityEngine;
namespace Animancer
{
/// https://kybernetik.com.au/animancer/api/Animancer/AnimancerState
partial class AnimancerState
{
/************************************************************************************************************************/
/// <summary>The system which manages the <see cref="SharedEvents"/>.</summary>
private AnimancerEvent.Dispatcher _EventDispatcher;
/************************************************************************************************************************/
/// <summary>
/// Events which will be triggered while this state plays
/// based on its <see cref="NormalizedTime"/>.
/// </summary>
///
/// <remarks>
/// This property tries to ensure that the event sequence is only referenced by this state.
/// <list type="bullet">
/// <item>
/// If the reference was <c>null</c>,
/// a new sequence will be created.
/// </item>
/// <item>
/// If a reference was assigned to <see cref="SharedEvents"/>,
/// it will be cloned so this state owns the clone.
/// </item>
/// </list>
/// <para></para>
/// Using <see cref="Events(object)"/> or <see cref="Events(object, out AnimancerEvent.Sequence)"/>
/// is often safer than this property since they help detect if multiple scripts are using the same
/// state which could lead to unexpected bugs if they each assign conflicting callbacks.
/// <para></para>
/// <strong>Documentation:</strong>
/// <see href="https://kybernetik.com.au/animancer/docs/manual/events/animancer">
/// Animancer Events</see>
/// </remarks>
public AnimancerEvent.Sequence OwnedEvents
{
get
{
_EventDispatcher ??= new(this);
_EventDispatcher.InitializeEvents(out var events);
return events;
}
set
{
if (value != null)
(_EventDispatcher ??= new(this)).SetEvents(value, true);
else
_EventDispatcher = null;
}
}
/************************************************************************************************************************/
/// <summary>
/// Events which will be triggered while this state plays
/// based on its <see cref="NormalizedTime"/>.
/// </summary>
///
/// <remarks>
/// This reference is <c>null</c> by default and once assigned it may be shared by multiple states.
/// <para></para>
/// <strong>Documentation:</strong>
/// <see href="https://kybernetik.com.au/animancer/docs/manual/events/animancer">
/// Animancer Events</see>
/// </remarks>
public AnimancerEvent.Sequence SharedEvents
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => _EventDispatcher?.Events;
set
{
if (value != null)
(_EventDispatcher ??= new(this)).SetEvents(value, false);
else
_EventDispatcher = null;
}
}
/************************************************************************************************************************/
/// <summary>Have the <see cref="SharedEvents"/> or <see cref="OwnedEvents"/> been initialized?</summary>
/// <remarks>
/// <strong>Documentation:</strong>
/// <see href="https://kybernetik.com.au/animancer/docs/manual/events/animancer">
/// Animancer Events</see>
/// </remarks>
public bool HasEvents
=> _EventDispatcher != null;
/************************************************************************************************************************/
/// <summary>Have the <see cref="OwnedEvents"/> been initialized?</summary>
/// <remarks>
/// <strong>Documentation:</strong>
/// <see href="https://kybernetik.com.au/animancer/docs/manual/events/animancer">
/// Animancer Events</see>
/// </remarks>
public bool HasOwnedEvents
=> _EventDispatcher != null
&& _EventDispatcher.HasOwnEvents;
/************************************************************************************************************************/
/// <summary>
/// If the <see cref="OwnedEvents"/> haven't been initialized yet,
/// this method gets them and returns <c>true</c>.
/// </summary>
///
/// <remarks>
/// This method tries to ensure that the event sequence is only referenced by this state.
/// <list type="bullet">
/// <item>
/// If the reference was <c>null</c>,
/// a new sequence will be created.
/// </item>
/// <item>
/// If a reference was assigned to <see cref="SharedEvents"/>,
/// it will be cloned so this state owns the clone.
/// </item>
/// </list>
/// In both of those cases, this method returns <c>true</c>
/// to indicate that the caller should initialize their event callbacks.
/// <para></para>
/// Also calls <see cref="AssertOwnership"/>.
/// <para></para>
/// <strong>Documentation:</strong>
/// <see href="https://kybernetik.com.au/animancer/docs/manual/events/animancer">
/// Animancer Events</see>
/// <para></para>
/// <strong>Example:</strong>
/// <code>
/// public static readonly StringReference EventName = "Event Name";
///
/// ...
///
/// AnimancerState state = animancerComponent.Play(animation);
/// if (state.Events(this, out AnimancerEvent.Sequence events))
/// {
/// events.SetCallback(EventName, OnAnimationEvent);
/// events.OnEnd = OnAnimationEnded;
/// }
/// </code>
/// If multiple different owners need to take turns reusing the same state,
/// use <see cref="Events(ref AnimancerEvent.Sequence)"/> instead.
/// <para></para>
/// If you only need to initialize the End Event,
/// consider using <see cref="Events(object)"/> instead.
/// </remarks>
public bool Events(object owner, out AnimancerEvent.Sequence events)
{
AssertOwnership(owner);
_EventDispatcher ??= new(this);
return _EventDispatcher.InitializeEvents(out events);
}
/************************************************************************************************************************/
/// <summary>
/// If the <see cref="OwnedEvents"/> haven't been initialized yet,
/// this method gets them and returns <c>true</c>.
/// </summary>
///
/// <remarks>
/// This method tries to ensure that the event sequence is only referenced by this state.
/// <list type="bullet">
/// <item>
/// If the reference was <c>null</c>,
/// a new sequence will be created.
/// </item>
/// <item>
/// If a reference was assigned to <see cref="SharedEvents"/>,
/// it will be cloned so this state owns the clone.
/// </item>
/// </list>
/// <para></para>
/// Also calls <see cref="AssertOwnership"/>.
/// <para></para>
/// <strong>Documentation:</strong>
/// <see href="https://kybernetik.com.au/animancer/docs/manual/events/animancer">
/// Animancer Events</see>
/// <para></para>
/// <strong>Example:</strong>
/// <code>
/// AnimancerState state = animancerComponent.Play(animation);
/// state.Events(this).OnEnd ??= OnAnimationEnded;
/// </code>
/// If multiple different owners need to take turns reusing the same state,
/// use <see cref="Events(ref AnimancerEvent.Sequence)"/> instead.
/// <para></para>
/// If you need to initialize more than just the End Event,
/// use <see cref="Events(object, out AnimancerEvent.Sequence)"/> instead.
/// </remarks>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public AnimancerEvent.Sequence Events(object owner)
{
Events(owner, out var events);
return events;
}
/************************************************************************************************************************/
/// <summary>
/// If the `events` are <c>null</c>, this method assigns a <c>new</c> <see cref="AnimancerEvent.Sequence"/>
/// and returns <c>true</c> to indicate that the caller should now initialize their event callbacks.
/// Otherwise, this method simply assigns the provided `events` to this state and returns <c>false</c>.
/// </summary>
///
/// <remarks>
/// If this state already had events, the <c>new</c> <see cref="AnimancerEvent.Sequence"/>
/// will be a copy of those events for the caller to own.
/// <para></para>
/// This method allows multiple callers to safely take turns using the same state
/// as long as they each call this method to assign their own events.
/// <para></para>
/// Also calls <see cref="AssertOwnership"/>.
/// <para></para>
/// <strong>Documentation:</strong>
/// <see href="https://kybernetik.com.au/animancer/docs/manual/events/animancer">
/// Animancer Events</see>
/// <para></para>
/// <strong>Example:</strong>
/// <code>
/// public static readonly StringReference EventName = "Event Name";
///
/// private AnimancerEvent.Sequence _Events;// Don't new() this.
///
/// ...
///
/// AnimancerState state = animancerComponent.Play(animation);
///
/// // The first time this is called it will assign a new event sequence
/// // to the _Events and return true so you can initialize it.
///
/// // After that, it will just re-assign the _Events to the state.
/// // and return false so you don't need to re-initialize the events.
///
/// if (state.Events(ref _Events))
/// {
/// _Events.SetCallback(EventName, OnAnimationEvent);
/// _Events.OnEnd = OnAnimationEnded;
/// }
/// </code>
/// </remarks>
public bool Events(ref AnimancerEvent.Sequence events)
{
_EventDispatcher ??= new(this);
var justInitialized = events == null;
if (justInitialized)
events = new(_EventDispatcher.Events);
#if UNITY_ASSERTIONS
// Normally swapping owners is an error,
// but with this method it's fine to swap between event sequences since each caller is responsible for its own.
if (Owner != null &&
Owner != events &&
Owner is not AnimancerEvent.Sequence)
AssertOwnership(events);
else
Owner = events;
#endif
_EventDispatcher.SetEvents(events, false);
return justInitialized;
}
/************************************************************************************************************************/
/// <summary>Copies the contents of the <see cref="_EventDispatcher"/>.</summary>
private void CopyEvents(AnimancerState copyFrom, CloneContext context)
{
if (copyFrom._EventDispatcher != null)
{
var original = copyFrom._EventDispatcher.Events;
var events = context.GetOrCreateCloneOrOriginal(original);
if (events != null)
{
_EventDispatcher ??= new(this);
_EventDispatcher.SetEvents(events, true);
return;
}
}
_EventDispatcher = null;
}
/************************************************************************************************************************/
/// <summary>Should events be raised on a state which is currently fading out?</summary>
/// <remarks>
/// Default <c>false</c>.
/// <para></para>
/// <strong>Documentation:</strong>
/// <see href="https://kybernetik.com.au/animancer/docs/manual/events/animancer">
/// Animancer Events</see>
/// </remarks>
public static bool RaiseEventsDuringFadeOut { get; set; }
/// <summary>Should this state check for events to invoke?</summary>
private bool ShouldRaiseEvents
=> TargetWeight > 0
|| RaiseEventsDuringFadeOut;
/************************************************************************************************************************/
/// <summary>
/// Checks if any events should be invoked based on the current time of this state.
/// </summary>
protected internal virtual void UpdateEvents()
=> _EventDispatcher?.UpdateEvents(ShouldRaiseEvents);
/// <summary>
/// Checks if any events should be invoked on the `parent` and its children recursively.
/// </summary>
public static void UpdateEventsRecursive(AnimancerState parent)
=> UpdateEventsRecursive(
parent,
parent.ShouldRaiseEvents);
/// <summary>
/// Checks if any events should be invoked on the `parent` and its children recursively.
/// </summary>
public static void UpdateEventsRecursive(AnimancerState parent, bool raiseEvents)
{
parent._EventDispatcher?.UpdateEvents(raiseEvents);
for (int i = parent.ChildCount - 1; i >= 0; i--)
{
var child = parent.GetChild(i);
UpdateEventsRecursive(child, raiseEvents && child.Weight > 0);
}
}
/************************************************************************************************************************/
/// <summary>
/// Sets the <see cref="NormalizedTime"/> to the <see cref="NormalizedEndTime"/>
/// and invokes any remaining <see cref="AnimancerEvent"/>s.
/// </summary>
public void FinishImmediately()
{
if (_EventDispatcher != null)
_EventDispatcher.FinishImmediately();
else
NormalizedTime = AnimancerEvent.Sequence.GetDefaultNormalizedEndTime(EffectiveSpeed);
}
/************************************************************************************************************************/
#if UNITY_ASSERTIONS
/************************************************************************************************************************/
/// <summary>[Assert-Only]
/// Returns <c>null</c> if Animancer Events will work properly on this type of state,
/// or a message explaining why they might not work.
/// </summary>
protected internal virtual string UnsupportedEventsMessage
=> null;
/************************************************************************************************************************/
/// <summary>[Assert-Only] An optional reference to the object that owns this state.</summary>
public object Owner { get; private set; }
/************************************************************************************************************************/
#endif
/************************************************************************************************************************/
/// <summary>[Assert-Conditional]
/// Sets the <see cref="Owner"/> and asserts that it wasn't already set to a different object.
/// </summary>
/// <remarks>This helps detect if multiple scripts attempt to manage the same state.</remarks>
[System.Diagnostics.Conditional(Strings.Assertions)]
public void AssertOwnership(object owner)
{
#if UNITY_ASSERTIONS
if (Owner == owner)
return;
if (Owner != null)
{
Debug.LogError(
$"Multiple objects have asserted ownership over the state '{ToString()}':" +
$"\n• Old Owner: {AnimancerUtilities.ToStringOrNull(Owner)}" +
$"\n• New Owner: {AnimancerUtilities.ToStringOrNull(owner)}" +
$"\n• State: {GetPath()}" +
$"\n• Graph: {Graph?.GetDescription("\n ")}",
Graph?.Component as Object);
}
Owner = owner;
#endif
}
/************************************************************************************************************************/
}
}

View File

@@ -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:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: bfafec2a7eda5e149b200fa583c51e15
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,64 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
using UnityEngine;
namespace Animancer
{
/// https://kybernetik.com.au/animancer/api/Animancer/AnimancerEvent
partial struct AnimancerEvent
{
/// <summary>A non-generic interface for <see cref="Parameter{T}"/>.</summary>
/// https://kybernetik.com.au/animancer/api/Animancer/IParameter
public interface IParameter
{
/************************************************************************************************************************/
/// <summary>The parameter value.</summary>
object Value { get; set; }
/************************************************************************************************************************/
}
/// <summary>
/// Base class for <see cref="IInvokable"/>s which assign the <see cref="CurrentParameter"/>.
/// </summary>
/// <remarks>
/// Inherit from <see cref="ParameterBoxed{T}"/>
/// instead of this if <typeparamref name="T"/> is a value type to avoid repeated boxing costs.
/// </remarks>
/// https://kybernetik.com.au/animancer/api/Animancer/Parameter_1
public abstract class Parameter<T> :
IParameter,
IInvokable
{
/************************************************************************************************************************/
[SerializeField]
private T _Value;
/// <summary>[<see cref="SerializeField"/>] The serialized <typeparamref name="T"/>.</summary>
public virtual T Value
{
get => _Value;
set => _Value = value;
}
/// <inheritdoc/>
object IParameter.Value
{
get => _Value;
set => _Value = (T)value;
}
/// <inheritdoc/>
public virtual void Invoke()
{
CurrentParameter = _Value;
Current.InvokeBoundCallback();
}
/************************************************************************************************************************/
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 5927c425f3f0c0e4baeb0cd737707442
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,68 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
using UnityEngine;
namespace Animancer
{
/// https://kybernetik.com.au/animancer/api/Animancer/AnimancerEvent
partial struct AnimancerEvent
{
/// <summary>
/// An <see cref="Parameter{T}"/>s which internally boxes value types
/// to avoid re-boxing them every <see cref="Invoke"/>.
/// </summary>
/// https://kybernetik.com.au/animancer/api/Animancer/ParameterBoxed_1
public abstract class ParameterBoxed<T> : Parameter<T>,
IParameter
#if UNITY_EDITOR
, ISerializationCallbackReceiver
#endif
where T : struct
{
/************************************************************************************************************************/
private object _Boxed;
/// <inheritdoc/>
public override T Value
{
get => base.Value;
set
{
base.Value = value;
_Boxed = null;
}
}
/// <inheritdoc/>
object IParameter.Value
{
get => Value;
set => Value = (T)value;
}
/// <inheritdoc/>
public override void Invoke()
{
CurrentParameter = _Boxed ??= base.Value;
Current.InvokeBoundCallback();
}
/************************************************************************************************************************/
#if UNITY_EDITOR
/************************************************************************************************************************/
/// <inheritdoc/>
void ISerializationCallbackReceiver.OnBeforeSerialize() { }
/// <inheritdoc/>
void ISerializationCallbackReceiver.OnAfterDeserialize()
=> _Boxed = null;
/************************************************************************************************************************/
#endif
/************************************************************************************************************************/
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 30eb95e632f815a49a7ac0f1d68cb006
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,56 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
using System;
namespace Animancer
{
/// https://kybernetik.com.au/animancer/api/Animancer/AnimancerEvent
partial struct AnimancerEvent
{
/************************************************************************************************************************/
// Reference Types.
/************************************************************************************************************************/
/// <summary>An <see cref="Parameter{T}"/> for <see cref="UnityEngine.Object"/>.</summary>
/// https://kybernetik.com.au/animancer/api/Animancer/ParameterObject
[Serializable]
public class ParameterObject : Parameter<UnityEngine.Object> { }
/// <summary>An <see cref="Parameter{T}"/> for <see cref="string"/>.</summary>
/// https://kybernetik.com.au/animancer/api/Animancer/ParameterString
[Serializable]
public class ParameterString : Parameter<string> { }
/************************************************************************************************************************/
// Value Types.
/************************************************************************************************************************/
/// <summary>An <see cref="Parameter{T}"/> for <see cref="bool"/>.</summary>
/// https://kybernetik.com.au/animancer/api/Animancer/ParameterBool
[Serializable]
public class ParameterBool : ParameterBoxed<bool> { }
/// <summary>An <see cref="Parameter{T}"/> for <see cref="double"/>.</summary>
/// https://kybernetik.com.au/animancer/api/Animancer/ParameterDouble
[Serializable]
public class ParameterDouble : ParameterBoxed<double> { }
/// <summary>An <see cref="Parameter{T}"/> for <see cref="float"/>.</summary>
/// https://kybernetik.com.au/animancer/api/Animancer/ParameterFloat
[Serializable]
public class ParameterFloat : ParameterBoxed<float> { }
/// <summary>An <see cref="Parameter{T}"/> for <see cref="int"/>.</summary>
/// https://kybernetik.com.au/animancer/api/Animancer/ParameterInt
[Serializable]
public class ParameterInt : ParameterBoxed<int> { }
/// <summary>An <see cref="Parameter{T}"/> for <see cref="long"/>.</summary>
/// https://kybernetik.com.au/animancer/api/Animancer/ParameterLong
[Serializable]
public class ParameterLong : ParameterBoxed<long> { }
/************************************************************************************************************************/
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: ddc1c2d634269b94891d46befabcb48b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,16 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
#if ULT_EVENTS
using System;
namespace Animancer
{
/// <summary>An <see cref="UltEvents.UltEvent"/> which implements <see cref="IInvokable"/>.</summary>
/// https://kybernetik.com.au/animancer/api/Animancer/UltEvent
[Serializable]
public class UltEvent : UltEvents.UltEvent, IInvokable { }
}
#endif

View File

@@ -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:

View File

@@ -0,0 +1,12 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
using System;
namespace Animancer
{
/// <summary>A <see cref="UnityEngine.Events.UnityEvent"/> which implements <see cref="IInvokable"/>.</summary>
/// https://kybernetik.com.au/animancer/api/Animancer/UnityEvent
[Serializable]
public class UnityEvent : UnityEngine.Events.UnityEvent, IInvokable { }
}

View File

@@ -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:

View File

@@ -0,0 +1,334 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
using System;
using System.Collections;
using System.Collections.Generic;
namespace Animancer
{
/// <summary>A dictionary which maps event names to callbacks.</summary>
/// <remarks>
/// <strong>Documentation:</strong>
/// <see href="https://kybernetik.com.au/animancer/docs/manual/events/animancer">
/// Animancer Events</see>
/// </remarks>
/// https://kybernetik.com.au/animancer/api/Animancer/NamedEventDictionary
public class NamedEventDictionary : IDictionary<StringReference, Action>
{
/************************************************************************************************************************/
private readonly Dictionary<StringReference, Action>
Dictionary = new();
/************************************************************************************************************************/
/// <summary>The number of items in this dictionary.</summary>
public int Count
=> Dictionary.Count;
/************************************************************************************************************************/
#region Access
/************************************************************************************************************************/
/// <summary>Accesses a callback in this dictionary.</summary>
public Action this[StringReference name]
{
get => Dictionary[name];
set
{
AssertNotEndEvent(name);
Dictionary[name] = value;
}
}
/************************************************************************************************************************/
/// <summary>Returns the callback registered using the `name`.</summary>
/// <remarks>Returns <c>null</c> if nothing was registered.</remarks>
public Action Get(StringReference name)
=> Dictionary.Get(name);
/// <summary>Registers the callback using the `name`, replacing anything previously registered.</summary>
public void Set(StringReference name, Action callback)
{
AssertNotEndEvent(name);
Dictionary[name] = callback;
}
/************************************************************************************************************************/
/// <summary>Are any callbacks registered for the `name`?</summary>
/// <remarks>To get the registered callbacks at the same time, use <see cref="TryGetValue"/> instead.</remarks>
public bool ContainsKey(StringReference name)
=> Dictionary.ContainsKey(name);
/************************************************************************************************************************/
/// <summary>Tries to get the `callback` registered with the `name` and returns <c>true</c> if successful.</summary>
public bool TryGetValue(StringReference name, out Action callback)
=> Dictionary.TryGetValue(name, out callback);
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Add
/************************************************************************************************************************/
/// <summary>Adds the `callback` to any existing ones registered with the `name`.</summary>
/// <remarks>
/// If you want an exception to be thrown if something is already registered with the `name`,
/// use <see cref="AddNew(StringReference, Action)"/> instead.
/// </remarks>
public void AddTo(StringReference name, Action callback)
{
AssertNotEndEvent(name);
if (Dictionary.TryGetValue(name, out var existing))
callback = existing + callback;
Dictionary[name] = callback;
}
/************************************************************************************************************************/
/// <summary>
/// Registers the `callback` with the `name` but throws an <see cref="ArgumentException"/>
/// if something was already registered with the same `name`.
/// </summary>
/// <remarks>
/// This matches the standard <see cref="Dictionary{TKey, TValue}.Add(TKey, TValue)"/> behaviour,
/// unlike <see cref="AddTo(StringReference, Action)"/>.
/// </remarks>
public void AddNew(StringReference name, Action callback)
{
AssertNotEndEvent(name);
Dictionary.Add(name, callback);
}
void IDictionary<StringReference, Action>.Add(StringReference name, Action callback)
=> AddNew(name, callback);
/************************************************************************************************************************/
/// <summary>
/// Adds the `callback` to any existing ones registered with the `name`.
/// <para></para>
/// It will be invoked using <see cref="AnimancerEvent.GetCurrentParameter{T}"/> to get its parameter.
/// </summary>
/// <remarks>
/// If you want an exception to be thrown if something is already registered with the `name`,
/// use <see cref="AddNew{T}(StringReference, Action{T})"/> instead.
/// <para></para>
/// If <typeparamref name="T"/> is <see cref="string"/>,
/// consider using <see cref="AddTo(StringReference, Action{string})"/> instead of this overload.
/// <para></para>
/// If you want to later remove the `callback`,
/// you need to store and remove the returned <see cref="Action"/>.
/// </remarks>
public Action AddTo<T>(StringReference name, Action<T> callback)
{
AssertNotEndEvent(name);
var parametized = AnimancerEvent.Parametize(callback);
if (Dictionary.TryGetValue(name, out var existing))
parametized = existing + parametized;
Dictionary[name] = parametized;
return parametized;
}
/// <summary>
/// Registers the `callback` with the `name` but throws an <see cref="ArgumentException"/>
/// if something was already registered with the same `name`.
/// <para></para>
/// It will be invoked using <see cref="AnimancerEvent.GetCurrentParameter{T}"/> to get its parameter.
/// </summary>
/// <remarks>
/// This matches the standard <see cref="Dictionary{TKey, TValue}.Add(TKey, TValue)"/> behaviour,
/// unlike <see cref="AddTo{T}(StringReference, Action{T})"/>.
/// If <typeparamref name="T"/> is <see cref="string"/>,
/// consider using <see cref="AddTo(StringReference, Action{string})"/> instead of this overload.
/// <para></para>
/// If you want to later remove the `callback`,
/// you need to store and remove the returned <see cref="Action"/>.
/// </remarks>
public Action AddNew<T>(StringReference name, Action<T> callback)
{
AssertNotEndEvent(name);
var parametized = AnimancerEvent.Parametize(callback);
Dictionary.Add(name, parametized);
return parametized;
}
/************************************************************************************************************************/
/// <summary>
/// Adds the `callback` to any existing ones registered with the `name`.
/// <para></para>
/// It will be invoked using <see cref="object.ToString"/> on the
/// <see cref="AnimancerEvent.CurrentParameter"/>.
/// </summary>
/// <remarks>
/// If you want an exception to be thrown if something is already registered with the `name`,
/// use <see cref="AddNew{T}(StringReference, Action{T})"/> instead.
/// <para></para>
/// If you want to later remove the `callback`,
/// you need to store and remove the returned <see cref="Action"/>.
/// </remarks>
public Action AddTo(StringReference name, Action<string> callback)
{
AssertNotEndEvent(name);
var parametized = AnimancerEvent.Parametize(callback);
if (Dictionary.TryGetValue(name, out var existing))
parametized = existing + parametized;
Dictionary[name] = parametized;
return parametized;
}
/// <summary>
/// Registers the `callback` with the `name` but throws an <see cref="ArgumentException"/>
/// if something was already registered with the same `name`.
/// <para></para>
/// It will be invoked using <see cref="object.ToString"/> on the
/// <see cref="AnimancerEvent.CurrentParameter"/>.
/// </summary>
/// <remarks>
/// This matches the standard <see cref="Dictionary{TKey, TValue}.Add(TKey, TValue)"/>
/// behaviour, unlike <see cref="AddTo(StringReference, Action{string})"/>.
/// <para></para>
/// If you want to later remove the `callback`,
/// you need to store and remove the returned <see cref="Action"/>.
/// </remarks>
public Action AddNew(StringReference name, Action<string> callback)
{
AssertNotEndEvent(name);
var parametized = AnimancerEvent.Parametize(callback);
Dictionary.Add(name, parametized);
return parametized;
}
/************************************************************************************************************************/
/// <summary>[Assert-Conditional]
/// Throws an <see cref="ArgumentException"/> if the `name` is the <see cref="AnimancerEvent.EndEventName"/>.
/// </summary>
/// <remarks>
/// In order to minimise the performance cost of End Events when there isn't one,
/// the <see cref="AnimancerEvent.Dispatcher"/> won't even check the end time
/// when there is no <see cref="AnimancerEvent.Sequence.OnEnd"/> callback.
/// <para></para>
/// That means if a callback was bound to the <see cref="AnimancerEvent.EndEventName"/>
/// it would be triggered by any state with an <see cref="AnimancerEvent.Sequence.OnEnd"/>
/// callback, but not by states without one. That would be very counterintuitive so it isn't allowed.
/// </remarks>
/// <exception cref="ArgumentException"/>
[System.Diagnostics.Conditional(Strings.Assertions)]
public static void AssertNotEndEvent(StringReference name)
{
if (name == AnimancerEvent.EndEventName)
throw new ArgumentException(
$"Binding event callbacks to the " +
$"{nameof(AnimancerEvent)}.{nameof(AnimancerEvent.EndEventName)}" +
$" is not supported for performance optimization reasons. See the documentation of" +
$" {nameof(NamedEventDictionary)}.{nameof(AssertNotEndEvent)} for more details.",
nameof(name));
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Remove
/************************************************************************************************************************/
/// <summary>Removes all callbacks registered with the `name`.</summary>
public bool Remove(StringReference name)
=> Dictionary.Remove(name);
/// <summary>Removes a specific `callback` registered with the `name`.</summary>
public bool Remove(StringReference name, Action callback)
{
if (!Dictionary.TryGetValue(name, out var callbacks))
return false;
if (callbacks == callback)
Dictionary.Remove(name);
else
Dictionary[name] = callbacks - callback;
return true;
}
/************************************************************************************************************************/
/// <summary>Removes everything from this dictionary.</summary>
public void Clear()
=> Dictionary.Clear();
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Enumeration
/************************************************************************************************************************/
/// <summary>Returns an enumerator to go through every item in this dictionary.</summary>
public Dictionary<StringReference, Action>.Enumerator GetEnumerator()
=> Dictionary.GetEnumerator();
IEnumerator<KeyValuePair<StringReference, Action>>
IEnumerable<KeyValuePair<StringReference, Action>>.GetEnumerator()
=> Dictionary.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator()
=> Dictionary.GetEnumerator();
/************************************************************************************************************************/
/// <summary>The names in this dictionary.</summary>
public Dictionary<StringReference, Action>.KeyCollection Keys
=> Dictionary.Keys;
/// <summary>The values in this dictionary.</summary>
public Dictionary<StringReference, Action>.ValueCollection Values
=> Dictionary.Values;
ICollection<StringReference> IDictionary<StringReference, Action>.Keys
=> Dictionary.Keys;
ICollection<Action> IDictionary<StringReference, Action>.Values
=> Dictionary.Values;
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Explicit Dictionary Wrappers
/************************************************************************************************************************/
void ICollection<KeyValuePair<StringReference, Action>>
.Add(KeyValuePair<StringReference, Action> item)
=> AddTo(item.Key, item.Value);
bool ICollection<KeyValuePair<StringReference, Action>>
.Contains(KeyValuePair<StringReference, Action> item)
=> ((ICollection<KeyValuePair<StringReference, Action>>)Dictionary).Contains(item);
void ICollection<KeyValuePair<StringReference, Action>>
.CopyTo(KeyValuePair<StringReference, Action>[] array,
int arrayIndex)
=> ((ICollection<KeyValuePair<StringReference, Action>>)Dictionary).CopyTo(array, arrayIndex);
bool ICollection<KeyValuePair<StringReference, Action>>
.Remove(KeyValuePair<StringReference, Action> item)
=> Dictionary.Remove(item.Key);
bool ICollection<KeyValuePair<StringReference, Action>>.IsReadOnly
=> false;
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
}
}

View File

@@ -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:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 0d10e76a0343a30418b3cbc71bf313e3
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,733 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
#if UNITY_ASSERTIONS
//#define ANIMANCER_ASSERT_FADE_GRAPH
#endif
using System;
using System.Collections.Generic;
using System.Text;
using UnityEngine;
using UnityEngine.Playables;
using Object = UnityEngine.Object;
namespace Animancer
{
/// <summary>A group of <see cref="AnimancerNode"/>s which are cross-fading.</summary>
///
/// <remarks>
/// <strong>Documentation:</strong>
/// <see href="https://kybernetik.com.au/animancer/docs/manual/blending/fading/custom">
/// Custom Easing</see>
/// </remarks>
///
/// https://kybernetik.com.au/animancer/api/Animancer/FadeGroup
///
public partial class FadeGroup : Updatable,
ICloneable<FadeGroup>,
ICopyable<FadeGroup>,
IHasDescription
{
/************************************************************************************************************************/
#region Fields and Properties
/************************************************************************************************************************/
// Parameters.
/************************************************************************************************************************/
/// <summary>The 0-1 progress of this fade.</summary>
public float NormalizedTime { get; set; }
/// <summary>The <see cref="AnimancerNode.Weight"/> which the <see cref="FadeIn"/> is moving towards.</summary>
public float TargetWeight { get; set; }
/// <summary>The speed at which the <see cref="NormalizedTime"/> increases.</summary>
public float NormalizedFadeSpeed { get; set; }
/// <summary>The speed at which the <see cref="AnimancerNode.Weight"/>s change.</summary>
public float FadeSpeed
{
get => FadeDistance * NormalizedFadeSpeed;
set => NormalizedFadeSpeed = value / FadeDistance;
}
/// <summary>The distance from the starting weight to the <see cref="TargetWeight"/>.</summary>
public float FadeDistance
=> Math.Abs(TargetWeight - FadeIn.StartingWeight);
/// <summary>The total amount of time this fade will take to complete (in seconds).</summary>
public float FadeDuration
{
get => NormalizedFadeSpeed != 0
? 1 / NormalizedFadeSpeed
: float.PositiveInfinity;
set => NormalizedFadeSpeed = value != 0
? 1 / value
: float.PositiveInfinity;
}
/// <summary>The remaining amount of time this fade will take to complete (in seconds).</summary>
public float RemainingFadeDuration
{
get => NormalizedFadeSpeed != 0
? (1 - NormalizedTime) / NormalizedFadeSpeed
: float.PositiveInfinity;
set => NormalizedFadeSpeed = value != 0
? (1 - NormalizedTime) / value
: float.PositiveInfinity;
}
/************************************************************************************************************************/
// Parent.
/************************************************************************************************************************/
/// <summary>The <see cref="AnimancerNodeBase.Graph"/>.</summary>
public AnimancerGraph Graph { get; private set; }
/// <summary>The <see cref="AnimancerNodeBase.Graph"/>.</summary>
public AnimancerNodeBase Parent { get; private set; }
/// <summary>The <see cref="AnimancerNodeBase.Playable"/> of the <see cref="Parent"/>.</summary>
public Playable ParentPlayable { get; private set; }
/// <summary>Should the fading nodes always be connected to the <see cref="ParentPlayable"/>?</summary>
public bool KeepChildrenConnected { get; private set; }
/************************************************************************************************************************/
// Nodes.
/************************************************************************************************************************/
/// <summary>The node which is fading towards the <see cref="TargetWeight"/>.</summary>
public NodeWeight FadeIn { get; private set; }
internal readonly List<NodeWeight> FadeOutInternal = new();
/// <summary>The nodes which are fading out.</summary>
public IReadOnlyList<NodeWeight> FadeOut => FadeOutInternal;
/************************************************************************************************************************/
// Custom Fade.
/************************************************************************************************************************/
private Func<float, float> _Easing;
/// <summary>[Pro-Only] An optional function for modifying the fade curve.</summary>
/// <remarks>
/// The <see cref="NormalizedTime"/> is passed in and the return value is multiplied by the
/// <see cref="TargetWeight"/> to set the <see cref="AnimancerNode.Weight"/> of the <see cref="FadeIn"/>.
/// <para></para>
/// <see cref="Animancer.Easing"/> has various common functions that could be used here.
/// <para></para>
/// Note that the <see cref="AnimancerNode.FadeGroup"/> may be <c>null</c>
/// right after playing something if it was already playing, so
/// <see cref="FadeGroupExtensions.SetEasing(FadeGroup, Easing.Function)"/>
/// <see cref="FadeGroupExtensions.SetEasing(FadeGroup, Func{float, float})"/>
/// can be used to avoid needing to null-check it.
/// <para></para>
/// <em>Animancer Lite ignores this property in runtime builds.</em>
/// </remarks>
public Func<float, float> Easing
{
get => _Easing;
set
{
_Easing = value;
AssertNormalizedBounds(value, nameof(Easing));
}
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Initialization
/************************************************************************************************************************/
/// <summary>Assigns the target nodes that will be faded.</summary>
public void SetNodes(
AnimancerNode parent,
AnimancerNode fadeIn,
IReadOnlyList<AnimancerNode> fadeOut,
bool keepChildrenConnected)
{
Parent = parent;
Graph = parent.Graph;
ParentPlayable = parent.Playable;
KeepChildrenConnected = keepChildrenConnected;
FadeIn = new(fadeIn);
if (fadeIn.FadeGroup != this)
fadeIn.FadeGroup = this;
var count = fadeOut.Count;
for (int i = 0; i < count; i++)
{
var node = fadeOut[i];
if (node != fadeIn)
{
FadeOutInternal.Add(new(node));
if (node.FadeGroup != this)
node.FadeGroup = this;
}
}
}
/************************************************************************************************************************/
/// <summary>Assigns the <see cref="FadeIn"/> with no <see cref="FadeOut"/>.</summary>
public void SetFadeIn(AnimancerNode fadeIn)
{
Parent = fadeIn.Parent;
if (Parent != null)
{
Graph = fadeIn.Graph;
ParentPlayable = Parent.Playable;
KeepChildrenConnected = Parent.KeepChildrenConnected;
}
FadeIn = new(fadeIn);
fadeIn.FadeGroup = this;
}
/************************************************************************************************************************/
/// <summary>Adds a node to the <see cref="FadeOut"/> list.</summary>
public void AddFadeOut(AnimancerNode fadeOut)
{
FadeOutInternal.Add(new(fadeOut));
fadeOut.FadeGroup = this;
}
/************************************************************************************************************************/
/// <summary>Sets the starting values and registers this fade to be updated.</summary>
public void StartFade(
float targetWeight,
float normalizedFadeSpeed)
{
NormalizedTime = 0;
TargetWeight = targetWeight;
NormalizedFadeSpeed = normalizedFadeSpeed;
StartFade();
}
/// <summary>Registers this fade to be updated.</summary>
public void StartFade()
{
Graph?.RequirePreUpdate(this);
FadeIn.Node?.OnStartFade();
for (int i = FadeOutInternal.Count - 1; i >= 0; i--)
FadeOutInternal[i].Node.OnStartFade();
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Queries
/************************************************************************************************************************/
/// <summary>Should this fade continue?</summary>
public bool IsValid
=> NormalizedFadeSpeed > 0;
/************************************************************************************************************************/
/// <summary>Does this fade affect the `node`?</summary>
public bool Contains(AnimancerNode node)
{
if (FadeIn.Node == node)
return true;
for (int i = 0; i < FadeOutInternal.Count; i++)
if (FadeOutInternal[i].Node == node)
return true;
return false;
}
/************************************************************************************************************************/
/// <summary>
/// Returns the <see cref="TargetWeight"/> if the `node` is the <see cref="FadeIn"/>.
/// Otherwise, returns 0.
/// </summary>
public float GetTargetWeight(AnimancerNode node)
{
return FadeIn.Node == node
? TargetWeight
: 0;
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Methods
/************************************************************************************************************************/
/// <inheritdoc/>
public override void Update()
{
if (!IsValid)
{
Cancel();
return;
}
AssertGraph();
NormalizedTime += Math.Abs(AnimancerGraph.DeltaTime * Parent.EffectiveSpeed * NormalizedFadeSpeed);
if (NormalizedTime < 1)// Fade.
{
ApplyWeights();
}
else// End.
{
Finish();
}
}
/************************************************************************************************************************/
/// <summary>Immediately finishes this fade.</summary>
public void Finish()
{
NormalizedTime = 1;
if (KeepChildrenConnected)
{
ApplyWeights(1);
for (int i = FadeOutInternal.Count - 1; i >= 0; i--)
FadeOutInternal[i].Node.StopWithoutWeight();
if (TargetWeight == 0)
FadeIn.Node?.StopWithoutWeight();
}
else// Disconnect all faded out nodes and only apply the faded in weight.
{
for (int i = FadeOutInternal.Count - 1; i >= 0; i--)
StopAndDisconnect(FadeOutInternal[i].Node);
FadeOutInternal.Clear();
if (FadeIn.Node != null)
{
if (TargetWeight > 0)
FadeIn.Node.SetWeight(TargetWeight);
else
StopAndDisconnect(FadeIn.Node);
}
}
Cancel();
}
/************************************************************************************************************************/
/// <summary>
/// Recalculates the node weights based on the <see cref="NormalizedTime"/>.
/// </summary>
public void ApplyWeights()
{
if (NormalizedTime < 1)// Fade.
{
var progress = NormalizedTime;
if (_Easing != null)
progress = _Easing(progress);
ApplyWeights(progress);
}
else// End.
{
Finish();
}
}
private void ApplyWeights(float progress)
{
// Move FadeIn towards target (usually 1 or 0).
FadeIn.Node?.SetWeight(Mathf.LerpUnclamped(FadeIn.StartingWeight, TargetWeight, progress));
// Move FadeOut towards 0.
progress = 1 - progress;
for (int i = FadeOutInternal.Count - 1; i >= 0; i--)
{
var node = FadeOutInternal[i];
node.Node.SetWeight(node.StartingWeight * progress);
}
}
private void StopAndDisconnect(AnimancerNode node)
{
// Don't InternalClearFade because it's virtual.
node._FadeGroup = null;
node.Stop();
}
/************************************************************************************************************************/
private void Release()
{
NormalizedFadeSpeed = 0;
_Easing = null;
Graph = null;
Parent = null;
if (FadeIn.Node != null)
{
FadeIn.Node.InternalClearFade();
FadeIn = default;
}
for (int i = FadeOutInternal.Count - 1; i >= 0; i--)
FadeOutInternal[i].Node.InternalClearFade();
FadeOutInternal.Clear();
Pool.Instance.Release(this);
}
/************************************************************************************************************************/
/// <summary>Interrupts this fade and releases it to the <see cref="ObjectPool{T}"/>.</summary>
public void Cancel()
{
Graph?.CancelPreUpdate(this);
Release();
}
/************************************************************************************************************************/
/// <summary>Removes the `node` from this <see cref="FadeGroup"/> and returns true if successful.</summary>
public bool Remove(AnimancerNode node)
{
if (FadeIn.Node == node)
{
FadeIn = new(null, FadeIn.StartingWeight);
if (FadeOutInternal.Count == 0)
Cancel();
node.InternalClearFade();
return true;
}
for (int i = FadeOutInternal.Count - 1; i >= 0; i--)
{
if (FadeOutInternal[i].Node == node)
{
FadeOutInternal.RemoveAt(i);
if (FadeIn.Node == null && FadeOutInternal.Count == 0)
Cancel();
node.InternalClearFade();
return true;
}
}
return false;
}
/************************************************************************************************************************/
/// <inheritdoc/>
public virtual void AppendDescription(StringBuilder text, string separator = "\n")
{
text.Append(GetType().FullName);
if (!IsValid)
{
text.Append("(Cancelled)");
return;
}
if (!separator.StartsWithNewLine())
separator = "\n" + separator;
text.AppendField(separator, nameof(NormalizedTime), NormalizedTime);
text.AppendField(separator, nameof(NormalizedFadeSpeed), NormalizedFadeSpeed);
text.AppendField(separator, nameof(Easing), _Easing?.ToStringDetailed());
text.Append(separator).Append($"{nameof(FadeIn)}: ");
FadeIn.AppendDescription(text, TargetWeight);
text.AppendField(separator, nameof(FadeOut), FadeOutInternal.Count);
for (int i = 0; i < FadeOutInternal.Count; i++)
{
text.Append(separator)
.Append(Strings.Indent);
FadeOutInternal[i].AppendDescription(text, 0);
}
}
/************************************************************************************************************************/
/// <summary>[Assert-Conditional] Checks <see cref="OptionalWarning.FadeEasingBounds"/>.</summary>
[System.Diagnostics.Conditional(Strings.Assertions)]
public static void AssertNormalizedBounds(Func<float, float> easing, string name = "function")
{
#if UNITY_ASSERTIONS
if (easing != null && OptionalWarning.FadeEasingBounds.IsEnabled())
{
if (easing(0) != 0)
OptionalWarning.FadeEasingBounds.Log(name + "(0) != 0.");
if (easing(1) != 1)
OptionalWarning.FadeEasingBounds.Log(name + "(1) != 1.");
}
#endif
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Cloning
/************************************************************************************************************************/
/// <inheritdoc/>
public virtual FadeGroup Clone(CloneContext context)
{
if (!IsValid)
return null;
var clone = new FadeGroup();
clone.CopyFrom(this, context);
return clone;
}
/************************************************************************************************************************/
/// <inheritdoc/>
public virtual void CopyFrom(FadeGroup copyFrom, CloneContext context)
{
CopyNodesFrom(copyFrom, context);
var node = FadeIn.Node;
if (node == null)
{
if (FadeOut.Count == 0)
return;
node = FadeOut[0].Node;
}
ChangeParent(node);
CopyDetailsFrom(copyFrom);
}
/************************************************************************************************************************/
private void CopyNodesFrom(FadeGroup copyFrom, CloneContext context)
{
FadeIn = new(copyFrom.FadeIn, context);
FadeIn.Node.FadeGroup = this;
FadeOutInternal.Clear();
var count = copyFrom.FadeOutInternal.Count;
for (int i = 0; i < count; i++)
{
var nodeWeight = new NodeWeight(copyFrom.FadeOutInternal[i], context);
if (nodeWeight.Node != null)
{
FadeOutInternal.Add(nodeWeight);
nodeWeight.Node.FadeGroup = this;
}
}
}
/************************************************************************************************************************/
internal void ChangeParent(AnimancerNode child)
{
var parent = child.Parent;
if (Parent == parent)
return;
Parent = parent;
if (Parent != null)
{
ParentPlayable = Parent.Playable;
KeepChildrenConnected = Parent.KeepChildrenConnected;
ChangeGraph(child.Graph);
_AssertGraphNextFrame = true;
}
}
/************************************************************************************************************************/
internal void ChangeGraph(AnimancerGraph graph)
{
if (Graph == graph)
return;
Graph?.CancelPreUpdate(this);
Graph = graph;
Graph?.RequirePreUpdate(this);
_AssertGraphNextFrame = true;
}
/************************************************************************************************************************/
private bool _AssertGraphNextFrame;
private void AssertGraph()
{
if (!_AssertGraphNextFrame)
return;
_AssertGraphNextFrame = false;
if (FadeIn.Node != null && !AssertNode(FadeIn.Node))
return;
for (int i = 0; i < FadeOutInternal.Count; i++)
if (!AssertNode(FadeOutInternal[i].Node))
return;
}
private bool AssertNode(AnimancerNode node)
{
string propertyName;
string nodeValue, myValue;
if (node.Graph == Graph)
{
if (node.Parent == Parent)
return true;
propertyName = nameof(node.Parent);
nodeValue = AnimancerUtilities.ToStringOrNull(node.Parent);
myValue = AnimancerUtilities.ToStringOrNull(Parent);
}
else
{
propertyName = nameof(node.Graph);
nodeValue = AnimancerUtilities.ToStringOrNull(node.Graph);
myValue = AnimancerUtilities.ToStringOrNull(Graph);
}
var graph = Graph ?? node.Graph;
Debug.LogWarning(
$"{nameof(AnimancerNode)}.{propertyName} doesn't match {nameof(FadeGroup)}.{propertyName}." +
$"\n• Node: {node.GetPath()}" +
$"\n• Node.{propertyName}: {nodeValue}" +
$"\n• This.{propertyName}: {myValue}" +
$"\n• Graph: {graph?.GetDescription("\n ")}");
return false;
}
/************************************************************************************************************************/
private void CopyDetailsFrom(FadeGroup copyFrom)
{
NormalizedTime = copyFrom.NormalizedTime;
NormalizedFadeSpeed = copyFrom.NormalizedFadeSpeed;
TargetWeight = copyFrom.TargetWeight;
_Easing = copyFrom._Easing;
}
/************************************************************************************************************************/
/// <summary>Creates a clone of this <see cref="FadeGroup"/> for a single target node (`copyTo`).</summary>
public FadeGroup CloneForSingleTarget(AnimancerNode copyFrom, AnimancerNode copyTo)
{
if (!IsValid)
return null;
var clone = Pool.Instance.Acquire();
if (copyFrom == FadeIn.Node)
{
clone.FadeIn = new(copyTo, FadeIn.StartingWeight);
}
else
{
for (int i = 0; i < FadeOutInternal.Count; i++)
{
var fadeOut = FadeOutInternal[i];
if (fadeOut.Node == copyFrom)
{
clone.FadeOutInternal.Add(new(copyTo, fadeOut.StartingWeight));
goto CopyDetails;
}
}
return null;
}
CopyDetails:
clone.ChangeParent(copyTo);
clone.CopyDetailsFrom(this);
clone.StartFade();
return clone;
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Pooling
/************************************************************************************************************************/
/// <summary>An <see cref="ObjectPool{T}"/> for <see cref="FadeGroup"/>.</summary>
/// https://kybernetik.com.au/animancer/api/Animancer/Pool
public class Pool : ObjectPool<FadeGroup>
{
/************************************************************************************************************************/
/// <summary>Singleton.</summary>
public static Pool Instance = new();
/************************************************************************************************************************/
/// <inheritdoc/>
protected override FadeGroup New()
=> new();
/************************************************************************************************************************/
#if UNITY_ASSERTIONS
/************************************************************************************************************************/
/// <inheritdoc/>
public override FadeGroup Acquire()
{
var fade = base.Acquire();
Debug.Assert(fade.FadeIn.Node == null, $"{nameof(fade.FadeIn)} is not null");
Debug.Assert(fade.FadeOutInternal.Count == 0, $"{nameof(fade.FadeOutInternal)} is not empty");
Debug.Assert(fade.Easing == null, $"{nameof(fade.Easing)} is not null");
return fade;
}
/// <inheritdoc/>
public override void Release(FadeGroup item)
{
Debug.Assert(((IUpdatable)item).UpdatableIndex < 0,
$"Releasing {nameof(FadeGroup)} which is still registered for updates.",
item.Graph?.Component as Object);
base.Release(item);
}
/************************************************************************************************************************/
#endif
/************************************************************************************************************************/
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
}
}

View File

@@ -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:

View File

@@ -0,0 +1,73 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
using System.Text;
namespace Animancer
{
/// <summary>An <see cref="AnimancerNode"/> and its <see cref="StartingWeight"/>.</summary>
/// https://kybernetik.com.au/animancer/api/Animancer/NodeWeight
public readonly struct NodeWeight
{
/************************************************************************************************************************/
/// <summary>The <see cref="AnimancerNode"/>.</summary>
public readonly AnimancerNode Node;
/// <summary>The <see cref="AnimancerNode.Weight"/> from when this struct was captured.</summary>
public readonly float StartingWeight;
/************************************************************************************************************************/
/// <summary>Creates a new <see cref="NodeWeight"/>.</summary>
public NodeWeight(AnimancerNode node)
{
Node = node;
StartingWeight = node.Weight;
}
/************************************************************************************************************************/
/// <summary>Creates a new <see cref="NodeWeight"/>.</summary>
public NodeWeight(AnimancerNode node, float startingWeight)
{
Node = node;
StartingWeight = startingWeight;
}
/************************************************************************************************************************/
/// <summary>Creates a copy of `copyFrom`.</summary>
public NodeWeight(NodeWeight copyFrom, CloneContext context)
{
Node = context.GetOrCreateCloneOrOriginal(copyFrom.Node);
StartingWeight = copyFrom.StartingWeight;
}
/************************************************************************************************************************/
/// <summary>Appends a detailed descrption of this object.</summary>
public void AppendDescription(StringBuilder text, float targetWeight)
{
if (Node == null)
{
text.Append("Null: ")
.Append(StartingWeight)
.Append(" -> ")
.Append(targetWeight);
}
else
{
text.Append(Node.GetPath())
.Append(": ")
.Append(StartingWeight)
.Append(" -> ")
.Append(Node.Weight)
.Append(" -> ")
.Append(targetWeight);
}
}
/************************************************************************************************************************/
}
}

View File

@@ -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:

View File

@@ -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

View File

@@ -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:

View File

@@ -0,0 +1,819 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
using System;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Text;
using UnityEngine;
using UnityEngine.Playables;
using Object = UnityEngine.Object;
namespace Animancer
{
/// <summary>Base class for <see cref="Playable"/> wrapper objects in an <see cref="AnimancerGraph"/>.</summary>
/// <remarks>This is the base class of <see cref="AnimancerLayer"/> and <see cref="AnimancerState"/>.</remarks>
/// https://kybernetik.com.au/animancer/api/Animancer/AnimancerNode
public abstract class AnimancerNode : AnimancerNodeBase,
ICopyable<AnimancerNode>,
IEnumerable<AnimancerState>,
IEnumerator,
IHasDescription
{
/************************************************************************************************************************/
#region Playable
/************************************************************************************************************************/
#if UNITY_EDITOR
/// <summary>[Editor-Only] [Internal] Indicates whether the Inspector details for this node are expanded.</summary>
internal bool _IsInspectorExpanded;
#endif
/************************************************************************************************************************/
/// <summary>Creates and assigns the <see cref="Playable"/> managed by this node.</summary>
/// <remarks>This method also applies the <see cref="AnimancerNodeBase.Speed"/> if it was set beforehand.</remarks>
protected virtual void CreatePlayable()
{
#if UNITY_ASSERTIONS
if (Graph == null)
{
MarkAsUsed(this);
throw new InvalidOperationException($"{nameof(AnimancerNode)}.{nameof(Graph)}" +
$" is null when attempting to create its {nameof(Playable)}: {this}" +
$"\nThe {nameof(Graph)} is generally set when you first play a state," +
$" so you probably just need to play it before trying to access it.");
}
if (_Playable.IsValid())
Debug.LogWarning($"{nameof(AnimancerNode)}.{nameof(CreatePlayable)}" +
$" was called before destroying the previous {nameof(Playable)}: {this}", Graph?.Component as Object);
#endif
CreatePlayable(out _Playable);
#if UNITY_ASSERTIONS
if (!_Playable.IsValid())
throw new InvalidOperationException(
$"{nameof(AnimancerNode)}.{nameof(CreatePlayable)}" +
$" did not create a valid {nameof(Playable)} for {this}");
#endif
if (Speed != 1)
_Playable.SetSpeed(Speed);
}
/// <summary>Creates and assigns the <see cref="Playable"/> managed by this node.</summary>
protected abstract void CreatePlayable(out Playable playable);
/************************************************************************************************************************/
/// <summary>Destroys the <see cref="Playable"/>.</summary>
public void DestroyPlayable()
{
if (_Playable.IsValid())
Graph._PlayableGraph.DestroyPlayable(_Playable);
}
/************************************************************************************************************************/
/// <summary>Calls <see cref="DestroyPlayable"/> and <see cref="CreatePlayable()"/>.</summary>
public virtual void RecreatePlayable()
{
DestroyPlayable();
CreatePlayable();
}
/// <summary>Calls <see cref="RecreatePlayable"/> on this node and all its children recursively.</summary>
public void RecreatePlayableRecursive()
{
RecreatePlayable();
for (int i = ChildCount - 1; i >= 0; i--)
GetChild(i)?.RecreatePlayableRecursive();
}
/************************************************************************************************************************/
/// <summary>Copies the details of `copyFrom` into this node, replacing its previous contents.</summary>
public virtual void CopyFrom(AnimancerNode copyFrom, CloneContext context)
{
SetWeight(copyFrom._Weight);
FadeGroup = context.WillCloneUpdatables
? null
: copyFrom.FadeGroup?.CloneForSingleTarget(copyFrom, this);
Speed = copyFrom.Speed;
CopyIKFlags(copyFrom);
#if UNITY_ASSERTIONS
DebugName = context.GetCloneOrOriginal(copyFrom.DebugName);
#endif
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Graph
/************************************************************************************************************************/
/// <summary>The index of the port this node is connected to on the parent's <see cref="Playable"/>.</summary>
/// <remarks>
/// A negative value indicates that it is not assigned to a port.
/// <para></para>
/// Indices are generally assigned starting from 0, ascending in the order they are connected to their layer.
/// They will not usually change unless the <see cref="AnimancerNodeBase.Parent"/> changes or another state on
/// the same layer is destroyed so the last state is swapped into its place to avoid shuffling everything down
/// to cover the gap.
/// <para></para>
/// The setter is internal so user defined states cannot set it incorrectly. Ideally,
/// <see cref="AnimancerLayer"/> should be able to set the port in its constructor and
/// <see cref="AnimancerState.SetParent"/> should also be able to set it, but classes that further inherit from
/// there should not be able to change it without properly calling that method.
/// </remarks>
public int Index { get; internal set; } = int.MinValue;
/************************************************************************************************************************/
/// <summary>Creates a new <see cref="AnimancerNode"/>.</summary>
protected AnimancerNode()
{
#if UNITY_ASSERTIONS
if (TraceConstructor)
_ConstructorStackTrace = new(true);
#endif
}
/************************************************************************************************************************/
#if UNITY_ASSERTIONS
/************************************************************************************************************************/
/// <summary>[Assert-Only]
/// Should a <see cref="System.Diagnostics.StackTrace"/> be captured in the constructor of all new nodes so
/// <see cref="OptionalWarning.UnusedNode"/> can include it in the warning if that node ends up being unused?
/// </summary>
/// <remarks>This has a notable performance cost so it should only be used when trying to identify a problem.</remarks>
public static bool TraceConstructor { get; set; }
/************************************************************************************************************************/
/// <summary>[Assert-Only]
/// The stack trace of the constructor (or null if <see cref="TraceConstructor"/> was false).
/// </summary>
private System.Diagnostics.StackTrace _ConstructorStackTrace;
/// <summary>[Assert-Only]
/// Returns the stack trace of the constructor (or null if <see cref="TraceConstructor"/> was false).
/// </summary>
public static System.Diagnostics.StackTrace GetConstructorStackTrace(AnimancerNode node)
=> node._ConstructorStackTrace;
/************************************************************************************************************************/
/// <summary>[Assert-Only] Checks <see cref="OptionalWarning.UnusedNode"/>.</summary>
~AnimancerNode()
{
if (Graph != null ||
Parent != null ||
OptionalWarning.UnusedNode.IsDisabled())
return;
// ToString might throw an exception since finalizers arn't run on the main thread.
string name = null;
try { name = ToString(); }
catch { name = GetType().FullName; }
var message = $"The {nameof(Graph)} of '{name}'" +
$" is null during finalization (garbage collection)." +
$" This may have been caused by earlier exceptions, but otherwise it probably means" +
$" that this node was never used for anything and should not have been created.";
if (_ConstructorStackTrace != null)
message += "\n\nThis node was created at:\n" + _ConstructorStackTrace;
else
message += $"\n\nEnable {nameof(AnimancerNode)}.{nameof(TraceConstructor)} on startup" +
$" to allow this warning to include the {nameof(System.Diagnostics.StackTrace)}" +
$" of when the node was constructed.";
OptionalWarning.UnusedNode.Log(message);
}
/************************************************************************************************************************/
#endif
/************************************************************************************************************************/
/// <summary>Connects the `child`'s <see cref="Playable"/> to this node.</summary>
/// <remarks>This method is NOT safe to call if the child was already connected.</remarks>
protected internal void ConnectChildUnsafe(int index, AnimancerNode child)
{
#if UNITY_ASSERTIONS
if (index < 0)
{
MarkAsUsed(this);
throw new InvalidOperationException(
$"Invalid {nameof(index)} when attempting to connect to its parent:" +
"\n• Child: " + child +
"\n• Parent: " + this);
}
Validate.AssertPlayable(child);
#endif
Graph._PlayableGraph.Connect(_Playable, child._Playable, index, child._Weight);
}
/// <summary>Disconnects the <see cref="Playable"/> of the child at the specified `index` from this node.</summary>
/// <remarks>This method is safe to call if the child was already disconnected.</remarks>
protected void DisconnectChildSafe(int index)
{
if (_Playable.GetInput(index).IsValid())
Graph._PlayableGraph.Disconnect(_Playable, index);
}
/************************************************************************************************************************/
// IEnumerator for yielding in a coroutine to wait until animations have stopped.
/************************************************************************************************************************/
/// <summary>Is this node playing and not yet at its end?</summary>
/// <remarks>
/// This method is called by <see cref="IEnumerator.MoveNext"/> so this object can be used as a custom yield
/// instruction to wait until it finishes.
/// </remarks>
public abstract bool IsPlayingAndNotEnding();
bool IEnumerator.MoveNext()
=> IsPlayingAndNotEnding();
object IEnumerator.Current
=> null;
void IEnumerator.Reset() { }
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Children
/************************************************************************************************************************/
/// <inheritdoc/>
protected internal override AnimancerNode GetChildNode(int index)
=> GetChild(index);
/// <summary>Returns the state connected to the specified `index` as a child of this node.</summary>
/// <remarks>When overriding, don't call this base method because it throws an exception.</remarks>
/// <exception cref="NotSupportedException">This node can't have children.</exception>
public virtual AnimancerState GetChild(int index)
{
MarkAsUsed(this);
throw new NotSupportedException(this + " can't have children.");
}
/// <summary>Called when a child is connected with this node as its <see cref="AnimancerNodeBase.Parent"/>.</summary>
/// <remarks>When overriding, don't call this base method because it throws an exception.</remarks>
/// <exception cref="NotSupportedException">This node can't have children.</exception>
protected internal virtual void OnAddChild(AnimancerState child)
{
MarkAsUsed(this);
child.SetParentInternal(null);
throw new NotSupportedException(this + " can't have children.");
}
/************************************************************************************************************************/
// IEnumerable for 'foreach' statements.
/************************************************************************************************************************/
/// <summary>Gets an enumerator for all of this node's child states.</summary>
public virtual FastEnumerator<AnimancerState> GetEnumerator()
=> default;
IEnumerator<AnimancerState> IEnumerable<AnimancerState>.GetEnumerator()
=> GetEnumerator();
IEnumerator IEnumerable.GetEnumerator()
=> GetEnumerator();
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Weight
/************************************************************************************************************************/
/// <summary>[Internal] The current blend weight of this node. Accessed via <see cref="Weight"/>.</summary>
internal float _Weight;
/************************************************************************************************************************/
/// <summary>The current blend weight of this node which determines how much it affects the final output.</summary>
///
/// <remarks>
/// 0 has no effect while 1 applies the full effect and values inbetween apply a proportional effect.
/// <para></para>
/// Setting this property cancels any fade currently in progress. If you don't wish to do that, you can use
/// <see cref="SetWeight"/> instead.
/// <para></para>
/// <em>Animancer Lite only allows this value to be set to 0 or 1 in runtime builds.</em>
/// </remarks>
///
/// <example>
/// Calling <see cref="AnimancerLayer.Play(AnimationClip)"/> immediately sets the weight of all states to 0
/// and the new state to 1. Note that this is separate from other values like
/// <see cref="AnimancerState.IsPlaying"/> so a state can be paused at any point and still show its pose on the
/// character or it could be still playing at 0 weight if you want it to still trigger events (though states
/// are normally stopped when they reach 0 weight so you would need to explicitly set it to playing again).
/// <para></para>
/// Calling <see cref="AnimancerLayer.Play(AnimationClip, float, FadeMode)"/> doesn't immediately change
/// the weights, but instead calls <see cref="StartFade(float, float)"/> on every state to set their
/// <see cref="TargetWeight"/> and <see cref="FadeSpeed"/>. Then every update each state's weight will move
/// towards that target value at that speed.
/// </example>
public float Weight
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => _Weight;
set
{
FadeGroup = null;
SetWeight(value);
}
}
/// <summary>
/// Sets the current blend weight of this node which determines how much it affects the final output.
/// 0 has no effect while 1 applies the full effect of this node.
/// </summary>
/// <remarks>
/// This method allows any fade currently in progress to continue. If you don't wish to do that, you can set
/// the <see cref="Weight"/> property instead.
/// <para></para>
/// <em>Animancer Lite only allows this value to be set to 0 or 1 in runtime builds.</em>
/// </remarks>
public virtual void SetWeight(float value)
=> SetWeightInternal(value);
/// <summary>The internal non-<c>virtual</c> implementation of <see cref="SetWeight"/>.</summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void SetWeightInternal(float value)
{
if (_Weight == value)
return;
Validate.AssertSetWeight(this, value);
_Weight = value;
if (Graph != null)
Parent?.Playable.ApplyChildWeight(this);
}
/************************************************************************************************************************/
/// <inheritdoc/>
protected internal override float BaseWeight
=> Weight;
/// <summary>
/// The <see cref="Weight"/> of this state multiplied by the <see cref="Weight"/> of each of its parents down
/// the hierarchy to determine how much this state affects the final output.
/// </summary>
public float EffectiveWeight
{
get
{
var weight = Weight;
var parent = Parent;
while (parent != null)
{
weight *= parent.BaseWeight;
parent = parent.Parent;
}
return weight;
}
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Fading
/************************************************************************************************************************/
internal FadeGroup _FadeGroup;
/// <summary>The current fade being applied to this node (if any).</summary>
public FadeGroup FadeGroup
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => _FadeGroup;
internal set
{
_FadeGroup?.Remove(this);
_FadeGroup = value;
}
}
/// <summary>
/// The desired <see cref="Weight"/> which this node is fading towards according to the
/// <see cref="FadeSpeed"/>.
/// </summary>
public float TargetWeight
=> FadeGroup != null
? FadeGroup.GetTargetWeight(this)
: Weight;
/// <summary>The speed at which this node is fading towards the <see cref="TargetWeight"/>.</summary>
/// <remarks>
/// This value isn't affected by this node's <see cref="AnimancerNodeBase.Speed"/>,
/// but is affected by its parents.
/// </remarks>
public float FadeSpeed
=> FadeGroup != null
? FadeGroup.FadeSpeed
: 0;
/************************************************************************************************************************/
/// <summary>
/// Calls <see cref="OnStartFade"/> and starts fading the <see cref="Weight"/> over the course
/// of the <see cref="AnimancerGraph.DefaultFadeDuration"/> (in seconds).
/// </summary>
/// <remarks>
/// If the `targetWeight` is 0 then <see cref="Stop"/> will be called when the fade is complete.
/// <para></para>
/// If the <see cref="Weight"/> is already equal to the `targetWeight` then the fade will end
/// immediately.
/// </remarks>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void StartFade(float targetWeight)
=> StartFade(targetWeight, AnimancerGraph.DefaultFadeDuration);
/// <summary>
/// Calls <see cref="OnStartFade"/> and starts fading the <see cref="Weight"/>
/// over the course of the `fadeDuration` (in seconds).
/// </summary>
/// <remarks>
/// If the `targetWeight` is 0 then <see cref="Stop"/> will be called when the fade is complete.
/// <para></para>
/// If the <see cref="Weight"/> is already equal to the `targetWeight`
/// then the fade will end immediately.
/// <para></para>
/// <em>Animancer Lite only allows a `targetWeight` of 0 or 1
/// and the default `fadeDuration` (0.25 seconds) in runtime builds.</em>
/// </remarks>
public void StartFade(float targetWeight, float fadeDuration)
{
if (Weight == targetWeight && FadeGroup == null)
{
OnStartFade();
}
else if (fadeDuration > 0)
{
var fade = FadeGroup.Pool.Instance.Acquire();
fade.SetFadeIn(this);
fade.StartFade(targetWeight, 1 / fadeDuration);
}
else
{
Weight = targetWeight;
}
}
/************************************************************************************************************************/
/// <summary>Called by <see cref="StartFade(float, float)"/>.</summary>
protected internal abstract void OnStartFade();
/************************************************************************************************************************/
/// <summary>Removes this node from the <see cref="FadeGroup"/>.</summary>
public void CancelFade()
=> _FadeGroup?.Remove(this);
/// <summary>[Internal] Called by <see cref="FadeGroup.Remove"/>.</summary>
/// <remarks>Not called when a fade fully completes.</remarks>
protected internal virtual void InternalClearFade()
{
_FadeGroup = null;
}
/************************************************************************************************************************/
/// <summary>Stops the animation and makes it inactive immediately so it no longer affects the output.</summary>
/// <remarks>
/// Sets <see cref="Weight"/> = 0 by default unless overridden.
/// <para></para>
/// Note that playing something new will automatically stop the old animation.
/// </remarks>
public void Stop()
{
FadeGroup = null;
SetWeightInternal(0);
StopWithoutWeight();
}
/// <summary>[Internal] Stops this node without setting its <see cref="Weight"/>.</summary>
protected internal virtual void StopWithoutWeight() { }
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Inverse Kinematics
/************************************************************************************************************************/
/// <summary>
/// Should setting the <see cref="AnimancerNodeBase.Parent"/>
/// also set this node's <see cref="ApplyAnimatorIK"/> to match it?
/// Default is true.
/// </summary>
public static bool ApplyParentAnimatorIK { get; set; } = true;
/// <summary>
/// Should setting the <see cref="AnimancerNodeBase.Parent"/>
/// also set this node's <see cref="ApplyFootIK"/> to match it?
/// Default is true.
/// </summary>
public static bool ApplyParentFootIK { get; set; } = true;
/************************************************************************************************************************/
/// <summary>
/// Copies the IK settings from `copyFrom` into this node:
/// <list type="bullet">
/// <item>If <see cref="ApplyParentAnimatorIK"/> is true, copy <see cref="ApplyAnimatorIK"/>.</item>
/// <item>If <see cref="ApplyParentFootIK"/> is true, copy <see cref="ApplyFootIK"/>.</item>
/// </list>
/// </summary>
public virtual void CopyIKFlags(AnimancerNodeBase copyFrom)
{
if (Graph == null)
return;
if (ApplyParentAnimatorIK)
ApplyAnimatorIK = copyFrom.ApplyAnimatorIK;
if (ApplyParentFootIK)
ApplyFootIK = copyFrom.ApplyFootIK;
}
/************************************************************************************************************************/
/// <inheritdoc/>
public override bool ApplyAnimatorIK
{
get
{
for (int i = ChildCount - 1; i >= 0; i--)
{
var state = GetChild(i);
if (state.ApplyAnimatorIK)
return true;
}
return false;
}
set
{
for (int i = ChildCount - 1; i >= 0; i--)
{
var state = GetChild(i);
state.ApplyAnimatorIK = value;
}
}
}
/************************************************************************************************************************/
/// <inheritdoc/>
public override bool ApplyFootIK
{
get
{
for (int i = ChildCount - 1; i >= 0; i--)
{
var state = GetChild(i);
if (state.ApplyFootIK)
return true;
}
return false;
}
set
{
for (int i = ChildCount - 1; i >= 0; i--)
{
var state = GetChild(i);
state.ApplyFootIK = value;
}
}
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Descriptions
/************************************************************************************************************************/
#if UNITY_ASSERTIONS
/// <summary>[Assert-Only] The Inspector display name of this node.</summary>
/// <remarks>Set using <see cref="SetDebugName"/>.</remarks>
public object DebugName { get; private set; }
#endif
/// <summary>[Assert-Conditional] Sets the <see cref="DebugName"/> to display in the Inspector.</summary>
[System.Diagnostics.Conditional(Strings.Assertions)]
public void SetDebugName(object name)
{
#if UNITY_ASSERTIONS
DebugName = name;
#endif
}
/// <summary>The Inspector display name of this node.</summary>
public override string ToString()
{
#if UNITY_ASSERTIONS
if (NameCache.TryToString(DebugName, out var name))
return name;
#endif
return base.ToString();
}
/************************************************************************************************************************/
/// <inheritdoc/>
public void AppendDescription(StringBuilder text, string separator = "\n")
{
text.Append(ToString());
AppendDetails(text, separator);
if (ChildCount > 0)
{
text.AppendField(separator, nameof(ChildCount), ChildCount);
var indentedSeparator = separator + Strings.Indent;
var i = 0;
foreach (var child in this)
{
text.Append(separator)
.Append('[')
.Append(i++)
.Append("] ")
.AppendDescription(child, indentedSeparator, true);
}
}
}
/************************************************************************************************************************/
/// <summary>Called by <see cref="AppendDescription"/> to append the details of this node.</summary>
protected virtual void AppendDetails(StringBuilder text, string separator)
{
text.AppendField(separator, "Playable", _Playable.IsValid()
? _Playable.GetPlayableType().ToString()
: "Invalid");
var parent = Parent;
var isConnected =
parent != null &&
parent.Playable.GetInput(Index).IsValid();
text.AppendField(separator, "Connected", isConnected);
text.AppendField(separator, nameof(Index), Index);
if (Index < 0)
text.Append(" (No Parent)");
text.AppendField(separator, nameof(Speed), Speed);
var realSpeed = _Playable.IsValid()
? _Playable.GetSpeed()
: Speed;
if (realSpeed != Speed)
text.Append(" (Real ").Append(realSpeed).Append(')');
text.AppendField(separator, nameof(Weight), Weight);
if (Weight != TargetWeight)
{
text.AppendField(separator, nameof(TargetWeight), TargetWeight);
text.AppendField(separator, nameof(FadeSpeed), FadeSpeed);
}
AppendIKDetails(text, separator, this);
#if UNITY_ASSERTIONS
if (_ConstructorStackTrace != null)
text.AppendField(separator, "ConstructorStackTrace", _ConstructorStackTrace);
#endif
}
/************************************************************************************************************************/
/// <summary>
/// Appends the details of <see cref="AnimancerNodeBase.ApplyAnimatorIK"/> and
/// <see cref="AnimancerNodeBase.ApplyFootIK"/>.
/// </summary>
public static void AppendIKDetails(StringBuilder text, string separator, AnimancerNodeBase node)
{
if (!node.Playable.IsValid())
return;
text.Append(separator)
.Append("InverseKinematics: ");
if (node.ApplyAnimatorIK)
{
text.Append("OnAnimatorIK");
if (node.ApplyFootIK)
text.Append(", FootIK");
}
else if (node.ApplyFootIK)
{
text.Append("FootIK");
}
else
{
text.Append("None");
}
}
/************************************************************************************************************************/
/// <summary>Returns the hierarchy path of this node through its <see cref="AnimancerNodeBase.Parent"/>s.</summary>
public string GetPath()
{
var path = StringBuilderPool.Instance.Acquire();
if (Parent is AnimancerNode parent)
{
AppendPath(path, parent);
AppendPortAndType(path);
}
else
{
AppendPortAndType(path);
}
return path.ReleaseToString();
}
/// <summary>Appends the hierarchy path of this state through its <see cref="AnimancerNodeBase.Parent"/>s.</summary>
private static void AppendPath(StringBuilder path, AnimancerNode parent)
{
if (parent != null)
{
if (parent.Parent is AnimancerNode grandParent)
{
AppendPath(path, grandParent);
}
else
{
var layer = parent.Layer;
if (layer != null)
{
path.Append("Layers[")
.Append(parent.Layer.Index)
.Append("].States");
}
else
{
path.Append("NoLayer -> ")
.Append(parent.ToString());
}
return;
}
}
if (parent is AnimancerState state)
{
state.AppendPortAndType(path);
}
else
{
path.Append(" -> ")
.Append(parent.GetType());
}
}
/// <summary>Appends "[Index] -> ToString()".</summary>
private void AppendPortAndType(StringBuilder path)
{
path.Append('[')
.Append(Index)
.Append("] -> ")
.Append(ToString());
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 54f55b421ce0c584ab6bcb47947126e4
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,282 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
using System;
using UnityEngine.Playables;
namespace Animancer
{
/// <summary>Base class for objects that manage a <see cref="UnityEngine.Playables.Playable"/>.</summary>
/// <remarks>This is the base class of <see cref="AnimancerGraph"/> and <see cref="AnimancerNode"/>.</remarks>
/// https://kybernetik.com.au/animancer/api/Animancer/AnimancerNodeBase
public abstract class AnimancerNodeBase
{
/************************************************************************************************************************/
/// <summary>The <see cref="AnimancerGraph"/> containing this node.</summary>
public AnimancerGraph Graph { get; internal set; }
/************************************************************************************************************************/
/// <summary>The object which receives the output of the <see cref="Playable"/>.</summary>
/// <remarks>
/// This leads from <see cref="AnimancerState"/> to <see cref="AnimancerLayer"/> to
/// <see cref="AnimancerGraph"/> to <c>null</c>.
/// </remarks>
public AnimancerNodeBase Parent { get; protected set; }
/************************************************************************************************************************/
/// <summary>The root <see cref="AnimancerLayer"/> which this node is connected to (if any).</summary>
public virtual AnimancerLayer Layer
=> Parent?.Layer;
/************************************************************************************************************************/
/// <summary>The number of nodes using this as their <see cref="Parent"/>.</summary>
public virtual int ChildCount
=> 0;
/// <summary>Returns the node connected to the specified `index` as a child of this node.</summary>
/// <remarks>When overriding, don't call this base method because it throws an exception.</remarks>
/// <exception cref="NotSupportedException">This node can't have children.</exception>
protected internal virtual AnimancerNode GetChildNode(int index)
{
MarkAsUsed(this);
throw new NotSupportedException(this + " can't have children.");
}
/// <summary>Should child playables stay connected to the graph at all times?</summary>
/// <remarks>
/// If false, playables will be disconnected from the graph while they are inactive to stop it from
/// evaluating them every frame which usually improves performance.
/// </remarks>
/// <seealso cref="AnimancerGraph.KeepChildrenConnected"/>
public virtual bool KeepChildrenConnected
=> true;
/************************************************************************************************************************/
/// <summary>Called when a child's <see cref="AnimancerState.IsLooping"/> value changes.</summary>
protected virtual void OnChildIsLoopingChanged(bool value) { }
/// <summary>[Internal] Calls <see cref="OnChildIsLoopingChanged"/> for each <see cref="Parent"/> recursively.</summary>
protected internal void OnIsLoopingChangedRecursive(bool value)
{
var parent = Parent;
while (parent != null)
{
parent.OnChildIsLoopingChanged(value);
parent = parent.Parent;
}
}
/************************************************************************************************************************/
/// <summary>[Internal] Called when a child's <see cref="Parent"/> is changed from this node.</summary>
/// <remarks>When overriding, don't call this base method because it throws an exception.</remarks>
/// <exception cref="NotSupportedException">This node can't have children.</exception>
protected internal virtual void OnRemoveChild(AnimancerState child)
{
MarkAsUsed(this);
child.SetParentInternal(null);
throw new NotSupportedException(this + " can't have children.");
}
/************************************************************************************************************************/
/// <summary>[Internal] The <see cref="Playable"/>.</summary>
protected internal Playable _Playable;
/// <summary>The internal object this node manages in the <see cref="PlayableGraph"/>.</summary>
/// <remarks>
/// Must be set by <see cref="AnimancerNode.CreatePlayable()"/>. Failure to do so will throw the following
/// exception throughout the system when using this node: "<see cref="ArgumentException"/>: The playable passed
/// as an argument is invalid. To create a valid playable, please use the appropriate Create method".
/// </remarks>
public Playable Playable => _Playable;
/************************************************************************************************************************/
/// <summary>The current blend weight of this node which determines how much it affects the final output.</summary>
protected internal virtual float BaseWeight => 1;
/************************************************************************************************************************/
#region Speed
/************************************************************************************************************************/
private float _Speed = 1;
/// <summary>[Pro-Only] How fast the <see cref="AnimancerState.Time"/> is advancing every frame (default 1).</summary>
///
/// <remarks>
/// A negative value will play the animation backwards.
/// <para></para>
/// To pause an animation, consider setting <see cref="AnimancerState.IsPlaying"/> to false instead of setting
/// this value to 0.
/// <para></para>
/// <em>Animancer Lite doesn't allow this value to be changed in runtime builds.</em>
/// <para></para>
/// <strong>Example:</strong><code>
/// void SpeedExample(AnimancerComponent animancer, AnimationClip clip)
/// {
/// var state = animancer.Play(clip);
///
/// state.Speed = 1;// Normal speed.
/// state.Speed = 2;// Double speed.
/// state.Speed = 0.5f;// Half speed.
/// state.Speed = -1;// Normal speed playing backwards.
/// state.NormalizedTime = 1;// Start at the end to play backwards from there.
/// }
/// </code></remarks>
///
/// <exception cref="ArgumentOutOfRangeException">The value is not finite.</exception>
public virtual float Speed
{
get => _Speed;
set
{
if (_Speed == value)
return;
#if UNITY_ASSERTIONS
if (!value.IsFinite())
{
MarkAsUsed(this);
throw new ArgumentOutOfRangeException(
nameof(value),
value,
$"{nameof(Speed)} {Strings.MustBeFinite}");
}
#endif
_Speed = value;
if (_Playable.IsValid())
_Playable.SetSpeed(value);
}
}
/************************************************************************************************************************/
/// <summary>
/// The <see cref="Speed"/> of this node multiplied by the <see cref="Speed"/> of each of its parents to
/// determine the actual speed it's playing at.
/// </summary>
public float EffectiveSpeed
{
get => Speed * ParentEffectiveSpeed;
set => Speed = value / ParentEffectiveSpeed;
}
/************************************************************************************************************************/
/// <summary>
/// The multiplied <see cref="Speed"/> of each of the <see cref="Parent"/> down the hierarchy,
/// excluding the root <see cref="Speed"/>.
/// </summary>
private float ParentEffectiveSpeed
{
get
{
var parent = Parent;
if (parent == null)
return 1;
var speed = parent.Speed;
while ((parent = parent.Parent) != null)
{
speed *= parent.Speed;
}
return speed;
}
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
/// <summary>
/// Should Unity call <c>OnAnimatorIK</c> on the animated object while this object and its children have any
/// <see cref="AnimancerNode.Weight"/>?
/// </summary>
/// <remarks>
/// This is equivalent to the "IK Pass" toggle in Animator Controller layers, except that due to limitations in
/// the Playables API the <c>layerIndex</c> will always be zero.
/// <para></para>
/// This value starts false by default, but can be automatically changed by
/// <see cref="AnimancerNode.CopyIKFlags"/> when the <see cref="Parent"/> is set.
/// <para></para>
/// IK only takes effect while at least one <see cref="ClipState"/> has a <see cref="AnimancerNode.Weight"/>
/// above zero. Other node types either store the value to apply to their children or don't support IK.
/// <para></para>
/// <strong>Documentation:</strong>
/// <see href="https://kybernetik.com.au/animancer/docs/manual/ik#ik-pass">
/// IK Pass</see>
/// </remarks>
public abstract bool ApplyAnimatorIK { get; set; }
/************************************************************************************************************************/
/// <summary>Should this object and its children apply IK to the character's feet?</summary>
/// <remarks>
/// This is equivalent to the "Foot IK" toggle in Animator Controller states.
/// <para></para>
/// This value starts true by default for <see cref="ClipState"/>s (false for others), but can be automatically
/// changed by <see cref="AnimancerNode.CopyIKFlags"/> when the <see cref="Parent"/> is set.
/// <para></para>
/// IK only takes effect while at least one <see cref="ClipState"/> has a <see cref="AnimancerNode.Weight"/>
/// above zero. Other node types either store the value to apply to their children or don't support IK.
/// <para></para>
/// <strong>Documentation:</strong>
/// <see href="https://kybernetik.com.au/animancer/docs/manual/ik#foot-ik">
/// Foot IK</see>
/// </remarks>
public abstract bool ApplyFootIK { get; set; }
/************************************************************************************************************************/
/// <summary>[Internal] Applies a change to a child's <see cref="AnimancerState.IsActive"/>.</summary>
protected internal virtual void ApplyChildActive(AnimancerState child, bool setActive)
=> child.ShouldBeActive = setActive;
/************************************************************************************************************************/
/// <summary>[Assert-Conditional] Prevents the `node` from causing <see cref="OptionalWarning.UnusedNode"/>.</summary>
[System.Diagnostics.Conditional(Strings.Assertions)]
public static void MarkAsUsed(AnimancerNodeBase node)
{
#if UNITY_ASSERTIONS
if (node.Graph == null)
GC.SuppressFinalize(node);
#endif
}
/************************************************************************************************************************/
#if UNITY_EDITOR
/************************************************************************************************************************/
/// <summary>[Editor-Only]
/// Adds functions to show and set <see cref="ApplyAnimatorIK"/> and
/// <see cref="ApplyFootIK"/>.
/// </summary>
public static void AddContextMenuIK(UnityEditor.GenericMenu menu, AnimancerNodeBase ik)
{
#if UNITY_IMGUI
menu.AddItem(new("Inverse Kinematics/Apply Animator IK ?"),
ik.ApplyAnimatorIK,
() => ik.ApplyAnimatorIK = !ik.ApplyAnimatorIK);
menu.AddItem(new("Inverse Kinematics/Apply Foot IK ?"),
ik.ApplyFootIK,
() => ik.ApplyFootIK = !ik.ApplyFootIK);
#endif
}
/************************************************************************************************************************/
#endif
/************************************************************************************************************************/
}
}

View File

@@ -0,0 +1,13 @@
fileFormatVersion: 2
guid: 8abfd735619067c40b7e6e10bcc0b559
labels:
- Interface
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,67 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
namespace Animancer
{
/// https://kybernetik.com.au/animancer/api/Animancer/AnimancerState
public abstract partial class AnimancerState
{
/************************************************************************************************************************/
#if UNITY_ASSERTIONS
private static bool _SkipNextExpectFade;
private bool _ExpectFade;
#endif
/************************************************************************************************************************/
/// <summary>[Internal] Sets a flag for <see cref="OptionalWarning.ExpectFade"/>.</summary>
[System.Diagnostics.Conditional(Strings.Assertions)]
public static void SetExpectFade(AnimancerState state, float fadeDuration)
{
#if UNITY_ASSERTIONS
state._ExpectFade = fadeDuration > 0;
#endif
}
/************************************************************************************************************************/
/// <summary>[Internal] Sets the next <see cref="AssertNotExpectingFade"/> call to be skipped.</summary>
[System.Diagnostics.Conditional(Strings.Assertions)]
internal static void SkipNextExpectFade()
{
#if UNITY_ASSERTIONS
_SkipNextExpectFade = true;
#endif
}
/************************************************************************************************************************/
/// <summary>[Internal] Call when playing a `state` without a fade to check <see cref="OptionalWarning.ExpectFade"/>.</summary>
[System.Diagnostics.Conditional(Strings.Assertions)]
internal static void AssertNotExpectingFade(AnimancerState state)
{
#if UNITY_ASSERTIONS
if (_SkipNextExpectFade)
{
_SkipNextExpectFade = false;
return;
}
if (state._ExpectFade)
{
state._ExpectFade = false;// Don't log again for the same state.
OptionalWarning.ExpectFade.Log(
"A state was created by a transition with a non-zero Fade Duration" +
" but is now being played without a fade, which may be unintentional." +
" In most cases, the transition should be played so that it can properly" +
" apply its settings, unlike if the state is played directly.",
state.Graph?.Component);
}
#endif
}
/************************************************************************************************************************/
}
}

View File

@@ -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

View File

@@ -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:

View File

@@ -0,0 +1,217 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
using System;
using Unity.Collections;
using UnityEngine.Animations;
using UnityEngine.Playables;
namespace Animancer
{
/// <summary>[Pro-Only] A scripted animation for an <see cref="AnimationJobState{T}"/>.</summary>
/// <remarks>
/// <strong>Sample:</strong>
/// <see href="https://kybernetik.com.au/animancer/docs/samples/jobs/job-states">
/// Job States</see>
/// </remarks>
public interface IAnimancerStateJob : IDisposable
{
/************************************************************************************************************************/
/// <summary>The total time this job would take to play in seconds at normal speed.</summary>
float Length { get; }
/// <summary>Does this job loop back to the start when its time passes its <see cref="Length"/>?</summary>
bool IsLooping { get; }
/// <summary>Defines what do to when processing the root motion.</summary>
/// <remarks>
/// This is called by <see cref="IAnimationJob.ProcessRootMotion"/>
/// and receives the <see cref="AnimancerState.TimeD"/>.
/// </remarks>
void ProcessRootMotion(AnimationStream stream, double time);
/// <summary>Defines what do to when processing the animation.</summary>
/// <remarks>
/// This is called by <see cref="IAnimationJob.ProcessAnimation"/>
/// and receives the <see cref="AnimancerState.TimeD"/>.
/// </remarks>
void ProcessAnimation(AnimationStream stream, double time);
/************************************************************************************************************************/
}
/// <summary>[Pro-Only] An <see cref="AnimancerState"/> which plays an <see cref="IAnimancerStateJob"/>.</summary>
/// <remarks>
/// <strong>Sample:</strong>
/// <see href="https://kybernetik.com.au/animancer/docs/samples/jobs/job-states">
/// Job States</see>
/// </remarks>
/// https://kybernetik.com.au/animancer/api/Animancer/AnimationJobState_1
///
public class AnimationJobState<T> : AnimancerState, IUpdatable, IDisposable
where T : struct, IAnimancerStateJob
{
/************************************************************************************************************************/
/// <summary>
/// An <see cref="IAnimationJob"/> which wraps an <see cref="IAnimancerStateJob"/>
/// to provide its <see cref="Time"/> value.
/// </summary>
/// https://kybernetik.com.au/animancer/api/Animancer/TimedJob
public struct TimedJob : IAnimationJob, IDisposable
{
/// <summary>The <see cref="IAnimancerStateJob"/> data.</summary>
public T Job;
/// <summary>The <see cref="AnimancerState.TimeD"/> to be passed to the job.</summary>
public NativeArray<double> Time;
/// <summary>Cleans up the unmanaged resources used by this job.</summary>
public readonly void Dispose()
{
if (Time.IsCreated)
Time.Dispose();
Job.Dispose();
}
/// <summary>Defines what do to when processing the root motion.</summary>
public readonly void ProcessRootMotion(AnimationStream stream)
=> Job.ProcessRootMotion(stream, Time[0]);
/// <summary>Defines what do to when processing the animation.</summary>
public readonly void ProcessAnimation(AnimationStream stream)
=> Job.ProcessAnimation(stream, Time[0]);
}
/************************************************************************************************************************/
private new AnimationScriptPlayable _Playable;
private TimedJob _Job;
/// <summary>The data of the job to be executed by this state.</summary>
/// <remarks>
/// Setting this value has a minor performance cost. If it needs to be changed frequently,
/// consider using a single-item <c>NativeArray</c> in your job as demonstrated in the
/// <see href="https://kybernetik.com.au/animancer/docs/samples/jobs/hit-impacts#angle">
/// Hit Impacts</see> sample.
/// </remarks>
public T Job
{
get => _Job.Job;
set
{
_Job.Job = value;
_Playable.SetJobData(_Job);
}
}
/************************************************************************************************************************/
/// <inheritdoc/>
public override float Length
=> _Job.Job.Length;
/// <inheritdoc/>
public override bool IsLooping
=> _Job.Job.IsLooping;
/************************************************************************************************************************/
/// <inheritdoc/>
int IUpdatable.UpdatableIndex { get; set; } = IUpdatable.List.NotInList;
/************************************************************************************************************************/
/// <summary>Creates a new <see cref="AnimationJobState{T}"/>.</summary>
public AnimationJobState(T job)
{
_Job.Job = job;
}
/************************************************************************************************************************/
/// <inheritdoc/>
public override void SetGraph(AnimancerGraph graph)
{
Graph?.Disposables.Remove(this);
base.SetGraph(graph);
Graph?.Disposables.Add(this);
}
/************************************************************************************************************************/
/// <inheritdoc/>
protected override void CreatePlayable(out Playable playable)
{
if (!_Job.Time.IsCreated)
_Job.Time = AnimancerUtilities.CreateNativeReference<double>();
playable = _Playable = AnimationScriptPlayable.Create(Graph.PlayableGraph, _Job);
}
/************************************************************************************************************************/
/// <inheritdoc/>
protected override void OnSetIsPlaying()
{
base.OnSetIsPlaying();
if (IsPlaying)
Graph.RequirePreUpdate(this);
else
Graph.CancelPreUpdate(this);
}
/************************************************************************************************************************/
/// <summary>
/// Called every frame before the job is applied to send the
/// <see cref="AnimancerState.Time"/> to the <see cref="Job"/>.
/// </summary>
public virtual void Update()
{
var time = _Job.Time;
time[0] = TimeD;
}
/************************************************************************************************************************/
/// <inheritdoc/>
void IDisposable.Dispose()
=> _Job.Dispose();
/// <inheritdoc/>
public override void Destroy()
{
base.Destroy();
if (Graph != null)
{
Graph.CancelPreUpdate(this);
Graph.Disposables.Remove(this);
}
_Job.Dispose();
}
/************************************************************************************************************************/
/// <inheritdoc/>
public override string ToString()
=> _Job.Job.ToString();
/************************************************************************************************************************/
/// <inheritdoc/>
public override AnimancerState Clone(CloneContext context)
{
var clone = new AnimationJobState<T>(_Job.Job);
clone.CopyFrom(this, context);
return clone;
}
/************************************************************************************************************************/
}
}

View File

@@ -0,0 +1,13 @@
fileFormatVersion: 2
guid: 8a383b34eda0fe0408c12bc3a77108b5
timeCreated: 1515060256
licenseType: Store
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,190 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
using System;
using UnityEngine;
using UnityEngine.Animations;
using UnityEngine.Playables;
using Object = UnityEngine.Object;
namespace Animancer
{
/// <summary>An <see cref="AnimancerState"/> which plays an <see cref="AnimationClip"/>.</summary>
/// <remarks>
/// <strong>Documentation:</strong>
/// <see href="https://kybernetik.com.au/animancer/docs/manual/playing/states">
/// States</see>
/// </remarks>
/// https://kybernetik.com.au/animancer/api/Animancer/ClipState
///
public class ClipState : AnimancerState
{
/************************************************************************************************************************/
#region Fields and Properties
/************************************************************************************************************************/
private AnimationClip _Clip;
/// <summary>The <see cref="AnimationClip"/> which this state plays.</summary>
public override AnimationClip Clip
{
get => _Clip;
set
{
Validate.AssertAnimationClip(value, true, $"set {nameof(ClipState)}.{nameof(Clip)}");
if (ChangeMainObject(ref _Clip, value))
{
_Length = value.length;
var isLooping = value.isLooping;
if (_IsLooping != isLooping)
{
_IsLooping = isLooping;
OnIsLoopingChangedRecursive(isLooping);
}
}
}
}
/// <summary>The <see cref="AnimationClip"/> which this state plays.</summary>
public override Object MainObject
{
get => _Clip;
set => Clip = (AnimationClip)value;
}
#if UNITY_EDITOR
/// <inheritdoc/>
public override Type MainObjectType
=> typeof(AnimationClip);
#endif
/************************************************************************************************************************/
private float _Length;
/// <summary>The <see cref="AnimationClip.length"/>.</summary>
public override float Length
=> _Length;
/************************************************************************************************************************/
private bool _IsLooping;
/// <summary>The <see cref="Motion.isLooping"/>.</summary>
public override bool IsLooping
=> _IsLooping;
/************************************************************************************************************************/
/// <inheritdoc/>
public override AnimancerEvent.DispatchInfo GetEventDispatchInfo()
{
var length = _Length;
return new(
length,
length != 0 ? Time / length : 0,
_IsLooping);
}
/************************************************************************************************************************/
/// <inheritdoc/>
public override Vector3 AverageVelocity
=> _Clip.averageSpeed;
/************************************************************************************************************************/
#region Inverse Kinematics
/************************************************************************************************************************/
/// <inheritdoc/>
public override bool ApplyAnimatorIK
{
get => _Playable.IsValid() && ((AnimationClipPlayable)_Playable).GetApplyPlayableIK();
set
{
Validate.AssertPlayable(this);
((AnimationClipPlayable)_Playable).SetApplyPlayableIK(value);
}
}
/************************************************************************************************************************/
/// <inheritdoc/>
public override bool ApplyFootIK
{
get => _Playable.IsValid() && ((AnimationClipPlayable)_Playable).GetApplyFootIK();
set
{
Validate.AssertPlayable(this);
((AnimationClipPlayable)_Playable).SetApplyFootIK(value);
}
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Methods
/************************************************************************************************************************/
/// <summary>Creates a new <see cref="ClipState"/> and sets its <see cref="Clip"/>.</summary>
/// <exception cref="ArgumentNullException">The `clip` is null.</exception>
public ClipState(AnimationClip clip)
{
Validate.AssertAnimationClip(clip, true, $"create {nameof(ClipState)}");
_Clip = clip;
_Length = clip.length;
_IsLooping = clip.isLooping;
}
/************************************************************************************************************************/
/// <summary>Creates and assigns the <see cref="AnimationClipPlayable"/> managed by this node.</summary>
protected override void CreatePlayable(out Playable playable)
{
playable = AnimationClipPlayable.Create(Graph._PlayableGraph, _Clip);
}
/************************************************************************************************************************/
/// <inheritdoc/>
public override void RecreatePlayable()
{
var playable = (AnimationClipPlayable)_Playable;
var footIK = playable.GetApplyFootIK();
var playableIK = playable.GetApplyPlayableIK();
base.RecreatePlayable();
playable = (AnimationClipPlayable)_Playable;
playable.SetApplyFootIK(footIK);
playable.SetApplyPlayableIK(playableIK);
}
/************************************************************************************************************************/
/// <inheritdoc/>
public override void Destroy()
{
_Clip = null;
base.Destroy();
}
/************************************************************************************************************************/
/// <inheritdoc/>
public override AnimancerState Clone(CloneContext context)
{
var clip = context.GetCloneOrOriginal(_Clip);
var clone = new ClipState(clip);
clone.CopyFrom(this, context);
return clone;
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
}
}

View File

@@ -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:

View File

@@ -0,0 +1,626 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
#pragma warning disable CS0649 // Field is never assigned to, and will always have its default value.
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Animations;
using UnityEngine.Playables;
using Object = UnityEngine.Object;
namespace Animancer
{
/// <summary>[Pro-Only]
/// An <see cref="AnimancerState"/> which contains other states.
/// </summary>
/// https://kybernetik.com.au/animancer/api/Animancer/ParentState
///
public abstract partial class ParentState : AnimancerState,
ICopyable<ParentState>
{
/************************************************************************************************************************/
#region Properties
/************************************************************************************************************************/
/// <summary>The children contained within this state.</summary>
/// <remarks>Only states up to the <see cref="ChildCount"/> should be assigned.</remarks>
protected AnimancerState[] ChildStates { get; private set; }
= Array.Empty<AnimancerState>();
/************************************************************************************************************************/
private int _ChildCount;
/// <inheritdoc/>
public sealed override int ChildCount
=> _ChildCount;
/************************************************************************************************************************/
/// <summary>The size of the internal array of <see cref="ChildStates"/>.</summary>
/// <remarks>
/// This value starts at 0 then expands to <see cref="ChildCapacity"/>
/// when the first child is added.
/// </remarks>
public int ChildCapacity
{
get => ChildStates.Length;
set
{
if (value == ChildStates.Length)
return;
#if UNITY_ASSERTIONS
if (value <= 1 && OptionalWarning.MixerMinChildren.IsEnabled())
OptionalWarning.MixerMinChildren.Log(
$"The {nameof(ChildCapacity)} of '{this}' is being set to {value}." +
$" The purpose of a mixer is to mix multiple child states so this may be a mistake.",
Graph?.Component);
#endif
var newChildStates = new AnimancerState[value];
if (value > _ChildCount)// Increase size.
{
Array.Copy(ChildStates, newChildStates, _ChildCount);
}
else// Decrease size.
{
for (int i = value; i < _ChildCount; i++)
ChildStates[i].Destroy();
Array.Copy(ChildStates, newChildStates, value);
_ChildCount = value;
}
ChildStates = newChildStates;
if (_Playable.IsValid())
{
_Playable.SetInputCount(value);
}
else if (Graph != null)
{
CreatePlayable();
}
OnChildCapacityChanged();
}
}
/// <summary>Called when the <see cref="ChildCapacity"/> is changed.</summary>
protected virtual void OnChildCapacityChanged() { }
/// <summary><see cref="ChildCapacity"/> starts at 0 then expands to this value when the first child is added.</summary>
/// <remarks>Default 8.</remarks>
public static int DefaultChildCapacity { get; set; } = 8;
/// <summary>
/// Ensures that the remaining unused <see cref="ChildCapacity"/>
/// is greater than or equal to the specified `minimumCapacity`.
/// </summary>
public void EnsureRemainingChildCapacity(int minimumCapacity)
{
minimumCapacity += _ChildCount;
if (ChildCapacity < minimumCapacity)
{
var capacity = Math.Max(ChildCapacity, DefaultChildCapacity);
while (capacity < minimumCapacity)
capacity *= 2;
ChildCapacity = capacity;
}
}
/************************************************************************************************************************/
/// <inheritdoc/>
public sealed override AnimancerState GetChild(int index)
=> ChildStates[index];
/// <inheritdoc/>
public sealed override FastEnumerator<AnimancerState> GetEnumerator()
=> new(ChildStates, _ChildCount);
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Initialization
/************************************************************************************************************************/
/// <summary>Creates and assigns the <see cref="AnimationMixerPlayable"/> managed by this state.</summary>
protected override void CreatePlayable(out Playable playable)
{
playable = AnimationMixerPlayable.Create(Graph._PlayableGraph, ChildCapacity);
}
/************************************************************************************************************************/
/// <summary>Connects the `child` to this mixer at its <see cref="AnimancerNode.Index"/>.</summary>
protected internal override void OnAddChild(AnimancerState child)
{
Validate.AssertGraph(child, Graph);
var capacity = ChildCapacity;
if (_ChildCount >= capacity)
ChildCapacity = Math.Max(DefaultChildCapacity, capacity * 2);
child.Index = _ChildCount;
ChildStates[_ChildCount] = child;
_ChildCount++;
child.IsPlaying = IsPlaying;
if (Graph != null)
ConnectChildUnsafe(child.Index, child);
#if UNITY_ASSERTIONS
_CachedToString = null;
#endif
}
/************************************************************************************************************************/
/// <summary>Disconnects the `child` from this mixer at its <see cref="AnimancerNode.Index"/>.</summary>
protected internal override void OnRemoveChild(AnimancerState child)
{
Validate.AssertCanRemoveChild(child, ChildStates, _ChildCount);
// Shuffle all subsequent children down one place.
if (Graph == null || !Graph._PlayableGraph.IsValid())
{
Array.Copy(
ChildStates, child.Index + 1,
ChildStates, child.Index,
_ChildCount - child.Index - 1);
for (int i = child.Index; i < _ChildCount - 1; i++)
ChildStates[i].Index = i;
}
else
{
Graph._PlayableGraph.Disconnect(_Playable, child.Index);
for (int i = child.Index + 1; i < _ChildCount; i++)
{
var otherChild = ChildStates[i];
Graph._PlayableGraph.Disconnect(_Playable, otherChild.Index);
otherChild.Index = i - 1;
ChildStates[i - 1] = otherChild;
ConnectChildUnsafe(i - 1, otherChild);
}
}
_ChildCount--;
ChildStates[_ChildCount] = null;
#if UNITY_ASSERTIONS
_CachedToString = null;
#endif
}
/************************************************************************************************************************/
/// <inheritdoc/>
public override void Destroy()
{
DestroyChildren();
base.Destroy();
}
/************************************************************************************************************************/
/// <inheritdoc/>
public sealed override void CopyFrom(AnimancerState copyFrom, CloneContext context)
=> this.CopyFromBase(copyFrom, context);
/// <inheritdoc/>
public virtual void CopyFrom(ParentState copyFrom, CloneContext context)
{
base.CopyFrom(copyFrom, context);
DestroyChildren();
var childCount = copyFrom.ChildCount;
EnsureRemainingChildCapacity(childCount);
for (int i = 0; i < childCount; i++)
{
var child = copyFrom.ChildStates[i];
child = context.Clone(child);
Add(child);
}
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Child Configuration
/************************************************************************************************************************/
/// <summary>Assigns the `state` as a child of this mixer.</summary>
/// <remarks>This is the same as calling <see cref="AnimancerState.SetParent"/>.</remarks>
public virtual void Add(AnimancerState child)
=> child.SetParent(this);
/// <summary>Creates and returns a new <see cref="ClipState"/> to play the `clip` as a child of this mixer.</summary>
public virtual ClipState Add(AnimationClip clip)
{
var child = new ClipState(clip);
Add(child);
return child;
}
/// <summary>Calls <see cref="AnimancerUtilities.CreateStateAndApply"/> then <see cref="Add(AnimancerState)"/>.</summary>
public virtual AnimancerState Add(ITransition transition)
{
var child = transition.CreateStateAndApply(Graph);
Add(child);
return child;
}
/// <summary>Calls one of the other <see cref="Add(object)"/> overloads as appropriate for the `child`.</summary>
public virtual AnimancerState Add(object child)
{
if (child is AnimationClip clip)
return Add(clip);
if (child is ITransition transition)
return Add(transition);
if (child is AnimancerState state)
{
Add(state);
return state;
}
MarkAsUsed(this);
throw new ArgumentException(
$"Failed to {nameof(Add)} '{AnimancerUtilities.ToStringOrNull(child)}'" +
$" as child of '{this}' because it isn't an" +
$" {nameof(AnimationClip)}, {nameof(ITransition)}, or {nameof(AnimancerState)}.");
}
/************************************************************************************************************************/
/// <summary>Calls <see cref="Add(AnimationClip)"/> for each of the `clips`.</summary>
public void AddRange(IList<AnimationClip> clips)
{
var count = clips.Count;
EnsureRemainingChildCapacity(count);
for (int i = 0; i < count; i++)
Add(clips[i]);
}
/// <summary>Calls <see cref="Add(AnimationClip)"/> for each of the `clips`.</summary>
public void AddRange(params AnimationClip[] clips)
=> AddRange((IList<AnimationClip>)clips);
/************************************************************************************************************************/
/// <summary>Calls <see cref="Add(ITransition)"/> for each of the `transitions`.</summary>
public void AddRange(IList<ITransition> transitions)
{
var count = transitions.Count;
EnsureRemainingChildCapacity(count);
for (int i = 0; i < count; i++)
Add(transitions[i]);
}
/// <summary>Calls <see cref="Add(ITransition)"/> for each of the `transitions`.</summary>
public void AddRange(params ITransition[] transitions)
=> AddRange((IList<ITransition>)transitions);
/************************************************************************************************************************/
/// <summary>Calls <see cref="Add(object)"/> for each of the `children`.</summary>
public void AddRange(IList<object> children)
{
var count = children.Count;
EnsureRemainingChildCapacity(count);
for (int i = 0; i < count; i++)
Add(children[i]);
}
/// <summary>Calls <see cref="Add(object)"/> for each of the `children`.</summary>
public void AddRange(params object[] children)
=> AddRange((IList<object>)children);
/************************************************************************************************************************/
/// <summary>Removes the child at the specified `index`.</summary>
public void Remove(int index, bool destroy)
=> Remove(ChildStates[index], destroy);
/// <summary>Removes the specified `child`.</summary>
public void Remove(AnimancerState child, bool destroy)
{
#if UNITY_ASSERTIONS
if (child.Parent != this)
Debug.LogWarning($"Attempting to remove a state which is not a child of this {GetType().Name}." +
$" This will remove the child from its actual parent so you should directly call" +
$" child.{nameof(child.Destroy)} or child.{nameof(child.SetParent)}(null, -1) instead." +
$"\n• Child: {child}" +
$"\n• Removing From: {this}" +
$"\n• Actual Parent: {child.Parent}",
Graph?.Component as Object);
#endif
if (destroy)
child.Destroy();
else
child.SetParent(null);
}
/************************************************************************************************************************/
/// <summary>Replaces the `child` at the specified `index`.</summary>
public virtual void Set(int index, AnimancerState child, bool destroyPrevious)
{
#if UNITY_ASSERTIONS
if ((uint)index >= _ChildCount)
{
MarkAsUsed(this);
MarkAsUsed(child);
throw new IndexOutOfRangeException(
$"Invalid child index. Must be 0 <= index < {nameof(ChildCount)} ({ChildCount}).");
}
#endif
if (child.Parent != null)
child.SetParent(null);
var previousChild = ChildStates[index];
previousChild.SetParentInternal(null);
child.SetGraph(Graph);
ChildStates[index] = child;
child.SetParentInternal(this, index);
child.IsPlaying = IsPlaying;
if (Graph != null)
{
Graph._PlayableGraph.Disconnect(_Playable, index);
ConnectChildUnsafe(index, child);
}
child.CopyIKFlags(this);
if (destroyPrevious)
previousChild.Destroy();
#if UNITY_ASSERTIONS
_CachedToString = null;
#endif
}
/// <summary>Replaces the child at the specified `index` with a new <see cref="ClipState"/>.</summary>
public ClipState Set(int index, AnimationClip clip, bool destroyPrevious)
{
var child = new ClipState(clip);
Set(index, child, destroyPrevious);
return child;
}
/// <summary>Replaces the child at the specified `index` with a <see cref="ITransition.CreateState"/>.</summary>
public AnimancerState Set(int index, ITransition transition, bool destroyPrevious)
{
var child = transition.CreateStateAndApply(Graph);
Set(index, child, destroyPrevious);
return child;
}
/// <summary>Calls one of the other <see cref="Set(int, object, bool)"/> overloads as appropriate for the `child`.</summary>
public AnimancerState Set(int index, object child, bool destroyPrevious)
{
if (child is AnimationClip clip)
return Set(index, clip, destroyPrevious);
if (child is ITransition transition)
return Set(index, transition, destroyPrevious);
if (child is AnimancerState state)
{
Set(index, state, destroyPrevious);
return state;
}
MarkAsUsed(this);
throw new ArgumentException(
$"Failed to {nameof(Set)} '{AnimancerUtilities.ToStringOrNull(child)}'" +
$" as child of '{this}' because it isn't an" +
$" {nameof(AnimationClip)}, {nameof(ITransition)}, or {nameof(AnimancerState)}.");
}
/************************************************************************************************************************/
/// <summary>Returns the index of the specified `child` state.</summary>
public int IndexOf(AnimancerState child)
=> Array.IndexOf(ChildStates, child, 0, _ChildCount);
/************************************************************************************************************************/
/// <summary>
/// Destroys all <see cref="ChildStates"/> connected to this mixer.
/// This operation cannot be undone.
/// </summary>
public void DestroyChildren()
{
for (int i = _ChildCount - 1; i >= 0; i--)
ChildStates[i].Destroy();
Array.Clear(ChildStates, 0, _ChildCount);
_ChildCount = 0;
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Updates
/************************************************************************************************************************/
/// <inheritdoc/>
protected internal override void UpdateEvents()
=> UpdateEventsRecursive(this);
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Inverse Kinematics
/************************************************************************************************************************/
private bool _ApplyAnimatorIK;
/// <inheritdoc/>
public override bool ApplyAnimatorIK
{
get => _ApplyAnimatorIK;
set => base.ApplyAnimatorIK = _ApplyAnimatorIK = value;
}
/************************************************************************************************************************/
private bool _ApplyFootIK;
/// <inheritdoc/>
public override bool ApplyFootIK
{
get => _ApplyFootIK;
set => base.ApplyFootIK = _ApplyFootIK = value;
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Other Methods
/************************************************************************************************************************/
#if UNITY_ASSERTIONS
/// <summary>[Assert-Only] A string built by <see cref="ToString"/> to describe this mixer.</summary>
private string _CachedToString;
#endif
/// <summary>
/// Returns a string describing the type of this mixer and the name of states connected to it.
/// </summary>
public override string ToString()
{
#if UNITY_ASSERTIONS
if (NameCache.TryToString(DebugName, out var name))
return name;
if (_CachedToString != null)
return _CachedToString;
#endif
// Gather child names.
var childNames = ListPool.Acquire<string>();
var allSimple = true;
for (int i = 0; i < _ChildCount; i++)
{
var state = ChildStates[i];
if (state == null)
continue;
if (state.MainObject != null)
{
childNames.Add(state.MainObject.name);
}
else
{
childNames.Add(state.ToString());
allSimple = false;
}
}
// If they all have a main object, check if they all have the same prefix so it doesn't need to be repeated.
int prefixLength = 0;
var count = childNames.Count;
if (count <= 1 || !allSimple)
{
prefixLength = 0;
}
else
{
var prefix = childNames[0];
var shortest = prefixLength = prefix.Length;
for (int iName = 0; iName < count; iName++)
{
var childName = childNames[iName];
if (shortest > childName.Length)
{
shortest = prefixLength = childName.Length;
}
for (int iCharacter = 0; iCharacter < prefixLength; iCharacter++)
{
if (childName[iCharacter] != prefix[iCharacter])
{
prefixLength = iCharacter;
break;
}
}
}
if (prefixLength < 3 ||// Less than 3 characters probably isn't an intentional prefix.
prefixLength >= shortest)
prefixLength = 0;
}
// Build the parent name.
var parentName = StringBuilderPool.Instance.Acquire();
var type = GetType().Name;
if (type.EndsWith("State"))
parentName.Append(type, 0, type.Length - 5);
else
parentName.Append(type);
parentName.Append('(');
if (count > 0)
{
if (prefixLength > 0)
parentName.Append(childNames[0], 0, prefixLength).Append('[');
for (int i = 0; i < count; i++)
{
if (i > 0)
parentName.Append(", ");
var childName = childNames[i];
parentName.Append(childName, prefixLength, childName.Length - prefixLength);
}
parentName.Append(']');
}
ListPool.Release(childNames);
parentName.Append(')');
var result = parentName.ReleaseToString();
#if UNITY_ASSERTIONS
_CachedToString = result;
#endif
return result;
}
/************************************************************************************************************************/
/// <inheritdoc/>
public override void GatherAnimationClips(ICollection<AnimationClip> clips)
=> clips.GatherFromSource(ChildStates);
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
}
}

View File

@@ -0,0 +1,13 @@
fileFormatVersion: 2
guid: 053b822d8f58e2d4e977ce274eb61e6f
timeCreated: 1515060256
licenseType: Store
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,442 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
using System;
using System.Collections.Generic;
using System.Text;
using UnityEngine;
using UnityEngine.Animations;
using UnityEngine.Audio;
using UnityEngine.Playables;
using Object = UnityEngine.Object;
namespace Animancer
{
/// <summary>[Pro-Only] An <see cref="AnimancerState"/> which plays a <see cref="PlayableAsset"/>.</summary>
/// <remarks>
/// <strong>Documentation:</strong>
/// <see href="https://kybernetik.com.au/animancer/docs/manual/timeline">
/// Timeline</see>
/// </remarks>
/// https://kybernetik.com.au/animancer/api/Animancer/PlayableAssetState
public class PlayableAssetState : AnimancerState
{
/************************************************************************************************************************/
#region Fields and Properties
/************************************************************************************************************************/
/// <summary>The <see cref="PlayableAsset"/> which this state plays.</summary>
private PlayableAsset _Asset;
/// <summary>The <see cref="PlayableAsset"/> which this state plays.</summary>
public PlayableAsset Asset
{
get => _Asset;
set => ChangeMainObject(ref _Asset, value);
}
/// <summary>The <see cref="PlayableAsset"/> which this state plays.</summary>
public override Object MainObject
{
get => _Asset;
set => _Asset = (PlayableAsset)value;
}
#if UNITY_EDITOR
/// <inheritdoc/>
public override Type MainObjectType
=> typeof(PlayableAsset);
#endif
/************************************************************************************************************************/
private float _Length;
/// <summary>The <see cref="PlayableAsset.duration"/> (cached on initialization).</summary>
public override float Length
=> _Length;
/************************************************************************************************************************/
/// <inheritdoc/>
public override float Speed
{
get => base.Speed;
set
{
base.Speed = value;
for (int i = Outputs.Count - 1; i >= 0; i--)
Outputs[i].GetSourcePlayable().SetSpeed(value);
}
}
/************************************************************************************************************************/
/// <inheritdoc/>
public override AnimancerEvent.DispatchInfo GetEventDispatchInfo()
{
var length = _Length;
return new(
length,
length != 0 ? Time / length : 0,
false);
}
/************************************************************************************************************************/
/// <inheritdoc/>
protected override void OnSetIsPlaying()
{
if (!_Playable.IsValid())
return;
var inputCount = _Playable.GetInputCount();
for (int i = 0; i < inputCount; i++)
{
var playable = _Playable.GetInput(i);
if (!playable.IsValid())
continue;
if (IsPlaying)
playable.Play();
else
playable.Pause();
}
}
/************************************************************************************************************************/
/// <summary>IK cannot be dynamically enabled on a <see cref="PlayableAssetState"/>.</summary>
public override void CopyIKFlags(AnimancerNodeBase copyFrom) { }
/************************************************************************************************************************/
/// <summary>IK cannot be dynamically enabled on a <see cref="PlayableAssetState"/>.</summary>
public override bool ApplyAnimatorIK
{
get => false;
set
{
#if UNITY_ASSERTIONS
if (value)
OptionalWarning.UnsupportedIK.Log(
$"IK cannot be dynamically enabled on a {nameof(PlayableAssetState)}.", Graph?.Component);
#endif
}
}
/************************************************************************************************************************/
/// <summary>IK cannot be dynamically enabled on a <see cref="PlayableAssetState"/>.</summary>
public override bool ApplyFootIK
{
get => false;
set
{
#if UNITY_ASSERTIONS
if (value)
OptionalWarning.UnsupportedIK.Log(
$"IK cannot be dynamically enabled on a {nameof(PlayableAssetState)}.", Graph?.Component);
#endif
}
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Methods
/************************************************************************************************************************/
/// <summary>Creates a new <see cref="PlayableAssetState"/> to play the `asset`.</summary>
/// <exception cref="ArgumentNullException">The `asset` is null.</exception>
public PlayableAssetState(PlayableAsset asset)
{
if (asset == null)
throw new ArgumentNullException(nameof(asset));
_Asset = asset;
}
/************************************************************************************************************************/
/// <inheritdoc/>
protected override void CreatePlayable(out Playable playable)
{
playable = _Asset.CreatePlayable(Graph._PlayableGraph, Graph.Component.gameObject);
playable.SetDuration(9223372.03685477);// https://github.com/KybernetikGames/animancer/issues/111
_Length = (float)_Asset.duration;
if (!_HasInitializedBindings)
InitializeBindings();
}
/************************************************************************************************************************/
private readonly List<PlayableOutput>
Outputs = new();
private IList<Object> _Bindings;
private bool _HasInitializedBindings;
/************************************************************************************************************************/
/// <summary>The objects controlled by each track in the asset.</summary>
public IList<Object> Bindings
{
get => _Bindings;
set
{
DestroyBoundOutputs(false);
_Bindings = value;
InitializeBindings();
}
}
/************************************************************************************************************************/
/// <summary>Sets the <see cref="Bindings"/>.</summary>
public void SetBindings(params Object[] bindings)
{
Bindings = bindings;
}
/************************************************************************************************************************/
private void InitializeBindings()
{
if (Graph == null)
return;
_HasInitializedBindings = true;
Validate.AssertPlayable(this);
var graph = Graph._PlayableGraph;
var bindableIndex = 0;
var bindableCount = _Bindings != null
? _Bindings.Count
: 0;
var speed = Speed;
foreach (var binding in _Asset.outputs)
{
GetBindingDetails(binding, out var trackName, out var trackType, out var isMarkers);
var bindable = bindableIndex < bindableCount
? _Bindings[bindableIndex]
: null;
#if UNITY_ASSERTIONS
if (!isMarkers &&
trackType != null &&
bindable != null &&
!trackType.IsAssignableFrom(bindable.GetType()))
{
Debug.LogError(
$"Binding Type Mismatch: bindings[{bindableIndex}] is '{bindable}'" +
$" but should be a {trackType.FullName} for {trackName}",
Graph.Component as Object);
bindableIndex++;
continue;
}
#endif
var playable = _Playable.GetInput(bindableIndex);
if (speed != 1)
playable.SetSpeed(speed);
if (trackType == typeof(Animator))// AnimationTrack.
{
if (bindable != null)
{
#if UNITY_ASSERTIONS
if (bindable == Graph.Component?.Animator)
Debug.LogError(
$"{nameof(PlayableAsset)} tracks should not be bound to the same {nameof(Animator)} as" +
$" Animancer. Leaving the binding of the first Animation Track empty will automatically" +
$" apply its animation to the object being controlled by Animancer.",
Graph.Component as Object);
#endif
var playableOutput = AnimationPlayableOutput.Create(graph, trackName, (Animator)bindable);
playableOutput.SetReferenceObject(binding.sourceObject);
playableOutput.SetSourcePlayable(playable);
playableOutput.SetWeight(1);
Outputs.Add(playableOutput);
}
}
#if UNITY_AUDIO
else if (trackType == typeof(AudioSource))// AudioTrack.
{
if (bindable != null)
{
var playableOutput = AudioPlayableOutput.Create(graph, trackName, (AudioSource)bindable);
playableOutput.SetReferenceObject(binding.sourceObject);
playableOutput.SetSourcePlayable(playable);
playableOutput.SetWeight(1);
Outputs.Add(playableOutput);
}
}
#endif
else if (isMarkers)// Markers.
{
var animancer = Graph.Component as Component;
var playableOutput = ScriptPlayableOutput.Create(graph, trackName);
playableOutput.SetReferenceObject(binding.sourceObject);
playableOutput.SetSourcePlayable(playable);
playableOutput.SetWeight(1);
playableOutput.SetUserData(animancer);
Outputs.Add(playableOutput);
var receivers = ListPool.Acquire<INotificationReceiver>();
animancer.GetComponents(receivers);
for (int i = 0; i < receivers.Count; i++)
playableOutput.AddNotificationReceiver(receivers[i]);
ListPool.Release(receivers);
continue;// Don't increment the bindingIndex.
}
else// ActivationTrack, ControlTrack, PlayableTrack, SignalTrack.
{
var playableOutput = ScriptPlayableOutput.Create(graph, trackName);
playableOutput.SetReferenceObject(binding.sourceObject);
playableOutput.SetSourcePlayable(playable);
playableOutput.SetWeight(1);
playableOutput.SetUserData(bindable);
if (bindable is INotificationReceiver receiver)
playableOutput.AddNotificationReceiver(receiver);
Outputs.Add(playableOutput);
}
bindableIndex++;
}
}
/************************************************************************************************************************/
/// <summary>Gathers details about the `binding`.</summary>
public static void GetBindingDetails(
PlayableBinding binding,
out string name,
out Type type,
out bool isMarkers)
{
name = binding.streamName;
type = binding.outputTargetType;
isMarkers = type == typeof(GameObject) && name == "Markers";
}
/************************************************************************************************************************/
/// <inheritdoc/>
public override void SetWeight(float value)
{
base.SetWeight(value);
for (int i = Outputs.Count - 1; i >= 0; i--)
Outputs[i].SetWeight(value);
}
/************************************************************************************************************************/
/// <inheritdoc/>
public override void Destroy()
{
_Asset = null;
DestroyBoundOutputs(true);
base.Destroy();
}
/************************************************************************************************************************/
/// <summary>
/// Destroys all of the outputs created for the <see cref="Bindings"/>
/// and optionally the state playable itself.
/// </summary>
public void DestroyBoundOutputs(bool destroyStatePlayable)
{
if (Graph == null)
return;
var graph = Graph._PlayableGraph;
if (!graph.IsValid())
return;
for (int i = Outputs.Count - 1; i >= 0; i--)
{
var output = Outputs[i];
var playable = output.GetSourcePlayable();
if (playable.IsValid())
graph.DestroySubgraph(playable);
graph.DestroyOutput(output);
}
Outputs.Clear();
if (destroyStatePlayable && _Playable.IsValid())
graph.DestroySubgraph(_Playable);
}
/************************************************************************************************************************/
/// <inheritdoc/>
public override AnimancerState Clone(CloneContext context)
{
var asset = context.GetCloneOrOriginal(_Asset);
var clone = new PlayableAssetState(asset);
clone.CopyFrom(this, context);
return clone;
}
/************************************************************************************************************************/
/// <inheritdoc/>
protected override void AppendDetails(StringBuilder text, string separator)
{
base.AppendDetails(text, separator);
text.Append(separator)
.Append($"{nameof(Bindings)}: ");
int count;
if (_Bindings == null)
{
text.Append("Null");
count = 0;
}
else
{
count = _Bindings.Count;
text.Append('[')
.Append(count)
.Append(']');
}
text.Append(_HasInitializedBindings
? " (Initialized)"
: " (Not Initialized)");
for (int i = 0; i < count; i++)
{
text.Append(separator)
.Append($"{nameof(Bindings)}[")
.Append(i)
.Append("] = ")
.Append(AnimancerUtilities.ToStringOrNull(_Bindings[i]));
}
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
}
}

View File

@@ -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:

View File

@@ -0,0 +1,375 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
#pragma warning disable CS0649 // Field is never assigned to, and will always have its default value.
using System;
using System.Collections.Generic;
using UnityEngine;
namespace Animancer
{
/// <summary>[Pro-Only]
/// An <see cref="AnimancerState"/> which plays a sequence of other states.
/// </summary>
/// https://kybernetik.com.au/animancer/api/Animancer/SequenceState
///
public partial class SequenceState : ParentState,
ICopyable<SequenceState>,
IUpdatable
{
/************************************************************************************************************************/
#region Fields and Properties
/************************************************************************************************************************/
private double[] _TimeOffsets = Array.Empty<double>();
private double[] _FadeEndTimes = Array.Empty<double>();
private double[] _StateEndTimes = Array.Empty<double>();
/// <summary>The index of the child state which is active at the current time.</summary>
private int _ActiveChildIndex;
/************************************************************************************************************************/
/// <inheritdoc/>
public override float Length
=> ChildCount > 0
? (float)_StateEndTimes[ChildCount - 1]
: 0;
/************************************************************************************************************************/
/// <inheritdoc/>
public override double RawTime
{
get => base.RawTime;
set
{
base.RawTime = value;
if (ChildCount == 0)
return;
var activeChildIndex = GetActiveChildIndex(value);
SetActiveChildIndex(activeChildIndex);
for (int i = 0; i < ChildCount; i++)
{
var child = ChildStates[i];
var childTime = value;
if (i > 0)
childTime -= _StateEndTimes[i - 1];
childTime *= child.Speed;
child.TimeD = childTime + _TimeOffsets[i];
}
}
}
/************************************************************************************************************************/
/// <inheritdoc/>
public override void MoveTime(double time, bool normalized)
{
base.MoveTime(time, normalized);
for (int i = 0; i < ChildCount; i++)
{
var child = ChildStates[i];
var childTime = time;
if (i > 0)
childTime -= _StateEndTimes[i - 1];
childTime *= child.Speed;
child.MoveTime(childTime + _TimeOffsets[i], normalized);
}
}
/************************************************************************************************************************/
/// <summary>Sequences don't loop.</summary>
/// <remarks>
/// If the last state in the sequence is set to loop it will do so,
/// but the rest of the sequence won't replay automatically.
/// </remarks>
public override bool IsLooping
=> false;
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Initialisation
/************************************************************************************************************************/
/// <summary>Replaces the child states with new ones created from the `transitions`.</summary>
public void Set(params ITransition[] transitions)
=> Set((IList<ITransition>)transitions);
/// <summary>Replaces the child states with new ones created from the `transitions`.</summary>
public void Set(IList<ITransition> transitions)
{
var oldChildCount = ChildCount;
var newChildCount = transitions.Count;
ChildCapacity = newChildCount;
for (int i = 0; i < newChildCount; i++)
{
var transition = transitions[i];
var state = transition.CreateStateAndApply(Graph);
state.IsPlaying = IsPlaying;
if (i < oldChildCount)
Set(i, state, true);
else
Add(state);
_FadeEndTimes[i] += transition.FadeDuration;
}
while (oldChildCount > newChildCount)
Remove(--oldChildCount, true);
}
/************************************************************************************************************************/
/// <inheritdoc/>
protected internal override void OnAddChild(AnimancerState child)
{
base.OnAddChild(child);
GatherChildDetails(child);
}
/************************************************************************************************************************/
/// <summary>Gathers the timing details of a newly added `child` state.</summary>
private void GatherChildDetails(AnimancerState child)
{
var index = child.Index;
if (index == 0)
child.Weight = 1;
_TimeOffsets[index] = child.TimeD;
var startTime = GetStartTime(index);
_FadeEndTimes[index] = startTime;
var length = child.RemainingDuration;
if (length < 0)
length = 0;
startTime += length;
_StateEndTimes[index] = startTime;
}
/************************************************************************************************************************/
/// <inheritdoc/>
public override AnimancerState Add(ITransition transition)
{
var child = base.Add(transition);
_FadeEndTimes[child.Index] += transition.FadeDuration;
return child;
}
/************************************************************************************************************************/
/// <summary>Adds the `child` to the end of this sequence.</summary>
public void Add(AnimancerState child, float fadeDuration)
{
Add(child);
_FadeEndTimes[child.Index] += fadeDuration;
}
/// <summary>Adds the `clip` to the end of this sequence.</summary>
public void Add(AnimationClip clip, float fadeDuration)
{
var child = Add(clip);
_FadeEndTimes[child.Index] += fadeDuration;
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Execution
/************************************************************************************************************************/
/// <inheritdoc/>
protected override void OnChildCapacityChanged()
{
base.OnChildCapacityChanged();
var capacity = ChildCapacity;
Array.Resize(ref _TimeOffsets, capacity);
Array.Resize(ref _FadeEndTimes, capacity);
Array.Resize(ref _StateEndTimes, capacity);
}
/************************************************************************************************************************/
/// <inheritdoc/>
int IUpdatable.UpdatableIndex { get; set; } = IUpdatable.List.NotInList;
/************************************************************************************************************************/
/// <inheritdoc/>
protected override void OnSetIsPlaying()
{
base.OnSetIsPlaying();
var isPlaying = IsPlaying;
var childStates = ChildStates;
for (int i = 0; i < ChildCount; i++)
childStates[i].IsPlaying = isPlaying;
if (IsPlaying)
Graph.RequirePreUpdate(this);
else
Graph.CancelPreUpdate(this);
}
/************************************************************************************************************************/
/// <inheritdoc/>
public override void Destroy()
{
base.Destroy();
Graph.CancelPreUpdate(this);
}
/************************************************************************************************************************/
/// <summary>
/// Called every frame while this state is playing to control its children.
/// </summary>
public virtual void Update()
{
var time = Time;
var activeChildIndex = _ActiveChildIndex;
if (Speed >= 0)
{
while (activeChildIndex < ChildCount - 1 && time > _StateEndTimes[activeChildIndex])
activeChildIndex++;
}
else
{
while (activeChildIndex > 0 && time > _StateEndTimes[activeChildIndex - 1])
activeChildIndex--;
}
SetActiveChildIndex(activeChildIndex);
var startTime = GetStartTime(activeChildIndex);
var endFadeTime = _FadeEndTimes[activeChildIndex];
if (activeChildIndex == 0 || time > endFadeTime)
{
ChildStates[activeChildIndex].Weight = 1;
if (activeChildIndex > 0)
ChildStates[activeChildIndex - 1].Weight = 0;
}
else
{
var weight = Mathf.InverseLerp((float)startTime, (float)endFadeTime, time);
ChildStates[activeChildIndex].Weight = weight;
ChildStates[activeChildIndex - 1].Weight = 1 - weight;
}
}
/************************************************************************************************************************/
/// <summary>
/// Calculates the indices of the `first` and `last` child states
/// which should be active at the specified `time`.
/// </summary>
public int GetActiveChildIndex(double time)
{
var index = Array.BinarySearch(_StateEndTimes, time);
if (index < 0)
index = ~index;
if (index >= ChildCount)
index = ChildCount - 1;
return index;
}
/************************************************************************************************************************/
/// <summary>Clears the weights of any active chhildren and sets the newly active child.</summary>
private void SetActiveChildIndex(int index)
{
if (_ActiveChildIndex == index)
return;
ChildStates[_ActiveChildIndex].Weight = 0;
if (_ActiveChildIndex > 0)
ChildStates[_ActiveChildIndex - 1].Weight = 0;
_ActiveChildIndex = index;
}
/************************************************************************************************************************/
/// <summary>Gets the time when the specified child starts relative to the start of this sequence.</summary>
public double GetStartTime(int childIndex)
=> childIndex > 0
? _StateEndTimes[childIndex - 1]
: 0;
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Copying
/************************************************************************************************************************/
/// <inheritdoc/>
public override AnimancerState Clone(CloneContext context)
{
var clone = new SequenceState();
clone.CopyFrom(this, context);
return clone;
}
/************************************************************************************************************************/
/// <inheritdoc/>
public sealed override void CopyFrom(ParentState copyFrom, CloneContext context)
=> this.CopyFromBase(copyFrom, context);
/// <inheritdoc/>
public virtual void CopyFrom(SequenceState copyFrom, CloneContext context)
{
base.CopyFrom(copyFrom, context);
var childCount = Math.Min(copyFrom.ChildCount, ChildCount);
Array.Copy(copyFrom._TimeOffsets, 0, _TimeOffsets, 0, childCount);
Array.Copy(copyFrom._FadeEndTimes, 0, _FadeEndTimes, 0, childCount);
Array.Copy(copyFrom._StateEndTimes, 0, _StateEndTimes, 0, childCount);
_ActiveChildIndex = copyFrom._ActiveChildIndex;
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
}
}

View File

@@ -0,0 +1,13 @@
fileFormatVersion: 2
guid: 7c8e97dddf2a0d246b4cdfe6319d1255
timeCreated: 1515060256
licenseType: Store
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,60 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
using UnityEngine.Playables;
namespace Animancer
{
/// <summary>
/// A <see cref="PlayableBehaviour"/> which executes <see cref="IUpdatable.Update"/>
/// on each item in an <see cref="IUpdatable.List"/> every frame.
/// </summary>
public class UpdatableListPlayable : PlayableBehaviour
{
/************************************************************************************************************************/
/// <summary>
/// Since <see cref="ScriptPlayable{T}.Create(PlayableGraph, int)"/> needs to clone an existing instance,
/// we keep a static template to avoid allocating an extra garbage one every time.
/// This also means the fields can't be readonly because field initializers don't run on the clone.
/// </summary>
private static readonly UpdatableListPlayable Template = new();
/************************************************************************************************************************/
/// <summary>The <see cref="AnimancerGraph"/> this behaviour is connected to.</summary>
private AnimancerGraph _Graph;
/// <summary>Objects to be updated before time advances.</summary>
private IUpdatable.List _Updatables;
/************************************************************************************************************************/
/// <summary>Creates a new <see cref="UpdatableListPlayable"/>.</summary>
public static ScriptPlayable<UpdatableListPlayable> Create(
AnimancerGraph graph,
int inputCount,
IUpdatable.List updatables)
{
var playable = ScriptPlayable<UpdatableListPlayable>.Create(graph._PlayableGraph, Template, inputCount);
var instance = playable.GetBehaviour();
instance._Graph = graph;
instance._Updatables = updatables;
return playable;
}
/************************************************************************************************************************/
/// <summary>[Internal] Calls <see cref="IUpdatable.Update"/> on everything added to this list.</summary>
/// <remarks>
/// Called by the <see cref="PlayableGraph"/> after the rest of the <see cref="Playable"/>s are evaluated.
/// </remarks>
public override void PrepareFrame(Playable playable, FrameData info)
=> _Graph.UpdateAll(
_Updatables,
info.deltaTime * info.effectiveParentSpeed,
info.frameId);
/************************************************************************************************************************/
}
}

View File

@@ -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:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: db2c18cf91057604fba518cb84714b94
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,128 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
using System;
using Unity.Collections;
using UnityEngine;
using UnityEngine.Animations;
namespace Animancer
{
/// <summary>
/// A replacement for the default <see cref="AnimationLayerMixerPlayable"/> which uses custom
/// <see cref="BoneWeights"/> for each individual bone instead of just using an <see cref="AvatarMask"/>
/// to include or exclude them entirely.
/// </summary>
/// https://kybernetik.com.au/animancer/api/Animancer/WeightedMaskLayerList
public class WeightedMaskLayerList : AnimancerLayerList, IDisposable
{
/************************************************************************************************************************/
/// <summary>The objects being masked.</summary>
public readonly Transform[] Bones;
/// <summary>The job data.</summary>
private readonly WeightedMaskMixerJob _Job;
/************************************************************************************************************************/
/// <summary>The root motion weight of each layer.</summary>
public NativeArray<float> RootMotionWeights
=> _Job.rootMotionWeights;
/************************************************************************************************************************/
/// <summary>The number of objects being masked (excluding the root bone).</summary>
public int BoneCount
=> Bones.Length - 1;
/************************************************************************************************************************/
/// <summary>The blend weight of each of the <see cref="Bones"/>.</summary>
public NativeArray<float> BoneWeights
=> _Job.boneWeights;
/************************************************************************************************************************/
/// <summary>Returns the index of the value corresponding to the 'bone' in the <see cref="BoneWeights"/> array.</summary>
public int IndexOf(Transform bone)
=> Array.IndexOf(Bones, bone) - 1;// Index - 1 since the root is ignored.
/************************************************************************************************************************/
/// <summary>Creates a new <see cref="AnimancerGraph"/> and <see cref="WeightedMaskLayerList"/>.</summary>
/// <remarks>
/// This method can't be a constructor because it would need to
/// assign itself to the graph before being fully constructed.
/// </remarks>
public static WeightedMaskLayerList Create(Animator animator, int layerCount)
{
var graph = new AnimancerGraph();
var layers = new WeightedMaskLayerList(graph, animator, layerCount);
graph.Layers = layers;
return layers;
}
/// <summary>Creates a new <see cref="WeightedMaskLayerList"/>.</summary>
public WeightedMaskLayerList(AnimancerGraph graph, Animator animator, int layerCount)
: base(graph, layerCount)
{
if (layerCount < 2)
throw new ArgumentOutOfRangeException(
nameof(layerCount),
"Layer count must be at least 2 (Base + 1).");
graph.Layers = this;
Bones = animator.GetComponentsInChildren<Transform>(true);
var boneCount = BoneCount;
_Job = new WeightedMaskMixerJob()
{
boneTransforms = new(boneCount, Allocator.Persistent, NativeArrayOptions.UninitializedMemory),
boneWeights = new(boneCount * (layerCount - 1), Allocator.Persistent, NativeArrayOptions.ClearMemory),
layerCount = layerCount,
rootMotionWeights = new(layerCount, Allocator.Persistent, NativeArrayOptions.UninitializedMemory),
};
graph.Disposables.Add(this);
for (var i = 0; i < layerCount; i++)
_Job.rootMotionWeights[i] = 1;
for (var i = 0; i < boneCount; i++)
{
_Job.boneTransforms[i] = animator.BindStreamTransform(Bones[i + 1]);
_Job.boneWeights[i] = 1;
}
var playable = AnimationScriptPlayable.Create(graph, _Job, Capacity);
playable.SetProcessInputs(false);
Playable = playable;
}
/************************************************************************************************************************/
/// <inheritdoc/>
void IDisposable.Dispose()
{
_Job.rootMotionWeights.Dispose();
_Job.boneTransforms.Dispose();
_Job.boneWeights.Dispose();
}
/************************************************************************************************************************/
/// <inheritdoc/>
public override AnimancerLayer Add()
{
if (Count >= Capacity)
throw new InvalidOperationException(
$"{nameof(WeightedMaskLayerList)} doesn't support dynamically changing its layer count.");
return base.Add();
}
/************************************************************************************************************************/
}
}

View File

@@ -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:

View File

@@ -0,0 +1,264 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
using System;
using Unity.Collections;
using UnityEngine;
namespace Animancer
{
/// <summary>
/// Replaces the default <see cref="AnimancerLayerMixerList"/>
/// with a <see cref="WeightedMaskLayerList"/>.
/// </summary>
/// https://kybernetik.com.au/animancer/api/Animancer/WeightedMaskLayers
[AddComponentMenu(Strings.MenuPrefix + "Weighted Mask Layers")]
[AnimancerHelpUrl(typeof(WeightedMaskLayers))]
[DefaultExecutionOrder(-10000)]// Awake before anything else initializes Animancer.
public class WeightedMaskLayers : MonoBehaviour
{
/************************************************************************************************************************/
[SerializeField] private AnimancerComponent _Animancer;
/// <summary>[<see cref="SerializeField"/>] The component to apply the layers to.</summary>
public AnimancerComponent Animancer
=> _Animancer;
/************************************************************************************************************************/
[SerializeField] private WeightedMaskLayersDefinition _Definition;
/// <summary>[<see cref="SerializeField"/>]
/// The definition of transforms to control and weights to apply to them.
/// </summary>
public ref WeightedMaskLayersDefinition Definition
=> ref _Definition;
/************************************************************************************************************************/
[SerializeField] private int _LayerCount = 2;
/// <summary>[<see cref="SerializeField"/>] The number of layers (minimum 2).</summary>
public ref int LayerCount
=> ref _LayerCount;
/************************************************************************************************************************/
/// <summary>The layer list created at runtime and assigned to <see cref="AnimancerGraph.Layers"/>.</summary>
public WeightedMaskLayerList Layers { get; protected set; }
/************************************************************************************************************************/
/// <summary>The index of each of the <see cref="WeightedMaskLayersDefinition.Transforms"/>.</summary>
public int[] Indices { get; protected set; }
/************************************************************************************************************************/
/// <summary>Finds the <see cref="Animancer"/> reference if it was missing.</summary>
protected virtual void OnValidate()
{
gameObject.GetComponentInParentOrChildren(ref _Animancer);
if (LayerCount < 2)
LayerCount = 2;
}
/************************************************************************************************************************/
/// <summary>Initializes the <see cref="Layers"/> and applies the default group weights.</summary>
protected virtual void Awake()
{
if (Definition == null ||
!Definition.IsValid)
return;
if (_Animancer == null)
TryGetComponent(out _Animancer);
Layers = WeightedMaskLayerList.Create(_Animancer.Animator, LayerCount);
_Animancer.InitializeGraph(Layers.Graph);
Indices = Definition.CalculateIndices(Layers);
for (int i = 1; i < LayerCount; i++)// Start at 1.
SetWeights(i, 0);
}
/************************************************************************************************************************/
/// <summary>Applies the weights of the specified group to the specified layer.</summary>
public void SetWeights(int layerIndex, int groupIndex)
{
Definition.AssertGroupIndex(groupIndex);
var boneWeights = Layers.BoneWeights;
var definitionWeights = Definition.Weights;
var layerIndexOffset = (layerIndex - 1) * Layers.BoneCount;
var groupDefinitionStart = groupIndex * Indices.Length;
for (int i = 0; i < Indices.Length; i++)
{
var index = Indices[i];
if (index < 0)
continue;
var weight = definitionWeights[groupDefinitionStart + i];
boneWeights[layerIndexOffset + index] = weight;
}
var rootMotionWeights = Layers.RootMotionWeights;
rootMotionWeights[layerIndex] = Definition.RootMotionWeights[groupIndex];
}
/************************************************************************************************************************/
private Fade _Fade;
/// <summary>Fades the weights towards the specified group.</summary>
public void FadeWeights(
int layerIndex,
int groupIndex,
float fadeDuration,
Func<float, float> easing = null)
{
if (fadeDuration > 0)
{
_Fade ??= new();
_Fade.Start(this, layerIndex, groupIndex, fadeDuration, easing);
}
else
{
SetWeights(layerIndex, groupIndex);
}
}
/************************************************************************************************************************/
/// <summary>An <see cref="IUpdatable"/> which fades <see cref="WeightedMaskLayers"/> over time.</summary>
/// https://kybernetik.com.au/animancer/api/Animancer/Fade
public class Fade : Updatable
{
/************************************************************************************************************************/
private NativeArray<float> _CurrentWeights;
private NativeArray<float> _CurrentRootMotionWeights;
private float[] _OriginalWeights;
private WeightedMaskLayers _Layers;
private int _LayerIndex;
private int _LayerIndexOffset;
private int _TargetWeightIndex;
private float _OriginalRootMotionWeight;
private float _TargetRootMotionWeight;
private Func<float, float> _Easing;
/// <summary>The amount of time that has passed since the start of this fade (in seconds).</summary>
public float ElapsedTime;
/// <summary>The total amount of time this fade will take (in seconds).</summary>
public float Duration;
/************************************************************************************************************************/
/// <summary>Initializes this fade and registers it to receive updates.</summary>
public void Start(
WeightedMaskLayers layers,
int layerIndex,
int groupIndex,
float duration,
Func<float, float> easing = null)
{
layers.Definition.AssertGroupIndex(groupIndex);
_CurrentWeights = layers.Layers.BoneWeights;
_CurrentRootMotionWeights = layers.Layers.RootMotionWeights;
_OriginalRootMotionWeight = _CurrentRootMotionWeights[layerIndex];
_TargetRootMotionWeight = layers.Definition.RootMotionWeights[groupIndex];
_Easing = easing;
_Layers = layers;
_LayerIndex = layerIndex;
_TargetWeightIndex = layers.Definition.IndexOf(groupIndex, 0);
Duration = duration;
_LayerIndexOffset = (layerIndex - 1) * layers.Layers.BoneCount;
var indices = _Layers.Indices;
AnimancerUtilities.SetLength(ref _OriginalWeights, indices.Length);
for (int i = 0; i < indices.Length; i++)
{
var index = _LayerIndexOffset + indices[i];
_OriginalWeights[i] = _CurrentWeights[index];
}
ElapsedTime = 0;
layers.Layers.Graph.RequirePreUpdate(this);
}
/************************************************************************************************************************/
/// <inheritdoc/>
public override void Update()
{
ElapsedTime += AnimancerGraph.DeltaTime;
if (ElapsedTime < Duration)
{
ApplyFade(ElapsedTime / Duration);
}
else
{
ApplyTargetWeights();
AnimancerGraph.Current.CancelPreUpdate(this);
}
}
/************************************************************************************************************************/
/// <summary>Recalculates the weights by interpolating based on `t`.</summary>
private void ApplyFade(float t)
{
if (_Easing != null)
t = _Easing(t);
var targetWeights = _Layers.Definition.Weights;
var indices = _Layers.Indices;
var boneWeights = _CurrentWeights;
for (int i = 0; i < indices.Length; i++)
{
var index = _LayerIndexOffset + indices[i];
var from = _OriginalWeights[i];
var to = targetWeights[_TargetWeightIndex + i];
boneWeights[index] = Mathf.LerpUnclamped(from, to, t);
}
_CurrentRootMotionWeights[_LayerIndex] = Mathf.LerpUnclamped(
_OriginalRootMotionWeight,
_TargetRootMotionWeight,
t);
}
/// <summary>Recalculates the target weights.</summary>
private void ApplyTargetWeights()
{
var targetWeights = _Layers.Definition.Weights;
var indices = _Layers.Indices;
var boneWeights = _CurrentWeights;
for (int i = 0; i < indices.Length; i++)
{
var index = _LayerIndexOffset + indices[i];
var to = targetWeights[_TargetWeightIndex + i];
boneWeights[index] = to;
}
_CurrentRootMotionWeights[_LayerIndex] = _TargetRootMotionWeight;
}
/************************************************************************************************************************/
}
/************************************************************************************************************************/
}
}

View File

@@ -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:

View File

@@ -0,0 +1,386 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
using System;
using System.Runtime.CompilerServices;
using UnityEngine;
namespace Animancer
{
/// <summary>Serializable data which defines how to control a <see cref="WeightedMaskLayerList"/>.</summary>
/// https://kybernetik.com.au/animancer/api/Animancer/WeightedMaskLayersDefinition
[Serializable]
public class WeightedMaskLayersDefinition :
ICopyable<WeightedMaskLayersDefinition>,
IEquatable<WeightedMaskLayersDefinition>
#if UNITY_EDITOR
, ISerializationCallbackReceiver
#endif
{
/************************************************************************************************************************/
/// <summary>The name of the serialized backing field of <see cref="Transforms"/>.</summary>
public const string
TransformsField = nameof(_Transforms);
[SerializeField]
private Transform[] _Transforms;
/// <summary><see cref="Transform"/>s being controlled by this definition.</summary>
public ref Transform[] Transforms
=> ref _Transforms;
/************************************************************************************************************************/
/// <summary>The name of the serialized backing field of <see cref="Weights"/>.</summary>
public const string
WeightsField = nameof(_Weights);
[SerializeField]
private float[] _Weights;
/// <summary>Groups of weights which will be applied to the <see cref="Transforms"/>.</summary>
/// <remarks>
/// This is a flattened 2D array containing groups of target weights corresponding to the transforms.
/// With n transforms, indices 0 to n-1 are Group 0, n to n*2-1 are Group 1, etc.
/// </remarks>
public ref float[] Weights
=> ref _Weights;
/************************************************************************************************************************/
[SerializeField]
private float[] _RootMotionWeights;
/// <summary>Each group has a multiplier for the Root Motion output of any layer the group is applied to.</summary>
public ref float[] RootMotionWeights
=> ref _RootMotionWeights;
/************************************************************************************************************************/
/// <summary>The number of weight groups in this definition.</summary>
public int GroupCount
{
get => _Transforms == null || _Transforms.Length == 0 || _Weights == null
? 0
: _Weights.Length / _Transforms.Length;
set
{
if (_Transforms != null && value > 0)
{
Array.Resize(ref _Weights, _Transforms.Length * value);
Array.Resize(ref _RootMotionWeights, value);
}
else
{
_Weights = Array.Empty<float>();
_RootMotionWeights = Array.Empty<float>();
}
}
}
/************************************************************************************************************************/
/// <summary>[Assert-Conditional] Asserts that the `groupIndex` is valid.</summary>
[System.Diagnostics.Conditional(Strings.Assertions)]
public void AssertGroupIndex(int groupIndex)
{
if ((uint)groupIndex >= (uint)GroupCount)
throw new ArgumentOutOfRangeException(
nameof(groupIndex),
groupIndex,
$"Must be 0 <= {nameof(groupIndex)} < Group Count ({GroupCount})");
}
/************************************************************************************************************************/
/// <summary>Calculates the index of each of the <see cref="Transforms"/>.</summary>
public int[] CalculateIndices(WeightedMaskLayerList layers)
{
var indices = new int[_Transforms.Length];
for (int i = 0; i < _Transforms.Length; i++)
{
indices[i] = layers.IndexOf(_Transforms[i]);
#if UNITY_ASSERTIONS
if (indices[i] < 0)
Debug.LogWarning(
$"Unable to find index of {_Transforms[i]} in {nameof(WeightedMaskLayerList)}",
_Transforms[i]);
#endif
}
return indices;
}
/************************************************************************************************************************/
/// <summary>
/// Adds the `transform` at the specified `index`
/// along with any associated <see cref="_Weights"/>.
/// </summary>
public void AddTransform(Transform transform)
{
var index = _Transforms.Length;
AnimancerUtilities.InsertAt(ref _Transforms, index, transform);
if (_Transforms.Length == 1 && _Weights.IsNullOrEmpty())
{
_Weights = new float[1];
return;
}
while (index <= _Weights.Length)
{
AnimancerUtilities.InsertAt(ref _Weights, index, 0);
index += _Transforms.Length;
}
}
/// <summary>
/// Removes the `index` from the <see cref="_Transforms"/>
/// along with any associated <see cref="_Weights"/>.
/// </summary>
public void RemoveTransform(int index)
{
AnimancerUtilities.RemoveAt(ref _Transforms, index);
while (index < _Weights.Length)
{
AnimancerUtilities.RemoveAt(ref _Weights, index);
index += _Transforms.Length;
}
}
/************************************************************************************************************************/
/// <summary>Calculates the index in the <see cref="Weights"/> corresponding to the specified values.</summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public int IndexOfGroup(int groupIndex)
=> groupIndex * _Transforms.Length;
/// <summary>Calculates the index in the <see cref="Weights"/> corresponding to the specified values.</summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public int IndexOf(int groupIndex, int transformIndex)
=> groupIndex * _Transforms.Length + transformIndex;
/************************************************************************************************************************/
/// <summary>Gets the specified weight.</summary>
/// <remarks>Returns <see cref="float.NaN"/> if the indices are outside the <see cref="Weights"/>.</remarks>
public float GetWeight(int groupIndex, int transformIndex)
{
if (Weights == null)
return float.NaN;
var index = IndexOf(groupIndex, transformIndex);
return (uint)index < (uint)Weights.Length
? Weights[index]
: float.NaN;
}
/// <summary>Sets the specified weight.</summary>
/// <remarks>Returns false if the indices are outside the <see cref="Weights"/>.</remarks>
public bool SetWeight(int groupIndex, int transformIndex, float value)
{
if (Weights == null)
return false;
var index = IndexOf(groupIndex, transformIndex);
if ((uint)index < (uint)Weights.Length)
{
Weights[index] = value;
return true;
}
return false;
}
/************************************************************************************************************************/
/// <summary>Gets the specified RootMotion weight.</summary>
/// <remarks>Returns <see cref="float.NaN"/> if the indices are outside the <see cref="RootMotionWeights"/>.</remarks>
public float GetRmWeight(int groupIndex)
{
if (RootMotionWeights == null)
return float.NaN;
return (uint)groupIndex < (uint)RootMotionWeights.Length
? RootMotionWeights[groupIndex]
: float.NaN;
}
/// <summary>Sets the specified RootMotion weight.</summary>
/// <remarks>Returns false if the indices are outside the <see cref="RootMotionWeights"/>.</remarks>
public bool SetRmWeight(int groupIndex, float value)
{
if (RootMotionWeights == null)
return false;
if ((uint)groupIndex < (uint)RootMotionWeights.Length)
{
RootMotionWeights[groupIndex] = value;
return true;
}
return false;
}
/************************************************************************************************************************/
/// <inheritdoc/>
public void CopyFrom(WeightedMaskLayersDefinition copyFrom, CloneContext context)
{
AnimancerUtilities.CopyExactArray(copyFrom._Transforms, ref _Transforms);
AnimancerUtilities.CopyExactArray(copyFrom._Weights, ref _Weights);
AnimancerUtilities.CopyExactArray(copyFrom._RootMotionWeights, ref _RootMotionWeights);
}
/************************************************************************************************************************/
/// <summary>Does this definition contain valid data?</summary>
public bool IsValid
=> !_Transforms.IsNullOrEmpty()
&& _Weights != null && _Weights.Length >= _Transforms.Length
&& _RootMotionWeights != null && _RootMotionWeights.Length == GroupCount;
/************************************************************************************************************************/
/// <summary>Ensures that the data in this definition is value.</summary>
public void Validate()
{
ValidateArraySizes();
RemoveMissingAndDuplicate();
}
/// <summary>Ensures that all the arrays have valid sizes.</summary>
public void ValidateArraySizes()
{
if (_Transforms.IsNullOrEmpty())
{
_Transforms = Array.Empty<Transform>();
_Weights = Array.Empty<float>();
_RootMotionWeights = Array.Empty<float>();
}
if (_Weights == null ||
_Weights.Length < _Transforms.Length)
{
AnimancerUtilities.SetLength(ref _Weights, _Transforms.Length);
}
else
{
var expectedWeightCount = (int)Math.Ceiling(_Weights.Length / (double)_Transforms.Length);
expectedWeightCount *= _Transforms.Length;
AnimancerUtilities.SetLength(ref _Weights, expectedWeightCount);
}
var rootMotionWeightCount = _RootMotionWeights == null
? 0
: _RootMotionWeights.Length;
var groupCount = GroupCount;
if (rootMotionWeightCount != groupCount)
{
AnimancerUtilities.SetLength(ref _RootMotionWeights, groupCount);
for (int i = rootMotionWeightCount; i < groupCount; i++)
_RootMotionWeights[i] = 1;
}
}
/// <summary>Removes any missing or identical <see cref="_Transforms"/>.</summary>
public bool RemoveMissingAndDuplicate()
{
var removedAny = false;
for (int i = 0; i < _Transforms.Length; i++)
{
var transform = _Transforms[i];
if (transform == null)
{
RemoveTransform(i);
removedAny = true;
}
else
{
var nextIndex = i + 1;
RemoveDuplicates:
nextIndex = Array.IndexOf(_Transforms, transform, nextIndex);
if (nextIndex > i)
{
RemoveTransform(nextIndex);
removedAny = true;
goto RemoveDuplicates;
}
}
}
return removedAny;
}
/************************************************************************************************************************/
#if UNITY_EDITOR
/************************************************************************************************************************/
/// <inheritdoc/>
void ISerializationCallbackReceiver.OnBeforeSerialize()
=> Validate();
/// <inheritdoc/>
void ISerializationCallbackReceiver.OnAfterDeserialize()
=> Validate();
/************************************************************************************************************************/
#endif
/************************************************************************************************************************/
/// <summary>Returns a summary of this definition.</summary>
public override string ToString()
=> $"{nameof(WeightedMaskLayersDefinition)}(" +
$"{nameof(Transforms)}={(Transforms != null ? Transforms.Length : 0)}, " +
$"{nameof(Weights)}={(Weights != null ? Weights.Length : 0)}, " +
$"{nameof(RootMotionWeights)}={(RootMotionWeights != null ? RootMotionWeights.Length : 0)})";
/************************************************************************************************************************/
#region Equality
/************************************************************************************************************************/
/// <summary>Are all fields in this object equal to the equivalent in `obj`?</summary>
public override bool Equals(object obj)
=> Equals(obj as WeightedMaskLayersDefinition);
/// <summary>Are all fields in this object equal to the equivalent fields in `other`?</summary>
public bool Equals(WeightedMaskLayersDefinition other)
=> other != null
&& AnimancerUtilities.ContentsAreEqual(_Transforms, other._Transforms)
&& AnimancerUtilities.ContentsAreEqual(_Weights, other._Weights)
&& AnimancerUtilities.ContentsAreEqual(_RootMotionWeights, other._RootMotionWeights);
/// <summary>Are all fields in `a` equal to the equivalent fields in `b`?</summary>
public static bool operator ==(WeightedMaskLayersDefinition a, WeightedMaskLayersDefinition b)
=> a is null
? b is null
: a.Equals(b);
/// <summary>Are any fields in `a` not equal to the equivalent fields in `b`?</summary>
public static bool operator !=(WeightedMaskLayersDefinition a, WeightedMaskLayersDefinition b)
=> !(a == b);
/************************************************************************************************************************/
/// <summary>Returns a hash code based on the values of this object's fields.</summary>
public override int GetHashCode()
=> AnimancerUtilities.Hash(-871379578,
_Transforms.SafeGetHashCode(),
_Weights.SafeGetHashCode(),
_RootMotionWeights.SafeGetHashCode());
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
}
}

View File

@@ -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:

View File

@@ -0,0 +1,199 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
using Unity.Collections;
using UnityEngine;
using UnityEngine.Animations;
#if UNITY_BURST
using Unity.Burst;
#endif
namespace Animancer
{
/// <summary>
/// An <see cref="IAnimationJob"/> which mixes its inputs based on individual <see cref="boneWeights"/>.
/// </summary>
#if UNITY_BURST
[BurstCompile(FloatPrecision.Low, FloatMode.Fast
#if UNITY_BURST_1_6_0
, OptimizeFor = OptimizeFor.Performance
#endif
)]
#endif
public struct WeightedMaskMixerJob : IAnimationJob
{
/************************************************************************************************************************/
/// <summary>The number of layers being mixed.</summary>
public int layerCount;
/// <summary>The root motion weight of each layer.</summary>
public NativeArray<float> rootMotionWeights;
/// <summary>The handles for each bone being mixed.</summary>
/// <remarks>
/// All animated bones must be included,
/// even if their individual weight isn't modified.
/// </remarks>
public NativeArray<TransformStreamHandle> boneTransforms;
/// <summary>The blend weight of each bone.</summary>
/// <remarks>
/// This array corresponds to the <see cref="boneTransforms"/>,
/// repeated for each layer after the first and excluding the base layer.
/// For example, if there are 3 layers and 10 bones, then this array will have 20 elements
/// with the first 10 being for Layer 1 and the next 10 being for Layer 2.
/// </remarks>
public NativeArray<float> boneWeights;
/************************************************************************************************************************/
/// <inheritdoc/>
readonly void IAnimationJob.ProcessRootMotion(AnimationStream output)
{
var input = output.GetInputStream(0);
var velocity = input.velocity;
var angularVelocity = input.angularVelocity;
var hasRootMotionWeights = rootMotionWeights.IsCreated;
if (hasRootMotionWeights)
{
var baseLayerWeight = rootMotionWeights[0];
velocity *= baseLayerWeight;
angularVelocity *= baseLayerWeight;
}
for (int i = 1; i < layerCount; i++)// Start at 1.
{
input = output.GetInputStream(i);
if (!input.isValid)
continue;
var layerWeight = output.GetInputWeight(i);
if (hasRootMotionWeights)
layerWeight *= rootMotionWeights[i];
velocity = Vector3.LerpUnclamped(
velocity,
input.velocity,
layerWeight);
angularVelocity = Vector3.LerpUnclamped(
angularVelocity,
input.angularVelocity,
layerWeight);
}
output.velocity = velocity;
output.angularVelocity = angularVelocity;
}
/************************************************************************************************************************/
/// <inheritdoc/>
readonly void IAnimationJob.ProcessAnimation(AnimationStream output)
{
if (layerCount == 2)
{
ProcessAnimation2Layers(output);
return;
}
// Blending more than 2 layers is less efficient because we need to use these temporary arrays.
var transformCount = boneTransforms.Length;
var localPositions = new NativeArray<Vector3>(transformCount, Allocator.Temp, NativeArrayOptions.UninitializedMemory);
var localRotations = new NativeArray<Quaternion>(transformCount, Allocator.Temp, NativeArrayOptions.UninitializedMemory);
var input = output.GetInputStream(0);
for (var i = 0; i < transformCount; i++)
{
var transform = boneTransforms[i];
localPositions[i] = transform.GetLocalPosition(input);
localRotations[i] = transform.GetLocalRotation(input);
}
for (int iLayer = 1; iLayer < layerCount; iLayer++)// Start at 1.
{
input = output.GetInputStream(iLayer);
if (!input.isValid)
break;
var layerWeight = output.GetInputWeight(iLayer);
if (layerWeight == 0)
continue;
var weightOffset = (iLayer - 1) * transformCount;
for (var iTransform = 0; iTransform < transformCount; iTransform++)
{
var transform = boneTransforms[iTransform];
var weight = layerWeight * boneWeights[weightOffset + iTransform];
if (weight == 0)
continue;
localPositions[iTransform] = Vector3.LerpUnclamped(
localPositions[iTransform],
transform.GetLocalPosition(input),
weight);
localRotations[iTransform] = Quaternion.SlerpUnclamped(
localRotations[iTransform],
transform.GetLocalRotation(input),
weight);
}
}
for (var i = 0; i < transformCount; i++)
{
var transform = boneTransforms[i];
transform.SetLocalPosition(output, localPositions[i]);
transform.SetLocalRotation(output, localRotations[i]);
}
localPositions.Dispose();
localRotations.Dispose();
}
/************************************************************************************************************************/
/// <summary>Blends the layers in an optimized way when there are only 2.</summary>
private readonly void ProcessAnimation2Layers(AnimationStream output)
{
var input0 = output.GetInputStream(0);
var input1 = output.GetInputStream(1);
if (input1.isValid)
{
var layerWeight = output.GetInputWeight(1);
var transformCount = boneTransforms.Length;
for (var i = 0; i < transformCount; i++)
{
var transform = boneTransforms[i];
var weight = layerWeight * boneWeights[i];
var position0 = transform.GetLocalPosition(input0);
var position1 = transform.GetLocalPosition(input1);
transform.SetLocalPosition(output, Vector3.LerpUnclamped(position0, position1, weight));
var rotation0 = transform.GetLocalRotation(input0);
var rotation1 = transform.GetLocalRotation(input1);
transform.SetLocalRotation(output, Quaternion.SlerpUnclamped(rotation0, rotation1, weight));
}
}
else
{
var transformCount = boneTransforms.Length;
for (var i = 0; i < transformCount; i++)
{
var transform = boneTransforms[i];
transform.SetLocalPosition(output, transform.GetLocalPosition(input0));
transform.SetLocalRotation(output, transform.GetLocalRotation(input0));
}
}
}
/************************************************************************************************************************/
}
}

View File

@@ -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:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 3c7bab446ace0ae4bb75f4027e27d859
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,119 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
using System;
namespace Animancer
{
/// <summary>Determines how <see cref="AnimancerLayer.Play(AnimancerState, float, FadeMode)"/> works.</summary>
///
/// <remarks>
/// <strong>Documentation:</strong>
/// <see href="https://kybernetik.com.au/animancer/docs/manual/blending/fading/modes">
/// Fade Modes</see>
/// <para></para>
/// <strong>Sample:</strong>
/// <see href="https://kybernetik.com.au/animancer/docs/samples/basics/transitions">
/// Transitions</see>
/// </remarks>
///
/// https://kybernetik.com.au/animancer/api/Animancer/FadeMode
///
public enum FadeMode
{
/************************************************************************************************************************/
/// <summary>
/// Calculate the fade speed to bring the <see cref="AnimancerNode.Weight"/>
/// from 0 to 1 over the specified fade duration (in seconds),
/// regardless of the actual starting weight.
/// </summary>
///
/// <remarks>
/// <strong>Example:</strong>
/// A fade duration of 0.5 would make the fade last for 0.5 seconds, regardless of how long the animation is.
/// <para></para>
/// This is generally the same as <see cref="FixedDuration"/> but differs when starting the fade from a
/// non-zero <see cref="AnimancerNode.Weight"/>, for example:
/// <list type="bullet">
/// <item>Fade Duration: 0.25</item>
/// <item>To fade from 0 to 1 with either mode would get a speed of 4 and take 0.25 seconds</item>
/// <item>To fade from 0.5 to 1 with <see cref="FixedDuration"/> would get a speed of 2 and take 0.25 seconds.
/// It has half the distance to cover so it goes half as fast to maintain the expected duration.</item>
/// <item>To fade from 0.5 to 1 with <see cref="FixedSpeed"/> would get a speed of 4 and take 0.125 seconds.
/// It gets the same speed regardless of the distance to cover, so with less distance it completes faster.</item>
/// </list>
/// </remarks>
///
/// <exception cref="InvalidOperationException">The <see cref="AnimancerState.Clip"/> is null.</exception>
///
/// <exception cref="ArgumentOutOfRangeException">
/// More states have been created for the <see cref="AnimancerState.Clip"/> than the
/// <see cref="AnimancerLayer.MaxCloneCount"/> allows.
/// </exception>
FixedSpeed,
/// <summary>
/// Calculate the fade speed to bring the <see cref="AnimancerNode.Weight"/> to the target value over the
/// specified fade duration (in seconds).
/// </summary>
///
/// <remarks>
/// <strong>Example:</strong>
/// A fade duration of 0.5 would make the fade last for 0.5 seconds, regardless of how long the animation is.
/// <para></para>
/// This is generally the same as <see cref="FixedSpeed"/>, but differs when starting the fade from a
/// non-zero <see cref="AnimancerNode.Weight"/>:
/// <list type="bullet">
/// <item>Fade Duration: 0.25</item>
/// <item>To fade from 0 to 1 with either mode would get a speed of 4 and take 0.25 seconds</item>
/// <item>To fade from 0.5 to 1 with <see cref="FixedDuration"/> would get a speed of 2 and take 0.25 seconds.
/// It has half the distance to cover so it goes half as fast to maintain the expected duration.</item>
/// <item>To fade from 0.5 to 1 with <see cref="FixedSpeed"/> would get a speed of 4 and take 0.125 seconds.
/// It gets the same speed regardless of the distance to cover, so with less distance it completes faster.</item>
/// </list>
/// </remarks>
FixedDuration,
/// <summary>
/// If the <see cref="AnimancerNode.Weight"/> is above the <see cref="AnimancerLayer.WeightlessThreshold"/>,
/// this mode will use <see cref="AnimancerLayer.GetOrCreateWeightlessState"/> to get a copy of it that is at 0
/// weight so it can fade the copy in while the original fades out with all other states. This allows an
/// animation to fade into itself.
/// </summary>
///
/// <remarks>
/// This mode can be useful when you want to repeat an action while the previous animation is still fading out.
/// For example, if you play an 'Attack' animation, it ends and starts fading back to 'Idle', and while it is
/// doing so you want to start another 'Attack' with the same animation. The previous 'Attack' can't simply
/// snap back to the start, so you can use this mode to create a second 'Attack' state to fade in while the old
/// one fades out.
/// <para></para>
/// Using this mode repeatedly on subsequent frames will probably have undesirable effects because it will
/// create a new state each time. In such a situation you most likely want <see cref="FixedSpeed"/> instead.
/// <para></para>
/// This mode only works for <see cref="ClipState"/>s.
/// <para></para>
/// The <see href="https://kybernetik.com.au/animancer/docs/manual/blending/fading/modes">Fade Modes</see> page
/// explains this mode in more detail.
/// </remarks>
FromStart,
/// <summary>
/// Like <see cref="FixedSpeed"/>, except that the fade duration is multiplied by the animation length.
/// </summary>
NormalizedSpeed,
/// <summary>
/// Like <see cref="FixedDuration"/>, except that the fade duration is multiplied by the animation length.
/// </summary>
NormalizedDuration,
/// <summary>
/// Like <see cref="FromStart"/>, except that the fade duration is multiplied by the animation length.
/// </summary>
NormalizedFromStart,
/************************************************************************************************************************/
}
}

View File

@@ -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:

View File

@@ -0,0 +1,265 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
using System;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
namespace Animancer
{
/// <summary>
/// An <see cref="IEnumerator{T}"/> for any <see cref="IList{T}"/>
/// which doesn't bother checking if the target has been modified.
/// This gives it good performance but also makes it slightly less safe to use.
/// </summary>
/// <remarks>
/// This struct also implements <see cref="IEnumerable{T}"/>
/// so it can be used in <c>foreach</c> statements and <see cref="IList{T}"/>
/// to allow the target collection to be modified without breaking the enumerator
/// (though doing so is still somewhat dangerous so use with care).
/// <para></para>
/// <strong>Example:</strong><code>
/// var numbers = new int[] { 9, 8, 7, 6, 5, 4, 3, 2, 1, 0, };
/// var count = 4;
/// foreach (var number in new FastEnumerator&lt;int&gt;(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 &lt;= `index` &lt; <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 &lt; `count` &lt;= <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
}
/************************************************************************************************************************/
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 89db15324d7dd04468e475568b94623e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,623 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
//#define DEBUG_INDEXED_LISTS
using System;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
namespace Animancer
{
/// <summary>
/// An <see cref="IReadOnlyList{T}"/> which can remove items in <c>O(1)</c> time without searching and an inbuilt
/// enumerator which supports modifications at any time (including during enumeration).
/// </summary>
/// https://kybernetik.com.au/animancer/api/Animancer/IReadOnlyIndexedList_1
public interface IReadOnlyIndexedList<T> : IReadOnlyList<T>
{
/************************************************************************************************************************/
/// <summary>The number of items this list can contain before resizing is required.</summary>
public int Capacity { get; set; }// Can't reduce the Count so it's safe for a Read-Only interface.
/// <summary>Is the `item` currently in this list?</summary>
bool Contains(T item);
/// <summary>Is the `item` currently in this list at the specified `index`?</summary>
bool Contains(T item, int index);
/// <summary>Copies all the items from this list into the `array`, starting at the specified `index`.</summary>
void CopyTo(T[] array, int index);
/// <summary>Returns the index of the `item` in this list or <c>-1</c> if it's not in this list.</summary>
int IndexOf(T item);
/// <summary>Returns a string describing this list and its contents.</summary>
string DeepToString(string separator = "\n• ");
/************************************************************************************************************************/
}
/// <summary>An object which accesses the index of the items in an <see cref="IndexedList{TItem, TIndexer}"/>.</summary>
/// https://kybernetik.com.au/animancer/api/Animancer/IIndexer_1
public interface IIndexer<T>
{
/************************************************************************************************************************/
/// <summary>Returns the index of the `item`.</summary>
/// <remarks>
/// The index used by this method should be initialized at -1 and should not be modified by anything outside
/// this indexer.
/// </remarks>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
int GetIndex(T item);
/// <summary>Sets the index of the `item`.</summary>
/// <remarks>
/// The index used by this method should be initialized at -1 and should not be modified by anything outside
/// this indexer.
/// </remarks>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
void SetIndex(T item, int index);
/// <summary>Resets the index of the `item` to -1.</summary>
/// <remarks>
/// The index used by this method should be initialized at -1 and should not be modified by anything outside
/// this indexer.
/// </remarks>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
void ClearIndex(T item);
/************************************************************************************************************************/
}
/// <summary>
/// A <see cref="List{T}"/> which can remove items in <c>O(1)</c> time without searching and an inbuilt
/// enumerator which supports modifications at any time (including during enumeration).
/// </summary>
/// <remarks>
/// This implementation has several restrictions compared to a regular <see cref="List{T}"/>:
/// <list type="bullet">
/// <item>Items cannot be <c>null</c>.</item>
/// <item>
/// Items can only be in one <see cref="IndexedList{TItem, TIndexer}"/>
/// at a time and cannot appear multiple times in it.
/// </item>
/// </list>
/// </remarks>
/// https://kybernetik.com.au/animancer/api/Animancer/IndexedList_2
public class IndexedList<TItem, TIndexer> :
IList<TItem>,
IReadOnlyIndexedList<TItem>,
ICollection
where TItem : class
where TIndexer : IIndexer<TItem>
{
/************************************************************************************************************************/
#region Fields and Accessors
/************************************************************************************************************************/
private const string
SingleUse = "Each item can only be used in one " + nameof(IndexedList<TItem, TIndexer>) + " at a time.",
NotFound = "The specified item does not exist in this " + nameof(IndexedList<TItem, TIndexer>) + ".";
/// <summary>The index which indicates that an item isn't in a list.</summary>
public const int NotInList = -1;
/// <summary>The default <see cref="Capacity"/> which lists will expand to when their first item is added.</summary>
public static int DefaultCapacity = 16;
/************************************************************************************************************************/
/// <summary>The <see cref="IIndexer{T}"/> used to access the details of items.</summary>
public TIndexer Indexer;
private TItem[] _Items;
/************************************************************************************************************************/
/// <summary>Creates a new <see cref="IndexedList{TItem, TIndexer}"/> using the default <see cref="List{T}"/> constructor.</summary>
public IndexedList(TIndexer indexer = default)
{
Indexer = indexer;
_Items = Array.Empty<TItem>();
}
/// <summary>Creates a new <see cref="IndexedList{TItem, TIndexer}"/> with the specified initial `capacity`.</summary>
public IndexedList(int capacity, TIndexer indexer = default)
{
Indexer = indexer;
_Items = new TItem[capacity];
}
// No copy constructor because the indices will not work if they are used in multiple lists at once.
/************************************************************************************************************************/
/// <summary>The number of items currently in the list.</summary>
public int Count { get; private set; }
/// <inheritdoc/>
public int Capacity
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => _Items.Length;
set
{
if (value < Count)
throw new ArgumentOutOfRangeException(nameof(Count),
$"{nameof(Capacity)} can't be less than {nameof(Count)}." +
$" Excess items must be removed before the {nameof(Capacity)} can be reduced.");
Array.Resize(ref _Items, value);
}
}
/************************************************************************************************************************/
/// <summary>The item at the specified `index`.</summary>
/// <remarks>This indexer has <c>O(1)</c> complexity</remarks>
/// <exception cref="ArgumentException">The `value` was already in an <see cref="IndexedList{TItem, TIndexer}"/> (setter only).</exception>
public TItem this[int index]
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => _Items[index];
set
{
// Make sure it isn't already in a list.
if (Indexer.GetIndex(value) != NotInList)
throw new ArgumentException(SingleUse);
// Remove the old item at that index.
Indexer.ClearIndex(_Items[index]);
// Set the index of the new item and add it at that index.
Indexer.SetIndex(value, index);
_Items[index] = value;
AssertContents();
}
}
/************************************************************************************************************************/
/// <summary>Is the `item` currently in this list?</summary>
/// <remarks>This method has <c>O(1)</c> complexity.</remarks>
public bool Contains(TItem item)
=> item != null
&& Contains(item, Indexer.GetIndex(item));
/// <summary>Is the `item` currently in this list at the specified `index`?</summary>
/// <remarks>This method has <c>O(1)</c> complexity.</remarks>
public bool Contains(TItem item, int index)
=> (uint)index < (uint)Count
&& _Items[index] == item;
/************************************************************************************************************************/
/// <summary>Returns the index of the `item` in this list or <c>-1</c> if it's not in this list.</summary>
/// <remarks>This method has <c>O(1)</c> complexity.</remarks>
public int IndexOf(TItem item)
{
if (item == null)
return NotInList;
var index = Indexer.GetIndex(item);
if (Contains(item, index))
return index;
else
return NotInList;
}
/************************************************************************************************************************/
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void CopyTo(TItem[] array, int index)
=> _Items.CopyTo(array, index);
/// <summary>Copies all the items from this list into the `array`, starting at the specified `index`.</summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
void ICollection.CopyTo(Array array, int index)
=> _Items.CopyTo(array, index);
/************************************************************************************************************************/
/// <summary>Returns false.</summary>
bool ICollection<TItem>.IsReadOnly
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => false;
}
/// <summary>Is this list thread safe?</summary>
bool ICollection.IsSynchronized
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => _Items.IsSynchronized;
}
/// <summary>An object that can be used to synchronize access to this <see cref="ICollection"/>.</summary>
object ICollection.SyncRoot
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => _Items.SyncRoot;
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Add
/************************************************************************************************************************/
/// <summary>Adds the `item` to the end of this list.</summary>
/// <remarks>
/// This method has <c>O(1)</c> complexity if the <see cref="Capacity"/> doesn't need to be increased.
/// Otherwise, it's <c>O(N)</c> since all existing items need to be copied into a new array.
/// </remarks>
/// <exception cref="ArgumentException">
/// The `item` was already in an <see cref="IndexedList{TItem, TIndexer}"/>.
/// </exception>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
void ICollection<TItem>.Add(TItem item)
=> Add(item);
/// <summary>Adds the `item` to the end of this list if it wasn't already in it and returns true if successful.</summary>
/// <remarks>
/// This method has <c>O(1)</c> complexity if the <see cref="Capacity"/> doesn't need to be increased.
/// Otherwise, it's <c>O(N)</c> since all existing items need to be copied into a new array.
/// </remarks>
/// <exception cref="ArgumentException">
/// The `item` is already in a different list.
/// </exception>
/// <exception cref="IndexOutOfRangeException">
/// The `item` is already in a different list at an index larger than this list.
/// </exception>
public bool Add(TItem item)
{
var index = Indexer.GetIndex(item);
// Make sure it isn't already in a list.
if (index != NotInList)
{
if (_Items[index] == item)// If it's in this list, do nothing.
return false;
else// Otherwise, it's in another list so we can't add it to this one.
throw new ArgumentException(SingleUse);
}
// Set the index of the new item and add it to the list.
Indexer.SetIndex(item, Count);
InternalAdd(item);
AssertContents();
return true;
}
/************************************************************************************************************************/
/// <summary>Adds the `item` to this list at the specified `index`.</summary>
/// <remarks>
/// This method has <c>O(1)</c> complexity.
/// <para></para>
/// This does not maintain the order of items, but is more efficient than <see cref="List{T}.Insert(int, T)"/>
/// because it avoids the need to move every item after the target up one place.
/// </remarks>
public void Insert(int index, TItem item)
{
if (index >= Count)
{
Add(item);
return;
}
var oldItem = _Items[index];
Indexer.SetIndex(item, index);
Indexer.SetIndex(oldItem, Count);
_Items[index] = item;
InternalAdd(oldItem);
AssertContents();
}
/************************************************************************************************************************/
private void InternalAdd(TItem item)
{
var count = Count;
var capacity = Capacity;
if (count == capacity)
{
if (capacity == 0)
{
_Items = new TItem[DefaultCapacity];
}
else
{
capacity *= 2;
if (capacity < DefaultCapacity)
capacity = DefaultCapacity;
var events = new TItem[capacity];
Array.Copy(_Items, 0, events, 0, count);
_Items = events;
}
}
_Items[count] = item;
Count = count + 1;
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Remove
/************************************************************************************************************************/
/// <summary>Removes the item at the specified `index` by swapping the last item in this list into its place.</summary>
/// <remarks>
/// This method has <c>O(1)</c> complexity.
/// <para></para>
/// This does not maintain the order of items, but is more efficient than <see cref="List{T}.RemoveAt"/>
/// because it avoids the need to move every item after the target down one place.
/// </remarks>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void RemoveAt(int index)
=> RemoveAt(index, _Items[index]);
/// <summary>Removes the item at the specified `index` by swapping the last item in this list into its place.</summary>
/// <remarks>
/// This method has <c>O(1)</c> complexity.
/// <para></para>
/// This does not maintain the order of items, but is more efficient than <see cref="List{T}.RemoveAt"/>
/// because it avoids the need to move every item after the target down one place.
/// </remarks>
private void RemoveAt(int index, TItem item)
{
var lastIndex = Count - 1;
// Adjust the enumerator if necessary.
if (CurrentIndex > index)
{
CurrentIndex--;
// If the removal index is ahead of the current enumeration,
// swap the current item to that index and swap the last item to the current index.
// Otherwise simply swapping the last item into that slot would mean that it gets covered again
// when the enumerator reaches it.
if (CurrentIndex > index)
{
var lastItem = _Items[lastIndex];
var currentItem = _Items[CurrentIndex];
_Items[CurrentIndex] = lastItem;
_Items[index] = currentItem;
_Items[lastIndex] = null;
Count--;
Indexer.ClearIndex(item);
Indexer.SetIndex(currentItem, index);
Indexer.SetIndex(lastItem, CurrentIndex);
AssertContents();
return;
}
}
// If it wasn't the last item, move the last item over it.
if (lastIndex > index)
{
var lastItem = _Items[lastIndex];
_Items[index] = lastItem;
_Items[lastIndex] = null;
Count--;
Indexer.ClearIndex(item);
Indexer.SetIndex(lastItem, index);
}
else// If it was the last item, just remove it.
{
_Items[lastIndex] = null;
Count--;
Indexer.ClearIndex(item);
}
AssertContents();
}
/************************************************************************************************************************/
/// <summary>Removes the `item` by swapping the last item in this list into its place.</summary>
/// <remarks>
/// This method has <c>O(1)</c> complexity.
/// <para></para>
/// This method does not maintain the order of items, but is more efficient than <see cref="Remove"/> because
/// it avoids the need to move every item after the target down one place.
/// </remarks>
public bool Remove(TItem item)
{
var index = Indexer.GetIndex(item);
// If it isn't in this list, do nothing.
if (!Contains(item, index))
return false;
// Remove the item.
RemoveAt(index, item);
return true;
}
/************************************************************************************************************************/
/// <summary>Removes all items from this list.</summary>
/// <remarks>This method has <c>O(N)</c> complexity.</remarks>
public void Clear()
{
for (int i = Count - 1; i >= 0; i--)
Indexer.ClearIndex(_Items[i]);
Array.Clear(_Items, 0, Count);
Count = 0;
CurrentIndex = NotInList;
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Enumeration
/************************************************************************************************************************/
/// <summary>
/// If something is currently enumerating through this list, this value holds the index it's currently up
/// to. Otherwise, this value will be negative.
/// </summary>
public int CurrentIndex { get; private set; } = NotInList;
/************************************************************************************************************************/
/// <summary>The item at the <see cref="CurrentIndex"/>.</summary>
/// <exception cref="IndexOutOfRangeException">
/// The <see cref="CurrentIndex"/> is negative so this list isn't currently being enumerated.
/// </exception>
public TItem Current
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => _Items[CurrentIndex];
}
/************************************************************************************************************************/
/// <summary>
/// Has <see cref="BeginEnumeraton"/> been called and <see cref="TryEnumerateNext"/> not yet been called
/// enough times to go through all items?
/// </summary>
public bool IsEnumerating
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => CurrentIndex != NotInList;
}
/************************************************************************************************************************/
/// <summary>
/// Sets the <see cref="CurrentIndex"/> to the end of this list so that <see cref="TryEnumerateNext"/> can
/// iterate backwards to the start.
/// </summary>
/// <exception cref="InvalidOperationException">
/// This method was called multiple times without <see cref="TryEnumerateNext"/> going over all items. This
/// list can only be enumerated by one thing at a time and it must fully complete before the next can begin.
/// This limitation is necessary to allow items to be safely added and removed at any time.
/// </exception>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void BeginEnumeraton()
{
if (IsEnumerating)
throw new InvalidOperationException(
$"{GetType().Name}<{typeof(TItem).Name}> was already enumerating." +
$" Recursive enumeration is not supported.");
CurrentIndex = Count;
}
/************************************************************************************************************************/
/// <summary>
/// Moves the <see cref="CurrentIndex"/> so the <see cref="Current"/> property points to the next item in
/// this list.
/// </summary>
/// <remarks>
/// This method should only be called after <see cref="BeginEnumeraton"/> and it should be called
/// repeatedly until it returns false. <see cref="CancelEnumeration"/> can be used to cancel the
/// enumeration early.
/// </remarks>
/// <returns>False if there are no more items to move to. Otherwise true.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool TryEnumerateNext()
=> --CurrentIndex >= 0;
/************************************************************************************************************************/
/// <summary>
/// Clears the <see cref="CurrentIndex"/> so that <see cref="BeginEnumeraton"/> can be used again without
/// needing to call <see cref="TryEnumerateNext"/> repeatedly until it returns false.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void CancelEnumeration()
=> CurrentIndex = NotInList;
/************************************************************************************************************************/
/// <summary>Returns an enumerator which iterates through this list.</summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public FastEnumerator<TItem> GetEnumerator()
=> new(_Items, Count);
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
IEnumerator<TItem> IEnumerable<TItem>.GetEnumerator()
=> GetEnumerator();
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
IEnumerator IEnumerable.GetEnumerator()
=> GetEnumerator();
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Debugging
/************************************************************************************************************************/
/// <summary>Asserts that the indices stored in all items actually match their index in this list.</summary>
[System.Diagnostics.Conditional("DEBUG_INDEXED_LISTS")]
private void AssertContents(string name = null)
{
for (int i = 0; i < Count; i++)
if (i != Indexer.GetIndex(_Items[i]))
throw new ArgumentException($"Index mismatch at {i} in {name} {DeepToString()}");
}
/************************************************************************************************************************/
/// <inheritdoc/>
public string DeepToString(string separator = "\n• ")
{
var text = StringBuilderPool.Instance.Acquire();
text.Append(GetType().GetNameCS())
.Append('[')
.Append(Count)
.Append(']');
for (int i = 0; i < Count; i++)
{
var item = _Items[i];
text.Append(separator)
.Append('[')
.Append(Indexer.GetIndex(item))
.Append("] ")
.Append(item);
}
return text.ReleaseToString();
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c4370ee24ad78f540b5840b7a5632c98
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 2a8cb96100e1eed4a81e0fca6c9c4834
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,48 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
using System.Collections.Generic;
namespace Animancer
{
/// <summary>An <see cref="ObjectPool{T}"/> for <see cref="ICollection{T}"/>.</summary>
/// https://kybernetik.com.au/animancer/api/Animancer/CollectionPool_2
public abstract class CollectionPool<TItem, TCollection> : ObjectPool<TCollection>
where TCollection : class, ICollection<TItem>// The non-generic ICollection doesn't have Count.
{
/************************************************************************************************************************/
/// <inheritdoc/>
public override TCollection Acquire()
{
var collection = base.Acquire();
AssertEmpty(collection);
return collection;
}
/************************************************************************************************************************/
/// <inheritdoc/>
public override void Release(TCollection collection)
{
collection.Clear();
base.Release(collection);
}
/************************************************************************************************************************/
/// <summary>[Assert-Conditional] Asserts that the `collection` is empty.</summary>
[System.Diagnostics.Conditional(Strings.Assertions)]
public static void AssertEmpty(TCollection collection)
{
#if UNITY_ASSERTIONS
if (collection.Count != 0)
throw new UnityEngine.Assertions.AssertionException(
$"A pooled {collection.GetType().GetNameCS()} is not empty.{NotResetError}",
null);
#endif
}
/************************************************************************************************************************/
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: e8da9ca455eec1c46b8d94a0029009b8
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,59 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
using System.Collections.Generic;
namespace Animancer
{
/// <summary>Convenience methods for accessing <see cref="DictionaryPool{TKey,TValue}"/>.</summary>
/// https://kybernetik.com.au/animancer/api/Animancer/DictionaryPool
public static class DictionaryPool
{
/************************************************************************************************************************/
/// <summary>Returns a spare <see cref="Dictionary{TKey,TValue}"/> if there are any, or creates a new one.</summary>
/// <remarks>Remember to <see cref="Release{TKey,TValue}(Dictionary{TKey,TValue})"/> it when you are done.</remarks>
public static Dictionary<TKey, TValue> Acquire<TKey, TValue>()
=> DictionaryPool<TKey, TValue>.Instance.Acquire();
/// <summary>Returns a spare <see cref="Dictionary{TKey,TValue}"/> if there are any, or creates a new one.</summary>
/// <remarks>Remember to <see cref="Release{TKey,TValue}(Dictionary{TKey,TValue})"/> it when you are done.</remarks>
public static void Acquire<TKey, TValue>(out Dictionary<TKey, TValue> dictionary)
=> dictionary = Acquire<TKey, TValue>();
/************************************************************************************************************************/
/// <summary>Clears the `dictionary` and adds it to the list of spares so it can be reused.</summary>
public static void Release<TKey, TValue>(this Dictionary<TKey, TValue> dictionary)
=> DictionaryPool<TKey, TValue>.Instance.Release(dictionary);
/// <summary>Clears the `dictionary`, adds it to the list of spares so it can be reused, and sets it to <c>null</c>.</summary>
public static void Release<TKey, TValue>(ref Dictionary<TKey, TValue> dictionary)
{
Release(dictionary);
dictionary = null;
}
/************************************************************************************************************************/
}
/************************************************************************************************************************/
/// <summary>An <see cref="ObjectPool{T}"/> for <see cref="Dictionary{TKey,TValue}"/>.</summary>
/// https://kybernetik.com.au/animancer/api/Animancer/DictionaryPool_2
public class DictionaryPool<TKey, TValue> : CollectionPool<KeyValuePair<TKey, TValue>, Dictionary<TKey, TValue>>
{
/************************************************************************************************************************/
/// <summary>Singleton.</summary>
public static DictionaryPool<TKey, TValue> Instance = new();
/************************************************************************************************************************/
/// <inheritdoc/>
protected override Dictionary<TKey, TValue> New()
=> new();
/************************************************************************************************************************/
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 812d2888d17283b45a11286c8a1f2c53
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,59 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
using System.Collections.Generic;
namespace Animancer
{
/// <summary>Convenience methods for accessing <see cref="ListPool{T}"/>.</summary>
/// https://kybernetik.com.au/animancer/api/Animancer/ListPool
public static class ListPool
{
/************************************************************************************************************************/
/// <summary>Returns a spare <see cref="List{T}"/> if there are any, or creates a new one.</summary>
/// <remarks>Remember to <see cref="Release{T}(List{T})"/> it when you are done.</remarks>
public static List<T> Acquire<T>()
=> ListPool<T>.Instance.Acquire();
/// <summary>Returns a spare <see cref="List{T}"/> if there are any, or creates a new one.</summary>
/// <remarks>Remember to <see cref="Release{T}(List{T})"/> it when you are done.</remarks>
public static void Acquire<T>(out List<T> list)
=> list = Acquire<T>();
/************************************************************************************************************************/
/// <summary>Clears the `list` and adds it to the list of spares so it can be reused.</summary>
public static void Release<T>(this List<T> list)
=> ListPool<T>.Instance.Release(list);
/// <summary>Clears the `list`, adds it to the list of spares so it can be reused, and sets it to <c>null</c>.</summary>
public static void Release<T>(ref List<T> list)
{
Release(list);
list = null;
}
/************************************************************************************************************************/
}
/************************************************************************************************************************/
/// <summary>An <see cref="ObjectPool{T}"/> for <see cref="List{T}"/>.</summary>
/// https://kybernetik.com.au/animancer/api/Animancer/ListPool_1
public class ListPool<T> : CollectionPool<T, List<T>>
{
/************************************************************************************************************************/
/// <summary>Singleton.</summary>
public static ListPool<T> Instance = new();
/************************************************************************************************************************/
/// <inheritdoc/>
protected override List<T> New()
=> new();
/************************************************************************************************************************/
}
}

Some files were not shown because too many files have changed in this diff Show More