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

This page may contain affiliate links.

Simple Enemy AI in Unity with Finite State Machines

Posted by Gemma Ellison
./
July 16, 2025

The relentless, unblinking gaze of an enemy AI can make or break a game. But too often, developers get bogged down in complex algorithms, overlooking the elegant simplicity of a Finite State Machine (FSM). Let’s ditch the over-engineering and build a solid, understandable FSM foundation for your Unity enemy AI, one that actually works and doesn’t require a PhD in artificial intelligence.

States: The Building Blocks of Behavior

An FSM, at its core, is about defining distinct states that an entity can be in. Think of these as different “modes” of operation. For an enemy, these might include Idle, Patrol, Chase, and Attack. Each state dictates how the enemy behaves.

Let’s represent this in C# within Unity:

public enum EnemyState {
    Idle,
    Patrol,
    Chase,
    Attack
}

public class EnemyAI : MonoBehaviour {
    public EnemyState currentState = EnemyState.Idle;
}

This simple enum and variable declaration are the foundation. The currentState variable will hold the enemy’s current behavior mode.

Transitions: Moving Between States

The magic of an FSM isn’t just in the states themselves, but in how you transition between them. These transitions are governed by conditions. For example, the enemy might transition from Patrol to Chase when the player enters its line of sight.

Here’s how you might implement a transition check:

public class EnemyAI : MonoBehaviour {
    public EnemyState currentState = EnemyState.Idle;
    public float chaseDistance = 10f;
    public Transform player; // Drag the player object into this field in the inspector

    void Update() {
        switch (currentState) {
            case EnemyState.Idle:
                // Implement Idle behavior here (e.g., do nothing)
                break;
            case EnemyState.Patrol:
                // Implement Patrol behavior here (e.g., walk a predefined path)
                if (Vector3.Distance(transform.position, player.position) < chaseDistance) {
                    currentState = EnemyState.Chase;
                    Debug.Log("Switching to Chase state!");
                }
                break;
            case EnemyState.Chase:
                // Implement Chase behavior here (e.g., move towards the player)
                break;
            case EnemyState.Attack:
                // Implement Attack behavior here (e.g., deal damage to the player)
                break;
        }
    }
}

This code checks the distance to the player during the Patrol state. If the player is within chaseDistance, the currentState is updated, triggering the Chase behavior. Note the important Debug.Log for testing your transitions.

Implementing Behaviors: Making it Real

Each state needs corresponding behavior. The Idle state might involve standing still. Patrol could involve walking a predefined path. Chase would involve moving towards the player. Attack could mean inflicting damage.

Here’s a rudimentary example of Chase behavior:

// Inside the Chase case in the Update() method:
case EnemyState.Chase:
    transform.position = Vector3.MoveTowards(transform.position, player.position, Time.deltaTime * 5f);
    if (Vector3.Distance(transform.position, player.position) > chaseDistance * 1.5f)
    {
        currentState = EnemyState.Patrol; //Go back to patrol if player gets too far
        Debug.Log("Switching back to Patrol state!");
    }
    break;

This uses Vector3.MoveTowards to move the enemy towards the player. Notice the extra check that sends the enemy back to the Patrol state if the player escapes. This is crucial for preventing infinite chasing.

Common Pitfalls and How to Avoid Them

One common mistake is creating overly complex state machines with too many states and transitions. Start simple, and only add complexity as needed. Don’t try to account for every possible scenario upfront.

Another pitfall is neglecting to add conditions to transition out of states. If you don’t define how an enemy stops chasing, for instance, it will chase the player forever, even into other levels! Make sure every state has a clear exit strategy.

A further challenge is “state starvation,” where a state never gets a chance to execute because other states constantly take precedence. Careful balancing of transition conditions is required to avoid this. Debugging statements that log when states are entered are invaluable in identifying this.

Making it Actionable: A Step-by-Step Approach

  1. Define your enemy’s core behaviors. What are the key actions it needs to perform?
  2. Create the EnemyState enum. List all the states you identified in step 1.
  3. Declare the currentState variable. Initialize it to a sensible default state (e.g., Idle or Patrol).
  4. Implement the Update() method with a switch statement. This is the heart of the FSM.
  5. For each state, implement the corresponding behavior. Start with simple movement or actions.
  6. Define transition conditions. What triggers a change from one state to another? Use if statements within each case.
  7. Test, test, test! Use Debug.Log statements liberally to track state transitions and identify issues.

Beyond the Basics: Layering Complexity

This is just the foundation. From here, you can add more states, more complex transition conditions (using raycasts for line of sight, for example), and more sophisticated behaviors. Consider using a hierarchical FSM for more advanced AI, where states can contain sub-states. The power of the FSM lies in its modularity, allowing you to build complex AI systems incrementally. Remember to prioritize clarity and maintainability over premature optimization. A well-structured FSM is easier to debug, modify, and extend, ultimately saving you time and frustration. Don’t be afraid to refactor as your AI evolves. Start simple, iterate often, and build your way to compelling enemy AI.