c# extension package-Stateless

Preparation

Stateless is a finite state machine extension package. It can be installed directly through NuGet in c# projects.

To use it, you need to first write all your possible states and sub-states with enumerations.
Such as move, crouch, idle, jump, swim, run, walk.
Among them, running and walking are sub-states of movement.

Then you need to write a trigger. All state transitions must have a trigger.
So you need to describe all the opportunities accurately, and describe them even if they are used in only one place.

class Player
{<!-- -->
enum State {<!-- --> move, crouch, idle, air, double jump, swim, run, walk }
enum Trigger {<!-- --> up, down, left, right, release down, land, fall into water, out of water, fall }

private StateMachine<State, Trigger> stateMachine = new StateMachine<State, Trigger>(State.idle);
}

Configuration state machine

Calling Configure on a state machine will enable configuration for this state.
All configuration methods called from this configuration will return the configuration in this state.
All states need to be configured this way.

 public Player()
{<!-- -->
stateMachine.Configure(State.idle)
.Permit(Trigger.Down, State.Squat)
.Permit(Trigger.right, State.walk)
.Permit(Trigger.Left, State.Walk);

stateMachine.Configure(State.Squat)
.Permit(Trigger. Release, State. Idle);

}

Trigger, target, condition.

There are many permutations and combinations of configuration methods, such as

  • Permit
  • PermitIf
  • PermitDynamic
  • PermitDynamicIf

A basic Permit method accepts two parameters, the first is the trigger, and the second is who it will become after triggering.
For example, idle will turn into crouching when triggered.
Permit(Trigger.Down, State.Squat)

Dynamic

If there is Dynamic, then the second parameter will be a delegate, and you can dynamically decide who to become.
PermitDynamic(Trigger.Down, () => Guid.NewGuid() > Guid.NewGuid() ? State.Squat : State.Idle)
For example, in this code, there is a 50% probability of becoming squatting, and a 50% probability of becoming idle.

If

If there is If, then there is a third parameter. It is also a delegate, indicating a condition that requires you to return bool.
PermitIf(Trigger.Down, State.Squat, () => Guid.NewGuid() > Guid.NewGuid())
This transformation can only succeed if the conditions are met.

When triggered once, all conversions will be judged. If there are multiple solutions that can pass (even if the goal is the same), an error will be reported.

  • There are multiple identical plans
  • A scheme registers its version of If when it exists, and If passes.
  • There are multiple If versions of the scheme all passed.

Switch target

According to the target grouping, there are the following categories:

  • Permit
  • PermitReentry
  • InternalTransition
  • InitialTransition

PermitBasic conversion, optional target.

PermitReentry Re-enters itself, which means triggering its own exit and entry methods once.
It is the same as filling in the Permit directly.

InternalTransition will not switch, nor will exit and entry be triggered.
Instead, given an action delegate, your action is triggered.
Makes it look like something happened because of switching states.
.InternalTransition(Trigger.up, () => { Console.WriteLine("Cannot jump while crouching"); })

InitialTransition specifies a substate that represents the default state of this state.
Once specified, it will not stay in this state directly, and will switch away once it comes back.
For example, walking and running are substates of movement.
The initial sub-state registered for movement is walking, then switching to movement in any way will change to walking.
Including switching from walking to moving.

Ignored and undefined triggers

The Ignore method can ignore this trigger and nothing will happen.
So what happens if the declaration is ignored and this conversion is called? An error will be reported, indicating that this conversion has not been registered.

You can register a delegate with a state machine (not a configuration) to do nothing if there are unregistered transitions.
stateMachine.OnUnhandledTrigger((s, t) => { });

Conversion with parameters

Sometimes, you get input from outside, such as the position of the mouse. In this case you cannot use an enumeration to cover all situations.
You need to call the SetTriggerParameters method on the state machine and then save the return value so that you can use it later.
var upTrigger = stateMachine.SetTriggerParameters(Trigger.up);

This thing can only be used for configuration if there is an If.
Because if you don’t have conditions, then this parameter is meaningless.
.PermitIf(upTrigger, State. Squat, i => i > 4);
Use what you just got as a trigger, so your conditional delegate can use its parameters.

Give the state machine trigger

Use the Fire method passed to the state machine trigger.
It will switch states according to the configuration.

If you use a trigger with registered parameters, you can also pass parameters.

stateMachine.Fire(Trigger.water);
stateMachine.Fire(upTrigger, 10086);

Register entry and exit

What happens when the OnEntry method registers to enter this state.
The OnExit method registers what happens when exiting this state.

OnEntryFrom version of the method. Indicates that from the xx trigger, the parameters can be read using the trigger with registered parameters.

.OnEntry(() => {<!-- --> Console.WriteLine("Enter into squat"); })
.OnExit(() => {<!-- --> Console.WriteLine("Exit from crouch"); })
.OnEntryFrom(Trigger.Out of water, () => {<!-- --> })
.OnEntryFrom(upTrigger, i => Console.WriteLine("The parameter carried is " + i));

Child status

A state can be declared as a child state of another state (only one direct parent state can be declared).

 stateMachine.Configure(State.Double jump)
.SubstateOf(State.air);

stateMachine.Configure(State.run)
.SubstateOf(State.Move);

stateMachine.Configure(State.walk)
.SubstateOf(State.Move);

If only child state changes are involved and the states are the same, entry and exit of the parent state will not be performed.
For example, switching from walking to running does not change in terms of movement.

If the child state is switched to other states beyond the parent state, their exits will be executed.
For example, switching from walking to idle. Then it cannot be counted as a moving state at this time, so the movement will also end together.

bool b1 = stateMachine.IsInState(State.Move);
bool b2 = stateMachine.State == State.Move;

You can use the IsInState method to perform state detection with parent and child levels.
For example, if you are currently walking, then b1 is true, because walking is moving.
b2 is false, it is directly compared.

Asynchronous tasks

Methods with Async can change registered delegates to asynchronous form.

StateMachine<string, int> st = new StateMachine<string, int>("1");
st.Configure("1")
.Permit(2, "2")
.Permit(3, "3")
;

st.Configure("2")
.OnEntryFromAsync(2, async () =>
{<!-- -->
await Task.Delay(100);
await Console.Out.WriteLineAsync("Enter the first asynchronous of 2");
await Task.Delay(100);
await Console.Out.WriteLineAsync("Enter the second async of 2");
})
.OnEntryFrom(3, () => Console.WriteLine("Enter synchronization of 2"))
.Permit(1, "1")
.Permit(3, "3")
.OnExit(() => Console.WriteLine("Leave 2"))
;

st.Configure("3")
.OnEntry(() => Console.WriteLine("Enter 3"))
.Permit(1, "1")
.Permit(3, "2")
;

var t = st.FireAsync(2);
_ = st.FireAsync(3);
_ = st.FireAsync(1);
await t;
Console.WriteLine(st.State);

It will have the following impacts

  • During the asynchronous period, all inputs will be queued until the asynchronous is completed and then executed sequentially.
    • If you do not use the Async suffix method, the registered delegate can also be asynchronous. The state machine considers it a normal method, and the program will continue asynchronously in the background.
  • If there is an asynchronous task for the exit of the source state of the transition or the entry of the target state, then FireAsync must be used
    • Even if OnEntryFromAsync is used to carry a condition and this condition is not passed, an error will be reported.
    • You can use Fire‘s conversion, and you can use FireAsync without reporting an error.
  • If async for any universal transformation is used, then all transformations must use FireAsync
    • OnTransitionCompletedAsync
    • OnTransitionedAsync
    • OnUnhandledTriggerAsync

Sequence

  1. From child to parent, go through all Exits in sequence.
  2. Perform conversion gaps.
  3. From the parent level to the child level, Entry is executed in sequence, and Entry with parameters enters the next level.
  4. After the switch is completed, the execution conversion is completed.
StateMachine<string, int> st = new StateMachine<string, int>("1.1");

st.Configure("1")
.OnExit(() => Console.WriteLine("Exit from 1"));

st.Configure("1.1")
   .SubstateOf("1")
   .Permit(0, "2.1")
   .OnExit(() => Console.WriteLine("Exit from 1.1"));

st.Configure("2")
.OnEntry(() => Console.WriteLine("Enter 2"))
.OnEntryFrom(0, () => Console.WriteLine("Enter 2 through 0"));

st.Configure("2.1")
.OnEntry(() => Console.WriteLine("Enter 2.1"))
.OnEntryFrom(0, () => Console.WriteLine("Enter 2.2 through 0"))
.SubstateOf("2");

st.OnTransitioned(st => Console.WriteLine("Complete all exits"));
st.OnTransitionCompleted(st => Console.WriteLine("Enter the target state"));

st.Fire(0);
/*
Exiting from 1.1
Exit from 1
Complete all exits
Enter 2
Pass 0 to 2
Enter 2.1
Enter 2.2 through 0
Enter the target state
*/

Example

class Player
{<!-- -->
enum State {<!-- --> move, crouch, idle, air, double jump, swim, run, walk }
enum Trigger {<!-- --> keyboard input, landing, falling into water, out of water, falling, timeout }

DateTime TimeOut;
StateMachine<State, Trigger> stateMachine;
StateMachine<State, Trigger>.TriggerWithParameters<Vector2> InputTrigger;

publicPlayer()
{<!-- -->
stateMachine = new StateMachine<State, Trigger>(State.Idle);
InputTrigger = stateMachine.SetTriggerParameters<Vector2>(Trigger.Keyboard input);
stateMachine.OnUnhandledTrigger((s, t) => {<!-- --> });

stateMachine.Configure(State.Move)
.PermitIf(InputTrigger, State.idle, vec => vec == Vector2.Zero)
.PermitIf(InputTrigger, State.air, vec => vec == Vector2.UnitY)
.PermitIf(InputTrigger, State. squat, vec => vec == -Vector2.UnitY)
.Permit(Trigger.Fall, State.Air)
.Permit(Trigger.Fall into water, State.Swim)
;

stateMachine.Configure(State.Squat)
.PermitIf(InputTrigger, State.idle, vec => vec == Vector2.Zero)
.PermitIf(InputTrigger, State.air, vec => vec == Vector2.UnitY)
;

stateMachine.Configure(State.idle)
.PermitIf(InputTrigger, State.walk, vec => vec.Y == 0 & amp; & amp; vec.X != 0)
.PermitIf(InputTrigger, State.air, vec => vec == Vector2.UnitY)
.PermitIf(InputTrigger, State. squat, vec => vec == -Vector2.UnitY)
;

stateMachine.Configure(State.Air)
.Permit(Trigger. Landing, State. Idle)
.PermitIf(InputTrigger, State.Double jump, vec => vec == Vector2.UnitY)
;

stateMachine.Configure(State.Double jump)
.SubstateOf(State.air)
.IgnoreIf(InputTrigger, vec => vec == Vector2.UnitY)
;

stateMachine.Configure(State.swim)
.Permit(Trigger.Water, State.Air)
;

stateMachine.Configure(State.run)
.SubstateOf(State.Move)
;

stateMachine.Configure(State.walk)
.SubstateOf(State.Move)
.PermitIf(Trigger.Timeout, State.Run, () => DateTime.Now - TimeOut > TimeSpan.FromMicroseconds(500))//If the last update time exceeds 500 seconds, it means that you just held down and walked for 0.5 seconds
.OnEntry(async () =>
{<!-- -->
TimeOut = DateTime.Now;
await Task.Delay(500); //Try to run after a 0.5 second delay
stateMachine.Fire(Trigger.Timeout);
})
.OnExit(() =>
{<!-- -->
TimeOut = DateTime.Now;
})
;

}
}