Get Your Personalized Game Dev Plan Tailored tips, tools, and next steps - just for you.

This page may contain affiliate links.

Simple AI with Finite State Machines: Elegance Over Complexity

Posted by Gemma Ellison
./
July 7, 2025

Forget the hype around neural networks for everything. Sometimes, the best AI is the simplest AI, and that’s where Finite State Machines (FSMs) shine. They’re predictable, understandable, and surprisingly powerful, especially when introducing AI concepts to beginners. Let’s dive into building basic AI using FSMs, and I’ll show you why, in many cases, elegance trumps complexity.

What is a Finite State Machine?

An FSM is a computational model that can be in exactly one of a finite number of states at any given time. The FSM can change from one state to another in response to some input; the change from one state to another is called a transition. Think of it like a light switch: it’s either ON or OFF (two states), and flipping the switch (the input) causes it to transition to the other state.

Defining States for Your AI

The first step in creating an FSM is defining the possible states your AI character can be in. Consider a simple enemy AI in a game.

Possible states could include:

  • Patrolling: Wandering around a predefined area.
  • Chasing: Pursuing the player.
  • Attacking: Engaging in combat with the player.
  • Idle: Not doing anything, perhaps waiting for input.

These states should be mutually exclusive. The AI can’t be both patrolling and chasing simultaneously. That’s crucial.

Implementing Transitions with Conditions

Transitions are the rules that dictate when and how the AI switches between states. These transitions are governed by specific conditions.

For our enemy AI, here’s how we might define some transitions:

  • Patrolling -> Chasing: If the player enters the enemy’s line of sight (condition: playerInSight == true).
  • Chasing -> Attacking: If the player is within attacking range (condition: distanceToPlayer <= attackRange).
  • Attacking -> Chasing: If the player moves out of attacking range (condition: distanceToPlayer > attackRange).
  • Chasing -> Patrolling: If the player is lost (condition: playerInSight == false && timeSinceLastSeen > patrolResumeTime).

Notice the use of clear, boolean conditions. This is what makes FSMs so easy to reason about. Each transition is explicitly defined.

Code Example in Unity (Simplified)

Here’s a simplified example in Unity, demonstrating the core principles.

using UnityEngine;

public class EnemyAI : MonoBehaviour
{
    public enum State { Idle, Patrolling, Chasing, Attacking }
    public State currentState = State.Idle;

    public float patrolSpeed = 2f;
    public float chaseSpeed = 5f;
    public float attackRange = 2f;
    public float sightRange = 10f;
    public float patrolResumeTime = 5f;

    private GameObject player;
    private float timeSinceLastSeen;
    private Vector3 patrolTarget;

    void Start()
    {
        player = GameObject.FindGameObjectWithTag("Player");
        SetNewPatrolTarget();
    }

    void Update()
    {
        // Transition Logic
        switch (currentState)
        {
            case State.Idle:
                if (PlayerInSight()) currentState = State.Chasing;
                else if (Random.Range(0f,1f) < 0.01f) currentState = State.Patrolling; //Chance to start patrolling
                break;
            case State.Patrolling:
                if (PlayerInSight())
                {
                    currentState = State.Chasing;
                }
                else if (Vector3.Distance(transform.position, patrolTarget) < 0.5f)
                {
                    SetNewPatrolTarget();
                }
                break;
            case State.Chasing:
                if (Vector3.Distance(transform.position, player.transform.position) <= attackRange)
                {
                    currentState = State.Attacking;
                }
                else if (!PlayerInSight())
                {
                    timeSinceLastSeen += Time.deltaTime;
                    if (timeSinceLastSeen > patrolResumeTime)
                    {
                        currentState = State.Patrolling;
                        SetNewPatrolTarget();
                        timeSinceLastSeen = 0;
                    }
                }
                break;
            case State.Attacking:
                if (Vector3.Distance(transform.position, player.transform.position) > attackRange)
                {
                    currentState = State.Chasing;
                }
                else
                {
                    //Implement attack logic here
                    Debug.Log("Attacking!");
                }
                break;
        }

        // State Behavior
        switch (currentState)
        {
            case State.Idle:
                // Do nothing (or perhaps play an idle animation)
                break;
            case State.Patrolling:
                transform.position = Vector3.MoveTowards(transform.position, patrolTarget, patrolSpeed * Time.deltaTime);
                break;
            case State.Chasing:
                transform.position = Vector3.MoveTowards(transform.position, player.transform.position, chaseSpeed * Time.deltaTime);
                break;
            case State.Attacking:
                // Already handled attack logic in transition.
                break;
        }
    }

    bool PlayerInSight()
    {
        return Vector3.Distance(transform.position, player.transform.position) <= sightRange;
    }

    void SetNewPatrolTarget()
    {
        patrolTarget = new Vector3(Random.Range(-10f, 10f), 0, Random.Range(-10f, 10f));
    }

    void OnDrawGizmosSelected()
    {
        Gizmos.color = Color.yellow;
        Gizmos.DrawWireSphere(transform.position, sightRange);
        Gizmos.color = Color.red;
        Gizmos.DrawWireSphere(transform.position, attackRange);
    }
}

This example showcases the separation of transition logic (deciding which state to be in) and state behavior (what to do in that state). This separation is critical for maintainability.

Challenges and Pitfalls

One common pitfall is creating overly complex FSMs. As the number of states and transitions grows, the system can become difficult to manage and debug.

Solution: Break down complex behaviors into smaller, more manageable FSMs. Consider using hierarchical state machines, where one state can contain another FSM.

Another challenge is the “ping-pong” effect, where an AI rapidly switches between two states due to conditions being met and unmet in quick succession. For example, rapidly switching between Chasing and Attacking.

Solution: Implement a “cooldown” or “hysteresis” period. For example, after entering the Attacking state, the AI must remain in it for a minimum amount of time, regardless of whether the player momentarily moves out of attack range.

Practical Applications and Beyond

FSMs are perfect for:

  • Simple enemy AI
  • Character controllers with distinct movement modes (e.g., walking, running, jumping)
  • Menu systems
  • Dialogue systems

While FSMs are powerful, they aren’t a silver bullet. For more complex behaviors, consider combining them with other AI techniques, such as behavior trees or utility AI. But for beginners, mastering FSMs provides a solid foundation for understanding AI concepts. They offer a transparent and manageable approach to creating believable and engaging character behaviors. And that, my friends, is a win.