What Is It?
The State pattern moves mode-specific behaviour out of one giant switch and into separate classes. A character controller does not ask “am I jumping?” in twenty places; it simply forwards Tick() to whatever current state object it holds.
Each state implements a small interface — typically Enter(), Tick() and Exit() — and decides for itself which inputs it responds to and which state comes next. JumpState knows that landing is impossible mid-ascent; IdleState knows that pressing jump is fine.
A state machine owns the current state and performs transitions: it calls Exit() on the old state, swaps the reference, and calls Enter() on the new one. That is where animations get triggered, sounds play, and timers reset — once, at the right moment.
When Is It Used?
Use it for anything with distinct modes that respond differently to the same input: player movement (idle, run, jump, fall), enemy AI (patrol, chase, attack), doors, menus and game flow.
It pays off once you notice boolean flags multiplying (isJumping, isFalling, isAttacking) and conditions that check combinations of them. Each combination is really a state.
For two or three trivial modes, an enum with a switch is perfectly fine. Reach for state classes when behaviour per mode becomes large or transitions need setup and teardown.
Interactive Demo
Click the controls and watch the objects collaborate. The console mirrors what the C# code below would log.
Code
using UnityEngine;
namespace Patterns.Behavioral.State
{
/// <summary>
/// Context: owns the state machine and the shared data states need.
/// Its Update is just "read input, tick the current state".
/// </summary>
[RequireComponent(typeof(Rigidbody2D))]
public class PlayerController : MonoBehaviour
{
[SerializeField] private float runSpeed = 6f;
[SerializeField] private float jumpSpeed = 12f;
[SerializeField] private LayerMask groundMask;
[SerializeField] private Transform feet;
public Rigidbody2D Body { get; private set; }
public Animator Animator { get; private set; }
public StateMachine Machine { get; } = new();
public IState Idle { get; private set; }
public IState Run { get; private set; }
public IState Jump { get; private set; }
public IState Fall { get; private set; }
public float MoveInput { get; private set; }
public bool JumpPressed { get; private set; }
public float JumpSpeed => jumpSpeed;
public bool IsGrounded => Physics2D.OverlapCircle(feet.position, 0.1f, groundMask);
private void Awake()
{
Body = GetComponent<Rigidbody2D>();
Animator = GetComponentInChildren<Animator>();
// Create each state once and reuse it.
Idle = new IdleState(this);
Run = new RunState(this);
Jump = new JumpState(this);
Fall = new FallState(this);
}
private void Start() => Machine.ChangeState(Idle);
private void Update()
{
MoveInput = Input.GetAxisRaw("Horizontal");
JumpPressed = Input.GetButtonDown("Jump") && IsGrounded;
Machine.Tick();
}
public void Move(float direction) => Body.linearVelocityX = direction * runSpeed;
}
}namespace Patterns.Behavioral.State
{
/// <summary>
/// One mode of behaviour. The state machine calls Enter once on
/// arrival, Tick every frame, and Exit once on departure.
/// </summary>
public interface IState
{
string Name { get; }
void Enter();
void Tick();
void Exit();
}
}using System;
using UnityEngine;
namespace Patterns.Behavioral.State
{
/// <summary>
/// Holds the current state and performs transitions. Deliberately
/// tiny: all behaviour lives in the states themselves.
/// </summary>
public class StateMachine
{
public IState Current { get; private set; }
public event Action<IState, IState> Changed; // (from, to)
public void ChangeState(IState next)
{
if (next == null || next == Current) return;
IState previous = Current;
previous?.Exit();
Current = next;
Debug.Log($"{previous?.Name ?? "None"} → {next.Name}");
Current.Enter();
Changed?.Invoke(previous, next);
}
public void Tick() => Current?.Tick();
}
}using UnityEngine;
namespace Patterns.Behavioral.State
{
/// <summary>Shared base: every state can reach the controller.</summary>
public abstract class PlayerState : IState
{
protected readonly PlayerController player;
protected PlayerState(PlayerController player) => this.player = player;
public abstract string Name { get; }
public virtual void Enter() => player.Animator.CrossFade(Name, 0.1f);
public abstract void Tick();
public virtual void Exit() { }
}
public class IdleState : PlayerState
{
public IdleState(PlayerController p) : base(p) { }
public override string Name => "Idle";
public override void Tick()
{
if (player.JumpPressed) player.Machine.ChangeState(player.Jump);
else if (player.MoveInput != 0f) player.Machine.ChangeState(player.Run);
}
}
public class RunState : PlayerState
{
public RunState(PlayerController p) : base(p) { }
public override string Name => "Run";
public override void Tick()
{
player.Move(player.MoveInput);
if (player.JumpPressed) player.Machine.ChangeState(player.Jump);
else if (player.MoveInput == 0f) player.Machine.ChangeState(player.Idle);
else if (!player.IsGrounded) player.Machine.ChangeState(player.Fall);
}
}
public class JumpState : PlayerState
{
public JumpState(PlayerController p) : base(p) { }
public override string Name => "Jump";
public override void Enter()
{
base.Enter();
player.Body.linearVelocityY = player.JumpSpeed;
}
public override void Tick()
{
player.Move(player.MoveInput);
if (player.Body.linearVelocityY <= 0f) player.Machine.ChangeState(player.Fall);
}
}
public class FallState : PlayerState
{
public FallState(PlayerController p) : base(p) { }
public override string Name => "Fall";
public override void Tick()
{
player.Move(player.MoveInput);
if (!player.IsGrounded) return;
player.Machine.ChangeState(Mathf.Approximately(player.MoveInput, 0f) ? player.Idle : player.Run);
}
}
}Advantages & Disadvantages
+ Advantages
- Each mode lives in one class with its own logic, so it is easy to read, change and test.
- Invalid transitions are impossible by construction — a state simply ignores input it does not handle.
Enter/Exithooks give a single, reliable place for animation triggers and cleanup.
− Disadvantages
- More classes and more indirection than a simple
switch. - States often need access to the owner’s components, which can lead to wide context objects.
- Flat state machines struggle with shared behaviour; you may need hierarchical states to avoid duplication.
Tips
- 01Pass a context (the controller) into each state’s constructor so it can read input and move the character without
GetComponentcalls. - 02Create state instances once and reuse them; allocating a new state on every transition generates garbage.
- 03Keep transition logic inside states, and keep the machine dumb — it just swaps and calls
Exit/Enter. - 04Unity’s Animator is itself a state machine. Let your code FSM drive gameplay and set Animator parameters from
Enter(), rather than reading gameplay state back out of the Animator. - 05Log transitions during development (
Idle → Run); it makes bugs in state flow obvious.