【Unity】Unity Design Pattern: Managing Complex AI and Character States with State Pattern

Created: 2025-12-07

Escape nested if/switch hell. Learn to implement complex character states (idle, attack, defend, flee) as clean, extensible state machines using the classic 'State Pattern' design pattern.

Overview

Player characters and enemy AI have various "states" depending on game situations. Enemy AI switches between "patrol state," "chase player state," "attack state," "idle state," "flee state," etc. Implementing these state transitions with nested if-else or switch statements in a giant Update method quickly becomes complex and makes adding new states or modifying behavior extremely difficult.

// Bad example: Giant Update method
void Update()
{
    if (state == "Patrol")
    {
        // Patrol processing
        if (CanSeePlayer())
        {
            state = "Chase";
        }
    }
    else if (state == "Chase")
    {
        // Chase processing
        if (IsInAttackRange())
        {
            state = "Attack";
        }
        else if (!CanSeePlayer())
        {
            state = "Patrol";
        }
    }
    else if (state == "Attack")
    {
        // Attack processing
        if (!IsInAttackRange())
        {
            state = "Chase";
        }
    }
    // ... more states keep adding
}

The object-oriented design pattern for elegantly managing such "state-dependent behavior variations" is the State Pattern. The core idea: represent "states" as classes and implement state-specific behavior as methods of those classes. The state-holding entity (Context) holds a reference to the current state object and "delegates" processing to it.

State Pattern Components

The State Pattern consists of three main elements:

  1. Context: The entity holding state. In this example, the enemy AI character. Holds a reference to the current state object (IState) and is responsible for state transitions.
  2. IState (Interface): Defines the common interface all state classes must implement. Methods like OnEnter() (processing when entering the state), OnUpdate() (per-frame processing in the state), OnExit() (processing when leaving the state).
  3. Concrete State: Specific state classes implementing the IState interface. PatrolState, ChaseState, AttackState, etc. Each class implements specific behavior for that state.

Unity Implementation Example: Enemy AI State Machine

Step 1: Define the IState Interface

First, define the interface as the blueprint for all state classes.

// IState.cs
public interface IState
{
    // Called once when entering this state
    void OnEnter(EnemyAI context);

    // Called every frame while in this state
    void OnUpdate();

    // Called once when leaving this state
    void OnExit();
}

Step 2: Implement Concrete State Classes

Next, create specific state classes. Each state class handles its own logic and checks conditions for transitioning to other states.

// PatrolState.cs
using UnityEngine;

public class PatrolState : IState
{
    private EnemyAI enemy;

    public void OnEnter(EnemyAI context)
    {
        this.enemy = context;
        Debug.Log("Transitioning to patrol state");
        // Start patrol animation, etc.
    }

    public void OnUpdate()
    {
        // Implement patrol logic here

        // If player spotted, transition to chase state
        if (enemy.CanSeePlayer())
        {
            enemy.ChangeState(new ChaseState());
        }
    }

    public void OnExit()
    {
        // Stop patrol animation, etc.
    }
}

// ChaseState.cs
using UnityEngine;

public class ChaseState : IState
{
    private EnemyAI enemy;

    public void OnEnter(EnemyAI context)
    {
        this.enemy = context;
        Debug.Log("Transitioning to chase state");
    }

    public void OnUpdate()
    {
        // Implement chase logic (follow player) here

        // If within attack range, transition to attack state
        if (enemy.IsInAttackRange())
        {
            enemy.ChangeState(new AttackState());
        }
        // If player lost, return to patrol state
        else if (!enemy.CanSeePlayer())
        {
            enemy.ChangeState(new PatrolState());
        }
    }

    public void OnExit() { }
}

// AttackState, etc... implemented similarly

Step 3: Implement the Context Class

Finally, implement the EnemyAI class that manages states. It holds the current state and delegates Update processing to the current state object.

// EnemyAI.cs
using UnityEngine;

public class EnemyAI : MonoBehaviour
{
    private IState currentState;

    void Start()
    {
        // Set initial state
        ChangeState(new PatrolState());
    }

    void Update()
    {
        // Call current state's Update processing
        if (currentState != null)
        {
            currentState.OnUpdate();
        }
    }

    // Method to switch states
    public void ChangeState(IState nextState)
    {
        // If current state exists, call its exit processing
        if (currentState != null)
        {
            currentState.OnExit();
        }

        // Switch to new state and call its initialization
        currentState = nextState;
        currentState.OnEnter(this);
    }

    // Helper methods used by state classes
    public bool CanSeePlayer() { /* Player visibility check logic */ return false; }
    public bool IsInAttackRange() { /* Attack range check logic */ return false; }
}

State Pattern Benefits

  • Separation of concerns: Each state's logic is completely isolated in its own class. EnemyAI no longer needs to know state-specific behavior details.
  • Extensibility: Adding new states (e.g., FleeState) just requires creating a new IState-implementing class—minimal impact on existing code.
  • Readability and maintainability: No more nested if-else hell. Code becomes very clean and readable. Modifying a state's behavior just means editing its class.

Summary

The State Pattern is a powerful design pattern for implementing objects with complex state transitions in an organized, extensible, maintainable way.

  • Represent states as classes, encapsulating state-specific behavior within them.
  • The Context (entity) holds a reference to the current state object and delegates processing to it.
  • State transition logic is managed by each state class itself.

Applicable to character AI, complex player actions (normal, swimming, climbing ladders), UI modal window management, and many game development scenarios. When your Update method starts bloating with if and switch statements, that's a good sign to consider introducing the State Pattern.