Net State Machine
v0.4.0
An state machine builder library for .NET.
The following example shows some the functions of the state machine.
using Enderlook.StateMachine;public class Character{private static StateMachineFactory<States, Events, Character>? factory;private readonly Random rnd = new();private readonly StateMachine<States, Events, Character> stateMachine;private int health = 100;private int food = 100;private enum States{Sleep,Play,GettingFood,Hunt,Gather,}private enum Events{HasFullHealth,LowHealth,IsHungry,IsNoLongerHungry,}public static async Task Main(){Character character = new();while (true){Console.Clear();// Executes an update call of the state machine and pass an arbitrary parameter to it.// Parameter is generic so it doesn't allocate on value types.// This parameter is passed to subscribed delegate which accepts the generic argument type in it's signature.// If you don't want to pass a parameter you can remove the .With() method call.// This parameter system can also be used with fire event methods.character.stateMachine.With(character.rnd.NextSingle()).Update();Console.WriteLine($"State: {character.stateMachine.CurrentState}.");Console.WriteLine($"Health: {character.health}.");Console.WriteLine($"Food: {character.food}.");await Task.Delay(10).ConfigureAwait(false);}}public Character(){// Creates an instance of the state machine.stateMachine = GetStateMachineFactory().Create(this); // Alternatively if you want to pass parameters to the initialization of the state machine you can do: // stateMachine = GetStateMachineFactory().With(parameter).Create(this). // The method `.With(parameter)` can be concatenated as many times you need. // The pattern `stateMachine.With(p1).With(p2)...With(pn).SomeMethod(...)` is also valid for methods `Fire()`, `FireImmediately()` and `Update()`.}private static StateMachineFactory<States, Events, Character> GetStateMachineFactory(){if (factory is not null)return factory;StateMachineFactory<States, Events, Character>? factory_ = StateMachine<States, Events, Character>// State machines are created from factories which makes the creations of multiple instances// cheaper in both CPU and memory since computation is done once and shared between created instances..CreateFactoryBuilder()// Determines the initial state of the state machine.// The second parameter determines how OnEntry delegates should be executed during the initialization of the state machine,// InitializationPolicy.Ignore means they should not be run..SetInitialState(States.Sleep, InitializationPolicy.Ignore)// Configures an state..In(States.Sleep)// Executed every time we enter to this state..OnEntry(() => Console.WriteLine("Going to bed."))// Executed every time we exit from this state..OnExit(() => Console.WriteLine("Getting up."))// Executed every time update method (either Update() or With<T>(T).Update()) is executed and is in this state.// All events provide an overload to pass a recipient, so it can be parametized during build of concrete instances.// Also provides an overload to pass a parameter of arbitrary type, so it can be parametized during call of With<T>(T).Update().// Also provides an overload to pass both a recipient and a parameter of arbitrary type.// This overloads also applies to OnEntry(...), OnExit(...), If(...) and Do(...) methods..OnUpdate(@this => @this.OnUpdateSleep()).On(Events.HasFullHealth)// Executed every time this event is fired in this state..Do(() => Console.WriteLine("Pick toys."))// New state to transite..Goto(States.Play)// Alternatively, you can configure the event execution policy during the transition.// The above method call is equivalent to:// .OnEntryPolicy(TransitionPolicy.ChildFirstWithCulling).OnExitPolicy(TransitionPolicy.ParentFirstWithCulling).Goto(States.Play)..On(Events.IsHungry)// Only execute the next call if the condition is true..If(@this => @this.IsVeryWounded())// We stay in our current state without executing OnEntry nor OnExit delegates..StaySelf()// The above method is a shortcut of:// .OnEntryPolicy(TransitionPolicy.Ignore).OnExitPolicy(TransitionPolicy.Ignore).Goto(States.Sleep).// If we wanted to execute those delegates we can use:// .GotoSelf(false)// Which is the shortcut of:// .OnEntryPolicy(TransitionPolicy.ChildFirstWithCullingInclusive).OnExitPolicy(TransitionPolicy.ParentFirstWithCullingInclusive).Goto(States.Sleep).// If additionally, we wanted to execute transition delegates from its parents states (something which is not useful in this example since State.Sleep is not a substate) we can do:// .GotoSelf(true)// Which is the shortcut of:// .OnEntryPolicy(TransitionPolicy.ChildFirst).OnExitPolicy(TransitionPolicy.ParentFirst).Goto(States.Sleep).// Else execute the next call if the condition is true..If(@this => @this.IsWounded()).Goto(States.Gather)// Else execute unconditionally..Goto(States.Hunt)// Ignore this event in this transition.// (If we don't add this and we accidentally fire this event an exception is thrown)..Ignore(Events.LowHealth)// Which is the shortcut of:// .On(Events.LowHealth).OnEntryPolicy(TransitionPolicy.Ignore).OnExitPolicy(TransitionPolicy.Ignore).Goto(States.Sleep)..In(States.Play).OnUpdate(@this => @this.OnUpdatePlay()).On(Events.IsHungry).If(@this => @this.IsWounded()).Goto(States.Gather).Goto(States.Hunt).In(States.GettingFood).OnEntry(() => Console.WriteLine("Going for food.")).OnExit(() => Console.WriteLine("Stop going for food.")).In(States.Gather)// Determines that this state is a substate of another.// This means that OnUpdate delegates in the parent state will also be run.// Also depending on the configured OnEntryPolicy and OnExitPolicy during transitions,// the OnEntry and OnExit delegates subscribted in this state may be run during transitions in substates..IsSubStateOf(States.GettingFood).OnUpdate((Character @this, float parameter) => @this.OnUpdateGather(parameter)).On(Events.IsNoLongerHungry).If(@this => @this.IsWounded()).Goto(States.Sleep).Goto(States.Play).On(Events.HasFullHealth).Goto(States.Hunt).In(States.Hunt).IsSubStateOf(States.GettingFood).OnEntry(() => Console.WriteLine("Take bow.")).OnExit(() => Console.WriteLine("Drop bow.")).OnUpdate((Character @this, float parameter) => @this.OnUpdateHunt(parameter)).On(Events.IsNoLongerHungry).Goto(States.Sleep).On(Events.LowHealth).Goto(States.Sleep).Finalize();// The interlocked is useful to reduce memory usage in multithreading situations.// That is because the factory contains common data between instances,// so if two instances are created from two different factories it will consume more memory// than two instances created from the same factory.Interlocked.CompareExchange(ref factory, factory_, null);return factory;}private bool IsVeryWounded() => health <= 50;private bool IsWounded() => health <= 75;private void OnUpdateHunt(float luck){food += (int)MathF.Round(rnd.Next(8) * luck);if (food >= 100){food = 100;stateMachine.Fire(Events.IsNoLongerHungry); // Alternatively if you want to pass parameters to the initialization of the state machine you can do: // stateMachine.With(paramter).Fire(Events.IsNoLongerHungry);}health -= (int)MathF.Round(rnd.Next(6) * (1 - luck));if (health <= 20)stateMachine.Fire(Events.LowHealth);}private void OnUpdateGather(float luck){food += (int)MathF.Round(rnd.Next(3) * luck);if (food >= 100){food = 100;stateMachine.Fire(Events.IsNoLongerHungry);}if (rnd.Next(1) % 1 == 0){health++;if (health >= 100){health = 100;stateMachine.Fire(Events.HasFullHealth);}}}private void OnUpdatePlay(){food -= 3;if (food <= 0){food = 0;stateMachine.Fire(Events.IsHungry);}}private void OnUpdateSleep(){health++;if (health >= 100){health = 100;stateMachine.Fire(Events.HasFullHealth);}food -= 2;if (food <= 0){food = 0;stateMachine.Fire(Events.IsHungry);}}}
public sealed class StateMachine<TState, TEvent, TRecipient>where TState : notnullwhere TEvent : notnull{/// Get current (sub)state of this state machine.public TState CurrentState { get; }/// Get current (sub)state and all its parent state hierarchy.public ReadOnlySlice<TState> CurrentStateHierarchy { get; }/// Get accepts events by current (sub)state.public ReadOnlySlice<TEvent> CurrentAcceptedEvents { get; }/// Creates a factory builder.public static StateMachineBuilder<TState, TEvent, TRecipient> CreateFactoryBuilder();/// Get the parent state of the specified state./// If state is not a substate, returns false.public bool GetParentStateOf(TState state, [NotNullWhen(true)] out TState? parentState);/// Get the parent hierarchy of the specified state. If state is not a substate, returns empty.public ReadOnlySlice<TState> GetParentHierarchyOf(TState state);/// Get the events that are accepted by the specified state.public ReadOnlySlice<TEvent> GetAcceptedEventsBy(TState state);/// Determines if the current state is the specified state or a (nested) substate of that specified state.public bool IsInState(TState state);/// Fire an event to the state machine./// If the state machine is already firing an state, it's enqueued to run after completion of the current event.public void Fire(TEvent @event);/// Fire an event to the state machine./// The event won't be enqueued but actually run, ignoring previously enqueued events./// If subsequent events are enqueued during the execution of the callbacks of this event, they will also be run after the completion of this event.public void FireImmediately(TEvent @event);/// Executes the update callbacks registered in the current state.public void Update();/// Stores a parameter(s) that can be passed to subscribed delegates.public ParametersBuilder With<T>(T parameter);public readonly struct ParametersBuilder{/// Stores a parameter tha can be passed to callbacks.public ParametersBuilder With<TParameter>(TParameter parameter);/// Same as Fire(TEvent) in parent class but includes all the stored value that can be passed to subscribed delegates.public void Fire(TEvent);/// Same as FireImmediately(TEvent) in parent class but includes all the stored value that can be passed to subscribed delegates.public void FireImmediately(TEvent);/// Same as Update(TEvent) in parent class but includes all the stored value that can be passed to subscribed delegates.public void Update(TEvent);}public readonly struct InitializeParametersBuilder{/// Stores a parameter tha can be passed to callbacks.public InitializeParametersBuilder With<TParameter>(TParameter parameter);/// Creates the state machine.public StateMachine<TState, TEvent, TRecipient> Create(TRecipient recipient);}}public sealed class StateMachineFactory<TState, TEvent, TRecipient>where TState : notnullwhere TEvent : notnull{/// Creates a configured and initialized state machine using the configuration provided by this factory.public StateMachine<TState, TEvent, TRecipient> Create(TRecipient recipient);/// Stores a parameter(s) that can be passed to subscribed delegates.public StateMachine<TState, TEvent, TRecipient>.InitializeParametersBuilder With<T>(T parameter);}public sealed class StateMachineBuilder<TState, TEvent, TRecipient> : IFinalizablewhere TState : notnullwhere TEvent : notnull{/// Determines the initial state of the state machine./// `initializationPolicy` determines how subscribed delegates to the OnEntry ovents of the specified state (and parent states) will be run during the initialization of the state machine.public StateMachineBuilder<TState, TEvent, TRecipient> SetInitialState(TState state, ExecutionPolicy initializationPolicy = ExecutionPolicy.ChildFirst);/// Add a new state or loads a previously added state.public StateBuilder<TState, TEvent, TRecipient> In(TState state);/// Creates a factory from using as configuration the builder.public StateMachineFactory<TState, TEvent, TRecipient> Finalize();}public sealed class StateBuilder<TState, TEvent, TRecipient> : IFinalizablewhere TState : notnullwhere TEvent : notnull{/// Fowards call to StateMachineBuilder<TState, TEvent, TRecipient>.In(TState state).public StateBuilder<TState, TEvent, TRecipient> In(TState state);/// Fowards call to StateMachineBuilder<TState, TEvent, TRecipient>.Finalize();public StateMachineFactory<TState, TEvent, TRecipient> Finalize();/// Marks this state as the substate of the specified state.public StateBuilder<TState, TEvent, TRecipient> IsSubStateOf(TState state);/// Determines an action to execute on entry to this state.public StateBuilder<TState, TEvent, TRecipient> OnEntry(Action action);/// Same as OnEntry(Action) but pass the recipient as parameter.public StateBuilder<TState, TEvent, TRecipient> OnEntry(Action<TRecipient> action);/// Same as OnEntry(Action) but pass to the delegate any parameter passed during the call which matches the generic parameter type./// If no parameter passed with the specified generic parameter is found, it's ignored.public StateBuilder<TState, TEvent, TRecipient> OnEntry<TParameter>(Action<TParameter> action);/// Combined version of OnEntry(Action<TRecipient>) and OnEntry(Action<TParameter>).public StateBuilder<TState, TEvent, TRecipient> OnEntry<TParameter>(Action<TRecipient, TParameter> action);/// Determines an action to execute on exit fropm this state.public StateBuilder<TState, TEvent, TRecipient> OnExit(Action action);/// Same as OnExit(Action) but pass the recipient as parameter.public StateBuilder<TState, TEvent, TRecipient> OnExit(Action<TRecipient> action);/// Same as OnExit(Action) but pass to the delegate any parameter passed during the call which matches the generic parameter type./// If no parameter passed with the specified generic parameter is found, it's ignored.public StateBuilder<TState, TEvent, TRecipient> OnExit<TParameter>(Action<TParameter> action);/// Combined version of OnExit(Action<TRecipient>) and OnExit(Action<TParameter>).public StateBuilder<TState, TEvent, TRecipient> OnExit<TParameter>(Action<TRecipient, TParameter> action);/// Determines an action to execute on update to this state.public StateBuilder<TState, TEvent, TRecipient> OnUpdate(Action action);/// Same as OnUpdate(Action) but pass the recipient as parameter.public StateBuilder<TState, TEvent, TRecipient> OnUpdate(Action<TRecipient> action);/// Same as OnUpdate(Action) but pass to the delegate any parameter passed during the call which matches the generic parameter type.public StateBuilder<TState, TEvent, TRecipient> OnUpdate<TParameter>(Action<TParameter> action);/// Combined version of OnUpdate(Action<TRecipient>) and OnUpdate(Action<TParameter>)./// If no parameter passed with the specified generic parameter is found, it's ignored.public StateBuilder<TState, TEvent, TRecipient> OnUpdate<TParameter>(Action<TRecipient, TParameter> action);/// Add a behaviour that is executed during the firing of the specified event.public TransitionBuilder<TState, TEvent, TRecipient, StateBuilder<TState, TEvent, TRecipient>> On(TEvent @event);/// Ignores the specified event./// If no behaviour is added to an event and it's fired, it will throw. This prevent throwing by ignoring the call at all.public StateBuilder<TState, TEvent, TRecipient> Ignore(TEvent @event);}public sealed class TransitionBuilder<TState, TEvent, TRecipient, TParent> : IFinalizable, ITransitionBuilder<TState>where TState : notnullwhere TEvent : notnull{/// Add a sub transition which is executed when the delegate returns true.public TransitionBuilder<TState, TEvent, TRecipient, TransitionBuilder<TState, TEvent, TRecipient, TParent>> If(Func<bool> guard);/// Same as If(Func<bool>) but pass the recipient as parameter.public TransitionBuilder<TState, TEvent, TRecipient, TransitionBuilder<TState, TEvent, TRecipient, TParent>> If(Func<TRecipient, bool> guard);/// Same as If(Func<bool>) but pass to the delegate any parameter passed during the call which matches the generic parameter type.public TransitionBuilder<TState, TEvent, TRecipient, TransitionBuilder<TState, TEvent, TRecipient, TParent>> If<TParameter>(Func<TParameter, bool> guard);/// Combined version of If(Func<TRecipient, bool>) and If(Func<TParameter, bool>).public TransitionBuilder<TState, TEvent, TRecipient, TransitionBuilder<TState, TEvent, TRecipient, TParent>> If<TParameter>(Func<TParameter, bool> guard);/// Determines an action to execute when the event is raised.public TransitionBuilder<TState, TEvent, TRecipient, TParent> Do(Action action);/// Same as Do(Action) but pass the recipient as parameter.public TransitionBuilder<TState, TEvent, TRecipient, TParent> Do(Action<TRecipient> action);/// Same as Do(Action) but pass to the delegate any parameter passed during the call which matches the generic parameter type./// If no parameter passed with the specified generic parameter is found, it's ignored.public TransitionBuilder<TState, TEvent, TRecipient, TParent> Do<TParameter>(Action<TParameter> action);/// Combined version of Do(Action<TRecipient>) and Do(Action<TParameter>).public TransitionBuilder<TState, TEvent, TRecipient, TParent> Do<TParameter>(Action<TRecipient, TParameter> action);/// Configures the policy of how subscribed delegates to on entry hook should be executed./// If this method is not executed, the default policy is TransitionPolicy.ParentFirstWithCulling.public GotoBuilder<TState, TEvent, TRecipient, TParent> OnEntryPolicy(TransitionPolicy policy);/// Configures the policy of how subscribed delegates to on exit hook should be executed./// If this method is not executed, the default policy is TransitionPolicy.ChildFirstWithCulling.public GotoBuilder<TState, TEvent, TRecipient, TParent> OnExitPolicy(TransitionPolicy policy);/// Determines to which state this transition goes./// This is equivalent to: OnEntryPolicy(TransitionPolicy.ChildFirstWithCulling).OnExitPolicy(TransitionPolicy.ParentFirstWithCulling).Goto(state).public TParent Goto(TState state);/// Determines to transite to the current state./// If runParentsActions is true: OnExit and OnEntry actions of current state (but not parent states in case of current state being a substate) will be executed./// This is equivalent to OnEntryPolicy(TransitionPolicy.ChildFirstWithCullingInclusive).OnExitPolicy(TransitionPolicy.ParentFirstWithCullingInclusive).Goto(currentState)./// If runParentActions is false: OnExit and OEntry actions of the current state (and parents in case of current state being a substate) will be executed./// This is equivalent to OnEntryPolicy(TransitionPolicy.ChildFirst).OnExitPolicy(TransitionPolicy.ParentFirst).Goto(currentState).public TParent GotoSelf(bool runParentsActions = false);/// Determines that will have no transition to any state, so no OnEntry nor OnExit event will be raised./// This is equivalent to OnEntryPolicy(TransitionPolicy.Ignore).OnExitPolicy(TransitionPolicy.Ignore).GotoSelf().public TParent StaySelf();}public sealed class GotoBuilder<TState, TEvent, TRecipient, TParent> : IGoto<TState>where TState : notnullwhere TEvent : notnull{/// Configures the policy of how subscribed delegates to on entry hook should be executed./// If this method is not executed, the default policy is TransitionPolicy.ParentFirstWithCulling.public GotoBuilder<TState, TEvent, TRecipient, TParent> OnEntryPolicy(TransitionPolicy policy);/// Configures the policy of how subscribed delegates to on exit hook should be executed./// If this method is not executed, the default policy is TransitionPolicy.ChildrenFirstWithCulling.public GotoBuilder<TState, TEvent, TRecipient, TParent> OnExitPolicy(TransitionPolicy policy);/// Determines to which state this transition goes.public TParent Goto(TState state);/// Determines to transite to the current state./// This is a shortcut of Goto(currentState).public TParent GotoSelf();}/// Determines the transition policy between two states./// This configures how subscribed delegates on states are run during transition between states.public enum TransitionPolicy{/// Determines that subscribed delegates should not run.Ignore = 0,/// Determines that subscribed delegates on parents are run first.ParentFirst = 1,/// Determines that subscribed delegates on children are run first.ChildFirst = 2,/// Determines that subscribed delegates on parents are run first from (excluding) the last common parent between the two states.ParentFirstWithCulling = 3,/// Determines that subscribed delegates on children are run first until reach (excluding) the last common parent between the two states.ChildFirstWithCulling = 4,/// Determines that subscribed delegates on parents are run first from (including) the last common parent between the two states.ParentFirstWithCullingInclusive = 5,/// Determines that subscribed delegates on children are run first until reach (including) the last common parent between the two states.ChildFirstWithCullingInclusive = 6,}/// Represent an slice of data.public readonly struct ReadOnlySlice<T> : IReadOnlyList<T>{/// Get the element specified at the index.public T this[int index] { get; }/// Get the count of the slice.public int Count { get; }/// Get an <see cref="ReadOnlyMemory{T}"/> of this slice.public ReadOnlyMemory<T> Memory { get; }/// Get an <see cref="ReadOnlySpan{T}"/> of this slice.public ReadOnlySpan<T> Span { get; }/// Get the enumerator of the slice.public Enumerator GetEnumerator();/// Enumerator of <see cref="ReadOnlySlice{T}"/>.public struct Enumerator : IEnumerator<T>{/// Get current element of the enumerator.public T Current { get; }/// Moves to the next element of the enumeration.public bool MoveNext();/// Reset the enumeration.public void Reset();}}