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 : notnull
where 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 : notnull
where 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> : IFinalizable
where TState : notnull
where 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> : IFinalizable
where TState : notnull
where 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 : notnull
where 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 : notnull
where 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();
}
}