# State Machines
###### tags: `state machine`
This is a state machine solution on demand. The base solution consists of the below classes. There are more types and classes hidden to this solution. This solution is very close to Unity’s Animator solution. The idea is that we define states, parameters and the transitions between the states using the parameters in order to define conditions.
## Structure
This is a typical definition structure for a state machine.
1. Define states as enums
2. Define parameters as enums
3. Add states
- You can use the ready generic states or create your own states as separate classes.
4. Register parameter
- In most cases, we have to register the parameters.
- In the case of some special parameters used by some generic states (e.g. `WaitForKey01State`) we do not register the parameters. The corresponding Add method will take care of the parameter registration.
5. Add transitions
### State Machine
To create a state machine we use the `StateMachineManager`. After we define the state machine we have to call the `StateMachineManager::StartStateMachine()` method to start the state machine.
### Task State Machine
In the case of a task state machine, we derive our class from `TaskStateMachineManager` and we define the state machine overriding the `protected override void InitStateMachine()` method. We do not start the state machine ourselves. The task state machine is managed by the `ModuleStateMachineManager.`.
## Examples
### Simple State Machine
A simple example of a door state machine with 2 states `Opened` and `Closed`. This state machine manages an Animator component.
```csharp
public class DoorStateMachineExample : MonoBehaviour
{
private Animator m_Animator;
private StateMachineManager m_SMManager;
private enum States { Opened, Closed }
private enum Params { SpacePressed }
// Start is called before the first frame update
void Start()
{
// get animator component
m_Animator = GetComponent<Animator>();
// create state machine
m_SMManager = StateMachineManager.Create(this.gameObject);
// add generic note states
m_SMManager.AddState(new OpenedState(), States.Opened, m_Animator);
m_SMManager.AddState(new ClosedState(), States.Closed, m_Animator);
// register trigger parameter
m_SMManager.RegisterTriggerParam(Params.SpacePressed);
// add transitions
m_SMManager.AddTransition(States.Opened, States.Closed, new TriggerCondition(Params.SpacePressed));
m_SMManager.AddTransition(States.Closed, States.Opened, new TriggerCondition(Params.SpacePressed));
// start state machine
m_SMManager.StartStateMachine();
}
private void Update()
{
// fire SpacePressed trigger param on Space key pressed
if (Input.GetKeyDown(KeyCode.Space))
m_SMManager.SetTriggerParam((int)Params.SpacePressed);
}
/// <summary>
/// Opened state.
/// </summary>
private class OpenedState : State
{
Animator m_Animator;
public override void OnInit()
{
// get animator component from payload
m_Animator = (Animator)payload[0];
}
public override void OnEnter()
{
m_Animator.SetBool("Open", true);
}
}
/// <summary>
/// Closed state.
/// </summary>
private class ClosedState : State
{
Animator m_Animator;
public override void OnInit()
{
// get animator component from payload
m_Animator = (Animator)payload[0];
}
public override void OnEnter()
{
m_Animator.SetBool("Open", false);
}
}
}
```
### Simple State Machine Simplified
The same state machine as above, but with one class-state defined using payloads.
```csharp
public class DoorStateMachineSimplifiedExample : MonoBehaviour
{
private Animator m_Animator;
private StateMachineManager m_SMManager;
private enum States { Opened, Closed }
private enum Params { SpacePressed }
// Start is called before the first frame update
void Start()
{
// get animator component
m_Animator = GetComponent<Animator>();
// create state machine
m_SMManager = StateMachineManager.Create(this.gameObject);
// add generic note states
m_SMManager.AddState(new AnimatorBoolState(), States.Opened, m_Animator, "Open", true);
m_SMManager.AddState(new AnimatorBoolState(), States.Closed, m_Animator, "Open", false);
// register trigger parameter
m_SMManager.RegisterTriggerParam(Params.SpacePressed);
// add transitions
m_SMManager.AddTransition(States.Opened, States.Closed, new TriggerCondition(Params.SpacePressed));
m_SMManager.AddTransition(States.Closed, States.Opened, new TriggerCondition(Params.SpacePressed));
// start state machine
m_SMManager.StartStateMachine();
}
private void Update()
{
// fire SpacePressed trigger param on Space key pressed
if (Input.GetKeyDown(KeyCode.Space))
m_SMManager.SetTriggerParam((int)Params.SpacePressed);
}
/// <summary>
/// Animator bool state.
/// </summary>
private class AnimatorBoolState : State
{
Animator m_Animator;
string m_AnimParamName;
bool m_AnimParamValue;
public override void OnInit()
{
// get animator component from payload
m_Animator = (Animator)payload[0];
// get animator param name
m_AnimParamName = (string)payload[1];
// get animator param value
m_AnimParamValue = (bool)payload[2];
}
public override void OnEnter()
{
m_Animator.SetBool(m_AnimParamName, m_AnimParamValue);
}
}
}
```
### Simple State Machine With Generic States
Same as the above state machine, replacing the defined state-class with the generic `ActionState`.
```csharp
public class DoorStateMachineGenericExample : MonoBehaviour
{
private Animator m_Animator;
private StateMachineManager m_SMManager;
private enum States { Opened, Closed }
private enum Params { SpacePressed }
// Start is called before the first frame update
void Start()
{
// get animator component
m_Animator = GetComponent<Animator>();
// create state machine
m_SMManager = StateMachineManager.Create(this.gameObject);
// add generic note states
m_SMManager.AddActionState(States.Opened, () => m_Animator.SetBool("Open", true));
m_SMManager.AddActionState(States.Closed, () => m_Animator.SetBool("Open", false));
// register trigger parameter
m_SMManager.RegisterTriggerParam(Params.SpacePressed);
// add transitions
m_SMManager.AddTransition(States.Opened, States.Closed, new TriggerCondition(Params.SpacePressed));
m_SMManager.AddTransition(States.Closed, States.Opened, new TriggerCondition(Params.SpacePressed));
// start state machine
m_SMManager.StartStateMachine();
}
private void Update()
{
// fire SpacePressed trigger param on Space key pressed
if (Input.GetKeyDown(KeyCode.Space))
m_SMManager.SetTriggerParam((int)Params.SpacePressed);
}
}
```
### Task State Machine With Generic States
This is a simple real example from the PSC1515 Module. This state machine has 5 states
```
private enum States { WaitForKey, Intro, OffSuit, OnSuit, Outside }
+------------+ +-----+ +--------+ +-------+ +-------+ +----+
|Wait For Key+-->+Intro+-->+OffSuite+<--->+OnSuite+<--->+Outside|-->|EXIT|
+------------+ +-----+ +--------+ +-------+ +-------+ +----+
```
and 4 parameters
```csharp
private enum Params { IsOutsideBool, InVehcileBool, HasSuitBool, WaitForKey }
```
The first state is a `WaitForKey01State` and we are using the `WaitForKey` parameter (which we do **not need to register** as the `AddWaitKey01State` method take cares of it) because `WaitForKey01State` needs a parameter id in order to work.
All the other states are simple `NotificationState`. We feed the notification states with the `UI.ContentNotificationData` assets which we assign through the inspector. Is important to notice that the Task State Machines does not have any direct reference to a scene game object.
We define the state machine in the `protected override void InitStateMachine()` method which is called when the task starts. Also, we initialize some parameters and some other global values in `protected override void OnAfterStartStateMachine()`, which is called after the state machine starts.
The other methods are used to change the state machine's parameter during the gameplay.
```csharp
public class IntroductionStateMachine : TaskStateMachineManager
{
private enum States { WaitForKey, Intro, OffSuit, OnSuit, Outside }
private enum Params { IsOutsideBool, InVehcileBool, HasSuitBool, WaitForKey }
[SerializeField] UI.ContentNotificationData m_Press1Not;
[SerializeField] UI.ContentNotificationData m_InsideBiodomeNotification;
[SerializeField] UI.ContentNotificationData m_GoOutsideNotification;
[SerializeField] UI.ContentNotificationData m_OutsideVehcileNotification;
protected override void InitStateMachine()
{
// add states
m_SMManager.AddWaitForKey01State(m_Press1Not, States.WaitForKey, Params.WaitForKey, States.Intro);
m_SMManager.AddNotificationState(States.Intro, m_InsideBiodomeNotification);
m_SMManager.AddNotificationState(States.OffSuit, m_GoOutsideNotification);
m_SMManager.AddNotificationState(States.OnSuit, m_GoOutsideNotification);
m_SMManager.AddNotificationState(States.Outside, m_OutsideVehcileNotification,
()=>this.SendContentMessage<string>(ContentMessage.AddCompassLandmark, "ROVER"));
// register params
m_SMManager.RegisterParam(Params.IsOutsideBool, false);
m_SMManager.RegisterParam(Params.InVehcileBool, false);
m_SMManager.RegisterParam(Params.HasSuitBool, false);
// define transitions
m_SMManager.AddTransition(States.Intro, States.OffSuit);
m_SMManager.AddTransition(States.OffSuit, States.OnSuit, new BoolCondition(Params.HasSuitBool, true));
m_SMManager.AddTransition(States.OnSuit, States.OffSuit, new BoolCondition(Params.HasSuitBool, false));
m_SMManager.AddTransition(States.OnSuit, States.Outside, new BoolCondition(Params.IsOutsideBool, true));
// Outside -> Inside
m_SMManager.AddTransition(States.Outside, States.OnSuit, new BoolCondition(Params.IsOutsideBool, false));
// Outside -> EXIT
m_SMManager.AddExitTransition(States.Outside, new BoolCondition(Params.InVehcileBool, true));
//for starting directly from outsid mars
m_SMManager.AddTransition(States.OffSuit, States.Outside, new BoolCondition(Params.IsOutsideBool, true));
this.ObserveContent(ContentMessage.OnBiodomeSceneStarted, OnBiodomeSceneStarted);
this.ObserveContent(ContentMessage.OnMarsOutsideSceneStarted, OnMarsOutsideSceneStarted);
this.ObserveContent(ContentMessage.VehicleEntered, OnVehicleEntered);
PSC1515_Blackboard.hasSpacesuit.AddListener(OnHasSuitValueChanged);
}
protected override void OnAfterStartStateMachine()
{
// init Is Outside param
m_SMManager.SetParam((int)Params.IsOutsideBool, PSC1515_Blackboard.scene == PSC1515_Blackboard.Scene.Mars);
PSC1515_Blackboard.canGoOutside.value = true;
PSC1515_Blackboard.canEnterBiodome.value = true;
PSC1515_Blackboard.roverInitLocation.value = Mars.Vehicle.RoverInitLocation.A;
}
private void OnDestroy()
{
this.RemoveContentObserver(ContentMessage.OnBiodomeSceneStarted, OnBiodomeSceneStarted);
this.RemoveContentObserver(ContentMessage.OnMarsOutsideSceneStarted, OnMarsOutsideSceneStarted);
this.RemoveContentObserver(ContentMessage.VehicleEntered, OnVehicleEntered);
PSC1515_Blackboard.hasSpacesuit.RemoveListener(OnHasSuitValueChanged);
}
private void OnHasSuitValueChanged (bool hasSuit)
{
m_SMManager.SetParam((int)Params.HasSuitBool, hasSuit);
}
private void OnBiodomeSceneStarted ()
{
Debug.Log("SM -> OnBiodomeSceneStarted");
m_SMManager.SetParam((int)Params.IsOutsideBool, false);
}
private void OnMarsOutsideSceneStarted()
{
Debug.Log("SM -> OnMarsOutsideSceneStarted");
m_SMManager.SetParam((int)Params.IsOutsideBool, true);
}
private void OnVehicleEntered ()
{
this.SendContentMessage<string>(ContentMessage.RemoveCompassLandmark, "ROVER");
m_SMManager.SetParam((int)Params.InVehcileBool, true);
}
}
}
```