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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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