State transitions
In order to define relations between states you need to create transitions. In a nutshell, a transition specifies a possible state change from an origin state to a target state when a specific action is triggered. As many transitions as needed can be defined.
Simple transitions
Simple transitions only define the origin state, target state and the action that triggers the transition. You can add a transition using the AddTransition
method from the defined state, indicating the action and the target state. Example:
var state = mb.AddState("StateA");
state.AddTransition("switch", "StateB");
The target state is StateB and the action is switch.
Lets see a working example:
- Diagram
- Machine config
var machineBlueprint = StateMachine<string, string, string>.Factory((mb) => {
// Define states
var stateA = mb.AddState("StateA");
mb.AddState("StateB");
// Define transitions
stateA.AddTransition("toB", "StateB");
}, "StateA");
This example shows us a transition from StateA to StateB. It only happens when the action toB is triggered.
Let's see another example:
- Diagram
- Machine config
var machineBlueprint = StateMachine<string, string, string>.Factory((mb) => {
// Define states
var stateA = mb.AddState("StateA");
mb.AddState("StateB");
mv.AddState("StateC");
// Define transitions
stateA.AddTransition("toB", "StateB");
stateA.AddTransition("toC", "StateC");
}, "StateA");
This example shows us two transitions from StateA:
- To StateB when the action toB is triggered.
- To StateC when the action toC is triggered.
We can see that the target state depends on the action that is triggered.
Conditional transitions
Conditional transitions work as simple transitions but they only work when a provided method returns true
. This is useful when you want to disable transitions based on programmatic behavior. One popular case is disabling transitions depending on the
context value.
You can add a condition to a simple transition by using the When
method from the transition instance. Example:
var stateA = mb.AddState("StateA");
var transitionToB = stateA.AddTransition("toB", "StateB");
transitionToB.When((info) => {
// When we return `true` the transition is enabled. Otherwise it is disabled.
return true;
});
Let's use the day counter example. The machine should stop transitioning to night once the days count reaches 10. This example uses the machine context, read the context documentation to understand it better:
- Diagram
- Machine config
var machineBlueprint = StateMachine<string, string, int>.Factory((mb) => {
// Define states
var day = mb.AddState("DAY");
var night = mb.AddState("NIGHT");
// Define transitions
var dayTransition = day.AddTransition("skip", "NIGHT");
night.AddTransition("skip", "DAY");
// Define conditions
dayTransition.When((info) => {
return info.Machine.Context < 10; // Allows the transition only when days count (stored in the context) is less than 10
});
// Define events
day.OnEnter((info) => {
var machine = info.Machine;
machine.MutateContext((context) => context + 1); // Increment the days count
});
}, "DAY", 0); // Initial state is 'DAY' and initial context is '0'
Multiple conditions
One transition can have multiple conditions. You can add more than one by simply invoking the When
method again. For example, let's add another condition to the past example:
// ...
// Define conditions
// Same condition as the previous example
dayTransition.When((info) => {
return info.Machine.Context < 10;
});
// New condition that will also be evaluated
dayTransition.When((info) => {
return false;
});
// ...
Even thought the first condition will sometimes return true
the second one will prevent the transition from changing the state. This is because the machine engine treats both conditions as a single AND condition (condition1 AND condition2), so if one of them is false
the transition is disabled.
Condition dependant transitions
Conditional transitions allow you to create condition dependant transitions. This happens when a transition needs a condition to obtain a proper working machine. For example, when a state has different two transitions to two different states with the same action. See the diagram:
- Diagram
- Machine config
var machineBlueprint = StateMachine<string, string, int>.Factory((mb) => {
// Define states
var stateA = mb.AddState("StateA");
var stateB = mb.AddState("StateB");
var stateC = mb.AddState("StateC");
// Define transitions
stateA.AddTransition("change", "StateB");
stateA.AddTransition("change", "StateC");
}, "StateA");
We can see that StateA has two transitions (to StateB and StateC) with the action change. This is a valid state machine, but it might lead to unexpected behaviors as the machine has no priority on which state it has to choose. Here is where conditional transitions come in.
To solve this you only need to specify a condition to the state that has to be conditionally evaluated before triggering the transition. This way the state machine will prioritize transitioning to the conditional transition. If the condition fails it tries to transition with the other transitions. Let's update the example.
- Diagram
- Machine config
var machineBlueprint = StateMachine<string, string, int>.Factory((mb) => {
// Define states
var stateA = mb.AddState("StateA");
var stateB = mb.AddState("StateB");
var stateC = mb.AddState("StateC");
// Define transitions
stateA.AddTransition("change", "StateB");
stateA.AddTransition("change", "StateC").When((info) => {
// Here you can define your own custom condition
return true;
});
}, "StateA");
With the previous modification you prioritized StateC over StateB when transitioning from StateA with the action change. If StateC transition condition fails it will transition to StateB.
If more than one transition has a condition you cannot known which transition will be picked first. You only known conditional transitions are evaluated before non conditional transitions.
Transitions from any state
Sometimes you might want to define an action that triggers a transition to a specific state from any state. This can be done by using the special any state. Take the following example:
- Diagram
- Machine config
var machineBlueprint = StateMachine<string, string, string>.Factory((mb) => {
// Define states
var idleState = mb.AddState("IDLE");
var walkingState = mb.AddState("WALKING");
var runningState = mb.AddState("RUNNING");
// Define transitions
idleState.AddTransition("increment_speed", walkingState);
walkingState.AddTransition("increment_speed", runningState);
walkingState.AddTransition("reduce_speed", idleState);
runningState.AddTransition("reduce_speed", walkingState);
// Here we use the special AnyState() to define the transition.
// It behaves like a normal state, so it can have more than one transition and handle events.
mb.AnyState().AddTransition("force_idle", idleState);
}, "IDLE");
When using the AnyState() you are given an anonymous state (it behaves like a state, but it has no state code). You can define more than one transition and add conditions.
Transitions which origin is AnyState have maximum priority. If other transitions sharing the same action code are defined, they will be evaluated after the transitions from AnyState. It does not matter if AnyState transitions have no conditions attached.