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,8 @@
fileFormatVersion: 2
guid: 79e7dc2b7dc4516429fdb2fa86e1d380
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,117 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
using System;
using System.Runtime.CompilerServices;
using UnityEngine;
namespace Animancer.TransitionLibraries
{
/// <summary>[<see cref="SerializableAttribute"/>] A <see cref="StringAsset"/> and <see cref="int"/> pair.</summary>
/// <remarks>
/// <strong>Documentation:</strong>
/// <see href="https://kybernetik.com.au/animancer/docs/manual/transitions/libraries">
/// Transition Libraries</see>
/// </remarks>
/// https://kybernetik.com.au/animancer/api/Animancer.TransitionLibraries/NamedIndex
[Serializable]
public struct NamedIndex :
IComparable<NamedIndex>,
IEquatable<NamedIndex>
{
/************************************************************************************************************************/
[SerializeField]
private StringAsset _Name;
/// <summary>The name.</summary>
public readonly StringAsset Name
=> _Name;
/************************************************************************************************************************/
[SerializeField]
private int _Index;
/// <summary>The index.</summary>
public readonly int Index
=> _Index;
/************************************************************************************************************************/
/// <summary>Creates a new <see cref="NamedIndex"/>.</summary>
public NamedIndex(StringAsset name, int index)
{
_Name = name;
_Index = index;
}
/************************************************************************************************************************/
/// <summary>Creates a new <see cref="NamedIndex"/>.</summary>
public readonly NamedIndex With(StringAsset name)
=> new(name, _Index);
/// <summary>Creates a new <see cref="NamedIndex"/>.</summary>
public readonly NamedIndex With(int index)
=> new(_Name, index);
/************************************************************************************************************************/
/// <summary>Describes this value.</summary>
public override readonly string ToString()
=> $"[{_Index}]{_Name}";
/************************************************************************************************************************/
#region Equality
/************************************************************************************************************************/
/// <summary>Compares the <see cref="Index"/> then <see cref="Name"/>.</summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly int CompareTo(NamedIndex other)
{
var result = _Index.CompareTo(other._Index);
if (result != 0)
return result;
else
return StringAsset.Compare(_Name, other._Name);
}
/************************************************************************************************************************/
/// <summary>Are all fields in this object equal to the equivalent in `obj`?</summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override readonly bool Equals(object obj)
=> obj is NamedIndex value
&& Equals(value);
/// <summary>Are all fields in this object equal to the equivalent fields in `other`?</summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly bool Equals(NamedIndex other)
=> _Index == other._Index
&& _Name == other._Name;
/// <summary>Are all fields in `a` equal to the equivalent fields in `b`?</summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool operator ==(NamedIndex a, NamedIndex b)
=> a.Equals(b);
/// <summary>Are any fields in `a` not equal to the equivalent fields in `b`?</summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool operator !=(NamedIndex a, NamedIndex b)
=> !(a == b);
/************************************************************************************************************************/
/// <summary>Returns a hash code based on the values of this object's fields.</summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override readonly int GetHashCode()
=> AnimancerUtilities.Hash(-871379578,
_Index.SafeGetHashCode(),
_Name.SafeGetHashCode());
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
}
}

View File

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

View File

@@ -0,0 +1,50 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
namespace Animancer.TransitionLibraries
{
/// <summary>Values which determine how a transition is played.</summary>
/// <remarks>
/// <strong>Documentation:</strong>
/// <see href="https://kybernetik.com.au/animancer/docs/manual/transitions/libraries">
/// Transition Libraries</see>
/// </remarks>
/// https://kybernetik.com.au/animancer/api/Animancer.TransitionLibraries/TransitionDetails
public struct TransitionDetails
{
/************************************************************************************************************************/
/// <summary>Creates a new <see cref="TransitionDetails"/> with all values as <see cref="float.NaN"/>.</summary>
public static TransitionDetails NaN
=> new(float.NaN, float.NaN);
/************************************************************************************************************************/
/// <summary><see cref="ITransition.FadeDuration"/></summary>
public float FadeDuration;
/// <summary><see cref="ITransition.NormalizedStartTime"/></summary>
public float NormalizedStartTime;
/************************************************************************************************************************/
/// <summary>Creates a new <see cref="TransitionDetails"/>.</summary>
public TransitionDetails(
float fadeDuration,
float normalizedStartTime)
{
FadeDuration = fadeDuration;
NormalizedStartTime = normalizedStartTime;
}
/// <summary>Creates a new <see cref="TransitionDetails"/>.</summary>
public TransitionDetails(
ITransition transition)
{
FadeDuration = transition.FadeDuration;
NormalizedStartTime = transition.NormalizedStartTime;
}
/************************************************************************************************************************/
}
}

View File

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

View File

@@ -0,0 +1,640 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
using System;
using System.Collections.Generic;
using UnityEngine;
namespace Animancer.TransitionLibraries
{
/// <summary>[Pro-Only]
/// A library of <see cref="ITransition"/>s which allows specific
/// transition combinations to be overridden without needing to be hard coded.
/// </summary>
/// <remarks>
/// <strong>Documentation:</strong>
/// <see href="https://kybernetik.com.au/animancer/docs/manual/transitions/libraries">
/// Transition Libraries</see>
/// </remarks>
/// https://kybernetik.com.au/animancer/api/Animancer.TransitionLibraries/TransitionLibrary
public class TransitionLibrary :
IAnimationClipCollection,
ICopyable<TransitionLibrary>
{
/************************************************************************************************************************/
#region Fields and Properties
/************************************************************************************************************************/
/// <summary>[Pro-Only] Modifiers in the order they are created.</summary>
/// <remarks>The <see cref="TransitionModifierGroup.Index"/> of each item corresponds to its position in this list.</remarks>
private readonly List<TransitionModifierGroup>
TransitionModifiers = new();
/// <summary>[Pro-Only] Modifiers registered by their <see cref="IHasKey.Key"/> as well as any custom aliases.</summary>
private readonly Dictionary<object, TransitionModifierGroup>
KeyedTransitionModifiers = new();
/************************************************************************************************************************/
/// <summary>[Pro-Only] The number of transitions in this library.</summary>
public int Count
=> TransitionModifiers.Count;
/// <summary>[Pro-Only] The number of transitions in this library plus any additional aliases.</summary>
public int AliasCount
=> KeyedTransitionModifiers.Count;
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Queries
/************************************************************************************************************************/
/// <summary>[Pro-Only]
/// Does this library contain a transition registered with the `key`?
/// </summary>
public bool ContainsKey(object key)
=> KeyedTransitionModifiers.ContainsKey(key);
/// <summary>[Pro-Only]
/// Does this library contain a transition registered with the <see cref="IHasKey.Key"/>?
/// </summary>
public bool ContainsKey(IHasKey hasKey)
=> ContainsKey(hasKey.Key);
/************************************************************************************************************************/
/// <summary>[Pro-Only]
/// Tries to find a <see cref="TransitionModifierGroup"/> registered with the `key`.
/// </summary>
public bool TryGetTransition(object key, out TransitionModifierGroup transition)
{
if (KeyedTransitionModifiers.TryGetValue(key, out transition))
return true;
if (key is AnimancerState state)
{
key = AnimancerUtilities.GetRootKey(state.Key);
return
key != null &&
KeyedTransitionModifiers.TryGetValue(key, out transition);
}
AssertStringReference(key);
return false;
}
/// <summary>[Pro-Only]
/// Tries to find a <see cref="TransitionModifierGroup"/> registered with the <see cref="IHasKey.Key"/>.
/// </summary>
public bool TryGetTransition(IHasKey hasKey, out TransitionModifierGroup transition)
=> TryGetTransition(hasKey.Key, out transition);
/// <summary>[Pro-Only]
/// Tries to find a <see cref="TransitionModifierGroup"/>
/// via its <see cref="TransitionModifierGroup.Index"/>.
/// </summary>
public bool TryGetTransition(int index, out TransitionModifierGroup transition)
=> TransitionModifiers.TryGet(index, out transition)
&& transition != null;
/************************************************************************************************************************/
/// <summary>[Pro-Only]
/// Finds the <see cref="TransitionModifierGroup.Index"/> of the group registered with the `key`
/// or returns <c>-1</c>.
/// </summary>
public int IndexOf(object key)
=> TryGetTransition(key, out var group)
? group.Index
: -1;
/// <summary>[Pro-Only]
/// Finds the <see cref="TransitionModifierGroup.Index"/> of the group registered with the `key`
/// or returns <c>-1</c>.
/// </summary>
public int IndexOf(IHasKey hasKey)
=> IndexOf(hasKey.Key);
/************************************************************************************************************************/
/// <summary>[Pro-Only]
/// Returns the fade duration to use when transitioning from `from` to the `transition`.
/// </summary>
public float GetFadeDuration(
object from,
ITransition to)
{
if (from != null &&
TryGetTransition(to.Key, out var group))
return group.GetFadeDuration(from);
return to.FadeDuration;
}
/// <summary>[Pro-Only]
/// Returns the fade duration to use when transitioning from `from` to the `transition`.
/// </summary>
public float GetFadeDuration(
IHasKey from,
ITransition to)
=> GetFadeDuration(from?.Key, to);
/// <summary>[Pro-Only]
/// Returns the fade duration to use when transitioning from the
/// <see cref="AnimancerLayer.CurrentState"/> to the `transition`.
/// </summary>
public float GetFadeDuration(
AnimancerLayer layer,
ITransition transition)
=> GetFadeDuration(layer.CurrentState?.Key, transition);
/// <summary>[Pro-Only]
/// Returns the fade duration to use when transitioning from the
/// <see cref="AnimancerLayer.CurrentState"/> to the `key`.
/// </summary>
public float GetFadeDuration(
AnimancerLayer layer,
object key,
float fadeDuration)
{
AssertStringReference(key);
var from = layer.CurrentState?.Key;
if (from != null &&
TryGetTransition(key, out var group))
return group.GetFadeDuration(from);
return fadeDuration;
}
/************************************************************************************************************************/
/// <summary>[Pro-Only] Gathers all the animations in this library.</summary>
public virtual void GatherAnimationClips(ICollection<AnimationClip> clips)
{
for (int i = TransitionModifiers.Count - 1; i >= 0; i--)
clips.GatherFromSource(TransitionModifiers[i].Transition);
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Add
/************************************************************************************************************************/
/// <summary>[Pro-Only] Adds the contents of the `definition` to this library.</summary>
/// <remarks>Existing values will be completely replaced.</remarks>
public void Initialize(TransitionLibraryDefinition definition)
{
Clear();
if (definition == null)
return;
var count = definition.Transitions.Length;
if (TransitionModifiers.Capacity < count)
{
var capacity = Math.Max(count, 16);
TransitionModifiers.Capacity = capacity;
KeyedTransitionModifiers.EnsureCapacity(capacity);
}
for (int i = 0; i < count; i++)
{
var transition = definition.Transitions[i];
if (transition != null)
SetTransition(transition);
}
for (int i = 0; i < definition.Modifiers.Length; i++)
SetModifier(definition.Modifiers[i]);
if (definition.AliasAllTransitions)
{
var modifierIndex = 0;
for (int i = 0; i < count; i++)
{
var transition = definition.Transitions[i];
if (transition == null)
continue;
var modifier = TransitionModifiers[modifierIndex++];
KeyedTransitionModifiers[StringReference.Get(transition.name)] = modifier;
}
}
for (int i = 0; i < definition.Aliases.Length; i++)
{
var alias = definition.Aliases[i];
if (alias.Name != null &&
TransitionModifiers.TryGet(alias.Index, out var group))
KeyedTransitionModifiers[alias.Name.Name] = group;
}
}
/************************************************************************************************************************/
/// <summary>[Pro-Only] Adds the `transition` to this library.</summary>
/// <exception cref="ArgumentException">A transition is already registered with the `key`.</exception>
public TransitionModifierGroup AddTransition(
object key,
ITransition transition)
{
AssertStringReference(key);
var modifier = new TransitionModifierGroup(TransitionModifiers.Count, transition);
KeyedTransitionModifiers.Add(key, modifier);
TransitionModifiers.Add(modifier);
return modifier;
}
/// <summary>[Pro-Only] Adds the `transition` to this library.</summary>
/// <exception cref="ArgumentException">A transition is already registered with the `key`.</exception>
public TransitionModifierGroup AddTransition(
IHasKey hasKey,
ITransition transition)
=> AddTransition(hasKey.Key, transition);
/// <summary>[Pro-Only] Adds the `transition` to this library.</summary>
/// <exception cref="ArgumentException">A transition is already registered with the `key`.</exception>
public TransitionModifierGroup AddTransition(
ITransition transition)
=> AddTransition(transition, transition);
/************************************************************************************************************************/
/// <summary>[Pro-Only]
/// Adds the `transition` to this library or replaces the existing one registered with the `key`.
/// </summary>
public TransitionModifierGroup SetTransition(
object key,
ITransition transition)
{
if (TryGetTransition(key, out var oldModifier))
{
oldModifier.Transition = transition;
return oldModifier;
}
return AddTransition(key, transition);
}
/// <summary>[Pro-Only]
/// Adds the `transition` to this library or replaces the existing one registered with the `key`.
/// </summary>
public TransitionModifierGroup SetTransition(
IHasKey hasKey,
ITransition transition)
=> SetTransition(hasKey.Key, transition);
/// <summary>[Pro-Only]
/// Adds the `transition` to this library or replaces the existing one registered with the `key`.
/// </summary>
public TransitionModifierGroup SetTransition(
ITransition transition)
=> SetTransition(transition, transition);
/************************************************************************************************************************/
/// <summary>[Pro-Only]
/// Sets the <see cref="ITransition.FadeDuration"/>
/// to use when transitioning from `from` to `to`.
/// </summary>
public void SetFadeDuration(
object from,
ITransition to,
float fadeDuration)
{
var group = SetTransition(to.Key, to);
group.SetFadeDuration(from, fadeDuration);
}
/// <summary>[Pro-Only]
/// Sets the <see cref="ITransition.FadeDuration"/>
/// to use when transitioning from `from` to `to`.
/// </summary>
public void SetFadeDuration(
IHasKey from,
ITransition to,
float fadeDuration)
=> SetFadeDuration(from.Key, to, fadeDuration);
/************************************************************************************************************************/
/// <summary>[Pro-Only]
/// Sets the <see cref="ITransition.NormalizedStartTime"/>
/// to use when transitioning from `from` to `to`.
/// </summary>
public void SetNormalizedStartTime(
object from,
ITransition to,
float normalizedStartTime)
{
var group = SetTransition(to.Key, to);
group.SetNormalizedStartTime(from, normalizedStartTime);
}
/// <summary>[Pro-Only]
/// Sets the <see cref="ITransition.NormalizedStartTime"/>
/// to use when transitioning from `from` to `to`.
/// </summary>
public void SetNormalizedStartTime(
IHasKey from,
ITransition to,
float normalizedStartTime)
=> SetNormalizedStartTime(from.Key, to, normalizedStartTime);
/************************************************************************************************************************/
/// <summary>[Pro-Only]
/// Sets the <see cref="ITransition.FadeDuration"/> and <see cref="ITransition.NormalizedStartTime"/>
/// to use when transitioning from `from` to `to`.
/// </summary>
public void SetModifier(
object from,
ITransition to,
TransitionDetails modifier)
{
var group = SetTransition(to.Key, to);
group.SetModifier(from, modifier);
}
/// <summary>[Pro-Only]
/// Sets the <see cref="ITransition.FadeDuration"/> and <see cref="ITransition.NormalizedStartTime"/>
/// to use when transitioning from `from` to `to`.
/// </summary>
public void SetModifier(
IHasKey from,
ITransition to,
TransitionDetails modifier)
=> SetModifier(from.Key, to, modifier);
/************************************************************************************************************************/
/// <summary>[Pro-Only]
/// Sets the <see cref="ITransition.FadeDuration"/> and <see cref="ITransition.NormalizedStartTime"/>
/// to use when transitioning from <see cref="TransitionModifierDefinition.FromIndex"/>
/// to <see cref="TransitionModifierDefinition.ToIndex"/>.
/// </summary>
public bool SetModifier(
TransitionModifierDefinition modifier)
{
if (!TransitionModifiers.TryGet(modifier.FromIndex, out var from) ||
!TransitionModifiers.TryGet(modifier.ToIndex, out var to))
return false;
to.SetModifier(
from.Transition.Key,
modifier.ToTransitionDetails());
return true;
}
/************************************************************************************************************************/
/// <summary>[Pro-Only] Registers the `group` with another `key`.</summary>
public void AddAlias(
object key,
TransitionModifierGroup group)
{
AssertStringReference(key);
AssertGroup(group);
KeyedTransitionModifiers.Add(key, group);
}
/// <summary>[Pro-Only] Registers the `transition` with the `key`.</summary>
/// <remarks>Also registers it with its <see cref="IHasKey.Key"/> if it wasn't already.</remarks>
public TransitionModifierGroup AddAlias(
object key,
ITransition transition)
{
var group = SetTransition(transition);
AddAlias(key, group);
return group;
}
/************************************************************************************************************************/
/// <summary>[Pro-Only] Adds the contents of `copyFrom` into this library.</summary>
/// <remarks>
/// This method adds and replaces values, but does not remove any
/// (unlike <see cref="CopyFrom(TransitionLibrary, CloneContext)"/>.
/// </remarks>
public void AddLibrary(TransitionLibrary library, CloneContext context)
{
if (library == null)
return;
for (int i = 0; i < TransitionModifiers.Count; i++)
{
var group = TransitionModifiers[i];
context[group.Transition] = group;
}
foreach (var group in library.KeyedTransitionModifiers)
{
var transition = group.Value.Transition;
if (context.TryGetClone(transition, out var clone) &&
clone is TransitionModifierGroup cloneGroup)
{
AssertGroup(cloneGroup);
KeyedTransitionModifiers[group.Key] = cloneGroup;
}
else
{
cloneGroup = SetTransition(group.Key, group.Value.Transition);
cloneGroup.CopyFrom(group.Value);
context[transition] = cloneGroup;
}
}
}
/// <summary>[Pro-Only] Adds the contents of `copyFrom` into this library.</summary>
/// <remarks>
/// This method adds and replaces values, but does not remove any
/// (unlike <see cref="CopyFrom(TransitionLibrary, CloneContext)"/>.
/// </remarks>
public void AddLibrary(TransitionLibrary library)
{
var context = CloneContext.Pool.Instance.Acquire();
AddLibrary(library, context);
CloneContext.Pool.Instance.Release(context);
}
/************************************************************************************************************************/
/// <inheritdoc/>
/// <remarks>See also <see cref="AddLibrary(TransitionLibrary, CloneContext)"/>.</remarks>
public void CopyFrom(TransitionLibrary copyFrom, CloneContext context)
{
Clear();
if (copyFrom == null)
return;
var count = copyFrom.TransitionModifiers.Count;
for (int i = 0; i < count; i++)
TransitionModifiers.Add(copyFrom.TransitionModifiers[i].Clone(context));
foreach (var group in copyFrom.KeyedTransitionModifiers)
{
var clone = TransitionModifiers[group.Value.Index];
KeyedTransitionModifiers.Add(group.Key, clone);
}
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Remove
/************************************************************************************************************************/
// Remove from the dictionary but not the list because there might be multiple aliases for that index.
/// <summary>[Pro-Only] Removes the transition registered with the `key`.</summary>
public bool RemoveTransition(object key)
=> KeyedTransitionModifiers.Remove(key);
/// <summary>[Pro-Only] Removes the transition registered with the <see cref="IHasKey.Key"/>.</summary>
public bool RemoveTransition(IHasKey hasKey)
=> RemoveTransition(hasKey.Key);
/************************************************************************************************************************/
/// <summary>[Pro-Only] Removes a modified fade duration for transitioning from `from` to `to`.</summary>
public bool RemoveFadeDuration(object from, object to)
=> TryGetTransition(to, out var group)
&& group.FromKeyToModifier != null
&& group.FromKeyToModifier.Remove(from);
/// <summary>[Pro-Only] Removes a modified fade duration for transitioning from `from` to `to`.</summary>
public bool RemoveFadeDuration(IHasKey from, IHasKey to)
=> RemoveFadeDuration(from.Key, to.Key);
/************************************************************************************************************************/
/// <summary>[Pro-Only] Removes everything from this library, leaving it empty.</summary>
public void Clear()
{
TransitionModifiers.Clear();
KeyedTransitionModifiers.Clear();
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Play
/************************************************************************************************************************/
/// <summary>
/// Calls <see cref="AnimancerLayer.Play(ITransition, float, FadeMode)"/>
/// with the fade duration potentially modified by this library.
/// </summary>
public AnimancerState Play(
AnimancerLayer layer,
ITransition transition)
=> TryGetTransition(transition, out var modifier)
? Play(layer, modifier)
: layer.Play(transition);
/// <summary>
/// Calls <see cref="AnimancerLayer.Play(ITransition, float, FadeMode)"/>
/// with the fade duration potentially modified by this library.
/// </summary>
public AnimancerState Play(
AnimancerLayer layer,
TransitionModifierGroup transition)
{
var from = layer.CurrentState?.Key;
var to = transition.Transition;
var details = transition.GetDetails(from);
if (float.IsNaN(details.FadeDuration))
details.FadeDuration = to.FadeDuration;
if (float.IsNaN(details.NormalizedStartTime))
return layer.Play(
to,
details.FadeDuration,
to.FadeMode);
var state = layer.Play(
to,
details.FadeDuration,
FadeMode.FromStart);
state.NormalizedTime = details.NormalizedStartTime;
return state;
}
/************************************************************************************************************************/
/// <summary>
/// Plays the transition registered with the specified `key` if there is one.
/// Otherwise, returns <c>null</c>.
/// </summary>
public AnimancerState TryPlay(
AnimancerLayer layer,
object key)
=> TryGetTransition(key, out var transition)
? Play(layer, transition)
: null;
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Assertions
/************************************************************************************************************************/
/// <summary>[Assert-Conditional]
/// Logs <see cref="OptionalWarning.StringReference"/> if the `key` is a <see cref="string"/>.
/// </summary>
[System.Diagnostics.Conditional(Strings.Assertions)]
private void AssertStringReference(object key)
{
#if UNITY_ASSERTIONS
if (key is string keyString)
{
if (StringReference.TryGet(keyString, out var keyReference) &&
KeyedTransitionModifiers.ContainsKey(keyReference))
Debug.LogError(
$"{nameof(TransitionLibrary)} key type mismatch:" +
$" attempted to use string '{keyString}'," +
$" but that value is registered as a {nameof(StringReference)}." +
$" Use a {nameof(StringReference)} to ensure the correct lookup.");
else
OptionalWarning.StringReference.Log(
$"A string '{keyString}' is being used as a key in a {nameof(TransitionLibrary)}." +
$" {nameof(StringReference)}s should be used instead of strings because they are more efficient" +
$" and to avoid mismatches with aliases in a {nameof(TransitionLibraryDefinition)}.");
}
#endif
}
/************************************************************************************************************************/
/// <summary>[Assert-Conditional]
/// Asserts that the <see cref="TransitionModifierGroup.Index"/>
/// corresponds to the <see cref="TransitionModifiers"/>.
/// </summary>
[System.Diagnostics.Conditional(Strings.Assertions)]
[HideInCallstack]
internal void AssertGroup(TransitionModifierGroup group)
{
#if UNITY_ASSERTIONS
if (!TransitionModifiers.TryGet(group.Index, out var registered) ||
registered != group)
Debug.LogError(
$"{nameof(CloneContext)} contains an {nameof(TransitionModifierGroup)}" +
$" which isn't part of this {nameof(TransitionLibrary)}." +
$" It must have been added to the context manually.");
#endif
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
}
}

View File

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

View File

@@ -0,0 +1,97 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
using System.Collections.Generic;
using UnityEngine;
namespace Animancer.TransitionLibraries
{
/// <summary>[Pro-Only]
/// A <see cref="ScriptableObject"/> which serializes a <see cref="TransitionLibraryDefinition"/>
/// and creates a <see cref="TransitionLibrary"/> from it at runtime.
/// </summary>
/// <remarks>
/// <strong>Documentation:</strong>
/// <see href="https://kybernetik.com.au/animancer/docs/manual/transitions/libraries">
/// Transition Libraries</see>
/// </remarks>
/// https://kybernetik.com.au/animancer/api/Animancer.TransitionLibraries/TransitionLibraryAsset
[CreateAssetMenu(
menuName = Strings.MenuPrefix + "Transition Library",
order = Strings.AssetMenuOrder + 0)]
[AnimancerHelpUrl(typeof(TransitionLibraryAsset))]
public class TransitionLibraryAsset : ScriptableObject,
IAnimationClipCollection
{
/************************************************************************************************************************/
[SerializeField]
private TransitionLibraryDefinition _Definition;
/// <summary>[<see cref="SerializeField"/>]
/// The serialized data which will be used to initialize the <see cref="Library"/> at runtime.
/// </summary>
/// <remarks>
/// If you modify the contents of this reference, either re-assign this property
/// or call <see cref="OnDefinitionModified"/> to apply any changes to the <see cref="Library"/>.
/// </remarks>
public TransitionLibraryDefinition Definition
{
get => _Definition;
set
{
_Definition = value ?? new();
OnDefinitionModified();
}
}
#if UNITY_EDITOR
/// <summary>[Editor-Only] [Internal]
/// The name of the field which stores the <see cref="Definition"/>.
/// </summary>
internal const string DefinitionField = nameof(_Definition);
#endif
/************************************************************************************************************************/
/// <summary>The runtime <see cref="TransitionLibrary"/> created from the <see cref="Definition"/>.</summary>
public TransitionLibrary Library { get; private set; }
/************************************************************************************************************************/
/// <summary>Initializes the <see cref="Library"/>.</summary>
protected virtual void OnEnable()
{
_Definition ??= new();
Library = new();
Library.Initialize(_Definition);
}
/************************************************************************************************************************/
/// <summary>
/// Adds the contents of the <see cref="Definition"/>
/// to the <see cref="Library"/> if it was already initialized.
/// </summary>
/// <remarks>
/// Call this after modifying the contents of the <see cref="Definition"/>
/// to ensure that the <see cref="Library"/> reflects any changes.
/// <para></para>
/// Note that this doesn't remove anything from the <see cref="Library"/>,
/// it only adds or replaces values.
/// </remarks>
public void OnDefinitionModified()
=> Library?.Initialize(_Definition);
/************************************************************************************************************************/
/// <summary>Gathers all the animations in the <see cref="Definition"/> and <see cref="Library"/>.</summary>
public virtual void GatherAnimationClips(ICollection<AnimationClip> clips)
{
clips.GatherFromSource(_Definition);
clips.GatherFromSource(Library);
}
/************************************************************************************************************************/
}
}

View File

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

View File

@@ -0,0 +1,407 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
using System;
using System.Collections.Generic;
using System.Text;
using UnityEngine;
namespace Animancer.TransitionLibraries
{
/// <summary>[<see cref="SerializableAttribute"/>]
/// A library of transitions and other details which can create a <see cref="TransitionLibrary"/>.
/// </summary>
/// <remarks>
/// <strong>Documentation:</strong>
/// <see href="https://kybernetik.com.au/animancer/docs/manual/transitions/libraries">
/// Transition Libraries</see>
/// </remarks>
/// https://kybernetik.com.au/animancer/api/Animancer.TransitionLibraries/TransitionLibraryDefinition
[Serializable]
public class TransitionLibraryDefinition :
IAnimationClipCollection,
ICopyable<TransitionLibraryDefinition>,
IEquatable<TransitionLibraryDefinition>,
IHasDescription
{
/************************************************************************************************************************/
#region Fields and Properties
/************************************************************************************************************************/
[SerializeField]
private TransitionAssetBase[]
_Transitions = Array.Empty<TransitionAssetBase>();
/// <summary>[<see cref="SerializeField"/>] The transitions in this library.</summary>
/// <remarks>This property uses an empty array instead of <c>null</c>.</remarks>
public TransitionAssetBase[] Transitions
{
get => _Transitions;
set => _Transitions = value.NullIsEmpty();
}
/************************************************************************************************************************/
[SerializeField]
private TransitionModifierDefinition[]
_Modifiers = Array.Empty<TransitionModifierDefinition>();
/// <summary>[<see cref="SerializeField"/>] Modified fade durations for specific transition combinations.</summary>
/// <remarks>This property uses an empty array instead of <c>null</c>.</remarks>
public TransitionModifierDefinition[] Modifiers
{
get => _Modifiers;
set => _Modifiers = value.NullIsEmpty();
}
/************************************************************************************************************************/
[SerializeField]
private NamedIndex[]
_Aliases = Array.Empty<NamedIndex>();
/// <summary>[<see cref="SerializeField"/>] Alternate names that can be used to look up transitions.</summary>
/// <remarks>
/// This array should always be sorted, use <see cref="SortAliases"/> if necessary.
/// <para></para>
/// This property uses an empty array instead of <c>null</c>.
/// </remarks>
public NamedIndex[] Aliases
{
get => _Aliases;
set => _Aliases = value.NullIsEmpty();
}
/************************************************************************************************************************/
[SerializeField]
[Tooltip(AliasAllTransitionsTooltip)]
private bool _AliasAllTransitions;
/// <summary>[<see cref="SerializeField"/>]
/// Should all Transitions automatically be registered using their name as an Alias?
/// </summary>
public ref bool AliasAllTransitions
=> ref _AliasAllTransitions;
#if UNITY_EDITOR
/// <summary>[Editor-Only] [Internal]
/// The name of the field which stores the <see cref="AliasAllTransitions"/>.
/// </summary>
internal const string AliasAllTransitionsField = nameof(_AliasAllTransitions);
#endif
/// <summary>Tooltip for the <see cref="AliasAllTransitions"/> field.</summary>
public const string AliasAllTransitionsTooltip =
"Should all Transitions automatically be registered using their name as an Alias?";
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Transitions
/************************************************************************************************************************/
/// <summary>
/// <see cref="AnimancerUtilities.TryGet{T}(IList{T}, int, out T)"/> for the <see cref="Transitions"/>.
/// </summary>
public bool TryGetTransition(
int index,
out TransitionAssetBase transition)
=> _Transitions.TryGet(index, out transition)
&& transition != null;
/************************************************************************************************************************/
/// <summary>Adds an item to the end of the <see cref="Transitions"/>.</summary>
public void AddTransition(
TransitionAssetBase transition)
=> AnimancerUtilities.InsertAt(
ref _Transitions,
_Transitions.Length,
transition);
/************************************************************************************************************************/
/// <summary>
/// Removes an item from the <see cref="Transitions"/>
/// and adjusts the other fields to account for the moved indices.
/// </summary>
public void RemoveTransition(int index)
{
if ((uint)index >= _Transitions.Length)
return;
AnimancerUtilities.RemoveAt(ref _Transitions, index);
for (int i = _Modifiers.Length - 1; i >= 0; i--)
{
var modifier = _Modifiers[i];
// Remove any modifiers targeting that transition.
if (modifier.FromIndex == index ||
modifier.ToIndex == index)
{
AnimancerUtilities.RemoveAt(ref _Modifiers, i);
}
else// Adjust the indices of any modifiers after it.
{
var fromIndex = modifier.FromIndex;
if (fromIndex > index)
fromIndex--;
var toIndex = modifier.ToIndex;
if (toIndex > index)
toIndex--;
_Modifiers[i] = modifier.WithIndices(fromIndex, toIndex);
}
}
for (int i = _Aliases.Length - 1; i >= 0; i--)
{
var alias = _Aliases[i];
// Remove any aliases targeting that transition.
if (alias.Index == index)
{
AnimancerUtilities.RemoveAt(ref _Aliases, i);
}
else// Adjust the indices of any aliases after it.
{
if (alias.Index > index)
_Aliases[i] = alias.With(alias.Index - 1);
}
}
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Modifiers
/************************************************************************************************************************/
/// <summary>Tries to find an item in the <see cref="Modifiers"/> with the specified indices.</summary>
/// <remarks>
/// If unsuccessful, the `modifier` is given the details
/// from the <see cref="Transitions"/> at the `toIndex`. and this method returns false.
/// </remarks>
public bool TryGetModifier(
int fromIndex,
int toIndex,
out TransitionModifierDefinition modifier)
{
var index = IndexOfModifier(fromIndex, toIndex);
if (index >= 0)
{
modifier = _Modifiers[index];
return true;
}
float fadeDuration, normalizedStartTime;
if (TryGetTransition(toIndex, out var transition))
{
fadeDuration = transition.TryGetFadeDuration();
normalizedStartTime = transition.TryGetNormalizedStartTime();
}
else
{
fadeDuration = float.NaN;
normalizedStartTime = float.NaN;
}
modifier = new(fromIndex, toIndex, fadeDuration, normalizedStartTime);
return false;
}
/************************************************************************************************************************/
/// <summary>
/// Returns the index in the <see cref="Modifiers"/> which matches the given
/// <see cref="TransitionModifierDefinition.FromIndex"/> and
/// <see cref="TransitionModifierDefinition.ToIndex"/> or -1 if no such item exists.
/// </summary>
public int IndexOfModifier(int fromIndex, int toIndex)
{
for (int i = _Modifiers.Length - 1; i >= 0; i--)
{
var modifier = _Modifiers[i];
if (modifier.FromIndex == fromIndex &&
modifier.ToIndex == toIndex)
return i;
}
return -1;
}
/************************************************************************************************************************/
/// <summary>Adds, replaces, or removes an item in the <see cref="Modifiers"/>.</summary>
public void SetModifier(
TransitionModifierDefinition modifier)
{
if (!modifier.Validate())
{
RemoveModifier(modifier);
return;
}
var index = IndexOfModifier(modifier.FromIndex, modifier.ToIndex);
if (index >= 0)
{
_Modifiers[index] = modifier;
}
else
{
AnimancerUtilities.InsertAt(ref _Modifiers, _Modifiers.Length, modifier);
}
}
/************************************************************************************************************************/
/// <summary>Removes an item from the <see cref="Modifiers"/>.</summary>
public bool RemoveModifier(
TransitionModifierDefinition modifier)
=> RemoveModifier(modifier.FromIndex, modifier.ToIndex);
/// <summary>Removes an item from the <see cref="Modifiers"/>.</summary>
public bool RemoveModifier(int fromIndex, int toIndex)
{
var index = IndexOfModifier(fromIndex, toIndex);
if (index < 0)
return false;
AnimancerUtilities.RemoveAt(ref _Modifiers, index);
return true;
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Aliases
/************************************************************************************************************************/
/// <summary>Adds an item to the <see cref="Aliases"/>, sorted by its values.</summary>
public int AddAlias(NamedIndex alias)
{
int i = 0;
for (; i < _Aliases.Length; i++)
if (alias.CompareTo(_Aliases[i]) <= 0)
break;
AnimancerUtilities.InsertAt(ref _Aliases, i, alias);
return i;
}
/************************************************************************************************************************/
/// <summary>Removes an item from the <see cref="Aliases"/>.</summary>
public bool RemoveAlias(NamedIndex alias)
{
var index = Array.IndexOf(_Aliases, alias);
if (index < 0)
return false;
RemoveAlias(index);
return true;
}
/// <summary>Removes an item from the <see cref="Aliases"/>.</summary>
public void RemoveAlias(int index)
=> AnimancerUtilities.RemoveAt(ref _Aliases, index);
/************************************************************************************************************************/
/// <summary>Ensures that the <see cref="Aliases"/> are sorted.</summary>
/// <remarks>This method shouldn't need to be called manually since aliases are always added in order.</remarks>
public void SortAliases()
=> Array.Sort(_Aliases, static (a, b) => a.CompareTo(b));
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#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 TransitionLibraryDefinition);
/// <summary>Are all fields in this object equal to the equivalent fields in `other`?</summary>
public bool Equals(TransitionLibraryDefinition other)
=> other != null
&& AnimancerUtilities.ContentsAreEqual(_Transitions, other._Transitions)
&& AnimancerUtilities.ContentsAreEqual(_Modifiers, other._Modifiers)
&& AnimancerUtilities.ContentsAreEqual(_Aliases, other._Aliases)
&& _AliasAllTransitions == other._AliasAllTransitions;
/// <summary>Are all fields in `a` equal to the equivalent fields in `b`?</summary>
public static bool operator ==(TransitionLibraryDefinition a, TransitionLibraryDefinition 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 !=(TransitionLibraryDefinition a, TransitionLibraryDefinition 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,
_Transitions.SafeGetHashCode(),
_Modifiers.SafeGetHashCode(),
_Aliases.SafeGetHashCode(),
_AliasAllTransitions.SafeGetHashCode());
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Other
/************************************************************************************************************************/
/// <summary>Gathers all the animations in this definition.</summary>
public virtual void GatherAnimationClips(ICollection<AnimationClip> clips)
=> clips.GatherFromSource(_Transitions);
/************************************************************************************************************************/
/// <inheritdoc/>
public void CopyFrom(TransitionLibraryDefinition copyFrom, CloneContext context)
{
AnimancerUtilities.CopyExactArray(copyFrom._Transitions, ref _Transitions);
AnimancerUtilities.CopyExactArray(copyFrom._Modifiers, ref _Modifiers);
AnimancerUtilities.CopyExactArray(copyFrom._Aliases, ref _Aliases);
_AliasAllTransitions = copyFrom._AliasAllTransitions;
}
/************************************************************************************************************************/
/// <inheritdoc/>
public void AppendDescription(StringBuilder text, string separator = "\n")
{
text.Append(GetType().Name);
if (!separator.StartsWithNewLine())
separator = "\n" + separator;
var indentedSeparator = separator + Strings.Indent;
text.AppendField(separator, nameof(Transitions), Transitions.Length);
for (int i = 0; i < Transitions.Length; i++)
text.AppendField(indentedSeparator, i.ToString(), Transitions[i]);
text.AppendField(separator, nameof(Modifiers), Modifiers.Length);
for (int i = 0; i < Modifiers.Length; i++)
text.AppendField(indentedSeparator, i.ToString(), Modifiers[i]);
text.AppendField(separator, nameof(Aliases), Aliases.Length);
for (int i = 0; i < Aliases.Length; i++)
text.AppendField(indentedSeparator, i.ToString(), Aliases[i]);
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
}
}

View File

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

View File

@@ -0,0 +1,162 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
using System;
using UnityEngine;
namespace Animancer.TransitionLibraries
{
/// <summary>[<see cref="SerializableAttribute"/>]
/// Details about how to modify a transition when it comes from a specific source.
/// </summary>
/// <remarks>
/// Multiple of these can be used to build a <see cref="TransitionModifierGroup"/> at runtime.
/// <para></para>
/// <strong>Documentation:</strong>
/// <see href="https://kybernetik.com.au/animancer/docs/manual/transitions/libraries">
/// Transition Libraries</see>
/// </remarks>
/// https://kybernetik.com.au/animancer/api/Animancer.TransitionLibraries/TransitionModifierDefinition
[Serializable]
public struct TransitionModifierDefinition :
IEquatable<TransitionModifierDefinition>
{
/************************************************************************************************************************/
[SerializeField]
private int _From;
/// <summary>The index of the source transition in the <see cref="TransitionLibraryDefinition"/>.</summary>
public readonly int FromIndex
=> _From;
/************************************************************************************************************************/
[SerializeField]
private int _To;
/// <summary>The index of the destination transition in the <see cref="TransitionLibraryDefinition"/>.</summary>
public readonly int ToIndex
=> _To;
/************************************************************************************************************************/
[SerializeField]
private float _Fade;
/// <summary>The fade duration for this modifier to use instead of the transition's default value.</summary>
public readonly float FadeDuration
=> _Fade;
/************************************************************************************************************************/
[SerializeField]
private float _NormalizedStartTime;
/// <summary>The normalized start time for this modifier to use instead of the transition's default value.</summary>
public readonly float NormalizedStartTime
=> _NormalizedStartTime;
/************************************************************************************************************************/
/// <summary>Creates a new <see cref="TransitionModifierDefinition"/>.</summary>
public TransitionModifierDefinition(
int fromIndex,
int toIndex,
float fadeDuration,
float normalizedStartTime)
{
_From = fromIndex;
_To = toIndex;
_Fade = fadeDuration;
_NormalizedStartTime = normalizedStartTime;
}
/************************************************************************************************************************/
/// <summary>Does this modifier contain valid values?</summary>
public bool Validate()
{
var noFade = float.IsNaN(_Fade);
var noStart = float.IsNaN(_NormalizedStartTime);
if (noFade && noStart)
return false;
if (!noFade)
{
if (_Fade < 0)
_Fade = 0;
}
return true;
}
/************************************************************************************************************************/
/// <summary>Creates a copy of this modifier with the specified <see cref="FadeDuration"/>.</summary>
public readonly TransitionModifierDefinition WithFadeDuration(float fadeDuration)
=> new(_From, _To, fadeDuration, _NormalizedStartTime);
/// <summary>Creates a copy of this modifier with the specified <see cref="NormalizedStartTime"/>.</summary>
public readonly TransitionModifierDefinition WithNormalizedStartTime(float normalizedStartTime)
=> new(_From, _To, _Fade, normalizedStartTime);
/// <summary>Creates a copy of this modifier with the specified <see cref="FadeDuration"/> and <see cref="NormalizedStartTime"/>.</summary>
public readonly TransitionModifierDefinition WithDetails(float fadeDuration, float normalizedStartTime)
=> new(_From, _To, fadeDuration, normalizedStartTime);
/// <summary>Creates a copy of this modifier with the specified <see cref="FromIndex"/> and <see cref="ToIndex"/>.</summary>
public readonly TransitionModifierDefinition WithIndices(int fromIndex, int toIndex)
=> new(fromIndex, toIndex, _Fade, _NormalizedStartTime);
/************************************************************************************************************************/
/// <summary>Creates a new <see cref="TransitionDetails"/> from this modifier.</summary>
public readonly TransitionDetails ToTransitionDetails()
=> new(_Fade, _NormalizedStartTime);
/************************************************************************************************************************/
/// <summary>Creates a new string describing this modifier.</summary>
public override readonly string ToString()
=> $"{nameof(TransitionModifierDefinition)}({_From}->{_To}, F={_Fade}, S={_NormalizedStartTime})";
/************************************************************************************************************************/
#region Equality
/************************************************************************************************************************/
/// <summary>Are all fields in this object equal to the equivalent in `obj`?</summary>
public override readonly bool Equals(object obj)
=> obj is TransitionModifierDefinition value
&& Equals(value);
/// <summary>Are all fields in this object equal to the equivalent fields in `other`?</summary>
public readonly bool Equals(TransitionModifierDefinition other)
=> _From == other._From
&& _To == other._To
&& _Fade.IsEqualOrBothNaN(other._Fade)
&& _NormalizedStartTime.IsEqualOrBothNaN(other._NormalizedStartTime);
/// <summary>Are all fields in `a` equal to the equivalent fields in `b`?</summary>
public static bool operator ==(TransitionModifierDefinition a, TransitionModifierDefinition b)
=> a.Equals(b);
/// <summary>Are any fields in `a` not equal to the equivalent fields in `b`?</summary>
public static bool operator !=(TransitionModifierDefinition a, TransitionModifierDefinition b)
=> !(a == b);
/************************************************************************************************************************/
/// <summary>Returns a hash code based on the values of this object's fields.</summary>
public override readonly int GetHashCode()
=> AnimancerUtilities.Hash(-871379578,
_From.GetHashCode(),
_To.GetHashCode(),
_Fade.GetHashCode(),
_NormalizedStartTime.GetHashCode());
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
}
}

View File

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

View File

@@ -0,0 +1,181 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
using System.Collections.Generic;
namespace Animancer.TransitionLibraries
{
/// <summary>
/// An <see cref="ITransition"/> and a dictionary to modify it based on the previous state.
/// </summary>
/// <remarks>
/// <strong>Documentation:</strong>
/// <see href="https://kybernetik.com.au/animancer/docs/manual/transitions/libraries">
/// Transition Libraries</see>
/// </remarks>
/// https://kybernetik.com.au/animancer/api/Animancer.TransitionLibraries/TransitionModifierGroup
public class TransitionModifierGroup :
ICloneable<TransitionModifierGroup>,
ICopyable<TransitionModifierGroup>
{
/************************************************************************************************************************/
/// <summary>The index at which this group was added to its <see cref="TransitionLibrary"/>.</summary>
public readonly int Index;
/************************************************************************************************************************/
private ITransition _Transition;
/// <summary>The target transition of this group.</summary>
/// <remarks>Can't be <c>null</c>.</remarks>
public ITransition Transition
{
get => _Transition;
set
{
AnimancerUtilities.Assert(
value != null,
$"{nameof(TransitionModifierGroup)}.{nameof(Transition)} can't be null.");
_Transition = value;
}
}
/************************************************************************************************************************/
/// <summary>
/// Custom modifiers to use when playing a <see cref="Transition"/>
/// depending on the <see cref="IHasKey.Key"/> of the source state it is coming from.
/// </summary>
/// <remarks>This is <c>null</c> by default until <see cref="SetModifier"/> adds something.</remarks>
public Dictionary<object, TransitionDetails> FromKeyToModifier;
/************************************************************************************************************************/
/// <summary>Creates a new <see cref="TransitionModifierGroup"/>.</summary>
public TransitionModifierGroup(
int index,
ITransition transition)
{
Index = index;
Transition = transition;
}
/************************************************************************************************************************/
/// <summary>Sets the `modifier` to use when transitioning from `from` to the <see cref="Transition"/>.</summary>
public void SetModifier(object from, TransitionDetails modifier)
{
FromKeyToModifier ??= new();
FromKeyToModifier[from] = modifier;
}
/// <summary>Removes the fade duration modifier set for transitioning from `from` to the <see cref="Transition"/>.</summary>
public void ResetModifier(object from)
=> FromKeyToModifier?.Remove(from);
/************************************************************************************************************************/
/// <summary>
/// Sets the <see cref="TransitionDetails.FadeDuration"/>
/// to use when transitioning from `from` to the <see cref="Transition"/>.
/// </summary>
public void SetFadeDuration(object from, float fadeDuration)
{
FromKeyToModifier ??= new();
if (!FromKeyToModifier.TryGetValue(from, out var modifier))
modifier = TransitionDetails.NaN;
modifier.FadeDuration = fadeDuration;
FromKeyToModifier[from] = modifier;
}
/// <summary>
/// Sets the <see cref="TransitionDetails.NormalizedStartTime"/>
/// to use when transitioning from `from` to the <see cref="Transition"/>.
/// </summary>
public void SetNormalizedStartTime(object from, float normalizedStartTime)
{
FromKeyToModifier ??= new();
if (!FromKeyToModifier.TryGetValue(from, out var modifier))
modifier = TransitionDetails.NaN;
modifier.NormalizedStartTime = normalizedStartTime;
FromKeyToModifier[from] = modifier;
}
/************************************************************************************************************************/
/// <summary>Returns the fade duration to use when transitioning from `from` to the <see cref="Transition"/>.</summary>
public TransitionDetails GetDetails(object from)
{
if (FromKeyToModifier != null && from != null)
{
from = AnimancerUtilities.GetRootKey(from);
if (FromKeyToModifier.TryGetValue(from, out var details))
{
if (float.IsNaN(details.FadeDuration))
details.FadeDuration = Transition.FadeDuration;
if (float.IsNaN(details.NormalizedStartTime))
details.NormalizedStartTime = Transition.NormalizedStartTime;
return details;
}
}
return new(Transition);
}
/************************************************************************************************************************/
/// <summary>Returns the fade duration to use when transitioning from `from` to the <see cref="Transition"/>.</summary>
public float GetFadeDuration(object from)
=> FromKeyToModifier != null
&& FromKeyToModifier.TryGetValue(AnimancerUtilities.GetRootKey(from), out var modifier)
? modifier.FadeDuration
: Transition.FadeDuration;
/************************************************************************************************************************/
/// <inheritdoc/>
public TransitionModifierGroup Clone(CloneContext context)
{
var clone = new TransitionModifierGroup(Index, null);
clone.CopyFrom(this);
return clone;
}
/************************************************************************************************************************/
/// <inheritdoc/>
public void CopyFrom(TransitionModifierGroup copyFrom, CloneContext context)
{
Transition = copyFrom.Transition;
if (copyFrom.FromKeyToModifier == null)
{
FromKeyToModifier?.Clear();
}
else
{
FromKeyToModifier ??= new();
foreach (var item in copyFrom.FromKeyToModifier)
FromKeyToModifier[item.Key] = item.Value;
}
}
/************************************************************************************************************************/
/// <summary>Describes this object.</summary>
public override string ToString()
=> $"{nameof(TransitionModifierGroup)}([{Index}] {AnimancerUtilities.ToStringOrNull(Transition)})";
/************************************************************************************************************************/
}
}

View File

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

View File

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

View File

@@ -0,0 +1,204 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
using Animancer.Units;
using System;
using System.Collections.Generic;
using UnityEngine;
using Object = UnityEngine.Object;
namespace Animancer
{
/// <inheritdoc/>
/// https://kybernetik.com.au/animancer/api/Animancer/ClipTransition
[Serializable]
public class ClipTransition : Transition<ClipState>,
IMotion,
IAnimationClipCollection,
ICopyable<ClipTransition>
{
/************************************************************************************************************************/
/// <summary>The name of the serialized backing field of <see cref="Clip"/>.</summary>
public const string ClipFieldName = nameof(_Clip);
[SerializeField, Tooltip("The animation to play")]
private AnimationClip _Clip;
/// <summary>[<see cref="SerializeField"/>] The animation to play.</summary>
/// <remarks>
/// If you set this property and this transition has been played on multiple characters,
/// you will need to call <see cref="Transition{T}.ReconcileMainObject(AnimancerGraph)"/>
/// for each of them to create new states for the newly assigned object.
/// </remarks>
public AnimationClip Clip
{
get => _Clip;
set
{
Validate.AssertAnimationClip(value, false, $"set {nameof(ClipTransition)}.{nameof(Clip)}");
_Clip = value;
if (BaseState != null)
ReconcileMainObject(BaseState);
}
}
/// <inheritdoc/>
public override Object MainObject => _Clip;
/************************************************************************************************************************/
[SerializeField]
[Tooltip(Strings.Tooltips.NormalizedStartTime)]
[DefaultValue(float.NaN, 0f)]
[AnimationTime(
AnimationTimeAttribute.Units.Normalized,
DisabledText = Strings.Tooltips.StartTimeDisabled)]
private float _NormalizedStartTime = float.NaN;
/// <inheritdoc/>
public override float NormalizedStartTime
{
get => _NormalizedStartTime;
set => _NormalizedStartTime = value;
}
/// <summary>
/// If this transition will set the <see cref="AnimancerState.Time"/>,
/// then it needs to use <see cref="FadeMode.FromStart"/>.
/// </summary>
public override FadeMode FadeMode
=> float.IsNaN(_NormalizedStartTime)
? default
: FadeMode.FromStart;
/************************************************************************************************************************/
/// <summary>
/// The length of the <see cref="Clip"/> (in seconds),
/// accounting for the <see cref="NormalizedStartTime"/>
/// and <see cref="AnimancerEvent.Sequence.NormalizedEndTime"/>
/// (but not <see cref="Speed"/>).
/// </summary>
public virtual float Length
{
get
{
if (!IsValid)
return 0;
var normalizedStartTime = AnimancerEvent.Sequence.GetNormalizedStartTime(
_NormalizedStartTime,
Speed);
var normalizedEndTime = Events.GetRealNormalizedEndTime(Speed);
return _Clip.length * (normalizedEndTime - normalizedStartTime);
}
}
/************************************************************************************************************************/
/// <inheritdoc/>
public override bool IsValid => _Clip != null && !_Clip.legacy;
/// <summary>[<see cref="ITransition"/>] Is the <see cref="Clip"/> looping?</summary>
public override bool IsLooping => _Clip != null && _Clip.isLooping;
/// <inheritdoc/>
public override float MaximumLength => _Clip != null ? _Clip.length : 0;
/// <inheritdoc/>
public virtual float AverageAngularSpeed => _Clip != null ? _Clip.averageAngularSpeed : default;
/// <inheritdoc/>
public virtual Vector3 AverageVelocity => _Clip != null ? _Clip.averageSpeed : default;
/************************************************************************************************************************/
/// <inheritdoc/>
public override ClipState CreateState()
{
#if UNITY_ASSERTIONS
if (_Clip == null)
throw new ArgumentException(
$"Unable to create {nameof(ClipState)} because the" +
$" {nameof(ClipTransition)}.{nameof(Clip)} is null.");
#endif
return State = new(_Clip);
}
/************************************************************************************************************************/
/// <inheritdoc/>
public override void Apply(AnimancerState state)
{
base.Apply(state);
ApplyNormalizedStartTime(state, _NormalizedStartTime);
}
/************************************************************************************************************************/
/// <summary>[<see cref="IAnimationClipCollection"/>]
/// Adds the <see cref="Clip"/> to the collection.
/// </summary>
public virtual void GatherAnimationClips(ICollection<AnimationClip> clips)
=> clips.Gather(_Clip);
/************************************************************************************************************************/
/// <inheritdoc/>
public override Transition<ClipState> Clone(CloneContext context)
{
var clone = new ClipTransition();
clone.CopyFrom(this, context);
return clone;
}
/// <inheritdoc/>
public sealed override void CopyFrom(Transition<ClipState> copyFrom, CloneContext context)
=> this.CopyFromBase(copyFrom, context);
/// <inheritdoc/>
public virtual void CopyFrom(ClipTransition copyFrom, CloneContext context)
{
base.CopyFrom(copyFrom, context);
_Clip = context.GetCloneOrOriginal(copyFrom._Clip);
_NormalizedStartTime = copyFrom._NormalizedStartTime;
}
/************************************************************************************************************************/
/// <summary>
/// Returns a new <see cref="ClipTransition"/>
/// if the `target` is an <see cref="AnimationClip"/>.
/// </summary>
[TryCreateTransition(typeof(AnimationClip))]
public static ITransition TryCreateTransition(Object target)
=> target is not AnimationClip clip
? null
: new ClipTransition()
{
Clip = clip,
};
/************************************************************************************************************************/
#if UNITY_EDITOR
/// <summary>[Editor-Only] Validates that the `command` is targeting an asset.</summary>
[UnityEditor.MenuItem("CONTEXT/" + nameof(AnimationClip) + "/Create Transition Asset", validate = true)]
private static bool ValidateCreateTransitionAsset(UnityEditor.MenuCommand command)
=> TryCreateTransitionAttribute.CanCreateAndSave(command.context);
/// <summary>[Editor-Only] Tries to create an asset containing an appropriate transition for the `command`.</summary>
[UnityEditor.MenuItem("CONTEXT/" + nameof(AnimationClip) + "/Create Transition Asset")]
private static void CreateTransitionAsset(UnityEditor.MenuCommand command)
=> TryCreateTransitionAttribute.TryCreateTransitionAsset(command.context, true);
#endif
/************************************************************************************************************************/
}
}

View File

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

View File

@@ -0,0 +1,355 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
using System;
using System.Collections.Generic;
using UnityEngine;
namespace Animancer
{
/// <summary>A group of <see cref="ClipTransition"/>s which play one after the other.</summary>
/// https://kybernetik.com.au/animancer/api/Animancer/ClipTransitionSequence
///
[Serializable]
[Obsolete("ClipTransitionSequence has been replaced by TransitionSequence" +
" which is much more powerful and works properly with Animancer Events." +
" This script still works the same as it always has so if you want to" +
" keep using it you can simply remove this [Obsolete] attribute." +
" This script will be removed in a future version of Animancer.")]
public class ClipTransitionSequence : ClipTransition,
ICopyable<ClipTransitionSequence>
{
/************************************************************************************************************************/
[DrawAfterEvents]
[SerializeField]
[Tooltip("The other transitions to play in order after the first one.")]
private ClipTransition[] _Others = Array.Empty<ClipTransition>();
/// <summary>[<see cref="SerializeField"/>] The transitions to play in order after the first one.</summary>
/// <remarks>
/// If you modify any individual items of this array at runtime without actually setting this property, you
/// must call <see cref="InitializeEndEventChain"/> afterwards.
/// </remarks>
public ClipTransition[] Others
{
get => _Others;
set
{
_Others = value;
InitializeEndEventChain();
}
}
/// <summary>The last of the <see cref="Others"/> (or <c>this</c> if there are none).</summary>
public ClipTransition LastTransition
=> _Others.Length > 0
? _Others[^1]
: this;
/************************************************************************************************************************/
private Action _OnEnd;
/// <summary>Initializes the End Events of each of the <see cref="Others"/> to play the next one.</summary>
public void InitializeEndEventChain()
{
if (_Others == null ||
_Others.Length <= 0)
return;
_OnEnd ??= () => AnimancerEvent.Current.State.Layer.Play(_Others[0]);
InitializeFirstEndEvent();
// Assign each of the other end events, but this first one will be set by Apply.
var previous = _Others[0];
for (int i = 1; i < _Others.Length; i++)
{
var next = _Others[i];
previous.Events.OnEnd += () => AnimancerEvent.Current.State.Layer.Play(next);
previous = next;
}
}
/************************************************************************************************************************/
/// <summary>
/// If an end event is assigned other than the one to play the next transition,
/// this method replaces it and move it to be the end event of the last transition instead.
/// </summary>
private void InitializeFirstEndEvent()
{
var onEnd = Events.OnEnd;
if (onEnd == _OnEnd ||
_Others == null ||
_Others.Length <= 0)
return;
Events.OnEnd = _OnEnd;
onEnd -= _OnEnd;
_Others[^1].Events.OnEnd += onEnd;
}
/************************************************************************************************************************/
/// <inheritdoc/>
public override ClipState CreateState()
{
if (_OnEnd == null)
InitializeEndEventChain();
else if (_Others.Length > 0)
InitializeFirstEndEvent();
return base.CreateState();
}
/************************************************************************************************************************/
/// <summary>Is everything in this sequence valid?</summary>
public override bool IsValid
{
get
{
if (!base.IsValid)
return false;
for (int i = 0; i < _Others.Length; i++)
if (!_Others[i].IsValid)
return false;
return true;
}
}
/************************************************************************************************************************/
/// <summary>Is the last animation in this sequence looping?</summary>
public override bool IsLooping
=> _Others.Length > 0
? LastTransition.IsLooping
: base.IsLooping;
/************************************************************************************************************************/
/// <inheritdoc/>
public override float Length
{
get
{
var length = base.Length;
for (int i = 0; i < _Others.Length; i++)
length += _Others[i].Length;
return length;
}
}
/************************************************************************************************************************/
/// <inheritdoc/>
public override float MaximumLength
{
get
{
var value = base.MaximumLength;
for (int i = 0; i < _Others.Length; i++)
value += _Others[i].MaximumLength;
return value;
}
}
/************************************************************************************************************************/
/// <inheritdoc/>
public override float AverageAngularSpeed
{
get
{
var speed = base.AverageAngularSpeed;
if (_Others.Length == 0)
return speed;
var duration = base.MaximumLength;
speed *= duration;
for (int i = 0; i < _Others.Length; i++)
{
var other = _Others[i];
var otherSpeed = other.AverageAngularSpeed;
var otherDuration = other.MaximumLength;
speed += otherSpeed * otherDuration;
duration += otherDuration;
}
return speed / duration;
}
}
/************************************************************************************************************************/
/// <inheritdoc/>
public override Vector3 AverageVelocity
{
get
{
var velocity = base.AverageVelocity;
if (_Others.Length == 0)
return velocity;
var duration = base.MaximumLength;
velocity *= duration;
for (int i = 0; i < _Others.Length; i++)
{
var other = _Others[i];
var otherVelocity = other.AverageVelocity;
var otherDuration = other.MaximumLength;
velocity += otherVelocity * otherDuration;
duration += otherDuration;
}
return velocity / duration;
}
}
/************************************************************************************************************************/
/// <summary>Adds the <see cref="ClipTransition.Clip"/> of everything in this sequence to the collection.</summary>
public override void GatherAnimationClips(ICollection<AnimationClip> clips)
{
base.GatherAnimationClips(clips);
for (int i = 0; i < _Others.Length; i++)
_Others[i].GatherAnimationClips(clips);
}
/************************************************************************************************************************/
/// <inheritdoc/>
public override Transition<ClipState> Clone(CloneContext context)
{
var clone = new ClipTransitionSequence();
clone.CopyFrom(this, context);
return clone;
}
/// <inheritdoc/>
public sealed override void CopyFrom(ClipTransition copyFrom, CloneContext context)
=> this.CopyFromBase(copyFrom, context);
/// <inheritdoc/>
public virtual void CopyFrom(ClipTransitionSequence copyFrom, CloneContext context)
{
base.CopyFrom(copyFrom, context);
context.CloneArray(copyFrom._Others, ref _Others);
}
/************************************************************************************************************************/
/// <summary>Tries to calculate the <see cref="AnimancerState.Time"/> relative to the start of this sequence.</summary>
/// <returns>
/// True if this sequence contains a transition matching the `state`.
/// Otherwise false, indicating that it isn't associated with this sequence.
/// </returns>
public bool TryGetCumulativeTime(AnimancerState state, out float time)
{
time = state.Time;
var clip = state.Clip;
var onEnd = state.SharedEvents?.OnEnd;
if (Clip == clip && _OnEnd == onEnd)
return true;
time += Clip.length;
for (int i = 0; i < _Others.Length; i++)
{
var other = _Others[i];
if (other.Clip == clip && other.Events.OnEnd == onEnd)
return true;
time += other.Length;
}
return false;
}
/************************************************************************************************************************/
#region Events
/************************************************************************************************************************/
/// <summary>The <see cref="AnimancerEvent.Sequence.EndEvent"/> of the last transition in this sequence.</summary>
public AnimancerEvent EndEvent
{
get => LastTransition.Events.EndEvent;
set => LastTransition.Events.EndEvent = value;
}
/// <summary>The <see cref="AnimancerEvent.Sequence.OnEnd"/> of the last transition in this sequence.</summary>
public Action OnEnd
{
get => LastTransition.Events.OnEnd;
set => LastTransition.Events.OnEnd = value;
}
/************************************************************************************************************************/
/// <summary>Adds an event at the specified time relative to the entire sequence.</summary>
public void AddEvent(float time, bool normalized, Action callback)
{
// Convert time to Seconds.
if (normalized)
time *= Length;
if (TryAddEvent(this, base.Length, ref time, callback))
return;
for (int i = 0; i < _Others.Length - 1; i++)
{
var other = _Others[i];
if (TryAddEvent(other, other.Length, ref time, callback))
return;
}
AddEvent(LastTransition, time, callback);
}
/// <summary>
/// Tries to add the `callback` as an event to the `transition` if the `time` is within the `length` and
/// returns true if successful. Otherwise subtracts the `length` from the `time` and returns false so it can be
/// tried in the next transition in the sequence.
/// </summary>
private static bool TryAddEvent(ClipTransition transition, float length, ref float time, Action callback)
{
if (time > length)
{
time -= length;
return false;
}
AddEvent(transition, time, callback);
return true;
}
/// <summary>
/// Adds the `callback` as an event to the `transition` at the specified `time` (in seconds, starting from the
/// <see cref="ClipTransition.NormalizedStartTime"/>).
/// </summary>
private static void AddEvent(ClipTransition transition, float time, Action callback)
{
var start = transition.NormalizedStartTime;
if (float.IsNaN(start))
start = AnimancerEvent.Sequence.GetDefaultNormalizedStartTime(start);
time /= transition.Clip.length * (1 - start);
time += start;
transition.Events.Add(time, callback);
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
}
}

View File

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

View File

@@ -0,0 +1,253 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
using System;
using System.Collections.Generic;
using UnityEngine;
using Object = UnityEngine.Object;
namespace Animancer
{
/// <inheritdoc/>
/// https://kybernetik.com.au/animancer/api/Animancer/ControllerTransition_1
[Serializable]
public abstract class ControllerTransition<TState> : Transition<TState>,
IAnimationClipCollection,
ICopyable<ControllerTransition<TState>>
where TState : ControllerState
{
/************************************************************************************************************************/
[SerializeField]
private RuntimeAnimatorController _Controller;
/// <summary>[<see cref="SerializeField"/>]
/// The <see cref="ControllerState.Controller"/> that will be used for the created state.
/// </summary>
/// <remarks>
/// If you set this property and this transition has been played on multiple characters,
/// you will need to call <see cref="Transition{T}.ReconcileMainObject(AnimancerGraph)"/>
/// for each of them to create new states for the newly assigned object.
/// </remarks>
public RuntimeAnimatorController Controller
{
get => _Controller;
set
{
_Controller = value;
if (BaseState != null)
ReconcileMainObject(BaseState);
}
}
/// <inheritdoc/>
public override Object MainObject => _Controller;
#if UNITY_EDITOR
/// <summary>[Editor-Only] The name of the serialized backing field of <see cref="Controller"/>.</summary>
public const string ControllerFieldName = nameof(_Controller);
#endif
/************************************************************************************************************************/
[SerializeField]
[Tooltip("Configure parameters in the Animator Controller to follow the values of Animancer parameters")]
private ControllerState.SerializableParameterBindings _ParameterBindings;
/// <summary>[<see cref="SerializeField"/>]
/// Parameter names which make parameters in the <see cref="RuntimeAnimatorController"/>
/// follow the values of parameters in the <see cref="AnimancerGraph.Parameters"/>.
/// </summary>
public ref ControllerState.SerializableParameterBindings ParameterBindings
=> ref _ParameterBindings;
/************************************************************************************************************************/
[SerializeField]
[Tooltip("Determines what each layer does when " +
nameof(ControllerState) + "." + nameof(ControllerState.Stop) + " is called." +
"\n• If empty, all layers will reset to their default state." +
"\n• If this array is smaller than the layer count," +
" any additional layers will use the last value in this array.")]
private ControllerState.ActionOnStop[] _ActionsOnStop;
/// <summary>[<see cref="SerializeField"/>]
/// Determines what each layer does when <see cref="ControllerState.Stop"/> is called.
/// </summary>
/// <remarks>
/// If empty, all layers will reset to their <see cref="ControllerState.ActionOnStop.DefaultState"/>.
/// <para></para>
/// If this array is smaller than the
/// <see cref="UnityEngine.Animations.AnimatorControllerPlayable.GetLayerCount"/>,
/// any additional layers will use the last value in this array.
/// </remarks>
public ref ControllerState.ActionOnStop[] ActionsOnStop
=> ref _ActionsOnStop;
/************************************************************************************************************************/
/// <inheritdoc/>
public override float MaximumLength
{
get
{
if (_Controller == null)
return 0;
var duration = 0f;
var clips = _Controller.animationClips;
for (int i = 0; i < clips.Length; i++)
{
var length = clips[i].length;
if (duration < length)
duration = length;
}
return duration;
}
}
/************************************************************************************************************************/
/// <inheritdoc/>
public override bool IsValid
=> _Controller != null;
/************************************************************************************************************************/
/// <summary>Returns the <see cref="Controller"/>.</summary>
public static implicit operator RuntimeAnimatorController(ControllerTransition<TState> transition)
=> transition?._Controller;
/************************************************************************************************************************/
/// <inheritdoc/>
public override void Apply(AnimancerState state)
{
if (state is ControllerState controllerState)
controllerState.ActionsOnStop = _ActionsOnStop;
base.Apply(state);
}
/************************************************************************************************************************/
/// <summary>Adds all clips in the <see cref="Controller"/> to the collection.</summary>
void IAnimationClipCollection.GatherAnimationClips(ICollection<AnimationClip> clips)
{
if (_Controller != null)
clips.Gather(_Controller.animationClips);
}
/************************************************************************************************************************/
/// <inheritdoc/>
public sealed override void CopyFrom(Transition<TState> copyFrom, CloneContext context)
=> this.CopyFromBase(copyFrom, context);
/// <inheritdoc/>
public virtual void CopyFrom(ControllerTransition<TState> copyFrom, CloneContext context)
{
base.CopyFrom(copyFrom, context);
_Controller = context.GetCloneOrOriginal(copyFrom._Controller);
_ActionsOnStop = copyFrom._ActionsOnStop;
_ParameterBindings = context.GetOrCreateClone(copyFrom._ParameterBindings);
}
/************************************************************************************************************************/
}
/************************************************************************************************************************/
/// <inheritdoc/>
/// https://kybernetik.com.au/animancer/api/Animancer/ControllerTransition
[Serializable]
public class ControllerTransition : ControllerTransition<ControllerState>,
ICopyable<ControllerTransition>
{
/************************************************************************************************************************/
/// <inheritdoc/>
public override ControllerState CreateState()
{
#if UNITY_ASSERTIONS
if (Controller == null)
throw new ArgumentException(
$"Unable to create {nameof(ControllerState)} because the" +
$" {nameof(ControllerTransition)}.{nameof(Controller)} is null.");
#endif
return State = new(Controller, ActionsOnStop)
{
SerializedParameterBindings = ParameterBindings
};
}
/************************************************************************************************************************/
/// <summary>Creates a new <see cref="ControllerTransition"/>.</summary>
public ControllerTransition() { }
/// <summary>Creates a new <see cref="ControllerTransition"/> with the specified Animator Controller.</summary>
public ControllerTransition(RuntimeAnimatorController controller)
=> Controller = controller;
/************************************************************************************************************************/
/// <summary>Creates a new <see cref="ControllerTransition"/> with the specified Animator Controller.</summary>
public static implicit operator ControllerTransition(RuntimeAnimatorController controller)
=> new(controller);
/************************************************************************************************************************/
/// <inheritdoc/>
public override Transition<ControllerState> Clone(CloneContext context)
{
var clone = new ControllerTransition();
clone.CopyFrom(this, context);
return clone;
}
/// <inheritdoc/>
public sealed override void CopyFrom(ControllerTransition<ControllerState> copyFrom, CloneContext context)
=> this.CopyFromBase(copyFrom, context);
/// <inheritdoc/>
public virtual void CopyFrom(ControllerTransition copyFrom, CloneContext context)
=> base.CopyFrom(copyFrom, context);
/************************************************************************************************************************/
/// <summary>
/// Returns a new <see cref="ControllerTransition"/>
/// if the `target` is an <see cref="RuntimeAnimatorController"/>.
/// </summary>
[TryCreateTransition(typeof(RuntimeAnimatorController))]
public static ITransition TryCreateTransition(Object target)
=> target is not RuntimeAnimatorController controller
? null
: new ControllerTransition()
{
Controller = controller,
};
/************************************************************************************************************************/
#if UNITY_EDITOR
/// <summary>[Editor-Only] Validates that the `command` is targeting an asset.</summary>
[UnityEditor.MenuItem("CONTEXT/" + nameof(RuntimeAnimatorController) + "/Create Transition Asset", validate = true)]
private static bool ValidateCreateTransitionAsset(UnityEditor.MenuCommand command)
=> TryCreateTransitionAttribute.CanCreateAndSave(command.context);
/// <summary>[Editor-Only] Tries to create an asset containing an appropriate transition for the `command`.</summary>
[UnityEditor.MenuItem("CONTEXT/" + nameof(RuntimeAnimatorController) + "/Create Transition Asset")]
private static void CreateTransitionAsset(UnityEditor.MenuCommand command)
=> TryCreateTransitionAttribute.TryCreateTransitionAsset(command.context, true);
#endif
/************************************************************************************************************************/
}
}

View File

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

View File

@@ -0,0 +1,169 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
using System;
using UnityEngine;
namespace Animancer
{
/// <inheritdoc/>
/// https://kybernetik.com.au/animancer/api/Animancer/LinearMixerTransition
[Serializable]
public class LinearMixerTransition : MixerTransition<LinearMixerState, float>,
ICopyable<LinearMixerTransition>
{
/************************************************************************************************************************/
[SerializeField]
[Tooltip("Should setting the Parameter above the highest threshold" +
" increase the Speed of the mixer proportionally?")]
private bool _ExtrapolateSpeed = true;
/// <summary>[<see cref="SerializeField"/>]
/// Should setting the <see cref="MixerState{TParameter}.Parameter"/> above the highest threshold
/// increase the <see cref="AnimancerNode.Speed"/> of the mixer proportionally?
/// </summary>
public ref bool ExtrapolateSpeed => ref _ExtrapolateSpeed;
/************************************************************************************************************************/
[SerializeField]
[Tooltip(Strings.Tooltips.MixerParameterBinding)]
private StringAsset _ParameterName;
/// <summary>[<see cref="SerializeField"/>] The <see cref="LinearMixerState.ParameterName"/>.</summary>
public ref StringAsset ParameterName => ref _ParameterName;
/************************************************************************************************************************/
/// <summary>
/// Are all <see cref="ManualMixerTransition{TMixer}.Animations"/> assigned and
/// <see cref="MixerTransition{TMixer, TParameter}.Thresholds"/> unique and sorted in ascending order?
/// </summary>
public override bool IsValid
{
get
{
if (!base.IsValid)
return false;
var previous = float.NegativeInfinity;
var thresholds = Thresholds;
for (int i = 0; i < thresholds.Length; i++)
{
var threshold = thresholds[i];
if (threshold < previous)
return false;
else
previous = threshold;
}
return true;
}
}
/************************************************************************************************************************/
/// <inheritdoc/>
public override LinearMixerState CreateState()
{
State = new()
{
ParameterName = ParameterName,
};
InitializeState();
return State;
}
/************************************************************************************************************************/
/// <inheritdoc/>
public override void Apply(AnimancerState state)
{
base.Apply(state);
State.ExtrapolateSpeed = _ExtrapolateSpeed;
}
/************************************************************************************************************************/
/// <summary>Sorts all states so that their thresholds go from lowest to highest.</summary>
/// <remarks>This method uses Bubble Sort which is inefficient for large numbers of states.</remarks>
public void SortByThresholds()
{
var thresholdCount = Thresholds.Length;
if (thresholdCount <= 1)
return;
var speedCount = Speeds.Length;
var syncCount = SynchronizeChildren.Length;
var previousThreshold = Thresholds[0];
for (int i = 1; i < thresholdCount; i++)
{
var threshold = Thresholds[i];
if (threshold >= previousThreshold)
{
previousThreshold = threshold;
continue;
}
Thresholds.Swap(i, i - 1);
Animations.Swap(i, i - 1);
if (i < speedCount)
Speeds.Swap(i, i - 1);
if (i == syncCount && !SynchronizeChildren[i - 1])
{
var sync = SynchronizeChildren;
Array.Resize(ref sync, ++syncCount);
sync[i - 1] = true;
sync[i] = false;
SynchronizeChildren = sync;
}
else if (i < syncCount)
{
SynchronizeChildren.Swap(i, i - 1);
}
if (i == 1)
{
i = 0;
previousThreshold = float.NegativeInfinity;
}
else
{
i -= 2;
previousThreshold = Thresholds[i];
}
}
}
/************************************************************************************************************************/
/// <inheritdoc/>
public override Transition<LinearMixerState> Clone(CloneContext context)
{
var clone = new LinearMixerTransition();
clone.CopyFrom(this, context);
return clone;
}
/// <inheritdoc/>
public sealed override void CopyFrom(MixerTransition<LinearMixerState, float> copyFrom, CloneContext context)
=> this.CopyFromBase(copyFrom, context);
/// <inheritdoc/>
public virtual void CopyFrom(LinearMixerTransition copyFrom, CloneContext context)
{
base.CopyFrom(copyFrom, context);
_ExtrapolateSpeed = copyFrom._ExtrapolateSpeed;
_ParameterName = copyFrom._ParameterName;
}
/************************************************************************************************************************/
}
}

View File

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

View File

@@ -0,0 +1,44 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
using System;
namespace Animancer
{
/// <inheritdoc/>
/// https://kybernetik.com.au/animancer/api/Animancer/ManualMixerTransition
[Serializable]
public class ManualMixerTransition : ManualMixerTransition<ManualMixerState>,
ICopyable<ManualMixerTransition>
{
/************************************************************************************************************************/
/// <inheritdoc/>
public override ManualMixerState CreateState()
{
State = new();
InitializeState();
return State;
}
/************************************************************************************************************************/
/// <inheritdoc/>
public override Transition<ManualMixerState> Clone(CloneContext context)
{
var clone = new ManualMixerTransition();
clone.CopyFrom(this, context);
return clone;
}
/// <inheritdoc/>
public sealed override void CopyFrom(ManualMixerTransition<ManualMixerState> copyFrom, CloneContext context)
=> this.CopyFromBase(copyFrom, context);
/// <inheritdoc/>
public virtual void CopyFrom(ManualMixerTransition copyFrom, CloneContext context)
=> base.CopyFrom(copyFrom, context);
/************************************************************************************************************************/
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b09fd97d6dbbd3540ab614b18c96bd32
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 Animancer.Units;
using System;
using System.Collections.Generic;
using UnityEngine;
using Object = UnityEngine.Object;
namespace Animancer
{
/// <inheritdoc/>
/// https://kybernetik.com.au/animancer/api/Animancer/ManualMixerTransition_1
[Serializable]
public abstract class ManualMixerTransition<TMixer> : Transition<TMixer>,
IMotion,
IAnimationClipCollection,
ICopyable<ManualMixerTransition<TMixer>>
where TMixer : ManualMixerState
{
/************************************************************************************************************************/
[SerializeField]
[UnityEngine.Serialization.FormerlySerializedAs("_Clips")]
[UnityEngine.Serialization.FormerlySerializedAs("_States")]
private Object[] _Animations;
/// <summary>[<see cref="SerializeField"/>] Objects that define how to create each state in the mixer.</summary>
/// <remarks>See <see cref="ManualMixerState.Initialize(Object[])"/> for more information.</remarks>
public ref Object[] Animations => ref _Animations;
/// <summary>The name of the serialized backing field of <see cref="Animations"/>.</summary>
public const string AnimationsField = nameof(_Animations);
/************************************************************************************************************************/
[SerializeField]
[AnimationSpeed(IsOptional = false)]
[DefaultValue(1f, -1f)]
private float[] _Speeds;
/// <summary>[<see cref="SerializeField"/>]
/// The <see cref="AnimancerNode.Speed"/> to use for each state in the mixer.
/// </summary>
/// <remarks>If the size of this array doesn't match the <see cref="Animations"/>, it will be ignored.</remarks>
public ref float[] Speeds => ref _Speeds;
/// <summary>The name of the serialized backing field of <see cref="Speeds"/>.</summary>
public const string SpeedsField = nameof(_Speeds);
/// <summary>Are there at least enough <see cref="Speeds"/> for each of the<see cref="Animations"/>?</summary>
public bool HasSpeeds => _Speeds != null && _Speeds.Length >= _Animations.Length;
/************************************************************************************************************************/
[SerializeField]
private bool[] _SynchronizeChildren;
/// <summary>[<see cref="SerializeField"/>]
/// The flags to be used in <see cref="ManualMixerState.InitializeSynchronizedChildren"/>.
/// </summary>
/// <remarks>The array can be null or empty. Any elements not in the array will be treated as true.</remarks>
public ref bool[] SynchronizeChildren => ref _SynchronizeChildren;
/// <summary>The name of the serialized backing field of <see cref="SynchronizeChildren"/>.</summary>
public const string SynchronizeChildrenField = nameof(_SynchronizeChildren);
/************************************************************************************************************************/
/// <summary>[<see cref="ITransition"/>] Are any of the <see cref="Animations"/> looping?</summary>
public override bool IsLooping
{
get
{
if (_Animations == null)
return false;
for (int i = _Animations.Length - 1; i >= 0; i--)
{
if (AnimancerUtilities.TryGetIsLooping(_Animations[i], out var isLooping) &&
isLooping)
return true;
}
return false;
}
}
/// <inheritdoc/>
public override float MaximumLength
{
get
{
if (_Animations == null)
return 0;
var duration = 0f;
var hasSpeeds = HasSpeeds;
for (int i = _Animations.Length - 1; i >= 0; i--)
{
if (!AnimancerUtilities.TryGetLength(_Animations[i], out var length))
continue;
if (hasSpeeds)
length *= _Speeds[i];
if (duration < length)
duration = length;
}
return duration;
}
}
/// <inheritdoc/>
public virtual float AverageAngularSpeed
{
get
{
if (_Animations == null)
return default;
var average = 0f;
var hasSpeeds = HasSpeeds;
var count = 0;
for (int i = _Animations.Length - 1; i >= 0; i--)
{
if (AnimancerUtilities.TryGetAverageAngularSpeed(_Animations[i], out var speed))
{
if (hasSpeeds)
speed *= _Speeds[i];
average += speed;
count++;
}
}
return average / count;
}
}
/// <inheritdoc/>
public virtual Vector3 AverageVelocity
{
get
{
if (_Animations == null)
return default;
var average = new Vector3();
var hasSpeeds = HasSpeeds;
var count = 0;
for (int i = _Animations.Length - 1; i >= 0; i--)
{
if (AnimancerUtilities.TryGetAverageVelocity(_Animations[i], out var velocity))
{
if (hasSpeeds)
velocity *= _Speeds[i];
average += velocity;
count++;
}
}
return average / count;
}
}
/************************************************************************************************************************/
/// <summary>Are all <see cref="Animations"/> assigned?</summary>
public override bool IsValid
{
get
{
if (_Animations == null ||
_Animations.Length == 0)
return false;
for (int i = _Animations.Length - 1; i >= 0; i--)
if (_Animations[i] == null)
return false;
return true;
}
}
/************************************************************************************************************************/
/// <summary>Initializes the <see cref="Transition{TState}.State"/> immediately after it is created.</summary>
public virtual void InitializeState()
{
var mixer = State;
var childCount = mixer.ChildCount;
var auto = ManualMixerState.SynchronizeNewChildren;
try
{
ManualMixerState.SynchronizeNewChildren = false;
mixer.AddRange(_Animations);
}
finally
{
ManualMixerState.SynchronizeNewChildren = auto;
}
mixer.InitializeSynchronizedChildren(_SynchronizeChildren);
if (_Speeds != null)
{
#if UNITY_ASSERTIONS
if (_Speeds.Length != 0 && _Speeds.Length != _Animations.Length)
Debug.LogError(
$"The number of serialized {nameof(Speeds)} ({_Speeds.Length})" +
$" does not match the number of {nameof(Animations)} ({_Animations.Length}).",
mixer.Graph?.Component as Object);
#endif
var count = Math.Min(_Animations.Length, _Speeds.Length);
for (int i = count - 1; i >= 0; i--)
mixer.GetChild(childCount + i).Speed = _Speeds[i];
}
}
/************************************************************************************************************************/
/// <inheritdoc/>
public override void Apply(AnimancerState state)
{
base.Apply(state);
for (int i = 0; i < _Animations.Length; i++)
if (_Animations[i] is ITransition transition)
transition.Apply(state.GetChild(i));
}
/************************************************************************************************************************/
/// <summary>Adds the <see cref="Animations"/> to the collection.</summary>
void IAnimationClipCollection.GatherAnimationClips(ICollection<AnimationClip> clips)
=> clips.GatherFromSource(_Animations);
/************************************************************************************************************************/
/// <inheritdoc/>
public sealed override void CopyFrom(Transition<TMixer> copyFrom, CloneContext context)
=> this.CopyFromBase(copyFrom, context);
/// <inheritdoc/>
public virtual void CopyFrom(ManualMixerTransition<TMixer> copyFrom, CloneContext context)
{
base.CopyFrom(copyFrom, context);
AnimancerUtilities.CopyExactArray(copyFrom._Animations, ref _Animations);
AnimancerUtilities.CopyExactArray(copyFrom._Speeds, ref _Speeds);
AnimancerUtilities.CopyExactArray(copyFrom._SynchronizeChildren, ref _SynchronizeChildren);
}
/************************************************************************************************************************/
}
}

View File

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

View File

@@ -0,0 +1,74 @@
// 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
{
/// <inheritdoc/>
/// https://kybernetik.com.au/animancer/api/Animancer/MixerTransition_2
[Serializable]
public abstract class MixerTransition<TMixer, TParameter> : ManualMixerTransition<TMixer>,
ICopyable<MixerTransition<TMixer, TParameter>>
where TMixer : MixerState<TParameter>
{
/************************************************************************************************************************/
[SerializeField]
[Tooltip("The parameter values at which each of the states are used and blended")]
private TParameter[] _Thresholds;
/// <summary>[<see cref="SerializeField"/>]
/// The parameter values at which each of the states are used and blended.
/// </summary>
public ref TParameter[] Thresholds => ref _Thresholds;
/// <summary>The name of the serialized backing field of <see cref="Thresholds"/>.</summary>
public const string ThresholdsField = nameof(_Thresholds);
/************************************************************************************************************************/
[SerializeField]
[Tooltip("The initial parameter value to give the mixer when first created")]
private TParameter _DefaultParameter;
/// <summary>[<see cref="SerializeField"/>]
/// The initial parameter value to give the mixer when first created.
/// </summary>
public ref TParameter DefaultParameter => ref _DefaultParameter;
/// <summary>The name of the serialized backing field of <see cref="DefaultParameter"/>.</summary>
public const string DefaultParameterField = nameof(_DefaultParameter);
/************************************************************************************************************************/
/// <inheritdoc/>
public override void InitializeState()
{
base.InitializeState();
State.SetThresholds(_Thresholds);
State.Parameter = _DefaultParameter;
}
/************************************************************************************************************************/
/// <inheritdoc/>
public sealed override void CopyFrom(ManualMixerTransition<TMixer> copyFrom, CloneContext context)
=> this.CopyFromBase(copyFrom, context);
/// <inheritdoc/>
public virtual void CopyFrom(MixerTransition<TMixer, TParameter> copyFrom, CloneContext context)
{
base.CopyFrom(copyFrom, context);
_DefaultParameter = copyFrom._DefaultParameter;
AnimancerUtilities.CopyExactArray(copyFrom._Thresholds, ref _Thresholds);
}
/************************************************************************************************************************/
}
}

View File

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

View File

@@ -0,0 +1,111 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
using System;
using UnityEngine;
namespace Animancer
{
/// <inheritdoc/>
/// https://kybernetik.com.au/animancer/api/Animancer/MixerTransition2D
[Serializable]
public class MixerTransition2D : MixerTransition<Vector2MixerState, Vector2>,
ICopyable<MixerTransition2D>
{
/************************************************************************************************************************/
/// <summary>
/// A type of <see cref="ManualMixerState"/> which can be
/// created by a <see cref="MixerTransition2D"/>.
/// </summary>
public enum MixerType
{
/// <summary><see cref="CartesianMixerState"/></summary>
Cartesian,
/// <summary><see cref="DirectionalMixerState"/></summary>
Directional,
}
[SerializeField]
private MixerType _Type;
/// <summary>[<see cref="SerializeField"/>]
/// The type of <see cref="ManualMixerState"/> that this transition will create.
/// </summary>
public ref MixerType Type => ref _Type;
/// <summary>The name of the serialized backing field of <see cref="Type"/>.</summary>
public const string TypeField = nameof(_Type);
/************************************************************************************************************************/
[SerializeField]
[Tooltip(Strings.Tooltips.MixerParameterBinding)]
private StringAsset _ParameterNameX;
/// <summary>[<see cref="SerializeField"/>] The <see cref="Vector2MixerState.ParameterNameX"/>.</summary>
public ref StringAsset ParameterNameX => ref _ParameterNameX;
/************************************************************************************************************************/
[SerializeField]
[Tooltip(Strings.Tooltips.MixerParameterBinding)]
private StringAsset _ParameterNameY;
/// <summary>[<see cref="SerializeField"/>] The <see cref="Vector2MixerState.ParameterNameY"/>.</summary>
public ref StringAsset ParameterNameY => ref _ParameterNameY;
/************************************************************************************************************************/
/// <summary>Creates and returns a new state depending on the <see cref="Type"/>.</summary>
/// <remarks>
/// Note that using methods like <see cref="AnimancerGraph.Play(ITransition)"/> will also call
/// <see cref="ITransition.Apply"/>, so if you call this method manually you may want to call that method
/// as well. Or you can just use <see cref="AnimancerUtilities.CreateStateAndApply"/>.
/// <para></para>
/// This method also assigns it as the <see cref="Transition{TState}.State"/>.
/// </remarks>
public override Vector2MixerState CreateState()
{
State = _Type switch
{
MixerType.Cartesian => new CartesianMixerState(),
MixerType.Directional => new DirectionalMixerState(),
_ => throw new ArgumentOutOfRangeException(nameof(_Type)),
};
State.ParameterNameX = ParameterNameX;
State.ParameterNameY = ParameterNameY;
InitializeState();
return State;
}
/************************************************************************************************************************/
/// <inheritdoc/>
public override Transition<Vector2MixerState> Clone(CloneContext context)
{
var clone = new MixerTransition2D();
clone.CopyFrom(this, context);
return clone;
}
/// <inheritdoc/>
public sealed override void CopyFrom(MixerTransition<Vector2MixerState, Vector2> copyFrom, CloneContext context)
=> this.CopyFromBase(copyFrom, context);
/// <inheritdoc/>
public virtual void CopyFrom(MixerTransition2D copyFrom, CloneContext context)
{
base.CopyFrom(copyFrom, context);
_Type = copyFrom._Type;
_ParameterNameX = copyFrom._ParameterNameX;
_ParameterNameY = copyFrom._ParameterNameY;
}
/************************************************************************************************************************/
}
}

View File

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

View File

@@ -0,0 +1,171 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
using Animancer.Units;
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Playables;
using Object = UnityEngine.Object;
namespace Animancer
{
/// <inheritdoc/>
/// https://kybernetik.com.au/animancer/api/Animancer/PlayableAssetTransition
[Serializable]
public class PlayableAssetTransition : Transition<PlayableAssetState>,
IAnimationClipCollection,
ICopyable<PlayableAssetTransition>
{
/************************************************************************************************************************/
[SerializeField, Tooltip("The asset to play")]
private PlayableAsset _Asset;
/// <summary>[<see cref="SerializeField"/>] The asset to play.</summary>
/// <remarks>
/// If you set this property and this transition has been played on multiple characters,
/// you will need to call <see cref="Transition{T}.ReconcileMainObject(AnimancerGraph)"/>
/// for each of them to create new states for the newly assigned object.
/// </remarks>
public PlayableAsset Asset
{
get => _Asset;
set
{
_Asset = value;
if (BaseState != null)
ReconcileMainObject(BaseState);
}
}
/// <summary>The name of the serialized backing field of <see cref="Asset"/>.</summary>
public const string AssetField = nameof(_Asset);
/// <inheritdoc/>
public override Object MainObject => _Asset;
/************************************************************************************************************************/
[SerializeField]
[Tooltip(Strings.Tooltips.NormalizedStartTime)]
[DefaultValue(float.NaN, 0f)]
[AnimationTime(
AnimationTimeAttribute.Units.Normalized,
DisabledText = Strings.Tooltips.StartTimeDisabled)]
private float _NormalizedStartTime = float.NaN;
/// <inheritdoc/>
public override float NormalizedStartTime
{
get => _NormalizedStartTime;
set => _NormalizedStartTime = value;
}
/************************************************************************************************************************/
[SerializeField]
[Tooltip("The objects controlled by each of the tracks in the Asset")]
[NonReorderable]
private Object[] _Bindings;
/// <summary>[<see cref="SerializeField"/>] The objects controlled by each of the tracks in the Asset.</summary>
public ref Object[] Bindings => ref _Bindings;
/// <summary>The name of the serialized backing field of <see cref="Bindings"/>.</summary>
public const string BindingsField = nameof(_Bindings);
/************************************************************************************************************************/
/// <inheritdoc/>
public override float MaximumLength
=> _Asset != null
? (float)_Asset.duration
: 0;
/// <inheritdoc/>
public override bool IsValid
=> _Asset != null;
/************************************************************************************************************************/
/// <inheritdoc/>
public override PlayableAssetState CreateState()
{
State = new(_Asset);
State.SetBindings(_Bindings);
return State;
}
/************************************************************************************************************************/
/// <inheritdoc/>
public override void Apply(AnimancerState state)
{
base.Apply(state);
ApplyNormalizedStartTime(state, _NormalizedStartTime);
}
/************************************************************************************************************************/
/// <summary>Gathers all the animations associated with this object.</summary>
void IAnimationClipCollection.GatherAnimationClips(ICollection<AnimationClip> clips)
=> clips.GatherFromAsset(_Asset);
/************************************************************************************************************************/
/// <inheritdoc/>
public override Transition<PlayableAssetState> Clone(CloneContext context)
{
var clone = new PlayableAssetTransition();
clone.CopyFrom(this, context);
return clone;
}
/// <inheritdoc/>
public sealed override void CopyFrom(Transition<PlayableAssetState> copyFrom, CloneContext context)
=> this.CopyFromBase(copyFrom, context);
/// <inheritdoc/>
public virtual void CopyFrom(PlayableAssetTransition copyFrom, CloneContext context)
{
base.CopyFrom(copyFrom, context);
_Asset = context.GetCloneOrOriginal(copyFrom._Asset);
_NormalizedStartTime = copyFrom._NormalizedStartTime;
context.CloneArray(copyFrom._Bindings, ref _Bindings);
}
/************************************************************************************************************************/
/// <summary>
/// Returns a new <see cref="PlayableAssetTransition"/>
/// if the `target` is an <see cref="PlayableAsset"/>.
/// </summary>
[TryCreateTransition(typeof(PlayableAsset))]
public static ITransition TryCreateTransition(Object target)
=> target is not PlayableAsset asset
? null
: new PlayableAssetTransition()
{
Asset = asset,
};
/************************************************************************************************************************/
#if UNITY_EDITOR
/// <summary>[Editor-Only] Validates that the `command` is targeting an asset.</summary>
[UnityEditor.MenuItem("CONTEXT/" + nameof(PlayableAsset) + "/Create Transition Asset", validate = true)]
private static bool ValidateCreateTransitionAsset(UnityEditor.MenuCommand command)
=> TryCreateTransitionAttribute.CanCreateAndSave(command.context);
/// <summary>[Editor-Only] Tries to create an asset containing an appropriate transition for the `command`.</summary>
[UnityEditor.MenuItem("CONTEXT/" + nameof(PlayableAsset) + "/Create Transition Asset")]
private static void CreateTransitionAsset(UnityEditor.MenuCommand command)
=> TryCreateTransitionAttribute.TryCreateTransitionAsset(command.context, true);
#endif
/************************************************************************************************************************/
}
}

View File

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

View File

@@ -0,0 +1,170 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
using Animancer.Units;
using System;
using System.Collections.Generic;
using UnityEngine;
namespace Animancer
{
/// <inheritdoc/>
/// <summary>A group of transitions which play one after the other.</summary>
/// https://kybernetik.com.au/animancer/api/Animancer/TransitionSequence
///
[Serializable]
public class TransitionSequence : Transition<SequenceState>,
IAnimationClipCollection,
ICopyable<TransitionSequence>
{
/************************************************************************************************************************/
[SerializeField]
[Tooltip(Strings.Tooltips.NormalizedStartTime)]
[DefaultValue(float.NaN, 0f)]
[AnimationTime(
AnimationTimeAttribute.Units.Normalized,
DisabledText = Strings.Tooltips.StartTimeDisabled)]
private float _NormalizedStartTime = float.NaN;
/// <inheritdoc/>
public override float NormalizedStartTime
{
get => _NormalizedStartTime;
set => _NormalizedStartTime = value;
}
/// <summary>
/// If this transition will set the <see cref="AnimancerState.Time"/>,
/// then it needs to use <see cref="FadeMode.FromStart"/>.
/// </summary>
public override FadeMode FadeMode
=> float.IsNaN(_NormalizedStartTime)
? default
: FadeMode.FromStart;
/************************************************************************************************************************/
[DrawAfterEvents]
[SerializeReference]
[Tooltip("The transitions to play in this sequence.")]
private ITransition[] _Transitions = Array.Empty<ITransition>();
/// <summary>[<see cref="SerializeField"/>] The transitions to play in this sequence.</summary>
public ref ITransition[] Transitions
=> ref _Transitions;
/************************************************************************************************************************/
/// <summary>Is everything in this sequence valid?</summary>
public override bool IsValid
{
get
{
for (int i = 0; i < _Transitions.Length; i++)
if (!_Transitions[i].IsValid())
return false;
return true;
}
}
/************************************************************************************************************************/
/// <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;
/************************************************************************************************************************/
/// <inheritdoc/>
public override float MaximumLength
{
get
{
var value = 0f;
for (int i = 0; i < _Transitions.Length; i++)
{
var transition = _Transitions[i];
if (!transition.IsValid())
continue;
var speed = transition.Speed;
var start = AnimancerEvent.Sequence.GetNormalizedStartTime(
transition.NormalizedStartTime,
speed);
var end = transition.SerializedEvents.GetNormalizedEndTime(
speed);
var normalizedLength = (end - start) / speed;
if (normalizedLength < 0)
continue;
value += transition.MaximumLength * normalizedLength;
}
return value;
}
}
/************************************************************************************************************************/
/// <inheritdoc/>
public override SequenceState CreateState()
{
var state = new SequenceState();
state.Set(_Transitions);
return state;
}
/************************************************************************************************************************/
/// <inheritdoc/>
public override void Apply(AnimancerState state)
{
base.Apply(state);
ApplyNormalizedStartTime(state, _NormalizedStartTime);
}
/************************************************************************************************************************/
/// <summary>Adds the <see cref="ClipTransition.Clip"/> of everything in this sequence to the collection.</summary>
public virtual void GatherAnimationClips(ICollection<AnimationClip> clips)
{
for (int i = 0; i < _Transitions.Length; i++)
clips.GatherFromSource(_Transitions[i]);
}
/************************************************************************************************************************/
/// <inheritdoc/>
public override Transition<SequenceState> Clone(CloneContext context)
{
var clone = new TransitionSequence();
clone.CopyFrom(this, context);
return clone;
}
/// <inheritdoc/>
public sealed override void CopyFrom(Transition<SequenceState> copyFrom, CloneContext context)
=> this.CopyFromBase(copyFrom, context);
/// <inheritdoc/>
public virtual void CopyFrom(TransitionSequence copyFrom, CloneContext context)
{
base.CopyFrom(copyFrom, context);
_NormalizedStartTime = copyFrom._NormalizedStartTime;
context.CloneArray(copyFrom._Transitions, ref _Transitions);
}
/************************************************************************************************************************/
}
}

View File

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

View File

@@ -0,0 +1,352 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
using Animancer.Units;
using System;
using UnityEngine;
using Object = UnityEngine.Object;
namespace Animancer
{
/// <summary>
/// A serializable <see cref="ITransition"/> which can create a particular type of
/// <see cref="AnimancerState"/> when passed into <see cref="AnimancerLayer.Play(ITransition)"/>.
/// </summary>
/// <remarks>
/// <strong>Documentation:</strong>
/// <see href="https://kybernetik.com.au/animancer/docs/manual/transitions">
/// Transitions</see>
/// </remarks>
/// https://kybernetik.com.au/animancer/api/Animancer/Transition_1
[Serializable]
public abstract class Transition<TState> :
IPolymorphic,
ITransition<TState>,
ITransition,
ICloneable<Transition<TState>>,
ICopyable<Transition<TState>>
where TState : AnimancerState
{
/************************************************************************************************************************/
[SerializeField]
[Tooltip(Strings.Tooltips.FadeDuration)]
[AnimationTime(AnimationTimeAttribute.Units.Seconds, Rule = Validate.Value.IsNotNegative)]
[DefaultFadeValue]
private float _FadeDuration = AnimancerGraph.DefaultFadeDuration;
/// <inheritdoc/>
/// <remarks>[<see cref="SerializeField"/>]</remarks>
/// <exception cref="ArgumentOutOfRangeException">Thrown when setting the value to a negative number.</exception>
public float FadeDuration
{
get => _FadeDuration;
set
{
if (value < 0)
throw new ArgumentOutOfRangeException(
nameof(value),
$"{nameof(FadeDuration)} must not be negative");
_FadeDuration = value;
}
}
/************************************************************************************************************************/
[SerializeField]
[Tooltip(Strings.Tooltips.OptionalSpeed)]
[AnimationSpeed(DisabledText = Strings.Tooltips.SpeedDisabled)]
[DefaultValue(1f, -1f)]
private float _Speed = 1;
/// <summary>[<see cref="SerializeField"/>]
/// Determines how fast the animation plays (1x = normal speed, 2x = double speed).
/// </summary>
/// <remarks>
/// This sets the <see cref="AnimancerNodeBase.Speed"/> when this transition is played.
/// </remarks>
public float Speed
{
get => _Speed;
set => _Speed = value;
}
/************************************************************************************************************************/
/// <inheritdoc/>
/// <remarks>Returns <c>false</c> unless overridden.</remarks>
public virtual bool IsLooping => false;
/// <inheritdoc/>
public virtual float NormalizedStartTime
{
get => float.NaN;
set { }
}
/// <inheritdoc/>
public abstract float MaximumLength { get; }
/************************************************************************************************************************/
[SerializeField, Tooltip(Strings.ProOnlyTag + "Events which will be triggered as the animation plays")]
private AnimancerEvent.Sequence.Serializable _Events;
/// <inheritdoc/>
/// <remarks>This property returns the <see cref="AnimancerEvent.Sequence.Serializable.Events"/>.</remarks>
public virtual AnimancerEvent.Sequence Events
{
get => (_Events ??= new()).Events;
set => (_Events ??= new()).Events = value;
}
/// <inheritdoc/>
public AnimancerEvent.Sequence.Serializable SerializedEvents
{
get => _Events;
set => _Events = value;
}
/************************************************************************************************************************/
/// <summary>
/// The state that was created by this object. Specifically, this is the state that was most recently
/// passed into <see cref="Apply"/> (usually by <see cref="AnimancerGraph.Play(ITransition)"/>).
/// <para></para>
/// You can use <see cref="AnimancerStateDictionary.GetOrCreate(ITransition)"/> or
/// <see cref="AnimancerLayer.GetOrCreateState(ITransition)"/> to get or create the state for a
/// specific object.
/// <para></para>
/// <see cref="State"/> is simply a shorthand for casting this to <typeparamref name="TState"/>.
/// </summary>
public AnimancerState BaseState { get; private set; }
/************************************************************************************************************************/
private TState _State;
/// <summary>
/// The state that was created by this object. Specifically, this is the state that was most recently
/// passed into <see cref="Apply"/> (usually by <see cref="AnimancerGraph.Play(ITransition)"/>).
/// </summary>
///
/// <remarks>
/// You can use <see cref="AnimancerStateDictionary.GetOrCreate(ITransition)"/> or
/// <see cref="AnimancerLayer.GetOrCreateState(ITransition)"/>
/// to get or create the state for a specific object.
/// <para></para>
/// This property is shorthand for casting the <see cref="BaseState"/> to <typeparamref name="TState"/>.
/// </remarks>
///
/// <exception cref="InvalidCastException">
/// The <see cref="BaseState"/> is not actually a <typeparamref name="TState"/>.
/// This should only happen if a different type of state was created by something else
/// and registered using the <see cref="Key"/>,
/// causing this <see cref="AnimancerGraph.Play(ITransition)"/> to pass that state into
/// <see cref="Apply"/> instead of calling <see cref="CreateState"/>
/// to make the correct type of state.
/// </exception>
public TState State
{
get => _State ??= (TState)BaseState;
protected set => BaseState = _State = value;
}
/************************************************************************************************************************/
/// <inheritdoc/>
/// <remarks>Returns <c>true</c> unless overridden.</remarks>
public virtual bool IsValid
=> true;
/// <summary>The <see cref="AnimancerState.Key"/> which the created state will be registered with.</summary>
/// <remarks>Returns <c>this</c> unless overridden.</remarks>
public virtual object Key
=> this;
/// <inheritdoc/>
/// <remarks>Returns <see cref="FadeMode.FixedSpeed"/> unless overridden.</remarks>
public virtual FadeMode FadeMode
=> default;
/************************************************************************************************************************/
/// <inheritdoc/>
public abstract TState CreateState();
/// <inheritdoc/>
AnimancerState ITransition.CreateState()
=> CreateAndInitializeState();
/// <summary>Calls <see cref="CreateState"/> and assigns the <see cref="Events"/> to the state.</summary>
public TState CreateAndInitializeState()
{
var state = CreateState();
AnimancerState.SetExpectFade(state, _FadeDuration);
State = state;
state.SharedEvents = _Events;
return state;
}
/************************************************************************************************************************/
/// <inheritdoc/>
public virtual void Apply(AnimancerState state)
{
#if UNITY_ASSERTIONS
if (state.MainObject != MainObject)
{
OptionalWarning.MainObjectMismatch.Log(
$"A state.{nameof(MainObject)} doesn't match the" +
$" transition.{nameof(MainObject)} being applied to it." +
$" transition.{nameof(ReconcileMainObject)} must be called" +
$" for every state created by the transition" +
$" after its {nameof(MainObject)} is changed." +
$" This includes {nameof(ClipTransition)}.{nameof(ClipTransition.Clip)}," +
$" {nameof(ControllerTransition)}.{nameof(ControllerTransition.Controller)}, and" +
$" {nameof(PlayableAssetTransition)}.{nameof(PlayableAssetTransition.Asset)}" +
$"\n• State: {state}" +
$"\n• State.{nameof(MainObject)}: {state.MainObject}" +
$"\n• Transition.{nameof(MainObject)}: {MainObject}" +
$"\n• Component: {state.Graph?.Component}",
state.Graph?.Component);
}
#endif
if (_State != state)
{
_State = null;
BaseState = state;
}
if (!float.IsNaN(_Speed))
state.Speed = _Speed;
}
/************************************************************************************************************************/
/// <summary>Applies the `normalizedStartTime` to the `state`.</summary>
public static void ApplyNormalizedStartTime(AnimancerState state, float normalizedStartTime)
{
if (!float.IsNaN(normalizedStartTime))
state.NormalizedTime = normalizedStartTime;
else if (!state.IsActive)
state.NormalizedTime = AnimancerEvent.Sequence.GetDefaultNormalizedStartTime(state.Speed);
}
/************************************************************************************************************************/
/// <summary>The <see cref="AnimancerState.MainObject"/> that the created state will have.</summary>
public virtual Object MainObject { get; }
/// <summary>The display name of this transition.</summary>
public virtual string Name
{
get
{
var mainObject = MainObject;
return mainObject != null ? mainObject.name : null;
}
}
/// <summary>Returns the <see cref="Name"/> and type of this transition.</summary>
public override string ToString()
{
var type = GetType().FullName;
var name = Name;
return name is null
? type :
$"{name} ({type})";
}
/************************************************************************************************************************/
/// <summary>
/// If a state exists with its <see cref="AnimancerState.MainObject"/> not matching the
/// <see cref="MainObject"/>, this method returns a new state for the correct object.
/// </summary>
/// <remarks>
/// This method only applies to the state registered with the <see cref="Key"/> so
/// if this transition is played on multiple different characters or used to create
/// multiple states for the same character, this method must be called for each state.
/// </remarks>
public AnimancerState ReconcileMainObject(AnimancerGraph animancer)
=> animancer.States.TryGet(this, out var state)
? ReconcileMainObject(state)
: null;
/************************************************************************************************************************/
/// <summary>
/// If the <see cref="AnimancerState.MainObject"/> doesn't match the <see cref="MainObject"/>,
/// this method returns a new state for the correct object.
/// </summary>
/// <remarks>
/// If this transition is played on multiple different characters or used to create
/// multiple states for the same character, this method must be called for each state.
/// </remarks>
public AnimancerState ReconcileMainObject(AnimancerState state)
{
var newMainObject = MainObject;
if (newMainObject == null)
return state;
var oldMainObject = state.MainObject;
if (oldMainObject == newMainObject)
return state;
#if UNITY_ASSERTIONS
if (oldMainObject == null)
Debug.LogError(
$"{state} had no {nameof(state.MainObject)} to change from.",
state.Graph?.Component as Object);
if (newMainObject == null)
Debug.LogError(
$"{this} has no {nameof(MainObject)} to change to.",
state.Graph?.Component as Object);
#endif
// Change the old state's key to its object so we can get it back later.
state.Key = oldMainObject;
// If there was already a state for the new object, give it the correct key.
if (state.Graph.States.TryGet(newMainObject, out var existingState))
{
existingState.Key = Key;
state = existingState;
}
else// Otherwise, create a state for the new object.
{
var layer = state.Layer;
state = CreateState();
state._Key = Key;
state.SetParent(layer);
}
_State = null;
BaseState = state;
return state;
}
/************************************************************************************************************************/
/// <inheritdoc/>
public abstract Transition<TState> Clone(CloneContext context);
/// <inheritdoc/>
public virtual void CopyFrom(Transition<TState> copyFrom, CloneContext context)
{
_FadeDuration = copyFrom._FadeDuration;
_Speed = copyFrom._Speed;
_Events = copyFrom._Events.Clone();
}
/************************************************************************************************************************/
}
}

View File

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

View File

@@ -0,0 +1,78 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
#endif
namespace Animancer
{
/// <inheritdoc/>
/// https://kybernetik.com.au/animancer/api/Animancer/TransitionAsset
[CreateAssetMenu(
menuName = Strings.MenuPrefix + "Transition Asset",
order = Strings.AssetMenuOrder + 1)]
[AnimancerHelpUrl(typeof(TransitionAsset))]
public class TransitionAsset : TransitionAsset<ITransition>
{
/************************************************************************************************************************/
#if UNITY_EDITOR
/************************************************************************************************************************/
/// <summary>[Editor-Only] Sets the <see cref="TransitionAssetBase.CreateInstance"/>.</summary>
[InitializeOnLoadMethod]
private static void SetMainImplementation()
=> CreateInstance = transition =>
{
var asset = CreateInstance<TransitionAsset>();
asset.Transition = transition;
return asset;
};
/************************************************************************************************************************/
/// <inheritdoc/>
protected override void Reset()
{
Transition = new ClipTransition();
}
/************************************************************************************************************************/
/// <summary>[Editor-Only] Validates that the `mainAsset` is actually an asset.</summary>
public static bool ValidateCreate(Object mainAsset)
{
var path = AssetDatabase.GetAssetPath(mainAsset);
return !string.IsNullOrEmpty(path);
}
/// <summary>[Editor-Only] Creates a <see cref="TransitionAsset"/> next to the `mainAsset`.</summary>
public static TransitionAsset Create(Object mainAsset)
{
var path = AssetDatabase.GetAssetPath(mainAsset);
if (string.IsNullOrEmpty(path))
{
Debug.LogError(
$"Can't create {nameof(TransitionAsset)} for something that isn't an asset.",
mainAsset);
return null;
}
path = System.IO.Path.GetDirectoryName(path);
path = System.IO.Path.Combine(path, $"{mainAsset.name}.asset");
path = AssetDatabase.GenerateUniqueAssetPath(path);
var asset = CreateInstance<TransitionAsset>();
AssetDatabase.CreateAsset(asset, path);
Selection.activeObject = asset;
return asset;
}
/************************************************************************************************************************/
#endif
/************************************************************************************************************************/
}
}

View File

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

View File

@@ -0,0 +1,173 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
using System;
using System.Collections.Generic;
using UnityEngine;
namespace Animancer
{
/// <summary>A <see cref="ScriptableObject"/> based <see cref="ITransition"/>.</summary>
/// <remarks>
/// <strong>Documentation:</strong>
/// <see href="https://kybernetik.com.au/animancer/docs/manual/transitions/assets">
/// Transition Assets</see>
/// <para></para>
/// When adding a <see cref="CreateAssetMenuAttribute"/> to any derived classes, you can use
/// <see cref="Strings.MenuPrefix"/> and <see cref="Strings.AssetMenuOrder"/>.
/// </remarks>
/// https://kybernetik.com.au/animancer/api/Animancer/TransitionAssetBase
[AnimancerHelpUrl(typeof(TransitionAssetBase))]
public abstract partial class TransitionAssetBase : ScriptableObject,
IAnimationClipSource,
ITransition,
IWrapper
{
/************************************************************************************************************************/
/// <summary>The name of the serialized backing field of <see cref="GetTransition"/>.</summary>
public const string TransitionField = "_Transition";
/************************************************************************************************************************/
/// <summary>Returns the <see cref="ITransition"/> wrapped by this <see cref="ScriptableObject"/>.</summary>
public abstract ITransition GetTransition();
/// <inheritdoc/>
object IWrapper.WrappedObject
=> this != null
? GetTransition()
: null;
/************************************************************************************************************************/
/// <inheritdoc/>
public virtual float FadeDuration
=> GetTransition().FadeDuration;
/// <inheritdoc/>
public virtual object Key
=> GetTransition().Key;
/// <inheritdoc/>
public virtual FadeMode FadeMode
=> GetTransition().FadeMode;
/// <inheritdoc/>
public virtual AnimancerState CreateState()
=> GetTransition().CreateState();
/// <inheritdoc/>
public virtual void Apply(AnimancerState state)
{
GetTransition().Apply(state);
state.SetDebugName(this);
}
/************************************************************************************************************************/
/// <summary>Can this transition create a valid <see cref="AnimancerState"/>?</summary>
/// <remarks>
/// Use <see cref="AnimancerUtilities.IsValid(ITransition)"/>
/// to also null check this reference, i.e: <c>transition.IsValid()</c>.
/// </remarks>
public virtual bool IsValid
=> this != null
&& GetTransition().IsValid();
/// <inheritdoc/>
public bool IsLooping
=> GetTransition().IsLooping;
/// <inheritdoc/>
public float NormalizedStartTime
{
get => GetTransition().NormalizedStartTime;
set => GetTransition().NormalizedStartTime = value;
}
/// <inheritdoc/>
public float MaximumLength
=> GetTransition().MaximumLength;
/// <inheritdoc/>
public float Speed
{
get => GetTransition().Speed;
set => GetTransition().Speed = value;
}
/************************************************************************************************************************/
/// <summary>Explains why Transition Assets warn about accessing their events.</summary>
public const string ObsoleteEventsMessage =
"Directly accessing the Events of a Transition Asset is generally not recommended" +
" because any modifications will affect all characters who share the same asset" +
" and will persist until the asset is destroyed (usually when the application exits)." +
"\n\n" +
"In most cases, the recommended approach is to initialize events on the " + nameof(AnimancerState) +
" returned when you Play the Transition rather than modifying the Asset itself." +
"\n\n" +
"If you really need to access these events, you can use the asset.Transition.Events.";
/// <inheritdoc/>
[Obsolete(ObsoleteEventsMessage)]
public AnimancerEvent.Sequence Events
=> GetTransition().Events;
/// <inheritdoc/>
[Obsolete(ObsoleteEventsMessage)]
public AnimancerEvent.Sequence.Serializable SerializedEvents
{
get => GetTransition().SerializedEvents;
set => GetTransition().SerializedEvents = value;
}
/************************************************************************************************************************/
/// <summary>[<see cref="IAnimationClipSource"/>]
/// Calls <see cref="AnimancerUtilities.GatherFromSource(ICollection{AnimationClip}, object)"/>.
/// </summary>
public virtual void GetAnimationClips(List<AnimationClip> clips)
=> clips.GatherFromSource(GetTransition());
/************************************************************************************************************************/
#if UNITY_EDITOR
/************************************************************************************************************************/
/// <summary>[Editor-Only] Creates an instance of the main non-abstract inheritor of this class.</summary>
/// <remarks><c>TransitionAsset</c> sets this to use itself by default.</remarks>
public static new Func<ITransition, TransitionAssetBase> CreateInstance { get; set; }
/************************************************************************************************************************/
private const string CreateFromSelectionMenu = Strings.CreateMenuPrefix + "Transition Assets From Selection";
[UnityEditor.MenuItem(
itemName: CreateFromSelectionMenu,
validate = true)]
private static bool ValidateCreateFromSelection()
{
var selection = UnityEditor.Selection.objects;
foreach (var item in selection)
if (TryCreateTransitionAttribute.CanCreateAndSave(item))
return true;
return false;
}
[UnityEditor.MenuItem(
itemName: CreateFromSelectionMenu,
priority = Strings.AssetMenuOrder + 2)]
private static void CreateFromSelection()
{
var selection = UnityEditor.Selection.objects;
foreach (var item in selection)
TryCreateTransitionAttribute.TryCreateTransitionAsset(item, true);
}
/************************************************************************************************************************/
#endif
/************************************************************************************************************************/
}
}

View File

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

View File

@@ -0,0 +1,167 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
using System;
using System.Collections.Generic;
using UnityEngine;
namespace Animancer
{
/// <summary>A [<see cref="SerializableAttribute"/>] wrapper around an <see cref="TransitionAssetBase"/>.</summary>
/// <remarks>
/// This allows Transition Assets to be referenced inside [<see cref="SerializeReference"/>]
/// fields which can't directly reference <see cref="UnityEngine.Object"/>s.
/// <para></para>
/// <strong>Documentation:</strong>
/// <see href="https://kybernetik.com.au/animancer/docs/manual/transitions/assets">
/// Transition Assets</see>
/// </remarks>
/// https://kybernetik.com.au/animancer/api/Animancer/TransitionAssetReference
[Serializable]
public class TransitionAssetReference :
IAnimationClipSource,
ICloneable<TransitionAssetReference>,
ICopyable<TransitionAssetReference>,
IPolymorphic,
ITransition,
IWrapper
{
/************************************************************************************************************************/
[SerializeField]
private TransitionAssetBase _Asset;
/// <summary>[<see cref="SerializeField"/>] The wrapped Transition Asset.</summary>
public ref TransitionAssetBase Asset
=> ref _Asset;
/************************************************************************************************************************/
/// <summary>Creates a new <see cref="TransitionAssetReference"/>.</summary>
public TransitionAssetReference() { }
/// <summary>Creates a new <see cref="TransitionAssetReference"/>.</summary>
public TransitionAssetReference(TransitionAssetBase asset)
{
_Asset = asset;
}
/************************************************************************************************************************/
/// <inheritdoc/>
object IWrapper.WrappedObject
=> _Asset;
/************************************************************************************************************************/
/// <summary>Can this transition create a valid <see cref="AnimancerState"/>?</summary>
public virtual bool IsValid
=> _Asset.IsValid();
/// <inheritdoc/>
public virtual float FadeDuration
=> _Asset != null
? _Asset.FadeDuration
: 0;
/// <inheritdoc/>
public virtual object Key
=> _Asset != null
? _Asset.Key
: null;
/// <inheritdoc/>
public virtual FadeMode FadeMode
=> _Asset != null
? _Asset.FadeMode
: default;
/// <inheritdoc/>
public bool IsLooping
=> _Asset != null
&& _Asset.IsLooping;
/// <inheritdoc/>
public float NormalizedStartTime
{
get => _Asset != null
? _Asset.NormalizedStartTime
: float.NaN;
set => _Asset.NormalizedStartTime = value;// No null check. Don't silently ignore commands.
}
/// <inheritdoc/>
public float MaximumLength
=> _Asset != null
? _Asset.MaximumLength
: 0;
/// <inheritdoc/>
public float Speed
{
get => _Asset != null
? _Asset.Speed
: 1;
set => _Asset.Speed = value;// No null check. Don't silently ignore commands.
}
/************************************************************************************************************************/
/// <inheritdoc/>
[Obsolete(TransitionAssetBase.ObsoleteEventsMessage)]
public AnimancerEvent.Sequence Events
=> _Asset != null
? _Asset.Events
: null;
/// <inheritdoc/>
[Obsolete(TransitionAssetBase.ObsoleteEventsMessage)]
public AnimancerEvent.Sequence.Serializable SerializedEvents
{
get => _Asset.SerializedEvents;
set => _Asset.SerializedEvents = value;
}
/************************************************************************************************************************/
/// <inheritdoc/>
public virtual AnimancerState CreateState()
=> _Asset.CreateState();
/// <inheritdoc/>
public virtual void Apply(AnimancerState state)
=> _Asset.Apply(state);
/************************************************************************************************************************/
/// <summary>[<see cref="IAnimationClipSource"/>]
/// Calls <see cref="AnimancerUtilities.GatherFromSource(ICollection{AnimationClip}, object)"/>.
/// </summary>
public virtual void GetAnimationClips(List<AnimationClip> clips)
=> clips.GatherFromSource(_Asset);
/************************************************************************************************************************/
/// <inheritdoc/>
public virtual TransitionAssetReference Clone(CloneContext context)
{
var clone = new TransitionAssetReference();
clone.CopyFrom(this, context);
return clone;
}
/// <inheritdoc/>
public void CopyFrom(TransitionAssetReference copyFrom, CloneContext context)
{
_Asset = context.GetCloneOrOriginal(copyFrom._Asset);
}
/************************************************************************************************************************/
/// <summary>Describes the <see cref="Asset"/>.</summary>
public override string ToString()
=> $"{nameof(TransitionAssetReference)}({_Asset})";
/************************************************************************************************************************/
}
}

View File

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

View File

@@ -0,0 +1,110 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
using System;
using System.Collections.Generic;
using UnityEngine;
namespace Animancer
{
/// <inheritdoc/>
/// https://kybernetik.com.au/animancer/api/Animancer/TransitionAsset_1
[AnimancerHelpUrl(typeof(TransitionAsset<ITransition>))]
public class TransitionAsset<TTransition> : TransitionAssetBase
where TTransition : ITransition
{
/************************************************************************************************************************/
[SerializeReference]
private TTransition _Transition;
/// <summary>[<see cref="SerializeReference"/>]
/// The <see cref="ITransition"/> wrapped by this <see cref="ScriptableObject"/>.
/// </summary>
/// <remarks>
/// WARNING: the <see cref="Transition{TState}.State"/> holds the most recently played state, so
/// if you're sharing this transition between multiple objects it will only remember one of them.
/// <para></para>
/// You can use <see cref="AnimancerStateDictionary.GetOrCreate(ITransition)"/>
/// or <see cref="AnimancerLayer.GetOrCreateState(ITransition)"/>
/// to get or create the state for a specific object.
/// </remarks>
public TTransition Transition
{
get
{
AssertTransition();
return _Transition;
}
set => _Transition = value;
}
/// <summary>Returns the <see cref="ITransition"/> wrapped by this <see cref="ScriptableObject"/>.</summary>
public override ITransition GetTransition()
{
AssertTransition();
return _Transition;
}
/************************************************************************************************************************/
/// <summary>[Assert-Conditional]
/// Throws a <see cref="NullReferenceException"/> if the <see cref="Transition"/> is null.
/// </summary>
[System.Diagnostics.Conditional(Strings.Assertions)]
private void AssertTransition()
{
if (_Transition == null)
throw new NullReferenceException(
$"'{name}' {nameof(Transition)} is null." +
$" {nameof(HasTransition)} can be used to check without triggering this error.");
}
/************************************************************************************************************************/
/// <inheritdoc/>
public override bool IsValid => _Transition.IsValid();
/// <inheritdoc/>
public override float FadeDuration => _Transition.FadeDuration;
/// <inheritdoc/>
public override object Key => _Transition.Key;
/// <inheritdoc/>
public override FadeMode FadeMode => _Transition.FadeMode;
/// <inheritdoc/>
public override AnimancerState CreateState() => _Transition.CreateState();
/// <inheritdoc/>
public override void Apply(AnimancerState state)
{
_Transition.Apply(state);
state.SetDebugName(this);
}
/// <inheritdoc/>
public override void GetAnimationClips(List<AnimationClip> clips)
=> clips.GatherFromSource(_Transition);
/************************************************************************************************************************/
/// <summary>Is the <see cref="Transition"/> assigned (i.e. not <c>null</c>)?</summary>
public bool HasTransition => _Transition != null;
/************************************************************************************************************************/
#if UNITY_EDITOR
/// <summary>[Editor-Only]
/// Assigns a default <typeparamref name="TTransition"/> to the <see cref="Transition"/> field.
/// </summary>
protected virtual void Reset()
{
_Transition = AnimancerReflection.CreateDefaultInstance<TTransition>();
}
#endif
/************************************************************************************************************************/
}
}

View File

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

View File

@@ -0,0 +1,156 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
using System;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
using Object = UnityEngine.Object;
namespace Animancer
{
/// <summary>Attribute for static methods which try to create a transition from an object.</summary>
/// <remarks>
/// The method signature must be:
/// <c>static ITransition TryCreateTransition(Object target)</c>
/// </remarks>
/// https://kybernetik.com.au/animancer/api/Animancer/TryCreateTransitionAttribute
[AttributeUsage(AttributeTargets.Method)]
public sealed class TryCreateTransitionAttribute : Attribute
{
/************************************************************************************************************************/
/// <summary>The base type of object which the attributed method can handle.</summary>
public Type ObjectType { get; private set; }
/************************************************************************************************************************/
/// <summary>Creates a new <see cref="TryCreateTransitionAttribute"/>.</summary>
public TryCreateTransitionAttribute(Type objectType)
{
ObjectType = objectType;
}
/************************************************************************************************************************/
#if UNITY_EDITOR
/************************************************************************************************************************/
private static List<Func<Object, ITransition>> _Methods;
private static List<Type> _TargetTypes;
/// <summary>[Editor-Only] Ensures that all methods with this attribute have been gathered.</summary>
private static void InitializeMethods()
{
if (_Methods != null)
return;
_Methods = new();
_TargetTypes = new();
foreach (var method in TypeCache.GetMethodsWithAttribute<TryCreateTransitionAttribute>())
{
try
{
var attributes = method.GetCustomAttributes(typeof(TryCreateTransitionAttribute), true);
if (!attributes.IsNullOrEmpty())
{
var attribute = attributes[0] as TryCreateTransitionAttribute;
if (attribute?.ObjectType != null)
_TargetTypes.Add(attribute.ObjectType);
}
var func = Delegate.CreateDelegate(typeof(Func<Object, ITransition>), method);
_Methods.Add((Func<Object, ITransition>)func);
}
catch (Exception exception)
{
Debug.LogError(
$"Failed to create delegate for" +
$" {method.DeclaringType.GetNameCS()}.{method.Name}," +
$" it must take one {typeof(Object).FullName} parameter" +
$" and return {typeof(ITransition).FullName}" +
$": {exception}");
}
}
}
/************************************************************************************************************************/
/// <summary>[Editor-Only] Validates that the `mainAsset` is actually an asset.</summary>
public static bool CanCreateAndSave(Object mainAsset)
{
if (TransitionAssetBase.CreateInstance == null)
return false;
if (!CanCreate(mainAsset.GetType()))
return false;
var path = AssetDatabase.GetAssetPath(mainAsset);
return !string.IsNullOrEmpty(path);
}
/// <summary>[Editor-Only] Validates that the `mainAsset` is actually an asset.</summary>
public static bool CanCreate(Type targetType)
{
InitializeMethods();
for (int i = 0; i < _TargetTypes.Count; i++)
if (_TargetTypes[i].IsAssignableFrom(targetType))
return true;
return false;
}
/************************************************************************************************************************/
/// <summary>[Editor-Only] Tries to create an asset containing an appropriate transition for the `target`.</summary>
public static TransitionAssetBase TryCreateTransitionAsset(Object target, bool saveNextToTarget = false)
{
if (target is TransitionAssetBase asset)
return asset;
var assetType = TransitionAssetBase.CreateInstance;
if (assetType == null)
return null;
InitializeMethods();
for (int i = 0; i < _Methods.Count; i++)
{
var transition = _Methods[i](target);
if (transition is not null)
{
var created = TransitionAssetBase.CreateInstance(transition);
created.name = target.name;
if (saveNextToTarget)
{
var path = AssetDatabase.GetAssetPath(target);
if (string.IsNullOrEmpty(path))
{
Debug.LogError(
$"Can't create TransitionAsset for '{target}' because it isn't an asset.",
target);
return created;
}
path = System.IO.Path.GetDirectoryName(path);
path = System.IO.Path.Combine(path, $"{target.name}.asset");
path = AssetDatabase.GenerateUniqueAssetPath(path);
AssetDatabase.CreateAsset(created, path);
Selection.activeObject = created;
}
return created;
}
}
return null;
}
/************************************************************************************************************************/
#endif
/************************************************************************************************************************/
}
}

View File

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