diff --git a/src/Base/IStateMachine.cs b/src/Base/IStateMachine.cs index 3f9bd18..87e60a1 100644 --- a/src/Base/IStateMachine.cs +++ b/src/Base/IStateMachine.cs @@ -1,6 +1,13 @@ +using System.Collections.Generic; + namespace FSM { + public interface IStateMachine : IState + { + IReadOnlyList States { get; } + } + /// /// A subset of features that every parent state machine has to provide. /// It is useful, as it allows the parent state machine to be independent from the @@ -9,7 +16,7 @@ namespace FSM /// => An abstraction layer /// /// They type of the names / ids of the sub states - public interface IStateMachine + public interface IStateMachine : IStateMachine { /// /// Tells the state machine that, if there is a state transition pending, @@ -21,5 +28,6 @@ public interface IStateMachine StateBase ActiveState { get; } TStateId ActiveStateName { get; } + TStateId PrevStateName { get; } } } diff --git a/src/Base/StateBase.cs b/src/Base/StateBase.cs index f4f4a58..d56def2 100644 --- a/src/Base/StateBase.cs +++ b/src/Base/StateBase.cs @@ -1,17 +1,26 @@ +using System; +using System.Collections.Generic; using UnityEngine; namespace FSM { + public interface IState + { + + } + /// /// The base class of all states /// - public class StateBase + public class StateBase : IState { public bool needsExitTime; public TStateId name; public IStateMachine fsm; + private Dictionary commandHandlers; + /// /// Initialises a new instance of the BaseState class /// @@ -46,6 +55,45 @@ public virtual void OnLogic() { } + /// + /// Set command handler + /// + /// Handler delegate + /// Type of command + protected void SetCommandHandler(Action handler) + { + if (commandHandlers == null) + { + commandHandlers = new Dictionary(); + } + + commandHandlers[typeof(TCommand)] = handler; + } + + protected bool CanProcessCommand() + { + if (commandHandlers != null) + { + commandHandlers.TryGetValue(typeof(TCommand), out var commandHandler); + return commandHandler is Action; + } + return false; + } + + /// + /// Called when state is active and fsm was call OnCommand + /// + /// + /// + public virtual void OnCommand(TCommand command = default) + { + if (commandHandlers != null) + { + commandHandlers.TryGetValue(typeof(TCommand), out var commandHandler); + (commandHandler as Action)?.Invoke(command); + } + } + /// /// Called when the state machine transitions from this state to another state (exits this state) /// diff --git a/src/HybridStateMachine.cs b/src/HybridStateMachine.cs index d7b5525..ebf1aae 100644 --- a/src/HybridStateMachine.cs +++ b/src/HybridStateMachine.cs @@ -1,5 +1,6 @@ using UnityEngine; using System; +using System.Collections.Generic; namespace FSM { @@ -33,11 +34,17 @@ public class HybridStateMachine : StateMachine + /// Comparer which will be used for lookup by state id. + /// Recommended for struct typed TStateId or custom lookup logic. + /// Comparer which will be used for lookup by event. + /// Recommended for struct typed TEvent or custom lookup logic. public HybridStateMachine( Action> onEnter = null, Action> onLogic = null, Action> onExit = null, - bool needsExitTime = false) : base(needsExitTime) + bool needsExitTime = false, + IEqualityComparer stateIdComparer = null, + IEqualityComparer eventComparer = null) : base(needsExitTime, stateIdComparer, eventComparer) { this.onEnter = onEnter; this.onLogic = onLogic; @@ -76,7 +83,9 @@ public HybridStateMachine( Action> onEnter = null, Action> onLogic = null, Action> onExit = null, - bool needsExitTime = false) : base(onEnter, onLogic, onExit, needsExitTime) + bool needsExitTime = false, + IEqualityComparer stateIdComparer = null, + IEqualityComparer eventComparer = null) : base(onEnter, onLogic, onExit, needsExitTime, stateIdComparer, eventComparer) { } } @@ -87,7 +96,8 @@ public HybridStateMachine( Action> onEnter = null, Action> onLogic = null, Action> onExit = null, - bool needsExitTime = false) : base(onEnter, onLogic, onExit, needsExitTime) + bool needsExitTime = false, + IEqualityComparer stateIdComparer = null) : base(onEnter, onLogic, onExit, needsExitTime, stateIdComparer) { } } diff --git a/src/State.cs b/src/State.cs index 6ce8c4b..1603169 100644 --- a/src/State.cs +++ b/src/State.cs @@ -28,21 +28,36 @@ public class State : StateBase /// Determins if the state is allowed to instantly /// exit on a transition (false), or if the state machine should wait until the state is ready for a /// state change (true) + /// Commands which will be registered for OnCommand calls public State( Action> onEnter = null, Action> onLogic = null, Action> onExit = null, Func, bool> canExit = null, - bool needsExitTime = false) : base(needsExitTime) + bool needsExitTime = false, + params CommandBase[] commands) : base(needsExitTime) { this.onEnter = onEnter; this.onLogic = onLogic; this.onExit = onExit; this.canExit = canExit; + if (commands != null) + { + foreach (var command in commands) + { + command?.Register(this); + } + } + this.timer = new Timer(); } + internal void SetCommandHandlerInternal(Action handler) + { + SetCommandHandler(handler); + } + public override void OnEnter() { timer.Reset(); @@ -76,7 +91,8 @@ public State( Action> onLogic = null, Action> onExit = null, Func, bool> canExit = null, - bool needsExitTime = false) : base(onEnter, onLogic, onExit, canExit, needsExitTime) + bool needsExitTime = false, + params CommandBase[] commands) : base(onEnter, onLogic, onExit, canExit, needsExitTime, commands) { } } diff --git a/src/StateMachine.cs b/src/StateMachine.cs index 33455e8..c2fa6aa 100644 --- a/src/StateMachine.cs +++ b/src/StateMachine.cs @@ -32,6 +32,12 @@ private class StateBundle public StateBase state; public List> transitions; public Dictionary>> triggerToTransitions; + private IEqualityComparer eventComparer; + + public StateBundle(IEqualityComparer eventComparer) + { + this.eventComparer = eventComparer; + } public void AddTransition(TransitionBase t) { @@ -40,8 +46,12 @@ public void AddTransition(TransitionBase t) } public void AddTriggerTransition(TEvent trigger, TransitionBase transition) { - triggerToTransitions = triggerToTransitions - ?? new Dictionary>>(); + if (triggerToTransitions == null) + { + triggerToTransitions = eventComparer == null + ? new Dictionary>>() + : new Dictionary>>(eventComparer); + } List> transitionsOfTrigger; @@ -55,6 +65,9 @@ public void AddTriggerTransition(TEvent trigger, TransitionBase transi } } + private IEqualityComparer eventComparer; + private IEqualityComparer stateIdComparer; + // A cached empty list of transitions (For improved readability, less GC) private static readonly List> noTransitions = new List>(0); @@ -65,8 +78,7 @@ private static readonly Dictionary>> noTri private (TStateId state, bool isPending) pendingState = (default, false); // Central storage of states - private Dictionary nameToStateBundle - = new Dictionary(); + private Dictionary nameToStateBundle; private StateBase activeState = null; private List> activeTransitions = noTransitions; @@ -77,6 +89,10 @@ private List> transitionsFromAny private Dictionary>> triggerTransitionsFromAny = new Dictionary>>(); + private readonly List states; + + public IReadOnlyList States { get; } + public StateBase ActiveState { get @@ -92,6 +108,7 @@ public StateBase ActiveState } } public TStateId ActiveStateName => ActiveState.name; + public TStateId PrevStateName { get; private set; } private bool IsRootFsm => fsm == null; @@ -102,9 +119,19 @@ public StateBase ActiveState /// Determins 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 the active state is ready for a /// state change (true). - public StateMachine(bool needsExitTime = true) : base(needsExitTime) + /// Comparer which will be used for lookup by state id. + /// Recommended for struct typed TStateId or custom lookup logic. + /// Comparer which will be used for lookup by event. + /// Recommended for struct typed TEvent or custom lookup logic. + public StateMachine(bool needsExitTime = true, IEqualityComparer stateIdComparer = null, IEqualityComparer eventComparer = null) : base(needsExitTime) { - + this.eventComparer = eventComparer; + this.stateIdComparer = stateIdComparer; + nameToStateBundle = stateIdComparer == null + ? new Dictionary() + : new Dictionary(stateIdComparer); + states = new List(); + States = states.AsReadOnly(); } /// @@ -151,6 +178,7 @@ private void ChangeState(TStateId name) activeTransitions = bundle.transitions ?? noTransitions; activeTriggerTransitions = bundle.triggerToTransitions ?? noTriggerTransitions; + PrevStateName = activeState != null ? activeState.name : default; activeState = bundle.state; activeState.OnEnter(); @@ -269,8 +297,17 @@ public override void OnLogic() TransitionBase transition = transitionsFromAny[i]; // Don't transition to the "to" state, if that state is already the active state - if (transition.to.Equals(activeState.name)) - continue; + if (stateIdComparer != null) + { + if (stateIdComparer.Equals(transition.to, activeState.name)) + continue; + } + else + { + if (transition.to.Equals(activeState.name)) + continue; + } + if (TryTransition(transition)) break; @@ -288,6 +325,28 @@ public override void OnLogic() activeState.OnLogic(); } + public override void OnCommand(TCommand command = default) + { + if (CanProcessCommand()) + { + base.OnCommand(command); + return; + } + + SendCommandToStates(command); + } + + protected void SendCommandToStates(TCommand command = default) + { + if (activeState == null) + { + throw new FSM.Exceptions.StateMachineNotInitializedException( + "Running OnCommand" + ); + } + activeState.OnCommand(command); + } + public override void OnExit() { if (activeState != null) @@ -310,7 +369,7 @@ private StateBundle GetOrCreateStateBundle(TStateId name) { StateBundle bundle; if (! nameToStateBundle.TryGetValue(name, out bundle)) { - bundle = new StateBundle(); + bundle = new StateBundle(eventComparer); nameToStateBundle.Add(name, bundle); } @@ -329,7 +388,12 @@ public void AddState(TStateId name, StateBase state) state.Init(); StateBundle bundle = GetOrCreateStateBundle(name); + if (bundle.state != null) + { + states.Remove(bundle.state); + } bundle.state = state; + states.Add(state); if (nameToStateBundle.Count == 1 && !startState.hasState) { @@ -429,8 +493,16 @@ private bool TryTrigger(TEvent trigger) { TransitionBase transition = triggerTransitions[i]; - if (transition.to.Equals(activeState.name)) - continue; + if (stateIdComparer != null) + { + if (stateIdComparer.Equals(transition.to, activeState.name)) + continue; + } + else + { + if (transition.to.Equals(activeState.name)) + continue; + } if (TryTransition(transition)) return true; @@ -581,7 +653,7 @@ public void AddTransitionFromAny( Func, bool> condition = null, bool forceInstantly = false) { - AddTransition(CreateOptimizedTransition(default, to, condition, forceInstantly)); + AddTransitionFromAny(CreateOptimizedTransition(default, to, condition, forceInstantly)); } /// @@ -618,14 +690,15 @@ public void AddTriggerTransitionFromAny( public class StateMachine : StateMachine { - public StateMachine(bool needsExitTime = true) : base(needsExitTime) + public StateMachine(bool needsExitTime = true, IEqualityComparer stateIdComparer = null, IEqualityComparer eventComparer = null) + : base(needsExitTime, stateIdComparer, eventComparer) { } } public class StateMachine : StateMachine { - public StateMachine(bool needsExitTime = true) : base(needsExitTime) + public StateMachine(bool needsExitTime = true, IEqualityComparer stateIdComparer = null) : base(needsExitTime, stateIdComparer) { } } diff --git a/src/Util/CommandBase.cs b/src/Util/CommandBase.cs new file mode 100644 index 0000000..14b916d --- /dev/null +++ b/src/Util/CommandBase.cs @@ -0,0 +1,38 @@ +using System; + +namespace FSM +{ + public abstract class CommandBase + { + internal abstract void Register(State state); + } + + public class Command: CommandBase + { + private readonly Action, TCommand> handler; + + public Command(Action, TCommand> handler) + { + this.handler = handler; + } + + internal override void Register(State state) + { + if (handler == null) + { + return; + } + state.SetCommandHandlerInternal(command => + { + handler?.Invoke(state, command); + }); + } + } + + public class Command : Command + { + public Command(Action, TCommand> handler) : base(handler) + { + } + } +} \ No newline at end of file diff --git a/src/Util/CommandBase.cs.meta b/src/Util/CommandBase.cs.meta new file mode 100644 index 0000000..4a7604d --- /dev/null +++ b/src/Util/CommandBase.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 72a64ad22f2c4f31a749008082781de3 +timeCreated: 1630781377 \ No newline at end of file