How to use the state machine - DarkOceanInteractive/project-wendigo GitHub Wiki

State context

Begin by creating a state context: a MonoBehaviour mainly responsible for holding the current state.

public class PlayerStateContext : AStateContext
{
    private void Start()
    {
        this.SetState(new PlayerStates.IdleState());
    }
}

AStateContext inherits from MonoBehaviour and provides you with a SetState method to change state at any time. By default, we can for instance set the current state to an IdleState that we will define later.

States

To avoid states names conflicting, we isolate states in namespaces. In this case, we want to create states for our player, so we add them to the PlayerStates namespace:

namespace PlayerStates
{
    public class IdleState : AState<PlayerStateContext>
    {
        override public void Update()
        {
        }
    }
}

The AState class provides the state class with a default implementation for each member of the IState interface so that each state can override only the methods needed.
It also exposes a context member in each state, which refers to the corresponding state context. You can access every member of the state context, including the members of a MonoBehaviour, such as transform or the GetComponent method. Another use case will be explained later.

State interface

The IState interface defines the methods a state can have and therefore override. Please refer to the current version of the state interface to have an up-to-date list of methods that can be overridden, as it may evolve with time.

Accessing the context's variables

States-specific variables should be declared in the states themselves as much as possible, but sometimes you might want to have some variables modifiable from Unity's Inspector tab. In that case, you can declare them in the state context, and access them from the states. Consider the following example:

  • A state context containing a public variable that can be modified through Unity's Inspector tab:
public class PlayerStateContext : AStateContext
{
    public moveSpeed = 10f;

    private void Start()
    {
        this.SetState(new PlayerStates.IdleState());
    }
}
  • States that can access the state context's public members:
namespace PlayerStates
{
    public class IdleState : AState<PlayerStateContext>
    {
        override public void Update()
        {
            // if any movement inputs, change state to `MovingState`
            if (Input.GetAxisRaw("Horizontal") != 0f || Input.GetAxisRaw("Vertical") != 0f)
                this.context.SetState(new MovingState());
        }
    }

    public class MovingState : AState<PlayerStateContext>
    {
        override public void Update()
        {
            float xAxis = Input.GetAxisRaw("Horizontal");
            float yAxis = Input.GetAxisRaw("Vertical");

            // if no more movement inputs, change state back to `IdleState`
            if (xAxis == 0f && yAxis == 0f)
            {
                this.context.SetState(new IdleState());
                return;
            }

            var direction = new Vector3(xAxis, 0f, yAxis);
            // get `transform` from the state context
            direction = this.context.transform.InverseTransformDirection(direction);
            // get `moveSpeed` variable from the state context
            this.context.transform.Translate(direction * this.context.moveSpeed * Time.deltaTime);
        }
    }
}

Note: each state should be in its own file. Here, two states are defined side by side for demo purposes.