FSM with Delegates in C#

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 defines Enter / 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.");
    }
}

Popular posts from this blog

Understanding Arrays as Reference Types in C#

Setting Up a Basic Follow Camera with Cinemachine 3.x

Understanding and Using ? (nullable) in C#