FSM with Delegates in C#
In game development, managing character states is a recurring need. When a player transitions between states like Idle → Walk → Die
, we often use a Finite State Machine (FSM) to handle this behavior.
Most developers start with a switch-case
structure. It’s straightforward and works well when there are only a few states and only an Update()
method per state. However, as the number of states grows and each state begins to have distinct phases like Enter
, Update
, and Exit
, the code quickly becomes bloated and harder to maintain.
This is where C# delegates come in. By leveraging delegates, we can create a more flexible and modular FSM structure.
How the FSM is Structured
This example uses the following structure:
- A
DelegateState
class that definesEnter / Update / Exit
actions. - An
FSM
class that holds the current state and triggers the proper action. - A
Player
MonoBehaviour class that defines the actual state logic. - States are represented using an
enum
for clarity, and are mapped to delegate-based state objects without further branching logic.
The best part of this design is that the overall FSM structure remains consistent, even as the number of states increases.
Comparing FSM Design Patterns
There are multiple ways to implement FSMs, and each has its pros and cons depending on the situation:
🔹 switch-case
Best for simple FSMs where you only need to branch inside Update()
. Easy to read and quick to write, especially with a small number of states.
🔹 delegate-based FSM
Ideal when states require multiple steps like Enter / Update / Exit. You can assign behaviors flexibly and avoid repetitive switch-case blocks.
🔹 IState-based FSM
Best suited for complex FSMs where each state has many lines of logic. Create a separate class per state implementing an IState
interface. The structure becomes very clean and maintainable, but can be overkill for simple FSMs. This pattern will be covered in a future post.
Conclusion
There’s no one-size-fits-all solution when it comes to FSMs. It depends on how many states you have and how complex each state is.
- For simple FSMs with only Update() → switch-case is great
- When each state has Enter / Update / Exit → consider delegate-based FSM
- If states are large and full-featured → go with IState pattern
As a rule of thumb :
When the number of states exceeds 5, or when each state involves multiple phases,
switch-case
structures start breaking down. That’s when it’s time to consider structuring your FSM more seriously.
The delegate approach introduced here is a solid first step toward cleaner, more scalable FSMs in Unity or any C#-based project.
Full Code Example
using System;
using UnityEngine;
public class DelegateState
{
public Action Enter;
public Action Update;
public Action Exit;
}
public class FSM
{
private DelegateState currentState;
public void Update()
{
currentState?.Update?.Invoke();
}
public void ChangeState(DelegateState newState)
{
if (newState == null)
{
Debug.LogWarning("Attempted to change to a null state.");
return;
}
currentState?.Exit?.Invoke();
currentState = newState;
currentState?.Enter?.Invoke();
}
}
public enum PlayerState
{
Idle,
Walk,
Die
}
public class Player : MonoBehaviour
{
private FSM fsm = new FSM();
private PlayerState currentState;
private DelegateState idleState;
private DelegateState walkState;
private DelegateState dieState;
private void Awake()
{
InitializeStates();
}
private void Start()
{
ChangeState(PlayerState.Idle);
}
private void Update()
{
fsm.Update();
}
private void InitializeStates()
{
idleState = new DelegateState();
idleState.Enter += OnEnterIdle;
idleState.Update += OnUpdateIdle;
idleState.Exit += OnExitIdle;
walkState = new DelegateState();
walkState.Enter += OnEnterWalk;
walkState.Update += OnUpdateWalk;
walkState.Exit += OnExitWalk;
dieState = new DelegateState();
dieState.Enter += OnEnterDie;
dieState.Update += OnUpdateDie;
dieState.Exit += OnExitDie;
}
private void ChangeState(PlayerState newState)
{
currentState = newState;
switch (newState)
{
case PlayerState.Idle:
fsm.ChangeState(idleState);
break;
case PlayerState.Walk:
fsm.ChangeState(walkState);
break;
case PlayerState.Die:
fsm.ChangeState(dieState);
break;
}
}
private void OnEnterIdle()
{
Debug.Log("[Idle] Player has entered idle state.");
}
private void OnUpdateIdle()
{
Debug.Log("[Idle] Player is idling...");
}
private void OnExitIdle()
{
Debug.Log("[Idle] Player is leaving idle state.");
}
private void OnEnterWalk()
{
Debug.Log("[Walk] Player has started walking.");
}
private void OnUpdateWalk()
{
Debug.Log("[Walk] Player is walking...");
}
private void OnExitWalk()
{
Debug.Log("[Walk] Player has stopped walking.");
}
private void OnEnterDie()
{
Debug.Log("[Die] Player has died.");
}
private void OnUpdateDie()
{
Debug.Log("[Die] Player remains dead.");
}
private void OnExitDie()
{
Debug.Log("[Die] Player should not be exiting die state.");
}
}