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