Skip to content
Back to skills

State Machine

ASecurity

Generic state machine patterns — IState interface, StateMachine<T>, game state management (menu/gameplay/pause), enemy AI states, hierarchical FSM. Load when implementing state-driven behavior.

  • 21 stars
  • 0 votes
  • 0 copies
  • 5 views
  • Added May 27, 2026
data-aic#testingrefactoring

Security analysis

A100/100

Scanned May 27, 2026

npx -y skills add XeldarAlz/everything-claude-unity --skill state-machine --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of State Machine?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for State Machine
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/xeldaralz-state-machine/badge)](https://www.skillsdirectory.com/skills/xeldaralz-state-machine)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: state-machine
description: "Generic state machine patterns — IState interface, StateMachine<T>, game state management (menu/gameplay/pause), enemy AI states, hierarchical FSM. Load when implementing state-driven behavior."
globs: ["**/State*.cs", "**/FSM*.cs", "**/*Machine*.cs"]
---

# 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.

```csharp
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.

```csharp
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

```csharp
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

```csharp
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

```csharp
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

```csharp
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 void CheckWall() { /* raycast check */ }
}
```

---

## Game State Management

Use the same state machine pattern for high-level game flow.

### Game States

```
MainMenu → Loading → Gameplay → Pause → GameOver
                       ↑          |
                       +----------+  (resume)
```

```csharp
public class GameManager : MonoBehaviour
{
    public static GameManager Instance { get; private set; }

    private StateMachine<GameManager> _stateMachine;

    private void Awake()
    {
        Instance = this;
        DontDestroyOnLoad(gameObject);

        _stateMachine = new StateMachine<GameManager>(this);
        _stateMachine.AddState(new MainMenuState(this, _stateMachine));
        _stateMachine.AddState(new LoadingState(this, _stateMachine));
        _stateMachine.AddState(new GameplayState(this, _stateMachine));
        _stateMachine.AddState(new PauseState(this, _stateMachine));
        _stateMachine.AddState(new GameOverState(this, _stateMachine));

        _stateMachine.ChangeState<MainMenuState>();
    }

    private void Update()
    {
        _stateMachine.Update();
    }

    // Convenience methods for external callers
    public void StartGame() => _stateMachine.ChangeState<LoadingState>();
    public void PauseGame() => _stateMachine.ChangeState<PauseState>();
    public void ResumeGame() => _stateMachine.RevertToPreviousState();
    public void GameOver() => _stateMachine.ChangeState<GameOverState>();
    public void ReturnToMenu() => _stateMachine.ChangeState<MainMenuState>();
}
```

### Pause State Example

```csharp
public class PauseState : IState
{
    private GameManager _owner;
    private StateMachine<GameManager> _sm;

    public PauseState(GameManager owner, StateMachine<GameManager> sm)
    {
        _owner = owner;
        _sm = sm;
    }

    public void Enter()
    {
        Time.timeScale = 0f;
        // Show pause menu UI
    }

    public void Exit()
    {
        Time.timeScale = 1f;
        // Hide pause menu UI
    }

    public void Update() { }

    // Called by InputView when the Pause action fires (UI action map while paused).
    public void OnPausePressed() => _sm.RevertToPreviousState();

    public void FixedUpdate() { }
}
```

---

## Enemy AI States

A patrol/chase/attack AI using the same state machine.

```
Idle → Patrol → Detect → Chase → Attack → Flee → Dead
```

### Patrol State

```csharp
public class EnemyPatrolState : IState
{
    private EnemyController _enemy;
    private StateMachine<EnemyController> _sm;
    private int _waypointIndex;

    public EnemyPatrolState(EnemyController enemy, StateMachine<EnemyController> sm)
    {
        _enemy = enemy;
        _sm = sm;
    }

    public void Enter()
    {
        _enemy.Animator.Play("Walk");
        _enemy.MoveSpeed = _enemy.PatrolSpeed;
    }

    public void Exit() { }

    public void Update()
    {
        // Check for player detection
        if (_enemy.CanSeePlayer())
        {
            _sm.ChangeState<EnemyChaseState>();
            return;
        }

        // Move toward current waypoint
        Vector3 target = _enemy.Waypoints[_waypointIndex].position;
        _enemy.MoveToward(target);

        // Advance to next waypoint when close enough
        if (Vector3.Distance(_enemy.transform.position, target) < 0.3f)
        {
            _waypointIndex = (_waypointIndex + 1) % _enemy.Waypoints.Length;
        }
    }

    public void FixedUpdate() { }
}
```

### Chase State

```csharp
public class EnemyChaseState : IState
{
    private EnemyController _enemy;
    private StateMachine<EnemyController> _sm;
    private float _losePlayerTimer;

    public EnemyChaseState(EnemyController enemy, StateMachine<EnemyController> sm)
    {
        _enemy = enemy;
        _sm = sm;
    }

    public void Enter()
    {
        _enemy.Animator.Play("Run");
        _enemy.MoveSpeed = _enemy.ChaseSpeed;
        _losePlayerTimer = 0f;
    }

    public void Exit() { }

    public void Update()
    {
        float distToPlayer = Vector3.Distance(
            _enemy.transform.position, _enemy.Player.position);

        // Close enough to attack
        if (distToPlayer <= _enemy.AttackRange)
        {
            _sm.ChangeState<EnemyAttackState>();
            return;
        }

        // Lost sight of player
        if (!_enemy.CanSeePlayer())
        {
            _losePlayerTimer += Time.deltaTime;
            if (_losePlayerTimer > _enemy.LosePlayerDelay)
            {
                _sm.ChangeState<EnemyPatrolState>();
                return;
            }
        }
        else
        {
            _losePlayerTimer = 0f;
        }

        // Low health: flee
        if (_enemy.HealthPercent < _enemy.FleeThreshold)
        {
            _sm.ChangeState<EnemyFleeState>();
            return;
        }

        _enemy.MoveToward(_enemy.Player.position);
    }

    public void FixedUpdate() { }
}
```

---

## Hierarchical FSM

A hierarchical state machine is a state that contains its own sub-state machine. This avoids state explosion when behaviors have nested phases.

Example: A "Combat" state that internally has Approach, Attack, and Retreat sub-states.

```csharp
public class EnemyCombatState : IState
{
    private EnemyController _enemy;
    private StateMachine<EnemyController> _parentSM;
    private StateMachine<EnemyController> _subSM;

    public EnemyCombatState(EnemyController enemy, StateMachine<EnemyController> parentSM)
    {
        _enemy = enemy;
        _parentSM = parentSM;

        _subSM = new StateMachine<EnemyController>(enemy);
        _subSM.AddState(new CombatApproachSubState(enemy, _subSM));
        _subSM.AddState(new CombatAttackSubState(enemy, _subSM));
        _subSM.AddState(new CombatRetreatSubState(enemy, _subSM));
    }

    public void Enter()
    {
        _subSM.ChangeState<CombatApproachSubState>();
    }

    public void Exit()
    {
        _subSM.CurrentState?.Exit();
    }

    public void Update()
    {
        // Parent-level transitions (flee, death) take priority
        if (_enemy.HealthPercent < _enemy.FleeThreshold)
        {
            _parentSM.ChangeState<EnemyFleeState>();
            return;
        }

        // Delegate to sub-state
        _subSM.Update();
    }

    public void FixedUpdate()
    {
        _subSM.FixedUpdate();
    }
}
```

This keeps the parent state machine simple (Patrol, Combat, Flee, Dead) while Combat handles its own complexity internally.

---

## Transition Conditions

For more complex state machines, define explicit transition rules rather than embedding transition logic in each state.

```csharp
using System;
using System.Collections.Generic;

public class TransitionRule
{
    public Type FromState;
    public Type ToState;
    public Func<bool> Condition;
}

public class StateMachineWithTransitions<T>
{
    private StateMachine<T> _inner;
    private List<TransitionRule> _transitions = new();

    public StateMachineWithTransitions(T owner)
    {
        _inner = new StateMachine<T>(owner);
    }

    public void AddState(IState state) => _inner.AddState(state);

    public void AddTransition<TFrom, TTo>(Func<bool> condition)
        where TFrom : IState where TTo : IState
    {
        _transitions.Add(new TransitionRule
        {
            FromState = typeof(TFrom),
            ToState = typeof(TTo),
            Condition = condition
        });
    }

    /// <summary>
    /// Add a transition that can fire from ANY state.
    /// </summary>
    public void AddAnyTransition<TTo>(Func<bool> condition) where TTo : IState
    {
        _transitions.Add(new TransitionRule
        {
            FromState = null, // null means "any"
            ToState = typeof(TTo),
            Condition = condition
        });
    }

    public void Update()
    {
        // Check transitions before updating current state
        foreach (var rule in _transitions)
        {
            if (rule.FromState != null &&
                _inner.CurrentState?.GetType() != rule.FromState)
                continue;

            if (rule.Condition())
            {
                // Use reflection or a lookup to change state by Type
                // (Simplified here; in production, store state instances by Type)
                break;
            }
        }

        _inner.Update();
    }
}
```

This approach centralizes transition logic and makes it easier to visualize the state graph.

---

## Event-Driven Transitions

Some transitions are triggered by external events (taking damage, picking up an item) rather than polled conditions.

```csharp
// In the state machine owner:
public void OnDamageTaken(int amount)
{
    if (amount > _staggerThreshold)
    {
        _stateMachine.ChangeState<PlayerHurtState>();
    }
}

public void OnHealthDepleted()
{
    _stateMachine.ChangeState<PlayerDeadState>();
}
```

Prefer event-driven transitions for one-shot triggers (damage, death, interaction). Use polled transitions for continuous conditions (grounded check, distance to enemy).

---

## ScriptableObject States

For designer-configurable behavior, define state parameters in ScriptableObjects. The SO holds data; a runtime state class reads it.

```csharp
[CreateAssetMenu(menuName = "AI/Patrol Config")]
public class PatrolStateConfig : ScriptableObject
{
    public float moveSpeed = 2f;
    public float waitTimeAtWaypoint = 1.5f;
    public float detectionRange = 8f;
    public string animationName = "Walk";
}

public class ConfigurablePatrolState : IState
{
    private EnemyController _enemy;
    private PatrolStateConfig _config;

    public ConfigurablePatrolState(EnemyController enemy, PatrolStateConfig config)
    {
        _enemy = enemy;
        _config = config;
    }

    public void Enter()
    {
        _enemy.Animator.Play(_config.animationName);
        _enemy.MoveSpeed = _config.moveSpeed;
    }

    // ... rest of state logic uses _config fields
}
```

This lets designers create different patrol profiles (slow cautious guard, fast perimeter patrol) without touching code.

---

## Practical Tips

- **Debug visualization:** Log state transitions with `Debug.Log($"[{owner.name}] {PreviousState?.GetType().Name} -> {CurrentState.GetType().Name}")`. Add a `#if UNITY_EDITOR` guard so it compiles out in builds.
- **Avoid string-based states.** Use type-safe state references (generics or enums). String comparisons are fragile and miss refactoring.
- **One state machine per concern.** Do not mix movement states and animation states in the same FSM. Use separate machines that communicate through the owner.
- **State pooling:** The `AddState` approach creates states once and reuses them. Never allocate new state objects per transition; this creates garbage collection pressure.
- **Animator integration:** Map each state to an animator state name. Call `Animator.Play()` in `Enter()` rather than setting bool parameters, which are harder to debug.
- **Testing states in isolation:** Because states are plain C# classes (not MonoBehaviours), you can unit test them with a mock owner. This is a major advantage over Animator-based state machines.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.