diff --git a/src/StateMachine/EditorStateMachineShortcuts.cs b/src/StateMachine/EditorStateMachineShortcuts.cs
new file mode 100644
index 0000000..0aca202
--- /dev/null
+++ b/src/StateMachine/EditorStateMachineShortcuts.cs
@@ -0,0 +1,251 @@
+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);
+
+ //surpress Animator warnings about transitions not having transition conditions
+ animatorMirror.parameters = new AnimatorControllerParameter[0];
+ animatorMirror.AddParameter(AnimatorExtensions.globalParameterName, AnimatorControllerParameterType.Bool);
+
+ //remove old transitions from state machine before setting it up freshly
+ RemoveTransitionsFromStateMachine(animatorMirror.layers[0].stateMachine);
+
+ SetupAnimatorStateMachine(animatorMirror.layers[0].stateMachine, hfsm, new(), new());
+ }
+
+ //Sets up an AnimatorStateMachine based upon the HFSM supplied as a parameter. Called recursively when entering a sub-state of an HFSM
+ 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);
+ }
+
+ 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)
+ {
+ if (stateBundle.state is StateMachine subFsm)
+ AddStateMachineTransitionsToAnimator(stateBundle, subFsm, animatorStateMachineDict, animatorStateDict);
+ else
+ {
+ 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,
+ Dictionary animatorStateDict, Dictionary animatorStateMachineDict)
+ {
+ var fromState = animatorStateDict[stateBundle.state.name];
+
+ foreach (var transition in stateBundle.transitions ?? Enumerable.Empty>())
+ {
+ 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
+ {
+ fromState.AddTransitionToStateMachineWithCondition(animatorStateMachineDict[transition.to]);
+ }
+ }
+
+ foreach (var transition in stateBundle.triggerToTransitions?.Values?.SelectMany(x => x) ?? Enumerable.Empty>())
+ {
+ 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
+ foreach (AnimatorTransition animatorTransition in animatorStateMachine.entryTransitions)
+ Undo.DestroyObjectImmediate(animatorTransition);
+
+ animatorStateMachine.entryTransitions = new AnimatorTransition[0];
+
+ //remove any-state transitions
+ foreach (AnimatorStateTransition animatorTransition in animatorStateMachine.anyStateTransitions)
+ Undo.DestroyObjectImmediate(animatorTransition);
+
+ animatorStateMachine.anyStateTransitions = new AnimatorStateTransition[0];
+ }
+
+ //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];
+
+ //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> transitionsFromAny = stateBundle.transitions;
+ if (stateBundle.triggerToTransitions != null)
+ transitionsFromAny.Concat(stateBundle.triggerToTransitions.Values.SelectMany(x => x));
+
+ foreach (var transition in transitionsFromAny)
+ {
+ //AnimatorStatemachine is not interchangable with AnimatorState, so we must check each dictionary separately
+ if (animatorStatemachineDict.ContainsKey(transition.to))
+ {
+ animatorStateMachine.AddTransitionToStateMachineWithCondition(animatorStatemachineDict[transition.to]);
+ }
+ else //if the destination is not a state machine, then it must be a state
+ {
+ animatorStateMachine.AddTransitionToStateWithCondition(animatorStateDict[transition.to]);
+ }
+ }
+ }
+
+ 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);
+ }
+
+ private 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));
+ }
+
+ 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
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..2f93ed0 100644
--- a/src/StateMachine/StateMachine.cs
+++ b/src/StateMachine/StateMachine.cs
@@ -25,7 +25,7 @@ public class StateMachine :
/// It's useful, as you only need to do one Dictionary lookup for these three items.
/// => Much better performance
///
- private class StateBundle
+ 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
@@ -101,21 +101,21 @@ private static readonly Dictionary>> noTri
///
public event Action> StateChanged;
- private (TStateId state, bool hasState) startState = (default, false);
+ internal (TStateId state, bool hasState) startState = (default, false);
private PendingTransition pendingTransition = default;
private bool rememberLastState = false;
// Central storage of states.
- private Dictionary stateBundlesByName
+ internal Dictionary stateBundlesByName
= new Dictionary();
private StateBase activeState = null;
private List> activeTransitions = noTransitions;
private Dictionary>> activeTriggerTransitions = noTriggerTransitions;
- private List> transitionsFromAny
+ internal List> transitionsFromAny
= new List>();
- private Dictionary>> triggerTransitionsFromAny
+ internal Dictionary>> triggerTransitionsFromAny
= new Dictionary>>();
public StateBase ActiveState
@@ -810,4 +810,4 @@ public StateMachine(bool needsExitTime = false, bool isGhostState = false, bool
{
}
}
-}
+}
\ No newline at end of file