Friday 10 August 2012

Finite State Machines (FSM)

What is a State Machine?

Generally, a state machine is a model for logic systems. It takes the form of an abstract "machine" which can be in one of many different "states". The machine can "transition" between states given some condition has been met.

As an example, a light switch can be modelled using a very basic state machine. It has two states, on and off. If the current state is 'on' and the switch gets toggled then the machine transitions to the 'off' state and vice versa if the current state is 'off'.

How do they apply to games development?

When coding AI (artificial intelligence) for characters, state machines are one of the most common tools in the programmer's arsenal. They provide an intuitive, easy to debug means for creating the various AI behaviours for a character. After all, it's very easy to imagine characters switching between clear behaviours (states), such as 'alerted', 'caution' and 'unaware' in a stealth game.

How could you implement them?

A naive approach would be to simply use a series of if/else statements or a switch structure. For anything more than the most rudimentary of AI behaviour though, this usually leads to messy code that's difficult to debug and hard to follow.
public void Update(GameTime gameTime)
{
    // Change states.
    if (PlayerVisible)
    {
        state = ChaseState;
    }
    else
    {
        // Decide if we should wander or idle.
        if (random.NextDouble() < 0.5)
        {
            state = WanderState;
        }
        else
        {
            state = IdleState;
        }
    }

    // State logic.
    switch (state)
    {
        case ChaseState:
            // Chase logic.
            break;

        case WanderState:
            // Wander logic.
            break;

        // Etc.
    }
}
In this blog post, I'll be discussing a more structured approach that I feel better compartmentalises AI behaviour and keeps the Update() logic easier to follow. It revolves around a central StateMachine class, which manages instances of various State classes.

State Base Class

The base state class will be used for all state behaviour and should declare all of the core functions for any state. For this purpose we'll use a main Update() method for processing logic and Enter() and Exit() methods for initialisation and tidying up when changing states.
abstract class State<T>
{
    public abstract void Update(T owner, GameTime gameTime);

    public abstract void Enter(T owner);
    public abstract void Exit(T owner);
}
We want the state class to be generic, such that it can be used for any character, however if we only use basic object arguments then we won't be able to access any of the class-specific properties and methods for any character. As such, we use the generic type T when declaring the class and functions so that regardless of which character we're creating behaviour for, we can access their properties.

The abstract keyword specifies that both the classes and methods should not be used directly, they must be inherited/overwritten. This concept is similar, though not identical, to the virtual keyword, which means the class/method can be inheritted/overwritten but is still able to be directly used.

StateMachine Class

The state machine class is the class that manages a character's state and should declare methods for the owner to update the state logic and for states to transition between each other. As such we have an Update() method, as well as a ChangeState() and RevertState() method.
class StateMachine<T>
{
    T owner;
    State<T> currentState;
    State<T> previousState;
    State<T> globalState;

    public State<T> CurrentState
    {
        get { return currentState; }
    }

    public State<T> GlobalState
    {
        get { return globalState; }
    }

    public State<T> PreviousState
    {
        get { return previousState; }
    }

    public StateMachine(T owner, State<T> global,
        State<T> current)
    {
        this.owner = owner;
        this.globalState = global;
        this.currentState = current;
        this.previousState = current;
    }

    public void Update(GameTime gameTime)
    {
        if (globalState != null)
        {
            globalState.Update(owner, gameTime);
        }

        if (currentState != null)
        {
            currentState.Update(owner, gameTime);
        }
    }

    public void ChangeState(State<T> newState)
    {
        if (newState == null)
        {
            return;
        }

        previousState = currentState;

        currentState.Exit(owner);

        currentState = newState;

        currentState.Enter(owner);
    }

    public void RevertState()
    {
        ChangeState(previousState);
    }
}
As before, we use generic type T when creating the state machine so that when states are updated, the correct class type can be passed as an argument. In addition, we also store and update a global state which can be used for passive behaviour which is not specific to one state. For example, we might have an enemy who should always change to a chasing state upon spotting the player.

Individual States

Individual states inherit from the base state class, where the desired class is passed in the angle brackets to incorporate the generic type functionality we previously discussed. All the abstract methods are also overriden using this class to access the owner's properties and methods when performing logic.
class EnemyIdleState : State<Enemy>
{
    // Constants/Readonly.
    private static readonly EnemyIdleState instance = new EnemyIdleState();
    const float rethinkTime = 5;

    public static State<Enemy> Instance
    {
        get { return instance; }
    }

    public override void Update(Enemy owner, GameTime gameTime)
    {
        owner.Rethink -= gameTime.ElapsedGameTime;

        if (owner.Rethink <= TimeSpan.Zero)
        {
            owner.StateMachine.ChangeState(EnemyThinkState.Instance);
        }
    }

    public override void Enter(Enemy owner)
    {
        owner.Rethink = TimeSpan.FromSeconds(rethinkTime);
    }

    public override void Exit(Enemy owner)
    {
    }
}
In addition, we also use a concept known as the singleton pattern to prevent multiple copies of identical states from being instanced. The idea is that the class holds an instance of itself, the singleton, which can be accessed using a static property. This way instead of passing new instances when changing states, the singleton can be passed using State.Instance, as seen on line 18.



The concepts discussed in this blogpost are covered in far more detail in Mat Buckland's Programming Game AI by Example book, along with many other AI techniques.

No comments:

Post a Comment