123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743 |
- // Animancer // https://kybernetik.com.au/animancer // Copyright 2022 Kybernetik //
- using System;
- using UnityEngine;
- using Object = UnityEngine.Object;
- namespace Animancer
- {
- /// https://kybernetik.com.au/animancer/api/Animancer/AnimancerState
- partial class AnimancerState
- {
- /************************************************************************************************************************/
- /// <summary>The <see cref="IUpdatable"/> that manages the events of this state.</summary>
- /// <remarks>
- /// This field is null by default, acquires its reference from an <see cref="ObjectPool"/> when accessed, and
- /// if it contains no events at the end of an update it releases the reference back to the pool.
- /// </remarks>
- private EventDispatcher _EventDispatcher;
- /************************************************************************************************************************/
- /// <summary>
- /// A list of <see cref="AnimancerEvent"/>s that will occur while this state plays as well as one that
- /// specifically defines when this state ends.
- /// </summary>
- /// <remarks>
- /// Accessing this property will acquire a spare <see cref="AnimancerEvent.Sequence"/> from the
- /// <see cref="ObjectPool"/> if none was already assigned. You can use <see cref="HasEvents"/> to check
- /// beforehand.
- /// <para></para>
- /// These events will automatically be cleared by <see cref="Play"/>, <see cref="Stop"/>, and
- /// <see cref="OnStartFade"/> (unless <see cref="AutomaticallyClearEvents"/> is disabled).
- /// <para></para>
- /// <em>Animancer Lite does not allow the use of events in runtime builds, except for
- /// <see cref="AnimancerEvent.Sequence.OnEnd"/>.</em>
- /// <para></para>
- /// Documentation: <see href="https://kybernetik.com.au/animancer/docs/manual/events/animancer">Animancer Events</see>
- /// </remarks>
- public AnimancerEvent.Sequence Events
- {
- get
- {
- EventDispatcher.Acquire(this);
- return _EventDispatcher.Events;
- }
- set
- {
- if (value != null)
- {
- EventDispatcher.Acquire(this);
- _EventDispatcher.Events = value;
- }
- else if (_EventDispatcher != null)
- {
- _EventDispatcher.Events = null;
- }
- }
- }
- /************************************************************************************************************************/
- /// <summary>Does this state have an <see cref="AnimancerEvent.Sequence"/>?</summary>
- /// <remarks>Accessing <see cref="Events"/> would automatically get one from the <see cref="ObjectPool"/>.</remarks>
- public bool HasEvents => _EventDispatcher != null && _EventDispatcher.HasEvents;
- /************************************************************************************************************************/
- /// <summary>
- /// Should the <see cref="Events"/> be cleared automatically whenever <see cref="Play"/>, <see cref="Stop"/>,
- /// or <see cref="OnStartFade"/> are called? Default true.
- /// </summary>
- /// <remarks>
- /// Disabling this property is not usually recommended since it would allow events to continue being triggered
- /// while a state is fading out. For example, if a <em>Flinch</em> animation interrupts an <em>Attack</em>, you
- /// probably don't want the <em>Attack</em>'s <em>Hit</em> event to still get triggered while it's fading out.
- /// <para></para>
- /// Documentation: <see href="https://kybernetik.com.au/animancer/docs/manual/events/animancer#clear-automatically">
- /// Clear Automatically</see>
- /// </remarks>
- public static bool AutomaticallyClearEvents { get; set; } = true;
- /************************************************************************************************************************/
- #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 virtual string UnsupportedEventsMessage => null;
- #endif
- /************************************************************************************************************************/
- /// <summary>An <see cref="IUpdatable"/> which triggers events in an <see cref="AnimancerEvent.Sequence"/>.</summary>
- /// https://kybernetik.com.au/animancer/api/Animancer/EventDispatcher
- ///
- public class EventDispatcher : Key, IUpdatable
- {
- /************************************************************************************************************************/
- #region Pooling
- /************************************************************************************************************************/
- /// <summary>
- /// If the `state` has no <see cref="EventDispatcher"/>, this method gets one from the
- /// <see cref="ObjectPool"/>.
- /// </summary>
- internal static void Acquire(AnimancerState state)
- {
- ref var dispatcher = ref state._EventDispatcher;
- if (dispatcher != null)
- return;
- ObjectPool.Acquire(out dispatcher);
- #if UNITY_ASSERTIONS
- dispatcher._LoggedEndEventInterrupt = false;
- OptionalWarning.UnsupportedEvents.Log(state.UnsupportedEventsMessage, state.Root?.Component);
- if (dispatcher._State != null)
- Debug.LogError($"{dispatcher} already has a state even though it was in the list of spares.",
- state.Root?.Component as Object);
- if (dispatcher._Events != null)
- Debug.LogError($"{dispatcher} has event sequence even though it was in the list of spares.",
- state.Root?.Component as Object);
- if (dispatcher._GotEventsFromPool)
- Debug.LogError($"{dispatcher} is marked as having pooled events even though it has no events.",
- state.Root?.Component as Object);
- if (dispatcher._NextEventIndex != RecalculateEventIndex)
- Debug.LogError($"{dispatcher} has a {nameof(_NextEventIndex)} even though it was pooled.",
- state.Root?.Component as Object);
- if (IsInList(dispatcher))
- Debug.LogError($"{dispatcher} is currently in a Keyed List even though it was also in the list of spares.",
- state.Root?.Component as Object);
- #endif
- dispatcher._IsLooping = state.IsLooping;
- dispatcher._PreviousTime = state.NormalizedTime;
- dispatcher._State = state;
- state.Root?.RequirePostUpdate(dispatcher);
- }
- /************************************************************************************************************************/
- /// <summary>Returns this <see cref="EventDispatcher"/> to the <see cref="ObjectPool"/>.</summary>
- private void Release()
- {
- if (_State == null)
- return;
- _State.Root?.CancelPostUpdate(this);
- _State._EventDispatcher = null;
- _State = null;
- Events = null;
- ObjectPool.Release(this);
- }
- /************************************************************************************************************************/
- /// <summary>
- /// If the <see cref="AnimancerEvent.Sequence"/> was acquired from the <see cref="ObjectPool"/>, this
- /// method clears it. Otherwise it simply discards the reference.
- /// </summary>
- internal static void TryClear(EventDispatcher events)
- {
- if (events != null)
- events.Events = null;
- }
- /************************************************************************************************************************/
- #endregion
- /************************************************************************************************************************/
- private AnimancerState _State;
- private AnimancerEvent.Sequence _Events;
- private bool _GotEventsFromPool;
- private bool _IsLooping;
- private float _PreviousTime;
- private int _NextEventIndex = RecalculateEventIndex;
- private int _SequenceVersion;
- private bool _WasPlayingForwards;
- /// <summary>
- /// A special value used by the <see cref="_NextEventIndex"/> to indicate that it needs to be recalculated.
- /// </summary>
- private const int RecalculateEventIndex = int.MinValue;
- /// <summary>
- /// This system accounts for external modifications to the sequence, but modifying it while checking which
- /// of its events to update is not allowed because it would be impossible to efficiently keep track of
- /// which events have been checked/invoked and which still need to be checked.
- /// </summary>
- private const string SequenceVersionException =
- nameof(AnimancerState) + "." + nameof(AnimancerState.Events) + " sequence was modified while iterating through it." +
- " Events in a sequence must not modify that sequence.";
- /************************************************************************************************************************/
- /// <summary>Does this dispatcher have an <see cref="AnimancerEvent.Sequence"/>?</summary>
- /// <remarks>Accessing <see cref="Events"/> would automatically get one from the <see cref="ObjectPool"/>.</remarks>
- public bool HasEvents => _Events != null;
- /************************************************************************************************************************/
- /// <summary>The events managed by this dispatcher.</summary>
- /// <remarks>If <c>null</c>, a new sequence will be acquired from the <see cref="ObjectPool"/>.</remarks>
- internal AnimancerEvent.Sequence Events
- {
- get
- {
- if (_Events == null)
- {
- ObjectPool.Acquire(out _Events);
- _GotEventsFromPool = true;
- #if UNITY_ASSERTIONS
- if (!_Events.IsEmpty)
- Debug.LogError(_Events + " is not in its default state even though it was in the list of spares.",
- _State?.Root?.Component as Object);
- #endif
- }
- return _Events;
- }
- set
- {
- if (_GotEventsFromPool)
- {
- _Events.Clear();
- ObjectPool.Release(_Events);
- _GotEventsFromPool = false;
- }
- _Events = value;
- _NextEventIndex = RecalculateEventIndex;
- }
- }
- /************************************************************************************************************************/
- void IUpdatable.Update()
- {
- if (_Events == null || _Events.IsEmpty)
- {
- Release();
- return;
- }
- var length = _State.Length;
- if (length == 0)
- {
- UpdateZeroLength();
- return;
- }
- var currentTime = _State.Time / length;
- if (_PreviousTime == currentTime)
- return;
- // General events are triggered on the frame when their time passes.
- // This happens either once or repeatedly depending on whether the animation is looping or not.
- CheckGeneralEvents(currentTime);
- if (_Events == null)
- {
- Release();
- return;
- }
- // End events are triggered every frame after their time passes. This ensures that assigning the event
- // after the time has passed will still trigger it rather than leaving it playing indefinitely.
- var endEvent = _Events.EndEvent;
- if (endEvent.callback != null)
- {
- if (currentTime > _PreviousTime)// Playing Forwards.
- {
- var eventTime = float.IsNaN(endEvent.normalizedTime)
- ? 1
- : endEvent.normalizedTime;
- if (currentTime > eventTime)
- {
- ValidateBeforeEndEvent();
- endEvent.Invoke(_State);
- ValidateAfterEndEvent(endEvent.callback);
- }
- }
- else// Playing Backwards.
- {
- var eventTime = float.IsNaN(endEvent.normalizedTime)
- ? 0
- : endEvent.normalizedTime;
- if (currentTime < eventTime)
- {
- ValidateBeforeEndEvent();
- endEvent.Invoke(_State);
- ValidateAfterEndEvent(endEvent.callback);
- }
- }
- }
- // Store the current time as the previous for the next frame unless OnTimeChanged was called.
- if (_NextEventIndex != RecalculateEventIndex)
- _PreviousTime = currentTime;
- }
- /************************************************************************************************************************/
- #region End Event Validation
- /************************************************************************************************************************/
- #if UNITY_ASSERTIONS
- private bool _LoggedEndEventInterrupt;
- private static AnimancerLayer _BeforeEndLayer;
- private static int _BeforeEndCommandCount;
- #endif
- /************************************************************************************************************************/
- /// <summary>[Assert-Conditional]
- /// Called after the <see cref="AnimancerEvent.Sequence.EndEvent"/> is triggered to log a warning if the
- /// <see cref="_State"/> was not interrupted or the `callback` contains multiple calls to the same method.
- /// </summary>
- /// <remarks>
- /// It would be better if we could validate the callback when it is assigned to get a useful stack trace,
- /// but that is unfortunately not possible since <see cref="AnimancerEvent.Sequence.EndEvent"/> needs to be
- /// a field for efficiency.
- /// </remarks>
- [System.Diagnostics.Conditional(Strings.Assertions)]
- private void ValidateBeforeEndEvent()
- {
- #if UNITY_ASSERTIONS
- _BeforeEndLayer = _State.Layer;
- _BeforeEndCommandCount = _BeforeEndLayer.CommandCount;
- #endif
- }
- /************************************************************************************************************************/
- /// <summary>[Assert-Conditional]
- /// Called after the <see cref="AnimancerEvent.Sequence.EndEvent"/> is triggered to log a warning if the
- /// <see cref="_State"/> was not interrupted or the `callback` contains multiple calls to the same method.
- /// </summary>
- /// <remarks>
- /// It would be better if we could validate the callback when it is assigned to get a useful stack trace,
- /// but that is unfortunately not possible since <see cref="AnimancerEvent.Sequence.EndEvent"/> needs to be
- /// a field for efficiency.
- /// </remarks>
- [System.Diagnostics.Conditional(Strings.Assertions)]
- private void ValidateAfterEndEvent(Action callback)
- {
- #if UNITY_ASSERTIONS
- if (ShouldLogEndEventInterrupt(callback))
- {
- _LoggedEndEventInterrupt = true;
- if (OptionalWarning.EndEventInterrupt.IsEnabled())
- OptionalWarning.EndEventInterrupt.Log(
- "An End Event did not actually end the animation:" +
- $"\n - State: {_State}" +
- $"\n - Callback: {callback.Method.DeclaringType.Name}.{callback.Method.Name}" +
- "\n\nEnd Events are triggered every frame after their time has passed," +
- " so if that is not desired behaviour then it might be necessary to explicitly set the" +
- $" state.{nameof(AnimancerState.Events)}.{nameof(AnimancerEvent.Sequence.OnEnd)} = null" +
- " or simply use a regular event instead.",
- _State.Root?.Component);
- }
- if (OptionalWarning.DuplicateEvent.IsDisabled())
- return;
- if (!AnimancerUtilities.TryGetInvocationListNonAlloc(callback, out var delegates) ||
- delegates == null)
- return;
- var count = delegates.Length;
- for (int iA = 0; iA < count; iA++)
- {
- var a = delegates[iA];
- for (int iB = iA + 1; iB < count; iB++)
- {
- var b = delegates[iB];
- if (a == b)
- {
- OptionalWarning.DuplicateEvent.Log(
- $"The {nameof(AnimancerEvent)}.{nameof(AnimancerEvent.Sequence)}.{nameof(AnimancerEvent.Sequence.OnEnd)}" +
- " callback being invoked contains multiple identical delegates which may mean" +
- " that they are being unintentionally added multiple times." +
- $"\n - State: {_State}" +
- $"\n - Method: {a.Method.Name}",
- _State.Root?.Component);
- }
- else if (a?.Method == b?.Method)
- {
- OptionalWarning.DuplicateEvent.Log(
- $"The {nameof(AnimancerEvent)}.{nameof(AnimancerEvent.Sequence)}.{nameof(AnimancerEvent.Sequence.OnEnd)}" +
- " callback being invoked contains multiple delegates using the same method with different targets." +
- " This often happens when a Transition is shared by multiple objects," +
- " in which case it can be avoided by giving each object its own" +
- $" {nameof(AnimancerEvent)}.{nameof(AnimancerEvent.Sequence)} as explained in the documentation:" +
- $" {Strings.DocsURLs.SharedEventSequences}" +
- $"\n - State: {_State}" +
- $"\n - Method: {a.Method.Name}",
- _State.Root?.Component);
- }
- }
- }
- #endif
- }
- /************************************************************************************************************************/
- #if UNITY_ASSERTIONS
- /// <summary>Should <see cref="OptionalWarning.EndEventInterrupt"/> be logged?</summary>
- private bool ShouldLogEndEventInterrupt(Action callback)
- {
- if (_LoggedEndEventInterrupt ||
- _Events == null ||
- _Events.OnEnd != callback)
- return false;
- var layer = _State.Layer;
- if (_BeforeEndLayer != layer ||
- _BeforeEndCommandCount != layer.CommandCount ||
- !_State.Root.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 return false;// Speed 0.
- }
- #endif
- /************************************************************************************************************************/
- #endregion
- /************************************************************************************************************************/
- /// <summary>Notifies this dispatcher that the target's <see cref="Time"/> has changed.</summary>
- internal void OnTimeChanged()
- {
- _PreviousTime = _State.NormalizedTime;
- _NextEventIndex = RecalculateEventIndex;
- }
- /************************************************************************************************************************/
- /// <summary>If the state has zero length, trigger its end event every frame.</summary>
- private void UpdateZeroLength()
- {
- var speed = _State.EffectiveSpeed;
- if (speed == 0)
- return;
- if (_Events.Count > 0)
- {
- var sequenceVersion = _Events.Version;
- int playDirectionInt;
- if (speed < 0)
- {
- playDirectionInt = -1;
- if (_NextEventIndex == RecalculateEventIndex ||
- _SequenceVersion != sequenceVersion ||
- _WasPlayingForwards)
- {
- _NextEventIndex = Events.Count - 1;
- _SequenceVersion = sequenceVersion;
- _WasPlayingForwards = false;
- }
- }
- else
- {
- playDirectionInt = 1;
- if (_NextEventIndex == RecalculateEventIndex ||
- _SequenceVersion != sequenceVersion ||
- !_WasPlayingForwards)
- {
- _NextEventIndex = 0;
- _SequenceVersion = sequenceVersion;
- _WasPlayingForwards = true;
- }
- }
- if (!InvokeAllEvents(1, playDirectionInt))
- return;
- }
- var endEvent = _Events.EndEvent;
- if (endEvent.callback != null)
- endEvent.Invoke(_State);
- }
- /************************************************************************************************************************/
- private void CheckGeneralEvents(float currentTime)
- {
- var count = _Events.Count;
- if (count == 0)
- {
- _NextEventIndex = 0;
- return;
- }
- ValidateNextEventIndex(ref currentTime, out var playDirectionFloat, out var playDirectionInt);
- if (_IsLooping)// Looping.
- {
- var animancerEvent = _Events[_NextEventIndex];
- var eventTime = animancerEvent.normalizedTime * playDirectionFloat;
- var loopDelta = GetLoopDelta(_PreviousTime, currentTime, eventTime);
- if (loopDelta == 0)
- return;
- // For each additional loop, invoke all events without needing to check their times.
- if (!InvokeAllEvents(loopDelta - 1, playDirectionInt))
- return;
- var loopStartIndex = _NextEventIndex;
- Invoke:
- animancerEvent.Invoke(_State);
- if (!NextEventLooped(playDirectionInt) ||
- _NextEventIndex == loopStartIndex)
- return;
- animancerEvent = _Events[_NextEventIndex];
- eventTime = animancerEvent.normalizedTime * playDirectionFloat;
- if (loopDelta == GetLoopDelta(_PreviousTime, currentTime, eventTime))
- goto Invoke;
- }
- else// Non-Looping.
- {
- while ((uint)_NextEventIndex < (uint)count)
- {
- var animancerEvent = _Events[_NextEventIndex];
- var eventTime = animancerEvent.normalizedTime * playDirectionFloat;
- if (currentTime <= eventTime)
- return;
- animancerEvent.Invoke(_State);
- if (!NextEvent(playDirectionInt))
- return;
- }
- }
- }
- /************************************************************************************************************************/
- private void ValidateNextEventIndex(ref float currentTime,
- out float playDirectionFloat, out int playDirectionInt)
- {
- var sequenceVersion = _Events.Version;
- if (currentTime < _PreviousTime)// Playing Backwards.
- {
- var previousTime = _PreviousTime;
- _PreviousTime = -previousTime;
- currentTime = -currentTime;
- playDirectionFloat = -1;
- playDirectionInt = -1;
- if (_NextEventIndex == RecalculateEventIndex ||
- _SequenceVersion != sequenceVersion ||
- _WasPlayingForwards)
- {
- _NextEventIndex = _Events.Count - 1;
- _SequenceVersion = sequenceVersion;
- _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 ||
- _SequenceVersion != sequenceVersion ||
- !_WasPlayingForwards)
- {
- _NextEventIndex = 0;
- _SequenceVersion = sequenceVersion;
- _WasPlayingForwards = true;
- var previousTime = _PreviousTime;
- 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="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 bool InvokeAllEvents(int count, int playDirectionInt)
- {
- var loopStartIndex = _NextEventIndex;
- while (count-- > 0)
- {
- do
- {
- _Events[_NextEventIndex].Invoke(_State);
- if (!NextEventLooped(playDirectionInt))
- return false;
- }
- while (_NextEventIndex != loopStartIndex);
- }
- return true;
- }
- /************************************************************************************************************************/
- private bool NextEvent(int playDirectionInt)
- {
- if (_NextEventIndex == RecalculateEventIndex)
- return false;
- if (_Events.Version != _SequenceVersion)
- throw new InvalidOperationException(SequenceVersionException);
- _NextEventIndex += playDirectionInt;
- return true;
- }
- /************************************************************************************************************************/
- private bool NextEventLooped(int playDirectionInt)
- {
- if (!NextEvent(playDirectionInt))
- return false;
- var count = _Events.Count;
- if (_NextEventIndex >= count)
- _NextEventIndex = 0;
- else if (_NextEventIndex < 0)
- _NextEventIndex = count - 1;
- return true;
- }
- /************************************************************************************************************************/
- /// <summary>Returns "<see cref="EventDispatcher"/> (Target State)".</summary>
- public override string ToString()
- {
- return _State != null ?
- $"{nameof(EventDispatcher)} ({_State})" :
- $"{nameof(EventDispatcher)} (No Target State)";
- }
- /************************************************************************************************************************/
- }
- /************************************************************************************************************************/
- }
- }
|