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:
- 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. - 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). - Concrete State: Specific state classes implementing the
IStateinterface.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.
EnemyAIno longer needs to know state-specific behavior details. - Extensibility: Adding new states (e.g.,
FleeState) just requires creating a newIState-implementing class—minimal impact on existing code. - Readability and maintainability: No more nested
if-elsehell. 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.