State Machine Patterns
A generic, reusable finite state machine for Unity. Covers player states, enemy AI, game flow (menu/gameplay/pause), hierarchical FSMs, and ScriptableObject-driven states for designer configuration.
IState Interface
The contract every state must fulfill. Keep it minimal: enter, exit, tick, and physics tick.
public interface IState
{
/// <summary>Called once when entering this state.</summary>
void Enter();
/// <summary>Called once when leaving this state.</summary>
void Exit();
/// <summary>Called every frame while this state is active.</summary>
void Update();
/// <summary>Called every fixed timestep while this state is active.</summary>
void FixedUpdate();
}
If your game does not need FixedUpdate in states (e.g., turn-based game), drop it from the interface. Keep the interface as lean as your project requires.
StateMachine Generic Class
A generic state machine that can be used with any state type. The type parameter T is typically the owner (player, enemy, game manager) so states can access it.
using System;
using System.Collections.Generic;
using UnityEngine;
public class StateMachine<T>
{
public IState CurrentState { get; private set; }
public IState PreviousState { get; private set; }
private T _owner;
private Dictionary<Type, IState> _states = new();
public StateMachine(T owner)
{
_owner = owner;
}
/// <summary>
/// Register a state instance. Call during initialization.
/// </summary>
public void AddState(IState state)
{
_states[state.GetType()] = state;
}
/// <summary>
/// Transition to a new state by type. Calls Exit on current, Enter on new.
/// </summary>
public void ChangeState<TState>() where TState : IState
{
var type = typeof(TState);
if (!_states.TryGetValue(type, out var newState))
{
Debug.LogError($"State {type.Name} not registered in state machine.");
return;
}
if (CurrentState == newState) return; // Already in this state
PreviousState = CurrentState;
CurrentState?.Exit();
CurrentState = newState;
CurrentState.Enter();
}
/// <summary>
/// Change state by instance (useful when states are not unique by type,
/// e.g., ScriptableObject states).
/// </summary>
public void ChangeState(IState newState)
{
if (newState == null || CurrentState == newState) return;
PreviousState = CurrentState;
CurrentState?.Exit();
CurrentState = newState;
CurrentState.Enter();
}
/// <summary>
/// Return to the previous state.
/// </summary>
public void RevertToPreviousState()
{
if (PreviousState != null)
ChangeState(PreviousState);
}
/// <summary>
/// Call from the owner's Update().
/// </summary>
public void Update()
{
CurrentState?.Update();
}
/// <summary>
/// Call from the owner's FixedUpdate().
/// </summary>
public void FixedUpdate()
{
CurrentState?.FixedUpdate();
}
/// <summary>
/// Check if the current state is of a given type.
/// </summary>
public bool IsInState<TState>() where TState : IState
{
return CurrentState is TState;
}
public T Owner => _owner;
}
Player State Example
A concrete example: player states for a 2D platformer. Each state is a class that holds a reference to the player controller.
Base Player State
public abstract class PlayerState : IState
{
protected PlayerController Player { get; }
protected StateMachine<PlayerController> StateMachine { get; }
protected PlayerState(PlayerController player, StateMachine<PlayerController> stateMachine)
{
Player = player;
StateMachine = stateMachine;
}
public virtual void Enter() { }
public virtual void Exit() { }
public virtual void Update() { }
public virtual void FixedUpdate() { }
}
Idle State
public class PlayerIdleState : PlayerState
{
public PlayerIdleState(PlayerController player, StateMachine<PlayerController> sm)
: base(player, sm) { }
public override void Enter()
{
Player.Animator.Play("Idle");
Player.Rb.velocity = new Vector2(0f, Player.Rb.velocity.y);
}
public override void Update()
{
if (!Player.IsGrounded)
{
StateMachine.ChangeState<PlayerFallState>();
return;
}
if (Player.JumpRequested)
{
StateMachine.ChangeState<PlayerJumpState>();
return;
}
if (Mathf.Abs(Player.MoveInput.x) > 0.1f)
{
StateMachine.ChangeState<PlayerRunState>();
return;
}
if (Player.DashRequested)
{
StateMachine.ChangeState<PlayerDashState>();
return;
}
}
}
Jump State
public class PlayerJumpState : PlayerState
{
public PlayerJumpState(PlayerController player, StateMachine<PlayerController> sm)
: base(player, sm) { }
public override void Enter()
{
Player.Animator.Play("Jump");
Player.ExecuteJump();
}
public override void Update()
{
// Transition to fall when velocity turns downward
if (Player.Rb.velocity.y <= 0f)
{
StateMachine.ChangeState<PlayerFallState>();
return;
}
// Variable jump height: cut velocity on release
if (Player.JumpReleased && Player.Rb.velocity.y > 0f)
{
Player.CutJumpShort();
}
// Allow wall slide if touching wall
if (Player.IsTouchingWall && !Player.IsGrounded)
{
StateMachine.ChangeState<PlayerWallSlideState>();
return;
}
}
public override void FixedUpdate()
{
Player.ApplyHorizontalMovement();
}
}
Wiring It Up in the Player Controller
using UnityEngine;
public class PlayerController : MonoBehaviour
{
// Public properties that states read
public Rigidbody2D Rb { get; private set; }
public Animator Animator { get; private set; }
public Vector2 MoveInput { get; private set; }
public bool IsGrounded { get; private set; }
public bool IsTouchingWall { get; private set; }
public bool JumpRequested { get; set; }
public bool JumpReleased { get; set; }
public bool DashRequested { get; set; }
private StateMachine<PlayerController> _stateMachine;
private void Awake()
{
Rb = GetComponent<Rigidbody2D>();
Animator = GetComponent<Animator>();
_stateMachine = new StateMachine<PlayerController>(this);
_stateMachine.AddState(new PlayerIdleState(this, _stateMachine));
_stateMachine.AddState(new PlayerRunState(this, _stateMachine));
_stateMachine.AddState(new PlayerJumpState(this, _stateMachine));
_stateMachine.AddState(new PlayerFallState(this, _stateMachine));
_stateMachine.AddState(new PlayerWallSlideState(this, _stateMachine));
_stateMachine.AddState(new PlayerDashState(this, _stateMachine));
_stateMachine.ChangeState<PlayerIdleState>();
}
private void Update()
{
ReadInput();
CheckGround();
CheckWall();
_stateMachine.Update();
}
private void FixedUpdate()
{
_stateMachine.FixedUpdate();
}
// Movement methods called by states
public void ExecuteJump() { /* see character-controller skill */ }
public void CutJumpShort() { /* see character-controller skill */ }
public void ApplyHorizontalMovement() { /* see character-controller skill */ }
private void ReadInput() { /* read from Input System */ }
private void CheckGround() { /* overlap circle check */ }
private