From cd0b9baa14a310874f243b6f8017ad1b9c036dd5 Mon Sep 17 00:00:00 2001 From: Sander Grindheim Date: Mon, 5 Aug 2024 18:53:52 -0700 Subject: [PATCH 1/3] added Animator generation extension which can be called upon a StateMachine. Modified StateMachine.cs to expose a few private fields as internal --- .../EditorStateMachineShortcuts.cs | 228 +++ .../EditorStateMachineShortcuts.cs.meta | 11 + src/StateMachine/StateMachine.cs | 1626 ++++++++--------- 3 files changed, 1052 insertions(+), 813 deletions(-) create mode 100644 src/StateMachine/EditorStateMachineShortcuts.cs create mode 100644 src/StateMachine/EditorStateMachineShortcuts.cs.meta diff --git a/src/StateMachine/EditorStateMachineShortcuts.cs b/src/StateMachine/EditorStateMachineShortcuts.cs new file mode 100644 index 0000000..6ffd53b --- /dev/null +++ b/src/StateMachine/EditorStateMachineShortcuts.cs @@ -0,0 +1,228 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using UnityEditor.Animations; +using UnityEditor; +using UnityEngine; + +#if UNITY_EDITOR +namespace UnityHFSM +{ + public static class EditorStateMachineShortcuts + { + /// + /// Prints the animator states and transitions to an Animator for easy viewing. Only call this after all states and transitions have been added! + /// + /// Leave this empty if you want to use the default path of Assets/DebugAnimators/ + public static void PrintToAnimator(this StateMachine hfsm, + string pathToFolderForDebugAnimator = "", string animatorName = "StateMachineDebugger.controller") + { + if (hfsm.stateBundlesByName.Count == 0) + { + Debug.LogError("Trying to print an empty HFSM. You probably forgot to add the states and transitions before calling this method."); + return; + } + + if (!animatorName.Contains(".controller")) + { + animatorName = string.Concat(animatorName, ".controller"); + } + + if (pathToFolderForDebugAnimator == "") + pathToFolderForDebugAnimator = Path.Combine("Assets", "DebugAnimators" + Path.DirectorySeparatorChar); + + if (!Directory.Exists(pathToFolderForDebugAnimator)) + Directory.CreateDirectory(pathToFolderForDebugAnimator); + + var fullPathToDebugAnimator = Path.Combine(pathToFolderForDebugAnimator, animatorName); + + var animatorMirror = AssetDatabase.LoadAssetAtPath(fullPathToDebugAnimator); + if (animatorMirror == null) + animatorMirror = AnimatorController.CreateAnimatorControllerAtPath(fullPathToDebugAnimator); + + //remove old transitions from state machine before setting it up freshly + RemoveTransitionsFromStateMachine(animatorMirror.layers[0].stateMachine); + + SetupAnimatorStateMachine(animatorMirror.layers[0].stateMachine, hfsm, new(), new()); + } + + private static void SetupAnimatorStateMachine(AnimatorStateMachine animatorStateMachine, StateMachine hfsm, + Dictionary animatorStateDict, Dictionary animatorStateMachineDict) + { + //Add Animator states mirroring HFSM states + foreach (StateBase state in hfsm.stateBundlesByName.Values?.Select(bundle => bundle.state)) + { + if (state is StateMachine subFsm) + AddStateMachineToAnimator(subFsm, state.name, animatorStateMachine, animatorStateMachineDict, animatorStateDict); + else + AddStateToAnimator(state, hfsm, animatorStateMachine, animatorStateDict); + } + + //Add transitions to Animator which mirror transitions in the HFSM. + //This cannot be in the same loop as above because the state which is receiving a transition might not have been created yet + foreach (StateMachine.StateBundle stateBundle in hfsm.stateBundlesByName.Values) + { + if (stateBundle.state is StateMachine subFsm) + AddStateMachineTransitionsToAnimator(stateBundle, subFsm, animatorStateMachineDict, animatorStateDict); + else + AddStateTransitionsToAnimator(stateBundle, animatorStateDict, animatorStateMachineDict); + } + + foreach (var transition in hfsm.transitionsFromAny) + { + animatorStateMachine.AddAnyStateTransition(animatorStateDict[transition.to]); + } + + foreach (var transitionList in hfsm.triggerTransitionsFromAny.Values) + { + foreach (var transition in transitionList) + { + animatorStateMachine.AddAnyStateTransition(animatorStateDict[transition.to]); + } + } + } + + private static void AddStateTransitionsToAnimator(StateMachine.StateBundle stateBundle, + Dictionary animatorStateDict, Dictionary animatorStateMachineDict) + { + var fromState = animatorStateDict[stateBundle.state.name]; + + //remove all existing transitions so that they can be replaced + AnimatorStateTransition[] transitionsCopy = new AnimatorStateTransition[fromState.transitions.Length]; + Array.Copy(fromState.transitions, transitionsCopy, fromState.transitions.Length); + foreach (var animatorTransition in transitionsCopy) + fromState.RemoveTransition(animatorTransition); + + if (stateBundle.transitions != null) + { + foreach (var transition in stateBundle.transitions) + { + if (animatorStateDict.ContainsKey(transition.to)) + fromState.AddTransition(animatorStateDict[transition.to]); + else //if the destination is not a state, then it must be a state machine + { + AnimatorStateMachine destinationStateMachine = animatorStateMachineDict[transition.to]; + fromState.AddTransition(destinationStateMachine); + } + } + } + + if (stateBundle.triggerToTransitions == null) + return; + + foreach (var transitionList in stateBundle.triggerToTransitions.Values) + { + foreach (TransitionBase transition in transitionList) + { + AnimatorState destinationState = animatorStateDict[transition.to]; + fromState.AddTransition(destinationState); + } + } + } + + private static void RemoveTransitionsFromStateMachine(AnimatorStateMachine animatorStateMachine) + { + //remove all entry transitions + AnimatorTransition[] entryTransitionsCopy = new AnimatorTransition[animatorStateMachine.entryTransitions.Length]; + Array.Copy(animatorStateMachine.entryTransitions, entryTransitionsCopy, animatorStateMachine.entryTransitions.Length); + foreach (AnimatorTransition animatorTransition in entryTransitionsCopy) + animatorStateMachine.RemoveEntryTransition(animatorTransition); + + //remove any-state transitions + AnimatorStateTransition[] anyTransitionsCopy = new AnimatorStateTransition[animatorStateMachine.anyStateTransitions.Length]; + Array.Copy(animatorStateMachine.anyStateTransitions, anyTransitionsCopy, animatorStateMachine.anyStateTransitions.Length); + foreach (AnimatorStateTransition animatorTransition in anyTransitionsCopy) + animatorStateMachine.RemoveAnyStateTransition(animatorTransition); + } + + private static void AddStateMachineTransitionsToAnimator(StateMachine.StateBundle stateBundle, StateMachine subFsm, + Dictionary animatorStatemachineDict, Dictionary animatorStateDict) + { + AnimatorStateMachine animatorStateMachine = animatorStatemachineDict[stateBundle.state.name]; + + //Remove all transitoins so that they can be re-placed + RemoveTransitionsFromStateMachine(animatorStateMachine); + + //Add Any state transitions + foreach (var transition in subFsm.transitionsFromAny) + { + animatorStateMachine.AddAnyStateTransition(animatorStateDict[transition.to]); + } + + foreach (var transitionList in subFsm.triggerTransitionsFromAny.Values) + { + foreach (var transition in transitionList) + { + animatorStateMachine.AddAnyStateTransition(animatorStateDict[transition.to]); + } + } + + //trigger transitions are treated exactly the same as normal transitions, so concatenate them into one IEnumerable + IEnumerable> transitions = stateBundle.transitions; + if (stateBundle.triggerToTransitions != null) + { + foreach (var transitionList in stateBundle.triggerToTransitions.Values) + transitions.Concat(transitionList); + } + + foreach (var transition in transitions) + { + //AnimatorStatemachine is not interchangable with AnimatorState, so we must check each dictionary separately + if (animatorStatemachineDict.ContainsKey(transition.to)) + { + AnimatorStateMachine destinationState = animatorStatemachineDict[transition.to]; + animatorStateMachine.AddStateMachineTransition(destinationState); + } + else //if the destination is not a state machine, then it must be a state + { + AnimatorState destinationState = animatorStateDict[transition.to]; + animatorStateMachine.AddStateMachineTransition(animatorStateMachine, destinationState); + } + } + } + + private static void AddStateToAnimator(StateBase stateToAdd, StateMachine parentFSM, + AnimatorStateMachine animatorStateMachine, Dictionary animatorStateDict) + { + //search to see if the state machine contains a state with the same name as the stateToAdd + var (foundStateWithSameName, foundChildState) = animatorStateMachine.states.FirstOrFalse(state => state.state.name == stateToAdd.name.ToString()); + + if (!foundStateWithSameName) + foundChildState.state = animatorStateMachine.AddState(stateToAdd.name.ToString()); + + //if the parent fsm doesn't have a start state or we are the start state, then make this state the default in the animator + if (parentFSM.startState.hasState == false || parentFSM.startState.state.ToString() == stateToAdd.name.ToString()) + animatorStateMachine.defaultState = foundChildState.state; + + animatorStateDict.Add(stateToAdd.name, foundChildState.state); + } + + private static void AddStateMachineToAnimator(StateMachine subFsm, TStateId stateMachineName, AnimatorStateMachine parentAnimatorStateMachine, + Dictionary stateMachineDictionary, Dictionary animatorStateDict) + { + //search to see if the state machine contains a child state machine with the same name as stateToAdd + var (didFindStateMachine, childStateMachine) = parentAnimatorStateMachine.stateMachines.FirstOrFalse(childStateMachine => childStateMachine.stateMachine.name == subFsm.name.ToString()); + + if (!didFindStateMachine) + childStateMachine.stateMachine = parentAnimatorStateMachine.AddStateMachine(subFsm.name.ToString()); + + stateMachineDictionary.Add(stateMachineName, childStateMachine.stateMachine); + + SetupAnimatorStateMachine(childStateMachine.stateMachine, subFsm, animatorStateDict, stateMachineDictionary); + } + + public static (bool didFind, T element) FirstOrFalse(this IEnumerable collection, Func predicate) + { + foreach (T element in collection) + { + if (predicate(element)) + return (true, element); + } + + return (false, default(T)); + } + } +} +#endif diff --git a/src/StateMachine/EditorStateMachineShortcuts.cs.meta b/src/StateMachine/EditorStateMachineShortcuts.cs.meta new file mode 100644 index 0000000..0586f95 --- /dev/null +++ b/src/StateMachine/EditorStateMachineShortcuts.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 2eb94d9c7c996574e8acc6d525a07749 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/src/StateMachine/StateMachine.cs b/src/StateMachine/StateMachine.cs index 8e095e8..7f9f138 100644 --- a/src/StateMachine/StateMachine.cs +++ b/src/StateMachine/StateMachine.cs @@ -1,813 +1,813 @@ -using System; -using System.Collections.Generic; - -/** - * Hierarchical finite state machine for Unity - * by Inspiaaa - * - * Version: 2.1.0 - */ - -namespace UnityHFSM -{ - /// - /// A finite state machine that can also be used as a state of a parent state machine to create - /// a hierarchy (-> hierarchical state machine). - /// - public class StateMachine : - StateBase, - ITriggerable, - IStateMachine, - IActionable - { - /// - /// A bundle of a state together with the outgoing transitions and trigger transitions. - /// It's useful, as you only need to do one Dictionary lookup for these three items. - /// => Much better performance - /// - private class StateBundle - { - // By default, these fields are all null and only get a value when you need them. - // => Lazy evaluation => Memory efficient, when you only need a subset of features - public StateBase state; - public List> transitions; - public Dictionary>> triggerToTransitions; - - public void AddTransition(TransitionBase t) - { - transitions = transitions ?? new List>(); - transitions.Add(t); - } - - public void AddTriggerTransition(TEvent trigger, TransitionBase transition) - { - triggerToTransitions = triggerToTransitions - ?? new Dictionary>>(); - - List> transitionsOfTrigger; - - if (!triggerToTransitions.TryGetValue(trigger, out transitionsOfTrigger)) - { - transitionsOfTrigger = new List>(); - triggerToTransitions.Add(trigger, transitionsOfTrigger); - } - - transitionsOfTrigger.Add(transition); - } - } - - private struct PendingTransition - { - public TStateId targetState; - - public bool isExitTransition; - - // Optional (may be null), used for callbacks when the transition succeeds. - public ITransitionListener listener; - - // As this type is not nullable (it is a value type), an additional field is required - // to see if the pending transition has been set yet. - public bool isPending; - - public static PendingTransition CreateForExit(ITransitionListener listener = null) - => new PendingTransition { - targetState = default, - isExitTransition = true, - listener = listener, - isPending = true - }; - - public static PendingTransition CreateForState(TStateId target, ITransitionListener listener = null) - => new PendingTransition { - targetState = target, - isExitTransition = false, - listener = listener, - isPending = true - }; - } - - // A cached empty list of transitions (For improved readability, less GC). - private static readonly List> noTransitions - = new List>(0); - private static readonly Dictionary>> noTriggerTransitions - = new Dictionary>>(0); - - /// - /// Event that is raised when the active state changes. - /// - /// - /// It is triggered when the state machine enters its initial state, and after a transition is performed. - /// Note that it is not called when the state machine exits. - /// - public event Action> StateChanged; - - private (TStateId state, bool hasState) startState = (default, false); - private PendingTransition pendingTransition = default; - private bool rememberLastState = false; - - // Central storage of states. - private Dictionary stateBundlesByName - = new Dictionary(); - - private StateBase activeState = null; - private List> activeTransitions = noTransitions; - private Dictionary>> activeTriggerTransitions = noTriggerTransitions; - - private List> transitionsFromAny - = new List>(); - private Dictionary>> triggerTransitionsFromAny - = new Dictionary>>(); - - public StateBase ActiveState - { - get - { - EnsureIsInitializedFor("Trying to get the active state"); - return activeState; - } - } - public TStateId ActiveStateName => ActiveState.name; - - public IStateMachine ParentFsm => fsm; - - private bool IsRootFsm => fsm == null; - - public bool HasPendingTransition => pendingTransition.isPending; - - /// - /// Initialises a new instance of the StateMachine class. - /// - /// (Only for hierarchical states): - /// Determines whether the state machine as a state of a parent state machine is allowed to instantly - /// exit on a transition (false), or if it should wait until an explicit exit transition occurs. - /// (Only for hierarchical states): - /// If true, the state machine will return to its last active state when it enters, instead - /// of to its original start state. - /// - public StateMachine(bool needsExitTime = false, bool isGhostState = false, bool rememberLastState = false) - : base(needsExitTime: needsExitTime, isGhostState: isGhostState) - { - this.rememberLastState = rememberLastState; - } - - /// - /// Throws an exception if the state machine is not initialised yet. - /// - /// String message for which action the fsm should be initialised for. - private void EnsureIsInitializedFor(string context) - { - if (activeState == null) - throw UnityHFSM.Exceptions.Common.NotInitialized(context); - } - - /// - /// Notifies the state machine that the state can cleanly exit, - /// and if a state change is pending, it will execute it. - /// - public void StateCanExit() - { - if (!pendingTransition.isPending) - return; - - ITransitionListener listener = pendingTransition.listener; - if (pendingTransition.isExitTransition) - { - pendingTransition = default; - - listener?.BeforeTransition(); - PerformVerticalTransition(); - listener?.AfterTransition(); - } - else - { - TStateId state = pendingTransition.targetState; - - // When the pending state is a ghost state, ChangeState() will have - // to try all outgoing transitions, which may overwrite the pendingState. - // That's why it is first cleared, and not afterwards, as that would overwrite - // a new, valid pending state. - pendingTransition = default; - ChangeState(state, listener); - } - } - - /// - /// Instantly changes to the target state. - /// - /// The name / identifier of the active state. - /// Optional object that receives callbacks before and after changing state. - private void ChangeState(TStateId name, ITransitionListener listener = null) - { - listener?.BeforeTransition(); - activeState?.OnExit(); - - StateBundle bundle; - - if (!stateBundlesByName.TryGetValue(name, out bundle) || bundle.state == null) - { - throw UnityHFSM.Exceptions.Common.StateNotFound(name.ToString(), context: "Switching states"); - } - - activeTransitions = bundle.transitions ?? noTransitions; - activeTriggerTransitions = bundle.triggerToTransitions ?? noTriggerTransitions; - - activeState = bundle.state; - activeState.OnEnter(); - - for (int i = 0, count = activeTransitions.Count; i < count; i++) - { - activeTransitions[i].OnEnter(); - } - - foreach (List> transitions in activeTriggerTransitions.Values) - { - for (int i = 0, count = transitions.Count; i < count; i++) - { - transitions[i].OnEnter(); - } - } - - listener?.AfterTransition(); - - StateChanged?.Invoke(activeState); - - if (activeState.isGhostState) - { - TryAllDirectTransitions(); - } - } - - /// - /// Signals to the parent fsm that this fsm can exit which allows the parent - /// fsm to transition to the next state. - /// - private void PerformVerticalTransition() - { - fsm?.StateCanExit(); - } - - /// - /// Requests a state change, respecting the needsExitTime property of the active state. - /// - /// The name / identifier of the target state. - /// Overrides the needsExitTime of the active state if true, - /// therefore forcing an immediate state change. - /// Optional object that receives callbacks before and after the transition. - public void RequestStateChange( - TStateId name, - bool forceInstantly = false, - ITransitionListener listener = null) - { - if (!activeState.needsExitTime || forceInstantly) - { - pendingTransition = default; - ChangeState(name, listener); - } - else - { - pendingTransition = PendingTransition.CreateForState(name, listener); - activeState.OnExitRequest(); - // If it can exit, the activeState would call - // -> state.fsm.StateCanExit() which in turn would call - // -> fsm.ChangeState(...) - } - } - - /// - /// Requests a "vertical transition", allowing the state machine to exit - /// to allow the parent fsm to transition to the next state. It respects the - /// needsExitTime property of the active state. - /// - /// Overrides the needsExitTime of the active state if true, - /// therefore forcing an immediate state change. - /// Optional object that receives callbacks before and after the transition. - public void RequestExit(bool forceInstantly = false, ITransitionListener listener = null) - { - if (!activeState.needsExitTime || forceInstantly) - { - pendingTransition = default; - listener?.BeforeTransition(); - PerformVerticalTransition(); - listener?.AfterTransition(); - } - else - { - pendingTransition = PendingTransition.CreateForExit(listener); - activeState.OnExitRequest(); - } - } - - /// - /// Checks if a transition can take place, and if this is the case, transition to the - /// "to" state and return true. Otherwise it returns false. - /// - private bool TryTransition(TransitionBase transition) - { - if (transition.isExitTransition) - { - if (fsm == null || !fsm.HasPendingTransition || !transition.ShouldTransition()) - return false; - - RequestExit(transition.forceInstantly, transition as ITransitionListener); - return true; - } - else - { - if (!transition.ShouldTransition()) - return false; - - RequestStateChange(transition.to, transition.forceInstantly, transition as ITransitionListener); - return true; - } - } - - /// - /// Tries the "global" transitions that can transition from any state. - /// - /// Returns true if a transition occurred. - private bool TryAllGlobalTransitions() - { - for (int i = 0, count = transitionsFromAny.Count; i < count; i++) - { - TransitionBase transition = transitionsFromAny[i]; - - // Don't transition to the "to" state, if that state is already the active state. - if (EqualityComparer.Default.Equals(transition.to, activeState.name)) - continue; - - if (TryTransition(transition)) - return true; - } - - return false; - } - - /// - /// Tries the "normal" transitions that transition from one specific state to another. - /// - /// Returns true if a transition occurred. - private bool TryAllDirectTransitions() - { - for (int i = 0, count = activeTransitions.Count; i < count; i++) - { - TransitionBase transition = activeTransitions[i]; - - if (TryTransition(transition)) - return true; - } - - return false; - } - - /// - /// Calls OnEnter if it is the root state machine, therefore initialising the state machine. - /// - public override void Init() - { - if (!IsRootFsm) return; - - OnEnter(); - } - - /// - /// Initialises the state machine and must be called before OnLogic is called. - /// It sets the activeState to the selected startState. - /// - public override void OnEnter() - { - if (!startState.hasState) - { - throw UnityHFSM.Exceptions.Common.MissingStartState(context: "Running OnEnter of the state machine."); - } - - // Clear any previous pending transition from the last run. - pendingTransition = default; - - ChangeState(startState.state); - - for (int i = 0, count = transitionsFromAny.Count; i < count; i++) - { - transitionsFromAny[i].OnEnter(); - } - - foreach (List> transitions in triggerTransitionsFromAny.Values) - { - for (int i = 0, count = transitions.Count; i < count; i++) - { - transitions[i].OnEnter(); - } - } - } - - /// - /// Runs one logic step. It does at most one transition itself and - /// calls the active state's logic function (after the state transition, if - /// one occurred). - /// - public override void OnLogic() - { - EnsureIsInitializedFor("Running OnLogic"); - - if (TryAllGlobalTransitions()) - goto runOnLogic; - - if (TryAllDirectTransitions()) - goto runOnLogic; - - runOnLogic: - activeState?.OnLogic(); - } - - public override void OnExit() - { - if (activeState == null) - return; - - if (rememberLastState) - { - startState = (activeState.name, true); - } - - activeState.OnExit(); - // By setting the activeState to null, the state's onExit method won't be called - // a second time when the state machine enters again (and changes to the start state). - activeState = null; - } - - public override void OnExitRequest() - { - if (activeState.needsExitTime) - activeState.OnExitRequest(); - } - - /// - /// Defines the entry point of the state machine. - /// - /// The name / identifier of the start state. - public void SetStartState(TStateId name) - { - startState = (name, true); - } - - /// - /// Gets the StateBundle belonging to the name state "slot" if it exists. - /// Otherwise it will create a new StateBundle, that will be added to the Dictionary, - /// and return the newly created instance. - /// - private StateBundle GetOrCreateStateBundle(TStateId name) - { - StateBundle bundle; - - if (!stateBundlesByName.TryGetValue(name, out bundle)) - { - bundle = new StateBundle(); - stateBundlesByName.Add(name, bundle); - } - - return bundle; - } - - /// - /// Adds a new node / state to the state machine. - /// - /// The name / identifier of the new state. - /// The new state instance, e.g. State, CoState, StateMachine. - public void AddState(TStateId name, StateBase state) - { - state.fsm = this; - state.name = name; - state.Init(); - - StateBundle bundle = GetOrCreateStateBundle(name); - bundle.state = state; - - if (stateBundlesByName.Count == 1 && !startState.hasState) - { - SetStartState(name); - } - } - - /// - /// Initialises a transition, i.e. sets its fsm attribute, and then calls its Init method. - /// - /// - private void InitTransition(TransitionBase transition) - { - transition.fsm = this; - transition.Init(); - } - - /// - /// Adds a new transition between two states. - /// - /// The transition instance. - public void AddTransition(TransitionBase transition) - { - InitTransition(transition); - - StateBundle bundle = GetOrCreateStateBundle(transition.from); - bundle.AddTransition(transition); - } - - /// - /// Adds a new transition that can happen from any possible state. - /// - /// The transition instance; The "from" field can be - /// left empty, as it has no meaning in this context. - public void AddTransitionFromAny(TransitionBase transition) - { - InitTransition(transition); - - transitionsFromAny.Add(transition); - } - - /// - /// Adds a new trigger transition between two states that is only checked - /// when the specified trigger is activated. - /// - /// The name / identifier of the trigger. - /// The transition instance, e.g. Transition, TransitionAfter, ... - public void AddTriggerTransition(TEvent trigger, TransitionBase transition) - { - InitTransition(transition); - - StateBundle bundle = GetOrCreateStateBundle(transition.from); - bundle.AddTriggerTransition(trigger, transition); - } - - /// - /// Adds a new trigger transition that can happen from any possible state, but is only - /// checked when the specified trigger is activated. - /// - /// The name / identifier of the trigger - /// The transition instance; The "from" field can be - /// left empty, as it has no meaning in this context. - public void AddTriggerTransitionFromAny(TEvent trigger, TransitionBase transition) - { - InitTransition(transition); - - List> transitionsOfTrigger; - - if (!triggerTransitionsFromAny.TryGetValue(trigger, out transitionsOfTrigger)) - { - transitionsOfTrigger = new List>(); - triggerTransitionsFromAny.Add(trigger, transitionsOfTrigger); - } - - transitionsOfTrigger.Add(transition); - } - - /// - /// Adds two transitions: - /// If the condition of the transition instance is true, it transitions from the "from" - /// state to the "to" state. Otherwise it performs a transition in the opposite direction, - /// i.e. from "to" to "from". - /// - /// - /// Internally the same transition instance will be used for both transitions - /// by wrapping it in a ReverseTransition. - /// For the reverse transition the afterTransition callback is called before the transition - /// and the onTransition callback afterwards. If this is not desired then replicate the behaviour - /// of the two way transitions by creating two separate transitions. - /// - public void AddTwoWayTransition(TransitionBase transition) - { - InitTransition(transition); - AddTransition(transition); - - ReverseTransition reverse = new ReverseTransition(transition, false); - InitTransition(reverse); - AddTransition(reverse); - } - - /// - /// Adds two transitions that are only checked when the specified trigger is activated: - /// If the condition of the transition instance is true, it transitions from the "from" - /// state to the "to" state. Otherwise it performs a transition in the opposite direction, - /// i.e. from "to" to "from". - /// - /// - /// Internally the same transition instance will be used for both transitions - /// by wrapping it in a ReverseTransition. - /// For the reverse transition the afterTransition callback is called before the transition - /// and the onTransition callback afterwards. If this is not desired then replicate the behaviour - /// of the two way transitions by creating two separate transitions. - /// - public void AddTwoWayTriggerTransition(TEvent trigger, TransitionBase transition) - { - InitTransition(transition); - AddTriggerTransition(trigger, transition); - - ReverseTransition reverse = new ReverseTransition(transition, false); - InitTransition(reverse); - AddTriggerTransition(trigger, reverse); - } - - /// - /// Adds a new exit transition from a state. It represents an exit point that - /// allows the fsm to exit and the parent fsm to continue to the next state. - /// It is only checked if the parent fsm has a pending transition. - /// - /// The transition instance. The "to" field can be - /// left empty, as it has no meaning in this context. - public void AddExitTransition(TransitionBase transition) - { - transition.isExitTransition = true; - AddTransition(transition); - } - - /// - /// Adds a new exit transition that can happen from any possible state. - /// It represents an exit point that allows the fsm to exit and the parent fsm to continue - /// to the next state. It is only checked if the parent fsm has a pending transition. - /// - /// The transition instance. The "from" and "to" fields can be - /// left empty, as they have no meaning in this context. - public void AddExitTransitionFromAny(TransitionBase transition) - { - transition.isExitTransition = true; - AddTransitionFromAny(transition); - } - - /// - /// Adds a new exit transition from a state that is only checked when the specified trigger - /// is activated. - /// It represents an exit point that allows the fsm to exit and the parent fsm to continue - /// to the next state. It is only checked if the parent fsm has a pending transition. - /// - /// The transition instance. The "to" field can be - /// left empty, as it has no meaning in this context. - public void AddExitTriggerTransition(TEvent trigger, TransitionBase transition) - { - transition.isExitTransition = true; - AddTriggerTransition(trigger, transition); - } - - /// - /// Adds a new exit transition that can happen from any possible state and is only checked - /// when the specified trigger is activated. - /// It represents an exit point that allows the fsm to exit and the parent fsm to continue - /// to the next state. It is only checked if the parent fsm has a pending transition. - /// - /// The transition instance. The "from" and "to" fields can be - /// left empty, as they have no meaning in this context. - public void AddExitTriggerTransitionFromAny(TEvent trigger, TransitionBase transition) - { - transition.isExitTransition = true; - AddTriggerTransitionFromAny(trigger, transition); - } - - /// - /// Activates the specified trigger, checking all targeted trigger transitions to see whether - /// a transition should occur. - /// - /// The name / identifier of the trigger. - /// True when a transition occurred, otherwise false. - private bool TryTrigger(TEvent trigger) - { - EnsureIsInitializedFor("Checking all trigger transitions of the active state"); - - List> triggerTransitions; - - if (triggerTransitionsFromAny.TryGetValue(trigger, out triggerTransitions)) - { - for (int i = 0, count = triggerTransitions.Count; i < count; i++) - { - TransitionBase transition = triggerTransitions[i]; - - if (EqualityComparer.Default.Equals(transition.to, activeState.name)) - continue; - - if (TryTransition(transition)) - return true; - } - } - - if (activeTriggerTransitions.TryGetValue(trigger, out triggerTransitions)) - { - for (int i = 0, count = triggerTransitions.Count; i < count; i++) - { - TransitionBase transition = triggerTransitions[i]; - - if (TryTransition(transition)) - return true; - } - } - - return false; - } - - /// - /// Activates the specified trigger in all active states of the hierarchy, checking all targeted - /// trigger transitions to see whether a transition should occur. - /// - /// The name / identifier of the trigger. - public void Trigger(TEvent trigger) - { - // If a transition occurs, then the trigger should not be activated - // in the new active state, that the state machine just switched to. - if (TryTrigger(trigger)) return; - - (activeState as ITriggerable)?.Trigger(trigger); - } - - /// - /// Only activates the specified trigger locally in this state machine. - /// - /// The name / identifier of the trigger. - public void TriggerLocally(TEvent trigger) - { - TryTrigger(trigger); - } - - /// - /// Runs an action on the currently active state. - /// - /// Name of the action. - public virtual void OnAction(TEvent trigger) - { - EnsureIsInitializedFor("Running OnAction of the active state"); - (activeState as IActionable)?.OnAction(trigger); - } - - /// - /// Runs an action on the currently active state and lets you pass one data parameter. - /// - /// Name of the action. - /// Any custom data for the parameter. - /// Type of the data parameter. - /// Should match the data type of the action that was added via AddAction(...). - public virtual void OnAction(TEvent trigger, TData data) - { - EnsureIsInitializedFor("Running OnAction of the active state"); - (activeState as IActionable)?.OnAction(trigger, data); - } - - public StateBase GetState(TStateId name) - { - StateBundle bundle; - - if (!stateBundlesByName.TryGetValue(name, out bundle) || bundle.state == null) - { - throw UnityHFSM.Exceptions.Common.StateNotFound(name.ToString(), context: "Getting a state"); - } - - return bundle.state; - } - - public StateMachine this[TStateId name] - { - get - { - StateBase state = GetState(name); - StateMachine subFsm = state as StateMachine; - - if (subFsm == null) - { - throw UnityHFSM.Exceptions.Common.QuickIndexerMisusedForGettingState(name.ToString()); - } - - return subFsm; - } - } - - public override string GetActiveHierarchyPath() - { - if (activeState == null) - { - // When the state machine is not active, then the active hierarchy path - // is empty. - return ""; - } - - return $"{name}/{activeState.GetActiveHierarchyPath()}"; - } - } - - // Overloaded classes to allow for an easier usage of the StateMachine for common cases. - // E.g. new StateMachine() instead of new StateMachine() - - public class StateMachine : StateMachine - { - public StateMachine(bool needsExitTime = false, bool isGhostState = false, bool rememberLastState = false) - : base(needsExitTime: needsExitTime, isGhostState: isGhostState, rememberLastState: rememberLastState) - { - } - } - - public class StateMachine : StateMachine - { - public StateMachine(bool needsExitTime = false, bool isGhostState = false, bool rememberLastState = false) - : base(needsExitTime: needsExitTime, isGhostState: isGhostState, rememberLastState: rememberLastState) - { - } - } - - public class StateMachine : StateMachine - { - public StateMachine(bool needsExitTime = false, bool isGhostState = false, bool rememberLastState = false) - : base(needsExitTime: needsExitTime, isGhostState: isGhostState, rememberLastState: rememberLastState) - { - } - } -} +using System; +using System.Collections.Generic; + +/** + * Hierarchical finite state machine for Unity + * by Inspiaaa + * + * Version: 2.1.0 + */ + +namespace UnityHFSM +{ + /// + /// A finite state machine that can also be used as a state of a parent state machine to create + /// a hierarchy (-> hierarchical state machine). + /// + public class StateMachine : + StateBase, + ITriggerable, + IStateMachine, + IActionable + { + /// + /// A bundle of a state together with the outgoing transitions and trigger transitions. + /// It's useful, as you only need to do one Dictionary lookup for these three items. + /// => Much better performance + /// + internal class StateBundle + { + // By default, these fields are all null and only get a value when you need them. + // => Lazy evaluation => Memory efficient, when you only need a subset of features + public StateBase state; + public List> transitions; + public Dictionary>> triggerToTransitions; + + public void AddTransition(TransitionBase t) + { + transitions = transitions ?? new List>(); + transitions.Add(t); + } + + public void AddTriggerTransition(TEvent trigger, TransitionBase transition) + { + triggerToTransitions = triggerToTransitions + ?? new Dictionary>>(); + + List> transitionsOfTrigger; + + if (!triggerToTransitions.TryGetValue(trigger, out transitionsOfTrigger)) + { + transitionsOfTrigger = new List>(); + triggerToTransitions.Add(trigger, transitionsOfTrigger); + } + + transitionsOfTrigger.Add(transition); + } + } + + private struct PendingTransition + { + public TStateId targetState; + + public bool isExitTransition; + + // Optional (may be null), used for callbacks when the transition succeeds. + public ITransitionListener listener; + + // As this type is not nullable (it is a value type), an additional field is required + // to see if the pending transition has been set yet. + public bool isPending; + + public static PendingTransition CreateForExit(ITransitionListener listener = null) + => new PendingTransition { + targetState = default, + isExitTransition = true, + listener = listener, + isPending = true + }; + + public static PendingTransition CreateForState(TStateId target, ITransitionListener listener = null) + => new PendingTransition { + targetState = target, + isExitTransition = false, + listener = listener, + isPending = true + }; + } + + // A cached empty list of transitions (For improved readability, less GC). + private static readonly List> noTransitions + = new List>(0); + private static readonly Dictionary>> noTriggerTransitions + = new Dictionary>>(0); + + /// + /// Event that is raised when the active state changes. + /// + /// + /// It is triggered when the state machine enters its initial state, and after a transition is performed. + /// Note that it is not called when the state machine exits. + /// + public event Action> StateChanged; + + internal (TStateId state, bool hasState) startState = (default, false); + private PendingTransition pendingTransition = default; + private bool rememberLastState = false; + + // Central storage of states. + internal Dictionary stateBundlesByName + = new Dictionary(); + + private StateBase activeState = null; + private List> activeTransitions = noTransitions; + private Dictionary>> activeTriggerTransitions = noTriggerTransitions; + + internal List> transitionsFromAny + = new List>(); + internal Dictionary>> triggerTransitionsFromAny + = new Dictionary>>(); + + public StateBase ActiveState + { + get + { + EnsureIsInitializedFor("Trying to get the active state"); + return activeState; + } + } + public TStateId ActiveStateName => ActiveState.name; + + public IStateMachine ParentFsm => fsm; + + private bool IsRootFsm => fsm == null; + + public bool HasPendingTransition => pendingTransition.isPending; + + /// + /// Initialises a new instance of the StateMachine class. + /// + /// (Only for hierarchical states): + /// Determines whether the state machine as a state of a parent state machine is allowed to instantly + /// exit on a transition (false), or if it should wait until an explicit exit transition occurs. + /// (Only for hierarchical states): + /// If true, the state machine will return to its last active state when it enters, instead + /// of to its original start state. + /// + public StateMachine(bool needsExitTime = false, bool isGhostState = false, bool rememberLastState = false) + : base(needsExitTime: needsExitTime, isGhostState: isGhostState) + { + this.rememberLastState = rememberLastState; + } + + /// + /// Throws an exception if the state machine is not initialised yet. + /// + /// String message for which action the fsm should be initialised for. + private void EnsureIsInitializedFor(string context) + { + if (activeState == null) + throw UnityHFSM.Exceptions.Common.NotInitialized(context); + } + + /// + /// Notifies the state machine that the state can cleanly exit, + /// and if a state change is pending, it will execute it. + /// + public void StateCanExit() + { + if (!pendingTransition.isPending) + return; + + ITransitionListener listener = pendingTransition.listener; + if (pendingTransition.isExitTransition) + { + pendingTransition = default; + + listener?.BeforeTransition(); + PerformVerticalTransition(); + listener?.AfterTransition(); + } + else + { + TStateId state = pendingTransition.targetState; + + // When the pending state is a ghost state, ChangeState() will have + // to try all outgoing transitions, which may overwrite the pendingState. + // That's why it is first cleared, and not afterwards, as that would overwrite + // a new, valid pending state. + pendingTransition = default; + ChangeState(state, listener); + } + } + + /// + /// Instantly changes to the target state. + /// + /// The name / identifier of the active state. + /// Optional object that receives callbacks before and after changing state. + private void ChangeState(TStateId name, ITransitionListener listener = null) + { + listener?.BeforeTransition(); + activeState?.OnExit(); + + StateBundle bundle; + + if (!stateBundlesByName.TryGetValue(name, out bundle) || bundle.state == null) + { + throw UnityHFSM.Exceptions.Common.StateNotFound(name.ToString(), context: "Switching states"); + } + + activeTransitions = bundle.transitions ?? noTransitions; + activeTriggerTransitions = bundle.triggerToTransitions ?? noTriggerTransitions; + + activeState = bundle.state; + activeState.OnEnter(); + + for (int i = 0, count = activeTransitions.Count; i < count; i++) + { + activeTransitions[i].OnEnter(); + } + + foreach (List> transitions in activeTriggerTransitions.Values) + { + for (int i = 0, count = transitions.Count; i < count; i++) + { + transitions[i].OnEnter(); + } + } + + listener?.AfterTransition(); + + StateChanged?.Invoke(activeState); + + if (activeState.isGhostState) + { + TryAllDirectTransitions(); + } + } + + /// + /// Signals to the parent fsm that this fsm can exit which allows the parent + /// fsm to transition to the next state. + /// + private void PerformVerticalTransition() + { + fsm?.StateCanExit(); + } + + /// + /// Requests a state change, respecting the needsExitTime property of the active state. + /// + /// The name / identifier of the target state. + /// Overrides the needsExitTime of the active state if true, + /// therefore forcing an immediate state change. + /// Optional object that receives callbacks before and after the transition. + public void RequestStateChange( + TStateId name, + bool forceInstantly = false, + ITransitionListener listener = null) + { + if (!activeState.needsExitTime || forceInstantly) + { + pendingTransition = default; + ChangeState(name, listener); + } + else + { + pendingTransition = PendingTransition.CreateForState(name, listener); + activeState.OnExitRequest(); + // If it can exit, the activeState would call + // -> state.fsm.StateCanExit() which in turn would call + // -> fsm.ChangeState(...) + } + } + + /// + /// Requests a "vertical transition", allowing the state machine to exit + /// to allow the parent fsm to transition to the next state. It respects the + /// needsExitTime property of the active state. + /// + /// Overrides the needsExitTime of the active state if true, + /// therefore forcing an immediate state change. + /// Optional object that receives callbacks before and after the transition. + public void RequestExit(bool forceInstantly = false, ITransitionListener listener = null) + { + if (!activeState.needsExitTime || forceInstantly) + { + pendingTransition = default; + listener?.BeforeTransition(); + PerformVerticalTransition(); + listener?.AfterTransition(); + } + else + { + pendingTransition = PendingTransition.CreateForExit(listener); + activeState.OnExitRequest(); + } + } + + /// + /// Checks if a transition can take place, and if this is the case, transition to the + /// "to" state and return true. Otherwise it returns false. + /// + private bool TryTransition(TransitionBase transition) + { + if (transition.isExitTransition) + { + if (fsm == null || !fsm.HasPendingTransition || !transition.ShouldTransition()) + return false; + + RequestExit(transition.forceInstantly, transition as ITransitionListener); + return true; + } + else + { + if (!transition.ShouldTransition()) + return false; + + RequestStateChange(transition.to, transition.forceInstantly, transition as ITransitionListener); + return true; + } + } + + /// + /// Tries the "global" transitions that can transition from any state. + /// + /// Returns true if a transition occurred. + private bool TryAllGlobalTransitions() + { + for (int i = 0, count = transitionsFromAny.Count; i < count; i++) + { + TransitionBase transition = transitionsFromAny[i]; + + // Don't transition to the "to" state, if that state is already the active state. + if (EqualityComparer.Default.Equals(transition.to, activeState.name)) + continue; + + if (TryTransition(transition)) + return true; + } + + return false; + } + + /// + /// Tries the "normal" transitions that transition from one specific state to another. + /// + /// Returns true if a transition occurred. + private bool TryAllDirectTransitions() + { + for (int i = 0, count = activeTransitions.Count; i < count; i++) + { + TransitionBase transition = activeTransitions[i]; + + if (TryTransition(transition)) + return true; + } + + return false; + } + + /// + /// Calls OnEnter if it is the root state machine, therefore initialising the state machine. + /// + public override void Init() + { + if (!IsRootFsm) return; + + OnEnter(); + } + + /// + /// Initialises the state machine and must be called before OnLogic is called. + /// It sets the activeState to the selected startState. + /// + public override void OnEnter() + { + if (!startState.hasState) + { + throw UnityHFSM.Exceptions.Common.MissingStartState(context: "Running OnEnter of the state machine."); + } + + // Clear any previous pending transition from the last run. + pendingTransition = default; + + ChangeState(startState.state); + + for (int i = 0, count = transitionsFromAny.Count; i < count; i++) + { + transitionsFromAny[i].OnEnter(); + } + + foreach (List> transitions in triggerTransitionsFromAny.Values) + { + for (int i = 0, count = transitions.Count; i < count; i++) + { + transitions[i].OnEnter(); + } + } + } + + /// + /// Runs one logic step. It does at most one transition itself and + /// calls the active state's logic function (after the state transition, if + /// one occurred). + /// + public override void OnLogic() + { + EnsureIsInitializedFor("Running OnLogic"); + + if (TryAllGlobalTransitions()) + goto runOnLogic; + + if (TryAllDirectTransitions()) + goto runOnLogic; + + runOnLogic: + activeState?.OnLogic(); + } + + public override void OnExit() + { + if (activeState == null) + return; + + if (rememberLastState) + { + startState = (activeState.name, true); + } + + activeState.OnExit(); + // By setting the activeState to null, the state's onExit method won't be called + // a second time when the state machine enters again (and changes to the start state). + activeState = null; + } + + public override void OnExitRequest() + { + if (activeState.needsExitTime) + activeState.OnExitRequest(); + } + + /// + /// Defines the entry point of the state machine. + /// + /// The name / identifier of the start state. + public void SetStartState(TStateId name) + { + startState = (name, true); + } + + /// + /// Gets the StateBundle belonging to the name state "slot" if it exists. + /// Otherwise it will create a new StateBundle, that will be added to the Dictionary, + /// and return the newly created instance. + /// + private StateBundle GetOrCreateStateBundle(TStateId name) + { + StateBundle bundle; + + if (!stateBundlesByName.TryGetValue(name, out bundle)) + { + bundle = new StateBundle(); + stateBundlesByName.Add(name, bundle); + } + + return bundle; + } + + /// + /// Adds a new node / state to the state machine. + /// + /// The name / identifier of the new state. + /// The new state instance, e.g. State, CoState, StateMachine. + public void AddState(TStateId name, StateBase state) + { + state.fsm = this; + state.name = name; + state.Init(); + + StateBundle bundle = GetOrCreateStateBundle(name); + bundle.state = state; + + if (stateBundlesByName.Count == 1 && !startState.hasState) + { + SetStartState(name); + } + } + + /// + /// Initialises a transition, i.e. sets its fsm attribute, and then calls its Init method. + /// + /// + private void InitTransition(TransitionBase transition) + { + transition.fsm = this; + transition.Init(); + } + + /// + /// Adds a new transition between two states. + /// + /// The transition instance. + public void AddTransition(TransitionBase transition) + { + InitTransition(transition); + + StateBundle bundle = GetOrCreateStateBundle(transition.from); + bundle.AddTransition(transition); + } + + /// + /// Adds a new transition that can happen from any possible state. + /// + /// The transition instance; The "from" field can be + /// left empty, as it has no meaning in this context. + public void AddTransitionFromAny(TransitionBase transition) + { + InitTransition(transition); + + transitionsFromAny.Add(transition); + } + + /// + /// Adds a new trigger transition between two states that is only checked + /// when the specified trigger is activated. + /// + /// The name / identifier of the trigger. + /// The transition instance, e.g. Transition, TransitionAfter, ... + public void AddTriggerTransition(TEvent trigger, TransitionBase transition) + { + InitTransition(transition); + + StateBundle bundle = GetOrCreateStateBundle(transition.from); + bundle.AddTriggerTransition(trigger, transition); + } + + /// + /// Adds a new trigger transition that can happen from any possible state, but is only + /// checked when the specified trigger is activated. + /// + /// The name / identifier of the trigger + /// The transition instance; The "from" field can be + /// left empty, as it has no meaning in this context. + public void AddTriggerTransitionFromAny(TEvent trigger, TransitionBase transition) + { + InitTransition(transition); + + List> transitionsOfTrigger; + + if (!triggerTransitionsFromAny.TryGetValue(trigger, out transitionsOfTrigger)) + { + transitionsOfTrigger = new List>(); + triggerTransitionsFromAny.Add(trigger, transitionsOfTrigger); + } + + transitionsOfTrigger.Add(transition); + } + + /// + /// Adds two transitions: + /// If the condition of the transition instance is true, it transitions from the "from" + /// state to the "to" state. Otherwise it performs a transition in the opposite direction, + /// i.e. from "to" to "from". + /// + /// + /// Internally the same transition instance will be used for both transitions + /// by wrapping it in a ReverseTransition. + /// For the reverse transition the afterTransition callback is called before the transition + /// and the onTransition callback afterwards. If this is not desired then replicate the behaviour + /// of the two way transitions by creating two separate transitions. + /// + public void AddTwoWayTransition(TransitionBase transition) + { + InitTransition(transition); + AddTransition(transition); + + ReverseTransition reverse = new ReverseTransition(transition, false); + InitTransition(reverse); + AddTransition(reverse); + } + + /// + /// Adds two transitions that are only checked when the specified trigger is activated: + /// If the condition of the transition instance is true, it transitions from the "from" + /// state to the "to" state. Otherwise it performs a transition in the opposite direction, + /// i.e. from "to" to "from". + /// + /// + /// Internally the same transition instance will be used for both transitions + /// by wrapping it in a ReverseTransition. + /// For the reverse transition the afterTransition callback is called before the transition + /// and the onTransition callback afterwards. If this is not desired then replicate the behaviour + /// of the two way transitions by creating two separate transitions. + /// + public void AddTwoWayTriggerTransition(TEvent trigger, TransitionBase transition) + { + InitTransition(transition); + AddTriggerTransition(trigger, transition); + + ReverseTransition reverse = new ReverseTransition(transition, false); + InitTransition(reverse); + AddTriggerTransition(trigger, reverse); + } + + /// + /// Adds a new exit transition from a state. It represents an exit point that + /// allows the fsm to exit and the parent fsm to continue to the next state. + /// It is only checked if the parent fsm has a pending transition. + /// + /// The transition instance. The "to" field can be + /// left empty, as it has no meaning in this context. + public void AddExitTransition(TransitionBase transition) + { + transition.isExitTransition = true; + AddTransition(transition); + } + + /// + /// Adds a new exit transition that can happen from any possible state. + /// It represents an exit point that allows the fsm to exit and the parent fsm to continue + /// to the next state. It is only checked if the parent fsm has a pending transition. + /// + /// The transition instance. The "from" and "to" fields can be + /// left empty, as they have no meaning in this context. + public void AddExitTransitionFromAny(TransitionBase transition) + { + transition.isExitTransition = true; + AddTransitionFromAny(transition); + } + + /// + /// Adds a new exit transition from a state that is only checked when the specified trigger + /// is activated. + /// It represents an exit point that allows the fsm to exit and the parent fsm to continue + /// to the next state. It is only checked if the parent fsm has a pending transition. + /// + /// The transition instance. The "to" field can be + /// left empty, as it has no meaning in this context. + public void AddExitTriggerTransition(TEvent trigger, TransitionBase transition) + { + transition.isExitTransition = true; + AddTriggerTransition(trigger, transition); + } + + /// + /// Adds a new exit transition that can happen from any possible state and is only checked + /// when the specified trigger is activated. + /// It represents an exit point that allows the fsm to exit and the parent fsm to continue + /// to the next state. It is only checked if the parent fsm has a pending transition. + /// + /// The transition instance. The "from" and "to" fields can be + /// left empty, as they have no meaning in this context. + public void AddExitTriggerTransitionFromAny(TEvent trigger, TransitionBase transition) + { + transition.isExitTransition = true; + AddTriggerTransitionFromAny(trigger, transition); + } + + /// + /// Activates the specified trigger, checking all targeted trigger transitions to see whether + /// a transition should occur. + /// + /// The name / identifier of the trigger. + /// True when a transition occurred, otherwise false. + private bool TryTrigger(TEvent trigger) + { + EnsureIsInitializedFor("Checking all trigger transitions of the active state"); + + List> triggerTransitions; + + if (triggerTransitionsFromAny.TryGetValue(trigger, out triggerTransitions)) + { + for (int i = 0, count = triggerTransitions.Count; i < count; i++) + { + TransitionBase transition = triggerTransitions[i]; + + if (EqualityComparer.Default.Equals(transition.to, activeState.name)) + continue; + + if (TryTransition(transition)) + return true; + } + } + + if (activeTriggerTransitions.TryGetValue(trigger, out triggerTransitions)) + { + for (int i = 0, count = triggerTransitions.Count; i < count; i++) + { + TransitionBase transition = triggerTransitions[i]; + + if (TryTransition(transition)) + return true; + } + } + + return false; + } + + /// + /// Activates the specified trigger in all active states of the hierarchy, checking all targeted + /// trigger transitions to see whether a transition should occur. + /// + /// The name / identifier of the trigger. + public void Trigger(TEvent trigger) + { + // If a transition occurs, then the trigger should not be activated + // in the new active state, that the state machine just switched to. + if (TryTrigger(trigger)) return; + + (activeState as ITriggerable)?.Trigger(trigger); + } + + /// + /// Only activates the specified trigger locally in this state machine. + /// + /// The name / identifier of the trigger. + public void TriggerLocally(TEvent trigger) + { + TryTrigger(trigger); + } + + /// + /// Runs an action on the currently active state. + /// + /// Name of the action. + public virtual void OnAction(TEvent trigger) + { + EnsureIsInitializedFor("Running OnAction of the active state"); + (activeState as IActionable)?.OnAction(trigger); + } + + /// + /// Runs an action on the currently active state and lets you pass one data parameter. + /// + /// Name of the action. + /// Any custom data for the parameter. + /// Type of the data parameter. + /// Should match the data type of the action that was added via AddAction(...). + public virtual void OnAction(TEvent trigger, TData data) + { + EnsureIsInitializedFor("Running OnAction of the active state"); + (activeState as IActionable)?.OnAction(trigger, data); + } + + public StateBase GetState(TStateId name) + { + StateBundle bundle; + + if (!stateBundlesByName.TryGetValue(name, out bundle) || bundle.state == null) + { + throw UnityHFSM.Exceptions.Common.StateNotFound(name.ToString(), context: "Getting a state"); + } + + return bundle.state; + } + + public StateMachine this[TStateId name] + { + get + { + StateBase state = GetState(name); + StateMachine subFsm = state as StateMachine; + + if (subFsm == null) + { + throw UnityHFSM.Exceptions.Common.QuickIndexerMisusedForGettingState(name.ToString()); + } + + return subFsm; + } + } + + public override string GetActiveHierarchyPath() + { + if (activeState == null) + { + // When the state machine is not active, then the active hierarchy path + // is empty. + return ""; + } + + return $"{name}/{activeState.GetActiveHierarchyPath()}"; + } + } + + // Overloaded classes to allow for an easier usage of the StateMachine for common cases. + // E.g. new StateMachine() instead of new StateMachine() + + public class StateMachine : StateMachine + { + public StateMachine(bool needsExitTime = false, bool isGhostState = false, bool rememberLastState = false) + : base(needsExitTime: needsExitTime, isGhostState: isGhostState, rememberLastState: rememberLastState) + { + } + } + + public class StateMachine : StateMachine + { + public StateMachine(bool needsExitTime = false, bool isGhostState = false, bool rememberLastState = false) + : base(needsExitTime: needsExitTime, isGhostState: isGhostState, rememberLastState: rememberLastState) + { + } + } + + public class StateMachine : StateMachine + { + public StateMachine(bool needsExitTime = false, bool isGhostState = false, bool rememberLastState = false) + : base(needsExitTime: needsExitTime, isGhostState: isGhostState, rememberLastState: rememberLastState) + { + } + } +} \ No newline at end of file From 665b72143b70aa72ce0bb841075a6cec8aa4ce62 Mon Sep 17 00:00:00 2001 From: Sander Grindheim Date: Mon, 5 Aug 2024 23:49:15 -0700 Subject: [PATCH 2/3] corrected line endings to lf --- src/StateMachine/StateMachine.cs | 1624 +++++++++++++++--------------- 1 file changed, 812 insertions(+), 812 deletions(-) diff --git a/src/StateMachine/StateMachine.cs b/src/StateMachine/StateMachine.cs index 7f9f138..2f93ed0 100644 --- a/src/StateMachine/StateMachine.cs +++ b/src/StateMachine/StateMachine.cs @@ -1,813 +1,813 @@ -using System; -using System.Collections.Generic; - -/** - * Hierarchical finite state machine for Unity - * by Inspiaaa - * - * Version: 2.1.0 - */ - -namespace UnityHFSM -{ - /// - /// A finite state machine that can also be used as a state of a parent state machine to create - /// a hierarchy (-> hierarchical state machine). - /// - public class StateMachine : - StateBase, - ITriggerable, - IStateMachine, - IActionable - { - /// - /// A bundle of a state together with the outgoing transitions and trigger transitions. - /// It's useful, as you only need to do one Dictionary lookup for these three items. - /// => Much better performance - /// - internal class StateBundle - { - // By default, these fields are all null and only get a value when you need them. - // => Lazy evaluation => Memory efficient, when you only need a subset of features - public StateBase state; - public List> transitions; - public Dictionary>> triggerToTransitions; - - public void AddTransition(TransitionBase t) - { - transitions = transitions ?? new List>(); - transitions.Add(t); - } - - public void AddTriggerTransition(TEvent trigger, TransitionBase transition) - { - triggerToTransitions = triggerToTransitions - ?? new Dictionary>>(); - - List> transitionsOfTrigger; - - if (!triggerToTransitions.TryGetValue(trigger, out transitionsOfTrigger)) - { - transitionsOfTrigger = new List>(); - triggerToTransitions.Add(trigger, transitionsOfTrigger); - } - - transitionsOfTrigger.Add(transition); - } - } - - private struct PendingTransition - { - public TStateId targetState; - - public bool isExitTransition; - - // Optional (may be null), used for callbacks when the transition succeeds. - public ITransitionListener listener; - - // As this type is not nullable (it is a value type), an additional field is required - // to see if the pending transition has been set yet. - public bool isPending; - - public static PendingTransition CreateForExit(ITransitionListener listener = null) - => new PendingTransition { - targetState = default, - isExitTransition = true, - listener = listener, - isPending = true - }; - - public static PendingTransition CreateForState(TStateId target, ITransitionListener listener = null) - => new PendingTransition { - targetState = target, - isExitTransition = false, - listener = listener, - isPending = true - }; - } - - // A cached empty list of transitions (For improved readability, less GC). - private static readonly List> noTransitions - = new List>(0); - private static readonly Dictionary>> noTriggerTransitions - = new Dictionary>>(0); - - /// - /// Event that is raised when the active state changes. - /// - /// - /// It is triggered when the state machine enters its initial state, and after a transition is performed. - /// Note that it is not called when the state machine exits. - /// - public event Action> StateChanged; - - internal (TStateId state, bool hasState) startState = (default, false); - private PendingTransition pendingTransition = default; - private bool rememberLastState = false; - - // Central storage of states. - internal Dictionary stateBundlesByName - = new Dictionary(); - - private StateBase activeState = null; - private List> activeTransitions = noTransitions; - private Dictionary>> activeTriggerTransitions = noTriggerTransitions; - - internal List> transitionsFromAny - = new List>(); - internal Dictionary>> triggerTransitionsFromAny - = new Dictionary>>(); - - public StateBase ActiveState - { - get - { - EnsureIsInitializedFor("Trying to get the active state"); - return activeState; - } - } - public TStateId ActiveStateName => ActiveState.name; - - public IStateMachine ParentFsm => fsm; - - private bool IsRootFsm => fsm == null; - - public bool HasPendingTransition => pendingTransition.isPending; - - /// - /// Initialises a new instance of the StateMachine class. - /// - /// (Only for hierarchical states): - /// Determines whether the state machine as a state of a parent state machine is allowed to instantly - /// exit on a transition (false), or if it should wait until an explicit exit transition occurs. - /// (Only for hierarchical states): - /// If true, the state machine will return to its last active state when it enters, instead - /// of to its original start state. - /// - public StateMachine(bool needsExitTime = false, bool isGhostState = false, bool rememberLastState = false) - : base(needsExitTime: needsExitTime, isGhostState: isGhostState) - { - this.rememberLastState = rememberLastState; - } - - /// - /// Throws an exception if the state machine is not initialised yet. - /// - /// String message for which action the fsm should be initialised for. - private void EnsureIsInitializedFor(string context) - { - if (activeState == null) - throw UnityHFSM.Exceptions.Common.NotInitialized(context); - } - - /// - /// Notifies the state machine that the state can cleanly exit, - /// and if a state change is pending, it will execute it. - /// - public void StateCanExit() - { - if (!pendingTransition.isPending) - return; - - ITransitionListener listener = pendingTransition.listener; - if (pendingTransition.isExitTransition) - { - pendingTransition = default; - - listener?.BeforeTransition(); - PerformVerticalTransition(); - listener?.AfterTransition(); - } - else - { - TStateId state = pendingTransition.targetState; - - // When the pending state is a ghost state, ChangeState() will have - // to try all outgoing transitions, which may overwrite the pendingState. - // That's why it is first cleared, and not afterwards, as that would overwrite - // a new, valid pending state. - pendingTransition = default; - ChangeState(state, listener); - } - } - - /// - /// Instantly changes to the target state. - /// - /// The name / identifier of the active state. - /// Optional object that receives callbacks before and after changing state. - private void ChangeState(TStateId name, ITransitionListener listener = null) - { - listener?.BeforeTransition(); - activeState?.OnExit(); - - StateBundle bundle; - - if (!stateBundlesByName.TryGetValue(name, out bundle) || bundle.state == null) - { - throw UnityHFSM.Exceptions.Common.StateNotFound(name.ToString(), context: "Switching states"); - } - - activeTransitions = bundle.transitions ?? noTransitions; - activeTriggerTransitions = bundle.triggerToTransitions ?? noTriggerTransitions; - - activeState = bundle.state; - activeState.OnEnter(); - - for (int i = 0, count = activeTransitions.Count; i < count; i++) - { - activeTransitions[i].OnEnter(); - } - - foreach (List> transitions in activeTriggerTransitions.Values) - { - for (int i = 0, count = transitions.Count; i < count; i++) - { - transitions[i].OnEnter(); - } - } - - listener?.AfterTransition(); - - StateChanged?.Invoke(activeState); - - if (activeState.isGhostState) - { - TryAllDirectTransitions(); - } - } - - /// - /// Signals to the parent fsm that this fsm can exit which allows the parent - /// fsm to transition to the next state. - /// - private void PerformVerticalTransition() - { - fsm?.StateCanExit(); - } - - /// - /// Requests a state change, respecting the needsExitTime property of the active state. - /// - /// The name / identifier of the target state. - /// Overrides the needsExitTime of the active state if true, - /// therefore forcing an immediate state change. - /// Optional object that receives callbacks before and after the transition. - public void RequestStateChange( - TStateId name, - bool forceInstantly = false, - ITransitionListener listener = null) - { - if (!activeState.needsExitTime || forceInstantly) - { - pendingTransition = default; - ChangeState(name, listener); - } - else - { - pendingTransition = PendingTransition.CreateForState(name, listener); - activeState.OnExitRequest(); - // If it can exit, the activeState would call - // -> state.fsm.StateCanExit() which in turn would call - // -> fsm.ChangeState(...) - } - } - - /// - /// Requests a "vertical transition", allowing the state machine to exit - /// to allow the parent fsm to transition to the next state. It respects the - /// needsExitTime property of the active state. - /// - /// Overrides the needsExitTime of the active state if true, - /// therefore forcing an immediate state change. - /// Optional object that receives callbacks before and after the transition. - public void RequestExit(bool forceInstantly = false, ITransitionListener listener = null) - { - if (!activeState.needsExitTime || forceInstantly) - { - pendingTransition = default; - listener?.BeforeTransition(); - PerformVerticalTransition(); - listener?.AfterTransition(); - } - else - { - pendingTransition = PendingTransition.CreateForExit(listener); - activeState.OnExitRequest(); - } - } - - /// - /// Checks if a transition can take place, and if this is the case, transition to the - /// "to" state and return true. Otherwise it returns false. - /// - private bool TryTransition(TransitionBase transition) - { - if (transition.isExitTransition) - { - if (fsm == null || !fsm.HasPendingTransition || !transition.ShouldTransition()) - return false; - - RequestExit(transition.forceInstantly, transition as ITransitionListener); - return true; - } - else - { - if (!transition.ShouldTransition()) - return false; - - RequestStateChange(transition.to, transition.forceInstantly, transition as ITransitionListener); - return true; - } - } - - /// - /// Tries the "global" transitions that can transition from any state. - /// - /// Returns true if a transition occurred. - private bool TryAllGlobalTransitions() - { - for (int i = 0, count = transitionsFromAny.Count; i < count; i++) - { - TransitionBase transition = transitionsFromAny[i]; - - // Don't transition to the "to" state, if that state is already the active state. - if (EqualityComparer.Default.Equals(transition.to, activeState.name)) - continue; - - if (TryTransition(transition)) - return true; - } - - return false; - } - - /// - /// Tries the "normal" transitions that transition from one specific state to another. - /// - /// Returns true if a transition occurred. - private bool TryAllDirectTransitions() - { - for (int i = 0, count = activeTransitions.Count; i < count; i++) - { - TransitionBase transition = activeTransitions[i]; - - if (TryTransition(transition)) - return true; - } - - return false; - } - - /// - /// Calls OnEnter if it is the root state machine, therefore initialising the state machine. - /// - public override void Init() - { - if (!IsRootFsm) return; - - OnEnter(); - } - - /// - /// Initialises the state machine and must be called before OnLogic is called. - /// It sets the activeState to the selected startState. - /// - public override void OnEnter() - { - if (!startState.hasState) - { - throw UnityHFSM.Exceptions.Common.MissingStartState(context: "Running OnEnter of the state machine."); - } - - // Clear any previous pending transition from the last run. - pendingTransition = default; - - ChangeState(startState.state); - - for (int i = 0, count = transitionsFromAny.Count; i < count; i++) - { - transitionsFromAny[i].OnEnter(); - } - - foreach (List> transitions in triggerTransitionsFromAny.Values) - { - for (int i = 0, count = transitions.Count; i < count; i++) - { - transitions[i].OnEnter(); - } - } - } - - /// - /// Runs one logic step. It does at most one transition itself and - /// calls the active state's logic function (after the state transition, if - /// one occurred). - /// - public override void OnLogic() - { - EnsureIsInitializedFor("Running OnLogic"); - - if (TryAllGlobalTransitions()) - goto runOnLogic; - - if (TryAllDirectTransitions()) - goto runOnLogic; - - runOnLogic: - activeState?.OnLogic(); - } - - public override void OnExit() - { - if (activeState == null) - return; - - if (rememberLastState) - { - startState = (activeState.name, true); - } - - activeState.OnExit(); - // By setting the activeState to null, the state's onExit method won't be called - // a second time when the state machine enters again (and changes to the start state). - activeState = null; - } - - public override void OnExitRequest() - { - if (activeState.needsExitTime) - activeState.OnExitRequest(); - } - - /// - /// Defines the entry point of the state machine. - /// - /// The name / identifier of the start state. - public void SetStartState(TStateId name) - { - startState = (name, true); - } - - /// - /// Gets the StateBundle belonging to the name state "slot" if it exists. - /// Otherwise it will create a new StateBundle, that will be added to the Dictionary, - /// and return the newly created instance. - /// - private StateBundle GetOrCreateStateBundle(TStateId name) - { - StateBundle bundle; - - if (!stateBundlesByName.TryGetValue(name, out bundle)) - { - bundle = new StateBundle(); - stateBundlesByName.Add(name, bundle); - } - - return bundle; - } - - /// - /// Adds a new node / state to the state machine. - /// - /// The name / identifier of the new state. - /// The new state instance, e.g. State, CoState, StateMachine. - public void AddState(TStateId name, StateBase state) - { - state.fsm = this; - state.name = name; - state.Init(); - - StateBundle bundle = GetOrCreateStateBundle(name); - bundle.state = state; - - if (stateBundlesByName.Count == 1 && !startState.hasState) - { - SetStartState(name); - } - } - - /// - /// Initialises a transition, i.e. sets its fsm attribute, and then calls its Init method. - /// - /// - private void InitTransition(TransitionBase transition) - { - transition.fsm = this; - transition.Init(); - } - - /// - /// Adds a new transition between two states. - /// - /// The transition instance. - public void AddTransition(TransitionBase transition) - { - InitTransition(transition); - - StateBundle bundle = GetOrCreateStateBundle(transition.from); - bundle.AddTransition(transition); - } - - /// - /// Adds a new transition that can happen from any possible state. - /// - /// The transition instance; The "from" field can be - /// left empty, as it has no meaning in this context. - public void AddTransitionFromAny(TransitionBase transition) - { - InitTransition(transition); - - transitionsFromAny.Add(transition); - } - - /// - /// Adds a new trigger transition between two states that is only checked - /// when the specified trigger is activated. - /// - /// The name / identifier of the trigger. - /// The transition instance, e.g. Transition, TransitionAfter, ... - public void AddTriggerTransition(TEvent trigger, TransitionBase transition) - { - InitTransition(transition); - - StateBundle bundle = GetOrCreateStateBundle(transition.from); - bundle.AddTriggerTransition(trigger, transition); - } - - /// - /// Adds a new trigger transition that can happen from any possible state, but is only - /// checked when the specified trigger is activated. - /// - /// The name / identifier of the trigger - /// The transition instance; The "from" field can be - /// left empty, as it has no meaning in this context. - public void AddTriggerTransitionFromAny(TEvent trigger, TransitionBase transition) - { - InitTransition(transition); - - List> transitionsOfTrigger; - - if (!triggerTransitionsFromAny.TryGetValue(trigger, out transitionsOfTrigger)) - { - transitionsOfTrigger = new List>(); - triggerTransitionsFromAny.Add(trigger, transitionsOfTrigger); - } - - transitionsOfTrigger.Add(transition); - } - - /// - /// Adds two transitions: - /// If the condition of the transition instance is true, it transitions from the "from" - /// state to the "to" state. Otherwise it performs a transition in the opposite direction, - /// i.e. from "to" to "from". - /// - /// - /// Internally the same transition instance will be used for both transitions - /// by wrapping it in a ReverseTransition. - /// For the reverse transition the afterTransition callback is called before the transition - /// and the onTransition callback afterwards. If this is not desired then replicate the behaviour - /// of the two way transitions by creating two separate transitions. - /// - public void AddTwoWayTransition(TransitionBase transition) - { - InitTransition(transition); - AddTransition(transition); - - ReverseTransition reverse = new ReverseTransition(transition, false); - InitTransition(reverse); - AddTransition(reverse); - } - - /// - /// Adds two transitions that are only checked when the specified trigger is activated: - /// If the condition of the transition instance is true, it transitions from the "from" - /// state to the "to" state. Otherwise it performs a transition in the opposite direction, - /// i.e. from "to" to "from". - /// - /// - /// Internally the same transition instance will be used for both transitions - /// by wrapping it in a ReverseTransition. - /// For the reverse transition the afterTransition callback is called before the transition - /// and the onTransition callback afterwards. If this is not desired then replicate the behaviour - /// of the two way transitions by creating two separate transitions. - /// - public void AddTwoWayTriggerTransition(TEvent trigger, TransitionBase transition) - { - InitTransition(transition); - AddTriggerTransition(trigger, transition); - - ReverseTransition reverse = new ReverseTransition(transition, false); - InitTransition(reverse); - AddTriggerTransition(trigger, reverse); - } - - /// - /// Adds a new exit transition from a state. It represents an exit point that - /// allows the fsm to exit and the parent fsm to continue to the next state. - /// It is only checked if the parent fsm has a pending transition. - /// - /// The transition instance. The "to" field can be - /// left empty, as it has no meaning in this context. - public void AddExitTransition(TransitionBase transition) - { - transition.isExitTransition = true; - AddTransition(transition); - } - - /// - /// Adds a new exit transition that can happen from any possible state. - /// It represents an exit point that allows the fsm to exit and the parent fsm to continue - /// to the next state. It is only checked if the parent fsm has a pending transition. - /// - /// The transition instance. The "from" and "to" fields can be - /// left empty, as they have no meaning in this context. - public void AddExitTransitionFromAny(TransitionBase transition) - { - transition.isExitTransition = true; - AddTransitionFromAny(transition); - } - - /// - /// Adds a new exit transition from a state that is only checked when the specified trigger - /// is activated. - /// It represents an exit point that allows the fsm to exit and the parent fsm to continue - /// to the next state. It is only checked if the parent fsm has a pending transition. - /// - /// The transition instance. The "to" field can be - /// left empty, as it has no meaning in this context. - public void AddExitTriggerTransition(TEvent trigger, TransitionBase transition) - { - transition.isExitTransition = true; - AddTriggerTransition(trigger, transition); - } - - /// - /// Adds a new exit transition that can happen from any possible state and is only checked - /// when the specified trigger is activated. - /// It represents an exit point that allows the fsm to exit and the parent fsm to continue - /// to the next state. It is only checked if the parent fsm has a pending transition. - /// - /// The transition instance. The "from" and "to" fields can be - /// left empty, as they have no meaning in this context. - public void AddExitTriggerTransitionFromAny(TEvent trigger, TransitionBase transition) - { - transition.isExitTransition = true; - AddTriggerTransitionFromAny(trigger, transition); - } - - /// - /// Activates the specified trigger, checking all targeted trigger transitions to see whether - /// a transition should occur. - /// - /// The name / identifier of the trigger. - /// True when a transition occurred, otherwise false. - private bool TryTrigger(TEvent trigger) - { - EnsureIsInitializedFor("Checking all trigger transitions of the active state"); - - List> triggerTransitions; - - if (triggerTransitionsFromAny.TryGetValue(trigger, out triggerTransitions)) - { - for (int i = 0, count = triggerTransitions.Count; i < count; i++) - { - TransitionBase transition = triggerTransitions[i]; - - if (EqualityComparer.Default.Equals(transition.to, activeState.name)) - continue; - - if (TryTransition(transition)) - return true; - } - } - - if (activeTriggerTransitions.TryGetValue(trigger, out triggerTransitions)) - { - for (int i = 0, count = triggerTransitions.Count; i < count; i++) - { - TransitionBase transition = triggerTransitions[i]; - - if (TryTransition(transition)) - return true; - } - } - - return false; - } - - /// - /// Activates the specified trigger in all active states of the hierarchy, checking all targeted - /// trigger transitions to see whether a transition should occur. - /// - /// The name / identifier of the trigger. - public void Trigger(TEvent trigger) - { - // If a transition occurs, then the trigger should not be activated - // in the new active state, that the state machine just switched to. - if (TryTrigger(trigger)) return; - - (activeState as ITriggerable)?.Trigger(trigger); - } - - /// - /// Only activates the specified trigger locally in this state machine. - /// - /// The name / identifier of the trigger. - public void TriggerLocally(TEvent trigger) - { - TryTrigger(trigger); - } - - /// - /// Runs an action on the currently active state. - /// - /// Name of the action. - public virtual void OnAction(TEvent trigger) - { - EnsureIsInitializedFor("Running OnAction of the active state"); - (activeState as IActionable)?.OnAction(trigger); - } - - /// - /// Runs an action on the currently active state and lets you pass one data parameter. - /// - /// Name of the action. - /// Any custom data for the parameter. - /// Type of the data parameter. - /// Should match the data type of the action that was added via AddAction(...). - public virtual void OnAction(TEvent trigger, TData data) - { - EnsureIsInitializedFor("Running OnAction of the active state"); - (activeState as IActionable)?.OnAction(trigger, data); - } - - public StateBase GetState(TStateId name) - { - StateBundle bundle; - - if (!stateBundlesByName.TryGetValue(name, out bundle) || bundle.state == null) - { - throw UnityHFSM.Exceptions.Common.StateNotFound(name.ToString(), context: "Getting a state"); - } - - return bundle.state; - } - - public StateMachine this[TStateId name] - { - get - { - StateBase state = GetState(name); - StateMachine subFsm = state as StateMachine; - - if (subFsm == null) - { - throw UnityHFSM.Exceptions.Common.QuickIndexerMisusedForGettingState(name.ToString()); - } - - return subFsm; - } - } - - public override string GetActiveHierarchyPath() - { - if (activeState == null) - { - // When the state machine is not active, then the active hierarchy path - // is empty. - return ""; - } - - return $"{name}/{activeState.GetActiveHierarchyPath()}"; - } - } - - // Overloaded classes to allow for an easier usage of the StateMachine for common cases. - // E.g. new StateMachine() instead of new StateMachine() - - public class StateMachine : StateMachine - { - public StateMachine(bool needsExitTime = false, bool isGhostState = false, bool rememberLastState = false) - : base(needsExitTime: needsExitTime, isGhostState: isGhostState, rememberLastState: rememberLastState) - { - } - } - - public class StateMachine : StateMachine - { - public StateMachine(bool needsExitTime = false, bool isGhostState = false, bool rememberLastState = false) - : base(needsExitTime: needsExitTime, isGhostState: isGhostState, rememberLastState: rememberLastState) - { - } - } - - public class StateMachine : StateMachine - { - public StateMachine(bool needsExitTime = false, bool isGhostState = false, bool rememberLastState = false) - : base(needsExitTime: needsExitTime, isGhostState: isGhostState, rememberLastState: rememberLastState) - { - } - } +using System; +using System.Collections.Generic; + +/** + * Hierarchical finite state machine for Unity + * by Inspiaaa + * + * Version: 2.1.0 + */ + +namespace UnityHFSM +{ + /// + /// A finite state machine that can also be used as a state of a parent state machine to create + /// a hierarchy (-> hierarchical state machine). + /// + public class StateMachine : + StateBase, + ITriggerable, + IStateMachine, + IActionable + { + /// + /// A bundle of a state together with the outgoing transitions and trigger transitions. + /// It's useful, as you only need to do one Dictionary lookup for these three items. + /// => Much better performance + /// + internal class StateBundle + { + // By default, these fields are all null and only get a value when you need them. + // => Lazy evaluation => Memory efficient, when you only need a subset of features + public StateBase state; + public List> transitions; + public Dictionary>> triggerToTransitions; + + public void AddTransition(TransitionBase t) + { + transitions = transitions ?? new List>(); + transitions.Add(t); + } + + public void AddTriggerTransition(TEvent trigger, TransitionBase transition) + { + triggerToTransitions = triggerToTransitions + ?? new Dictionary>>(); + + List> transitionsOfTrigger; + + if (!triggerToTransitions.TryGetValue(trigger, out transitionsOfTrigger)) + { + transitionsOfTrigger = new List>(); + triggerToTransitions.Add(trigger, transitionsOfTrigger); + } + + transitionsOfTrigger.Add(transition); + } + } + + private struct PendingTransition + { + public TStateId targetState; + + public bool isExitTransition; + + // Optional (may be null), used for callbacks when the transition succeeds. + public ITransitionListener listener; + + // As this type is not nullable (it is a value type), an additional field is required + // to see if the pending transition has been set yet. + public bool isPending; + + public static PendingTransition CreateForExit(ITransitionListener listener = null) + => new PendingTransition { + targetState = default, + isExitTransition = true, + listener = listener, + isPending = true + }; + + public static PendingTransition CreateForState(TStateId target, ITransitionListener listener = null) + => new PendingTransition { + targetState = target, + isExitTransition = false, + listener = listener, + isPending = true + }; + } + + // A cached empty list of transitions (For improved readability, less GC). + private static readonly List> noTransitions + = new List>(0); + private static readonly Dictionary>> noTriggerTransitions + = new Dictionary>>(0); + + /// + /// Event that is raised when the active state changes. + /// + /// + /// It is triggered when the state machine enters its initial state, and after a transition is performed. + /// Note that it is not called when the state machine exits. + /// + public event Action> StateChanged; + + internal (TStateId state, bool hasState) startState = (default, false); + private PendingTransition pendingTransition = default; + private bool rememberLastState = false; + + // Central storage of states. + internal Dictionary stateBundlesByName + = new Dictionary(); + + private StateBase activeState = null; + private List> activeTransitions = noTransitions; + private Dictionary>> activeTriggerTransitions = noTriggerTransitions; + + internal List> transitionsFromAny + = new List>(); + internal Dictionary>> triggerTransitionsFromAny + = new Dictionary>>(); + + public StateBase ActiveState + { + get + { + EnsureIsInitializedFor("Trying to get the active state"); + return activeState; + } + } + public TStateId ActiveStateName => ActiveState.name; + + public IStateMachine ParentFsm => fsm; + + private bool IsRootFsm => fsm == null; + + public bool HasPendingTransition => pendingTransition.isPending; + + /// + /// Initialises a new instance of the StateMachine class. + /// + /// (Only for hierarchical states): + /// Determines whether the state machine as a state of a parent state machine is allowed to instantly + /// exit on a transition (false), or if it should wait until an explicit exit transition occurs. + /// (Only for hierarchical states): + /// If true, the state machine will return to its last active state when it enters, instead + /// of to its original start state. + /// + public StateMachine(bool needsExitTime = false, bool isGhostState = false, bool rememberLastState = false) + : base(needsExitTime: needsExitTime, isGhostState: isGhostState) + { + this.rememberLastState = rememberLastState; + } + + /// + /// Throws an exception if the state machine is not initialised yet. + /// + /// String message for which action the fsm should be initialised for. + private void EnsureIsInitializedFor(string context) + { + if (activeState == null) + throw UnityHFSM.Exceptions.Common.NotInitialized(context); + } + + /// + /// Notifies the state machine that the state can cleanly exit, + /// and if a state change is pending, it will execute it. + /// + public void StateCanExit() + { + if (!pendingTransition.isPending) + return; + + ITransitionListener listener = pendingTransition.listener; + if (pendingTransition.isExitTransition) + { + pendingTransition = default; + + listener?.BeforeTransition(); + PerformVerticalTransition(); + listener?.AfterTransition(); + } + else + { + TStateId state = pendingTransition.targetState; + + // When the pending state is a ghost state, ChangeState() will have + // to try all outgoing transitions, which may overwrite the pendingState. + // That's why it is first cleared, and not afterwards, as that would overwrite + // a new, valid pending state. + pendingTransition = default; + ChangeState(state, listener); + } + } + + /// + /// Instantly changes to the target state. + /// + /// The name / identifier of the active state. + /// Optional object that receives callbacks before and after changing state. + private void ChangeState(TStateId name, ITransitionListener listener = null) + { + listener?.BeforeTransition(); + activeState?.OnExit(); + + StateBundle bundle; + + if (!stateBundlesByName.TryGetValue(name, out bundle) || bundle.state == null) + { + throw UnityHFSM.Exceptions.Common.StateNotFound(name.ToString(), context: "Switching states"); + } + + activeTransitions = bundle.transitions ?? noTransitions; + activeTriggerTransitions = bundle.triggerToTransitions ?? noTriggerTransitions; + + activeState = bundle.state; + activeState.OnEnter(); + + for (int i = 0, count = activeTransitions.Count; i < count; i++) + { + activeTransitions[i].OnEnter(); + } + + foreach (List> transitions in activeTriggerTransitions.Values) + { + for (int i = 0, count = transitions.Count; i < count; i++) + { + transitions[i].OnEnter(); + } + } + + listener?.AfterTransition(); + + StateChanged?.Invoke(activeState); + + if (activeState.isGhostState) + { + TryAllDirectTransitions(); + } + } + + /// + /// Signals to the parent fsm that this fsm can exit which allows the parent + /// fsm to transition to the next state. + /// + private void PerformVerticalTransition() + { + fsm?.StateCanExit(); + } + + /// + /// Requests a state change, respecting the needsExitTime property of the active state. + /// + /// The name / identifier of the target state. + /// Overrides the needsExitTime of the active state if true, + /// therefore forcing an immediate state change. + /// Optional object that receives callbacks before and after the transition. + public void RequestStateChange( + TStateId name, + bool forceInstantly = false, + ITransitionListener listener = null) + { + if (!activeState.needsExitTime || forceInstantly) + { + pendingTransition = default; + ChangeState(name, listener); + } + else + { + pendingTransition = PendingTransition.CreateForState(name, listener); + activeState.OnExitRequest(); + // If it can exit, the activeState would call + // -> state.fsm.StateCanExit() which in turn would call + // -> fsm.ChangeState(...) + } + } + + /// + /// Requests a "vertical transition", allowing the state machine to exit + /// to allow the parent fsm to transition to the next state. It respects the + /// needsExitTime property of the active state. + /// + /// Overrides the needsExitTime of the active state if true, + /// therefore forcing an immediate state change. + /// Optional object that receives callbacks before and after the transition. + public void RequestExit(bool forceInstantly = false, ITransitionListener listener = null) + { + if (!activeState.needsExitTime || forceInstantly) + { + pendingTransition = default; + listener?.BeforeTransition(); + PerformVerticalTransition(); + listener?.AfterTransition(); + } + else + { + pendingTransition = PendingTransition.CreateForExit(listener); + activeState.OnExitRequest(); + } + } + + /// + /// Checks if a transition can take place, and if this is the case, transition to the + /// "to" state and return true. Otherwise it returns false. + /// + private bool TryTransition(TransitionBase transition) + { + if (transition.isExitTransition) + { + if (fsm == null || !fsm.HasPendingTransition || !transition.ShouldTransition()) + return false; + + RequestExit(transition.forceInstantly, transition as ITransitionListener); + return true; + } + else + { + if (!transition.ShouldTransition()) + return false; + + RequestStateChange(transition.to, transition.forceInstantly, transition as ITransitionListener); + return true; + } + } + + /// + /// Tries the "global" transitions that can transition from any state. + /// + /// Returns true if a transition occurred. + private bool TryAllGlobalTransitions() + { + for (int i = 0, count = transitionsFromAny.Count; i < count; i++) + { + TransitionBase transition = transitionsFromAny[i]; + + // Don't transition to the "to" state, if that state is already the active state. + if (EqualityComparer.Default.Equals(transition.to, activeState.name)) + continue; + + if (TryTransition(transition)) + return true; + } + + return false; + } + + /// + /// Tries the "normal" transitions that transition from one specific state to another. + /// + /// Returns true if a transition occurred. + private bool TryAllDirectTransitions() + { + for (int i = 0, count = activeTransitions.Count; i < count; i++) + { + TransitionBase transition = activeTransitions[i]; + + if (TryTransition(transition)) + return true; + } + + return false; + } + + /// + /// Calls OnEnter if it is the root state machine, therefore initialising the state machine. + /// + public override void Init() + { + if (!IsRootFsm) return; + + OnEnter(); + } + + /// + /// Initialises the state machine and must be called before OnLogic is called. + /// It sets the activeState to the selected startState. + /// + public override void OnEnter() + { + if (!startState.hasState) + { + throw UnityHFSM.Exceptions.Common.MissingStartState(context: "Running OnEnter of the state machine."); + } + + // Clear any previous pending transition from the last run. + pendingTransition = default; + + ChangeState(startState.state); + + for (int i = 0, count = transitionsFromAny.Count; i < count; i++) + { + transitionsFromAny[i].OnEnter(); + } + + foreach (List> transitions in triggerTransitionsFromAny.Values) + { + for (int i = 0, count = transitions.Count; i < count; i++) + { + transitions[i].OnEnter(); + } + } + } + + /// + /// Runs one logic step. It does at most one transition itself and + /// calls the active state's logic function (after the state transition, if + /// one occurred). + /// + public override void OnLogic() + { + EnsureIsInitializedFor("Running OnLogic"); + + if (TryAllGlobalTransitions()) + goto runOnLogic; + + if (TryAllDirectTransitions()) + goto runOnLogic; + + runOnLogic: + activeState?.OnLogic(); + } + + public override void OnExit() + { + if (activeState == null) + return; + + if (rememberLastState) + { + startState = (activeState.name, true); + } + + activeState.OnExit(); + // By setting the activeState to null, the state's onExit method won't be called + // a second time when the state machine enters again (and changes to the start state). + activeState = null; + } + + public override void OnExitRequest() + { + if (activeState.needsExitTime) + activeState.OnExitRequest(); + } + + /// + /// Defines the entry point of the state machine. + /// + /// The name / identifier of the start state. + public void SetStartState(TStateId name) + { + startState = (name, true); + } + + /// + /// Gets the StateBundle belonging to the name state "slot" if it exists. + /// Otherwise it will create a new StateBundle, that will be added to the Dictionary, + /// and return the newly created instance. + /// + private StateBundle GetOrCreateStateBundle(TStateId name) + { + StateBundle bundle; + + if (!stateBundlesByName.TryGetValue(name, out bundle)) + { + bundle = new StateBundle(); + stateBundlesByName.Add(name, bundle); + } + + return bundle; + } + + /// + /// Adds a new node / state to the state machine. + /// + /// The name / identifier of the new state. + /// The new state instance, e.g. State, CoState, StateMachine. + public void AddState(TStateId name, StateBase state) + { + state.fsm = this; + state.name = name; + state.Init(); + + StateBundle bundle = GetOrCreateStateBundle(name); + bundle.state = state; + + if (stateBundlesByName.Count == 1 && !startState.hasState) + { + SetStartState(name); + } + } + + /// + /// Initialises a transition, i.e. sets its fsm attribute, and then calls its Init method. + /// + /// + private void InitTransition(TransitionBase transition) + { + transition.fsm = this; + transition.Init(); + } + + /// + /// Adds a new transition between two states. + /// + /// The transition instance. + public void AddTransition(TransitionBase transition) + { + InitTransition(transition); + + StateBundle bundle = GetOrCreateStateBundle(transition.from); + bundle.AddTransition(transition); + } + + /// + /// Adds a new transition that can happen from any possible state. + /// + /// The transition instance; The "from" field can be + /// left empty, as it has no meaning in this context. + public void AddTransitionFromAny(TransitionBase transition) + { + InitTransition(transition); + + transitionsFromAny.Add(transition); + } + + /// + /// Adds a new trigger transition between two states that is only checked + /// when the specified trigger is activated. + /// + /// The name / identifier of the trigger. + /// The transition instance, e.g. Transition, TransitionAfter, ... + public void AddTriggerTransition(TEvent trigger, TransitionBase transition) + { + InitTransition(transition); + + StateBundle bundle = GetOrCreateStateBundle(transition.from); + bundle.AddTriggerTransition(trigger, transition); + } + + /// + /// Adds a new trigger transition that can happen from any possible state, but is only + /// checked when the specified trigger is activated. + /// + /// The name / identifier of the trigger + /// The transition instance; The "from" field can be + /// left empty, as it has no meaning in this context. + public void AddTriggerTransitionFromAny(TEvent trigger, TransitionBase transition) + { + InitTransition(transition); + + List> transitionsOfTrigger; + + if (!triggerTransitionsFromAny.TryGetValue(trigger, out transitionsOfTrigger)) + { + transitionsOfTrigger = new List>(); + triggerTransitionsFromAny.Add(trigger, transitionsOfTrigger); + } + + transitionsOfTrigger.Add(transition); + } + + /// + /// Adds two transitions: + /// If the condition of the transition instance is true, it transitions from the "from" + /// state to the "to" state. Otherwise it performs a transition in the opposite direction, + /// i.e. from "to" to "from". + /// + /// + /// Internally the same transition instance will be used for both transitions + /// by wrapping it in a ReverseTransition. + /// For the reverse transition the afterTransition callback is called before the transition + /// and the onTransition callback afterwards. If this is not desired then replicate the behaviour + /// of the two way transitions by creating two separate transitions. + /// + public void AddTwoWayTransition(TransitionBase transition) + { + InitTransition(transition); + AddTransition(transition); + + ReverseTransition reverse = new ReverseTransition(transition, false); + InitTransition(reverse); + AddTransition(reverse); + } + + /// + /// Adds two transitions that are only checked when the specified trigger is activated: + /// If the condition of the transition instance is true, it transitions from the "from" + /// state to the "to" state. Otherwise it performs a transition in the opposite direction, + /// i.e. from "to" to "from". + /// + /// + /// Internally the same transition instance will be used for both transitions + /// by wrapping it in a ReverseTransition. + /// For the reverse transition the afterTransition callback is called before the transition + /// and the onTransition callback afterwards. If this is not desired then replicate the behaviour + /// of the two way transitions by creating two separate transitions. + /// + public void AddTwoWayTriggerTransition(TEvent trigger, TransitionBase transition) + { + InitTransition(transition); + AddTriggerTransition(trigger, transition); + + ReverseTransition reverse = new ReverseTransition(transition, false); + InitTransition(reverse); + AddTriggerTransition(trigger, reverse); + } + + /// + /// Adds a new exit transition from a state. It represents an exit point that + /// allows the fsm to exit and the parent fsm to continue to the next state. + /// It is only checked if the parent fsm has a pending transition. + /// + /// The transition instance. The "to" field can be + /// left empty, as it has no meaning in this context. + public void AddExitTransition(TransitionBase transition) + { + transition.isExitTransition = true; + AddTransition(transition); + } + + /// + /// Adds a new exit transition that can happen from any possible state. + /// It represents an exit point that allows the fsm to exit and the parent fsm to continue + /// to the next state. It is only checked if the parent fsm has a pending transition. + /// + /// The transition instance. The "from" and "to" fields can be + /// left empty, as they have no meaning in this context. + public void AddExitTransitionFromAny(TransitionBase transition) + { + transition.isExitTransition = true; + AddTransitionFromAny(transition); + } + + /// + /// Adds a new exit transition from a state that is only checked when the specified trigger + /// is activated. + /// It represents an exit point that allows the fsm to exit and the parent fsm to continue + /// to the next state. It is only checked if the parent fsm has a pending transition. + /// + /// The transition instance. The "to" field can be + /// left empty, as it has no meaning in this context. + public void AddExitTriggerTransition(TEvent trigger, TransitionBase transition) + { + transition.isExitTransition = true; + AddTriggerTransition(trigger, transition); + } + + /// + /// Adds a new exit transition that can happen from any possible state and is only checked + /// when the specified trigger is activated. + /// It represents an exit point that allows the fsm to exit and the parent fsm to continue + /// to the next state. It is only checked if the parent fsm has a pending transition. + /// + /// The transition instance. The "from" and "to" fields can be + /// left empty, as they have no meaning in this context. + public void AddExitTriggerTransitionFromAny(TEvent trigger, TransitionBase transition) + { + transition.isExitTransition = true; + AddTriggerTransitionFromAny(trigger, transition); + } + + /// + /// Activates the specified trigger, checking all targeted trigger transitions to see whether + /// a transition should occur. + /// + /// The name / identifier of the trigger. + /// True when a transition occurred, otherwise false. + private bool TryTrigger(TEvent trigger) + { + EnsureIsInitializedFor("Checking all trigger transitions of the active state"); + + List> triggerTransitions; + + if (triggerTransitionsFromAny.TryGetValue(trigger, out triggerTransitions)) + { + for (int i = 0, count = triggerTransitions.Count; i < count; i++) + { + TransitionBase transition = triggerTransitions[i]; + + if (EqualityComparer.Default.Equals(transition.to, activeState.name)) + continue; + + if (TryTransition(transition)) + return true; + } + } + + if (activeTriggerTransitions.TryGetValue(trigger, out triggerTransitions)) + { + for (int i = 0, count = triggerTransitions.Count; i < count; i++) + { + TransitionBase transition = triggerTransitions[i]; + + if (TryTransition(transition)) + return true; + } + } + + return false; + } + + /// + /// Activates the specified trigger in all active states of the hierarchy, checking all targeted + /// trigger transitions to see whether a transition should occur. + /// + /// The name / identifier of the trigger. + public void Trigger(TEvent trigger) + { + // If a transition occurs, then the trigger should not be activated + // in the new active state, that the state machine just switched to. + if (TryTrigger(trigger)) return; + + (activeState as ITriggerable)?.Trigger(trigger); + } + + /// + /// Only activates the specified trigger locally in this state machine. + /// + /// The name / identifier of the trigger. + public void TriggerLocally(TEvent trigger) + { + TryTrigger(trigger); + } + + /// + /// Runs an action on the currently active state. + /// + /// Name of the action. + public virtual void OnAction(TEvent trigger) + { + EnsureIsInitializedFor("Running OnAction of the active state"); + (activeState as IActionable)?.OnAction(trigger); + } + + /// + /// Runs an action on the currently active state and lets you pass one data parameter. + /// + /// Name of the action. + /// Any custom data for the parameter. + /// Type of the data parameter. + /// Should match the data type of the action that was added via AddAction(...). + public virtual void OnAction(TEvent trigger, TData data) + { + EnsureIsInitializedFor("Running OnAction of the active state"); + (activeState as IActionable)?.OnAction(trigger, data); + } + + public StateBase GetState(TStateId name) + { + StateBundle bundle; + + if (!stateBundlesByName.TryGetValue(name, out bundle) || bundle.state == null) + { + throw UnityHFSM.Exceptions.Common.StateNotFound(name.ToString(), context: "Getting a state"); + } + + return bundle.state; + } + + public StateMachine this[TStateId name] + { + get + { + StateBase state = GetState(name); + StateMachine subFsm = state as StateMachine; + + if (subFsm == null) + { + throw UnityHFSM.Exceptions.Common.QuickIndexerMisusedForGettingState(name.ToString()); + } + + return subFsm; + } + } + + public override string GetActiveHierarchyPath() + { + if (activeState == null) + { + // When the state machine is not active, then the active hierarchy path + // is empty. + return ""; + } + + return $"{name}/{activeState.GetActiveHierarchyPath()}"; + } + } + + // Overloaded classes to allow for an easier usage of the StateMachine for common cases. + // E.g. new StateMachine() instead of new StateMachine() + + public class StateMachine : StateMachine + { + public StateMachine(bool needsExitTime = false, bool isGhostState = false, bool rememberLastState = false) + : base(needsExitTime: needsExitTime, isGhostState: isGhostState, rememberLastState: rememberLastState) + { + } + } + + public class StateMachine : StateMachine + { + public StateMachine(bool needsExitTime = false, bool isGhostState = false, bool rememberLastState = false) + : base(needsExitTime: needsExitTime, isGhostState: isGhostState, rememberLastState: rememberLastState) + { + } + } + + public class StateMachine : StateMachine + { + public StateMachine(bool needsExitTime = false, bool isGhostState = false, bool rememberLastState = false) + : base(needsExitTime: needsExitTime, isGhostState: isGhostState, rememberLastState: rememberLastState) + { + } + } } \ No newline at end of file From bf1abeee46fecc18112690e54179add95441aea6 Mon Sep 17 00:00:00 2001 From: Sander Grindheim Date: Tue, 13 Aug 2024 03:36:59 -0700 Subject: [PATCH 3/3] Added a useless condition to all Animator transitions to surpress the warnings about invalid transitions. Refactored to improve code readibility and improve performance. Added comments. --- .../EditorStateMachineShortcuts.cs | 169 ++++++++++-------- 1 file changed, 96 insertions(+), 73 deletions(-) diff --git a/src/StateMachine/EditorStateMachineShortcuts.cs b/src/StateMachine/EditorStateMachineShortcuts.cs index 6ffd53b..0aca202 100644 --- a/src/StateMachine/EditorStateMachineShortcuts.cs +++ b/src/StateMachine/EditorStateMachineShortcuts.cs @@ -42,12 +42,17 @@ public static void PrintToAnimator(this StateMachine(AnimatorStateMachine animatorStateMachine, StateMachine hfsm, Dictionary animatorStateDict, Dictionary animatorStateMachineDict) { @@ -60,6 +65,8 @@ private static void SetupAnimatorStateMachine(Animator AddStateToAnimator(state, hfsm, animatorStateMachine, animatorStateDict); } + RemoveTransitionsFromStateMachine(animatorStateMachine); //Remove all transitions so that they can be re-placed + //Add transitions to Animator which mirror transitions in the HFSM. //This cannot be in the same loop as above because the state which is receiving a transition might not have been created yet foreach (StateMachine.StateBundle stateBundle in hfsm.stateBundlesByName.Values) @@ -67,21 +74,16 @@ private static void SetupAnimatorStateMachine(Animator if (stateBundle.state is StateMachine subFsm) AddStateMachineTransitionsToAnimator(stateBundle, subFsm, animatorStateMachineDict, animatorStateDict); else - AddStateTransitionsToAnimator(stateBundle, animatorStateDict, animatorStateMachineDict); - } - - foreach (var transition in hfsm.transitionsFromAny) - { - animatorStateMachine.AddAnyStateTransition(animatorStateDict[transition.to]); - } - - foreach (var transitionList in hfsm.triggerTransitionsFromAny.Values) - { - foreach (var transition in transitionList) { - animatorStateMachine.AddAnyStateTransition(animatorStateDict[transition.to]); + RemoveTransitionsFromState(animatorStateDict[stateBundle.state.name]); //remove existing transitions so that they can be replaced + AddStateTransitionsToAnimator(stateBundle, animatorStateDict, animatorStateMachineDict); } } + + //trigger transitions are treated exactly the same as normal transitions, so concatenate them into one IEnumerable + hfsm.transitionsFromAny + .Concat(hfsm.triggerTransitionsFromAny.Values.SelectMany(x => x)) + .ForEach(transition => animatorStateMachine.AddTransitionFromAnyStateWithCondition(animatorStateDict[transition.to])); } private static void AddStateTransitionsToAnimator(StateMachine.StateBundle stateBundle, @@ -89,96 +91,75 @@ private static void AddStateTransitionsToAnimator(Stat { var fromState = animatorStateDict[stateBundle.state.name]; - //remove all existing transitions so that they can be replaced - AnimatorStateTransition[] transitionsCopy = new AnimatorStateTransition[fromState.transitions.Length]; - Array.Copy(fromState.transitions, transitionsCopy, fromState.transitions.Length); - foreach (var animatorTransition in transitionsCopy) - fromState.RemoveTransition(animatorTransition); - - if (stateBundle.transitions != null) + foreach (var transition in stateBundle.transitions ?? Enumerable.Empty>()) { - foreach (var transition in stateBundle.transitions) + if (animatorStateDict.ContainsKey(transition.to)) + { + fromState.AddTransitionToStateWithCondition(animatorStateDict[transition.to]); + } + else //if the destination is not a state, then it must be a state machine { - if (animatorStateDict.ContainsKey(transition.to)) - fromState.AddTransition(animatorStateDict[transition.to]); - else //if the destination is not a state, then it must be a state machine - { - AnimatorStateMachine destinationStateMachine = animatorStateMachineDict[transition.to]; - fromState.AddTransition(destinationStateMachine); - } + fromState.AddTransitionToStateMachineWithCondition(animatorStateMachineDict[transition.to]); } } - if (stateBundle.triggerToTransitions == null) - return; - - foreach (var transitionList in stateBundle.triggerToTransitions.Values) + foreach (var transition in stateBundle.triggerToTransitions?.Values?.SelectMany(x => x) ?? Enumerable.Empty>()) { - foreach (TransitionBase transition in transitionList) - { - AnimatorState destinationState = animatorStateDict[transition.to]; - fromState.AddTransition(destinationState); - } + fromState.AddTransitionToStateWithCondition(animatorStateDict[transition.to]); } } + //Removes transitions between a state and other states + private static void RemoveTransitionsFromState(AnimatorState state) + { + foreach (AnimatorStateTransition animatorTransition in state.transitions) + Undo.DestroyObjectImmediate(animatorTransition); + + state.transitions = new AnimatorStateTransition[0]; + } + + //This removes entry and any-state transitions to and from the state machine itself. It does not remove transitions between states private static void RemoveTransitionsFromStateMachine(AnimatorStateMachine animatorStateMachine) { //remove all entry transitions - AnimatorTransition[] entryTransitionsCopy = new AnimatorTransition[animatorStateMachine.entryTransitions.Length]; - Array.Copy(animatorStateMachine.entryTransitions, entryTransitionsCopy, animatorStateMachine.entryTransitions.Length); - foreach (AnimatorTransition animatorTransition in entryTransitionsCopy) - animatorStateMachine.RemoveEntryTransition(animatorTransition); + foreach (AnimatorTransition animatorTransition in animatorStateMachine.entryTransitions) + Undo.DestroyObjectImmediate(animatorTransition); + + animatorStateMachine.entryTransitions = new AnimatorTransition[0]; //remove any-state transitions - AnimatorStateTransition[] anyTransitionsCopy = new AnimatorStateTransition[animatorStateMachine.anyStateTransitions.Length]; - Array.Copy(animatorStateMachine.anyStateTransitions, anyTransitionsCopy, animatorStateMachine.anyStateTransitions.Length); - foreach (AnimatorStateTransition animatorTransition in anyTransitionsCopy) - animatorStateMachine.RemoveAnyStateTransition(animatorTransition); + foreach (AnimatorStateTransition animatorTransition in animatorStateMachine.anyStateTransitions) + Undo.DestroyObjectImmediate(animatorTransition); + + animatorStateMachine.anyStateTransitions = new AnimatorStateTransition[0]; } - private static void AddStateMachineTransitionsToAnimator(StateMachine.StateBundle stateBundle, StateMachine subFsm, - Dictionary animatorStatemachineDict, Dictionary animatorStateDict) + //Adds transitions within a nested StateMachine + private static void AddStateMachineTransitionsToAnimator(StateMachine.StateBundle stateBundle, + StateMachine subFsm, Dictionary animatorStatemachineDict, Dictionary animatorStateDict) { AnimatorStateMachine animatorStateMachine = animatorStatemachineDict[stateBundle.state.name]; - //Remove all transitoins so that they can be re-placed - RemoveTransitionsFromStateMachine(animatorStateMachine); - - //Add Any state transitions - foreach (var transition in subFsm.transitionsFromAny) - { - animatorStateMachine.AddAnyStateTransition(animatorStateDict[transition.to]); - } - - foreach (var transitionList in subFsm.triggerTransitionsFromAny.Values) - { - foreach (var transition in transitionList) - { - animatorStateMachine.AddAnyStateTransition(animatorStateDict[transition.to]); - } - } + //Add transitionsFromAny and triggerTransitionsFromAny. Both are represented as AnyStateTransitions in the Animator + IEnumerable> subFsmTransitionsFromAny = subFsm.transitionsFromAny.Concat(subFsm.triggerTransitionsFromAny.Values.SelectMany(x => x)); + foreach (var transition in subFsmTransitionsFromAny) + animatorStateMachine.AddTransitionFromAnyStateWithCondition(animatorStateDict[transition.to]); //trigger transitions are treated exactly the same as normal transitions, so concatenate them into one IEnumerable - IEnumerable> transitions = stateBundle.transitions; + IEnumerable> transitionsFromAny = stateBundle.transitions; if (stateBundle.triggerToTransitions != null) - { - foreach (var transitionList in stateBundle.triggerToTransitions.Values) - transitions.Concat(transitionList); - } + transitionsFromAny.Concat(stateBundle.triggerToTransitions.Values.SelectMany(x => x)); - foreach (var transition in transitions) + foreach (var transition in transitionsFromAny) { //AnimatorStatemachine is not interchangable with AnimatorState, so we must check each dictionary separately if (animatorStatemachineDict.ContainsKey(transition.to)) { - AnimatorStateMachine destinationState = animatorStatemachineDict[transition.to]; - animatorStateMachine.AddStateMachineTransition(destinationState); + animatorStateMachine.AddTransitionToStateMachineWithCondition(animatorStatemachineDict[transition.to]); } else //if the destination is not a state machine, then it must be a state { - AnimatorState destinationState = animatorStateDict[transition.to]; - animatorStateMachine.AddStateMachineTransition(animatorStateMachine, destinationState); + animatorStateMachine.AddTransitionToStateWithCondition(animatorStateDict[transition.to]); } } } @@ -213,7 +194,7 @@ private static void AddStateMachineToAnimator(StateMac SetupAnimatorStateMachine(childStateMachine.stateMachine, subFsm, animatorStateDict, stateMachineDictionary); } - public static (bool didFind, T element) FirstOrFalse(this IEnumerable collection, Func predicate) + private static (bool didFind, T element) FirstOrFalse(this IEnumerable collection, Func predicate) { foreach (T element in collection) { @@ -223,6 +204,48 @@ public static (bool didFind, T element) FirstOrFalse(this IEnumerable coll return (false, default(T)); } + + private static void ForEach(this IEnumerable source, Action action) + { + foreach (T t in source) + action.Invoke(t); + } + } + + public static class AnimatorExtensions + { + //Used to surpress Animator warnings about transitions not having transition conditions + public const string globalParameterName = "Generated Parameter"; + + public static void AddTransitionFromAnyStateWithCondition(this AnimatorStateMachine fromStateMachine, AnimatorState destinationState) + { + AnimatorStateTransition animatorTransition = fromStateMachine.AddAnyStateTransition(destinationState); + animatorTransition.AddCondition(AnimatorConditionMode.If, 1f, globalParameterName); + } + + public static void AddTransitionToStateMachineWithCondition(this AnimatorStateMachine fromStateMachine, AnimatorStateMachine destinationStateMachine) + { + AnimatorTransition animatorTransition = fromStateMachine.AddStateMachineTransition(destinationStateMachine); + animatorTransition.AddCondition(AnimatorConditionMode.If, 1f, globalParameterName); + } + + public static void AddTransitionToStateWithCondition(this AnimatorStateMachine fromStateMachine, AnimatorState destinationState) + { + AnimatorTransition animatorTransition = fromStateMachine.AddStateMachineTransition(fromStateMachine, destinationState); + animatorTransition.AddCondition(AnimatorConditionMode.If, 1f, globalParameterName); + } + + public static void AddTransitionToStateWithCondition(this AnimatorState fromState, AnimatorState destinationState) + { + AnimatorStateTransition animatorTransition = fromState.AddTransition(destinationState); + animatorTransition.AddCondition(AnimatorConditionMode.If, 1f, globalParameterName); + } + + public static void AddTransitionToStateMachineWithCondition(this AnimatorState fromState, AnimatorStateMachine destinationStateMachine) + { + AnimatorStateTransition animatorTransition = fromState.AddTransition(destinationStateMachine); + animatorTransition.AddCondition(AnimatorConditionMode.If, 1f, globalParameterName); + } } } #endif