How Unity uses finite state machines and their derivatives to manage character and enemy AI

When making games, you often need to create a Player or Enemy object. Such objects can usually move, jump, attack, etc. Let’s briefly imagine how to implement these functions.

Move first

[SerializeField]private float speed=10.0f;
    
    void Update()
    {
        var horizontal = Input.GetAxis("Horizontal");
        var vertical = Input.GetAxis("Vertical");
        Vector2 moveDirection = new Vector2(horizontal, vertical);
        if (moveDirection.magnitude > 1)
            moveDirection = moveDirection.normalized;
        transform.Translate(speed*Time.deltaTime*moveDirection);
    }

The above code can easily realize the simple movement of a 2D plane object.

Next add a sprint function

[SerializeField]private float speed=10.0f;
    private bool _isDash;
    
    void Update()
    {
        var horizontal = Input.GetAxis("Horizontal");
        var vertical = Input.GetAxis("Vertical");
        Vector2 moveDirection = new Vector2(horizontal, vertical);
        if (moveDirection.magnitude > 1)
            moveDirection = moveDirection.normalized;
        if(!_isDash)
        transform.Translate(speed*Time.deltaTime*moveDirection);

        if (Input.GetKeyDown(KeyCode.Space))
            _isDash = true;
        if(_isDash)
            Dash();
    }
    private void Dash()
    {
        //The specific method of dash
        _isDash = false;
    }

As you can see, the code adds a Boolean variable to determine the sprint time, and the Dash method is executed once by pressing the Space key. Of course, the sprint may be executed within a period of time, and the Dash method can be executed within a period of time by adding a timer. method, but what the author wants to emphasize is not how to implement it, but the disadvantages of directly mixing the two states in the same piece of code.

In the code, it should not be possible to move while sprinting, so a bool is used to specify the timing of using the Translate method. Obviously, the sprint state affects the original movement state. Now imagine that we need to add a new attack state, which cannot move or sprint during the attack. Obviously, we need a new Boolean value to restrict the timing of using the Dash method and the Translate method.

It is conceivable that when states are continuously added, the past states need to continuously add new constraints based on the new states added, which will gradually become quite difficult to manage for larger projects.

So, is there any way to solve this problem.

One of the answers is Finite State Machine

Now let’s take a look at how to create a finite state machine

First create the folder Base

Create three corresponding scripts in this folder

The name depends on the object you need. If you need to create a state machine for an enemy object, you can also name it Enemy EnemyFSM EnemyState.

First edit the ActorState script

public class ActorState
{
    protected Actor actor; //role object class
    protected ActorFSM actorFsm; //role Fsm
    /// <summary>
    /// Obtain the role and role state machine references in the constructor
    /// </summary>
    /// <param name="actor">Role class</param>
    /// <param name="actorFsm">Actor State Machine</param>
    public ActorState(Actor actor,ActorFSM actorFsm)
    {
        this.actor = actor;
        this.actorFsm = actorFsm;
    }
    public virtual void OnEnter(){} //Execution function when entering this state
    public virtual void OnExit(){} //Execute function when exiting this state
    public virtual void OnFrameUpdate(){} //Refresh execution function for non-physical updates in the script frame
    public virtual void OnPhysicsUpdate(){} //Physical functions that need to be updated in FixedUpdate
    
}

This is the base class for all character states

Four virtual functions are provided, and their functions are as written in the comments. They can be rewritten through override as needed.

Then edit the ActorFSM script

public class ActorFSM
{
    public ActorState currentState; //current state

    /// <summary>
    /// Register initial state
    /// </summary>
    /// <param name="startingState"></param>
    public void Initialize(ActorState startingState)
    {
        currentState = startingState;
        startingState.OnEnter();
    }
    
    /// <summary>
    /// Switch state
    /// </summary>
    /// <param name="newState"></param>
    public void TransitionState(ActorState newState)
    {
        currentState.OnExit();
        currentState = newState;
        currentState.OnEnter();
    }
}

As you can see, the code uses an ActorState variable currentState to manage the current state. Two methods are written in it to implement the registration state and switching state by exiting and entering the function from the execution state.

Before editing the Actor script

First create a new folder

Create the required status script in this folder. For demonstration, I created the following script

First let the script inherit the ActorState base class

public class ActIdleState : ActorState
{
    public ActIdleState(Actor actor, ActorFSM actorFsm) : base(actor, actorFsm)
    {
    }
}
public class ActWalkState : ActorState
{
    public ActWalkState(Actor actor, ActorFSM actorFsm) : base(actor, actorFsm)
    {
    }
}

So we have two states, although they do nothing

Next, edit the actor script

public class Actor : MonoBehaviour
{
    public float moveSpeed; //moving speed
    public ActorFSM actorFsm; //fsm that manages state
    public Vector2 inputDirection; //Input direction vector
    public Rigidbody2D rb; //Rigid body component
    
    public ActIdleState actIdleState;
    public ActWalkState acrWalkState;
    protected void Awake()
    {
        actorFsm = new ActorFSM();
        inputDirection = new Vector2(0.0f, 0.0f);
        rb = GetComponent<Rigidbody2D>();

        actIdleState = new ActIdleState(this, actorFsm);
        acrWalkState = new ActWalkState(this, actorFsm);
    }

    protected void Start()
    {
        actorFsm.Initialize(actIdleState);
    }

    protected void Update()
    {
        actorFsm.currentState.OnFrameUpdate();
        GetInputValue();
    }
    protected void FixedUpdate()
    {
        actorFsm.currentState.OnPhysicsUpdate();
    }
    /// <summary>
    /// Get the input direction vector
    /// </summary>
    protected void GetInputValue()
    {
        var horizontal = Input.GetAxis("Horizontal");
        var vertical = Input.GetAxis("Vertical");
        inputDirection.x = horizontal;
        inputDirection.y = vertical;
        if (inputDirection.magnitude > 1)
            inputDirection = inputDirection.normalized;
    }
    
}

As you can see, a state machine object is declared in the script and assigned in wake.

Similarly, the state that needs to be used is also declared and assigned in wake.

The first state is registered in the Start method

Execute the corresponding methods in the status script in Update and FixedUpdate

So far, a simple state machine has been implemented, which allows state switching to be executed automatically in the state script, so that when we write a state script, we no longer have to worry about other states that will affect the script being written.

In the above code we cannot achieve any effect yet, because the two status scripts have not yet written any execution functions.

Next, write the execution functions of the two scripts

public class ActIdleState : ActorState
{
    public ActIdleState(Actor actor, ActorFSM actorFsm) : base(actor, actorFsm)
    {
    }

    public override void OnFrameUpdate()
    {
        if (actor.inputDirection.magnitude != 0)
            actorFsm.TransitionState(actor.acrWalkState);
    }
}
public class ActWalkState : ActorState
{
    public ActWalkState(Actor actor, ActorFSM actorFsm) : base(actor, actorFsm)
    {
    }

    public override void OnFrameUpdate()
    {
        if(actor.inputDirection.magnitude==0)
            actorFsm.TransitionState(actor.actIdleState);
    }

    public override void OnPhysicsUpdate()
    {
        actor.rb.velocity = actor.inputDirection * actor.moveSpeed;
    }
}

By enriching the above script, the object mounted on the actor can be simply moved to idle and switched. If you need a new state, you can also write a new state script to inherit ActorState, and then declare it in the Actor script. , edit the required execution function in the status function by yourself

Similarly, if there are multiple different operating roles, you can enrich new functions by inheriting the actor class

There are actually many unresolved problems in the above method, such as character steering, animation switching, audio playback, etc.

But similarly, we can add audio files and animation machines to the state base class, that is, ActorState, and play the required audio or animation at different execution times in the required way.

What the author wants to express is that we can continue to enrich the finite state machine to meet our own needs, but this structure also has its inelegant aspects. Due to time issues, the author will elaborate on some derivative concepts of the finite state machine below. If there are If necessary, the author can update the corresponding practical cases in the future.

First of all, if the character needs to have a weapon that can be operated, then there will be a lot of repeated code in the original different state classes. For example, bullets can be fired in the idle state, and bullets can also be fired in the walk state. We may use it in many situations. The same piece of code can be executed in any state. Such repeated code is not elegant. So do we have any solutions?

The answer is Concurrent State Machine

It is very simple. We only need to create another weapon state machine to manage the weapon. Let the weapon fsm manage its internal state by itself. Of course, we may have some states in the original state. Weapons cannot be used, but this only requires some simple if judgments.

Next, let’s imagine a scene. Our character has Squat (crouching), walk, idle, and attack states. Now we enter the attack state from any of the first three states. After exiting the attack state, which state should we return to? , if we need to return to the previous state, then what the author says next will solve this problem

The answer is Pushdown State Machine

First we need a stack structure. When a new state is executed, the new state is pushed onto the stack. When the state exits, the state is popped out of the stack. In this way, we realize a save of the past state, which allows us to return after executing the new state. to past state

In the book “Game Programming Patterns”, a type of Hierarchical State Machine is also mentioned. The concept is to use the base-level state as a subclass. Each state has its parent class. When there is a When an event is passed in, the executable state is matched according to the inheritance chain. If there is no executable state, nothing will be executed.

State machines are a good method for AI management. In addition to finite state machines, there are also fuzzy state machines that can achieve more realistic enemy objects. If necessary, the author can also explain fuzzy through actual combat next time state machine.