Unity Game AI with Finite State Machines

A simple definition of a Finite State Machine (FSM) is an algorithm that receives an input and returns an output given its internal state. They're widely used in games, not only for game AI, but for many other entities within the game as well. Your character in the game will act differently for the same inputs if the state is different. The character might punch when you press X, but will do something different when climbing a ladder.

This is one of the best explanations of a FSM used in games and I recommend it if the concept is new to you: Game Programming Patterns - State.

This post's objective is to guide you through the creation of a simple game AI for a 2D character using an FSM. With only two states, the NPC will patrol and follow the player when he gets close enough.

unity-state-machine.gif

I'll omit most of the code here, focusing only on what's important to understand for the FSM. You can check the entire source code in this small project that I wrote a while ago: unity-finite-state-machine.

why I'm not using the animation controller

I've seen some people using the animation controller as a way to handle states from the FSM. The controller is an FSM, but I don't like to use it for the NPC behaviour since they are two separate things. It ties your states to Unity's API and it makes it hard to customize. You'll eventually find your character's behaviour state is completely different from his animation state.

In the approach I describe here, we create our own classes. It's not complicated and is 100% customizable for your own use case.

components of a FSM

There are two types of components needed for a FSM to work.

  • states that define how the entity will act when the state is active
  • transitions that define what will trigger the change of state

creating the base classes

Base classes will be used as extensions for new FSMs, states and transitions.

public abstract class Transition
{
    readonly State state; 

    public Transition(State state)
    {
        this.state = state;
    }

    public State GetNextState() => state;
    public abstract bool IsValid();
    public abstract void OnTransition();
}

Transitions are simple, they have a condition that when met returns true in the IsValid method. They also hold a reference to the next state that will be triggered when the condition is met.

OnTransition is only used if you need a specific action when this transition happens.

public abstract class State
{
    protected Fsm fsm;
    Transition[] transitions;

    public State(Fsm fsm)
    {
         this.fsm = fsm;
    }

    public void SetTransitions(params Transition[] transitions)
        => this.transitions = transitions;

    public void Update()
    {
        Execute();

        if (CanStateBeChanged())
        {
            CheckTransitions();
        }
    }

    public abstract void OnEnter();
    protected abstract void Execute();
    public abstract void OnExit();

    private void CheckTransitions()
    {
        if (transitions == null) return;
        foreach (var transition in transitions)
        {
            if (transition.IsValid())
            {
                transition.OnTransition();
                fsm.ChangeState(transition.GetNextState());
            }
        }
    }

    public virtual bool CanStateBeChanged() => true;
}

Update is not the MonoBehaviour function, but will be called inside one. We can define the entity behavior in the Execute method, every Update call will check for transitions that have it's conditions met and will emit a change of state to the FSM instance.

public abstract class Fsm : MonoBehaviour
{
    public Controller.CharacterController controller;
    public IAwareness awareness;
    State currentState; 

    void Awake() => SetupStates();
    protected abstract void SetupStates();
    void Update() => currentState.Update(); 

    protected void SetupFirstState(State state)
    {
        currentState = state;
        currentState.OnEnter();
    }

    public void ChangeState(State newState)
    {
        if (!currentState.CanStateBeChanged()) return;

        currentState.OnExit();
        newState.OnEnter();
        currentState = newState;
    } 
}

Both the Controller.CharacterController and IAwareness will be abstracted in this post. We can consider that one deals with moving the character and the other checks if the player is around this character by using raycasts.

The FSM has a current state that will be executed in every frame and a method to change the current state and execute all transition functions. It also has an abstract method SetupStates that will be used by the actual FSM implementation to setup all states.

writing the actual behavior

image.png

We can start by coding the two states that this character has. It's nice to think beforehand if the new state can be used for other characters as well. Try to make them reusable so you don't have to write similar states/transitions every time. At some point, you'll be able to create complex behaviour with different small states.

public class Patrol : State
{
    readonly PatrolData patrolData; 
    int direction; 
    float currentTimer; 
    bool walking = false;

    public Patrol(Fsm fsm, PatrolData patrolData) : base(fsm)
    {
        this.patrolData = patrolData;
    }

    protected override void Execute()
    {
        currentTimer += Time.deltaTime;

        if (walking)
        {
             if (currentTimer > patrolData.timeWalking)
             {
                  walking = false;
                  currentTimer = 0;
                  fsm.controller.SetXInput(0);

                  // changes dir for next walk
                  direction = -direction;
             }
        }
        else
        {
            if (currentTimer > patrolData.timeStopped)
            {
                walking = true;
                currentTimer = 0;
                fsm.controller.SetXInput(direction);
            }
        }
    }

    public override void OnEnter()
    {
        currentTimer = 0;
        direction = patrolData.firstDirection;
    }

    public override void OnExit()
    {
        fsm.controller.SetXInput(0);
    }
}

[System.Serializable]
public struct PatrolData
{
    public float timeStopped;
    public float timeWalking;
    public int firstDirection;
}

I'm using a struct to pass some custom data to this state, meaning I can later customise it for new characters that have other patrol behaviors.

The behaviour is simple, it walks for a few seconds and stops after a while. By calling the SetInputX, the character walks in that direction until the value is changed to 0 again. This is done when the defined amount of time passes or when this state is being replaced and the OnExit method is called.

Notice that the State has a reference to the controller through the fsm instance. That could also be implemented in a way that the state itself holds this reference. I prefer to use the fsm one to avoid duplication.

public class FollowTarget : State
{
    int lastDirection; 
    bool stopped = false; 

    public FollowTarget(Fsm fsm) : base(fsm)  { } 

    protected override void Execute()
    {
        int targetDirection = fsm.awareness.GetTargetDirection();

        if (targetDirection != lastDirection || stopped)
        {
            stopped = false;
            fsm.controller.SetXInput(targetDirection);
        }

        lastDirection = targetDirection;
    }

    public override void OnEnter()
    {
        stopped = true;
        lastDirection = 0;
    }

    public override void OnExit()
    {
        fsm.awareness.ResetTarget();
        fsm.controller.SetXInput(0);
    } 
}

Now, instead of walking in any direction, the FollowTarget state checks in the awareness instance where the target is and walks that way.

public class TargetIsClose : Transition
{
    readonly float distance; 
    readonly IAwareness awareness; 

    public TargetIsClose(State state, float distance, IAwareness awareness) : base(state)
    {
        this.distance = distance;
        this.awareness = awareness;
    }

    public override bool IsValid() => awareness.GetTargetDistance() < distance;
    public override void OnTransition() { }
}

public class TargetIsFar : Transition
{
    readonly float distance; 
    readonly IAwareness awareness; 

    public TargetIsFar(State state, float distance, IAwareness awareness) : base(state)
    {
        this.distance = distance;
        this.awareness = awareness;
    }

    public override bool IsValid() => awareness.GetTargetDistance() > distance;
    public override void OnTransition() { }
}

Those two transitions are simple and could even be written as a single one, with parameters that define if it's supposed to react when further or closer.

Now we can build the actual EnemyFsm using those states and transitions.

public class EnemyFsm : Fsm
{
    [SerializeField] PatrolData patrolData; 
    [SerializeField] float distanceToStopFollowing;
    [SerializeField] float distanceToStartFollowing;
    [SerializeField] AreaAwareness areaAwareness;

    protected override void SetupStates()
    {
        State patrol = new Patrol(this, patrolData);
        State followTarget = new FollowTarget(this);

        Transition patrolToFollow = new TargetIsClose(followTarget, distanceToStartFollowing, areaAwareness); 
        Transition followToPatrol = new TargetIsFar(patrol, distanceToStopFollowing, areaAwareness);

        patrol.SetTransitions(patrolToFollow);
        followTarget.SetTransitions(followToPatrol);

        SetupFirstState(patrol);
    }
}
  • we first instantiate the states;
  • then we create the transitions and which state will be triggered if their condition is met;
  • each state can have its own transitions assigned to it, more than one can be added, but in this case there's only one for each;
  • lastly, we setup the initial state for the FSM.

downsides from the approach

I feel like using this for simple behaviours it's pretty nice. If you have the states and transitions already written it's quick and easy to design new enemies. However, for complex ones it might get messy, there's a way to implement states that have substates, as if they had their own FSM. I've used that, but it does add way more complexity to the approach.

The SetupStates method might not be so easy to describe the actual behaviour of each FSM. That's why I always draw the state with the transitions as I did at the beginning of this post.

debugging

I highly recommend hooking up some kind of debugger in the FSM. My repo has one that shows the current state on top of the character's head, but it would be nice to work more on that and log the states and their transitions.

other scenarios

As I mentioned in the beginning, this can be used for different things other than game AI. A gun that the character uses can have states - like loading, shooting, idle - and act in a different way given the inputs the player gives.

But that doesn't mean you have to implement all those classes to do it. You can also use simpler approaches with Enums, like described in the post that's linked in the beginning of the post.