Finite State Machine Using C# Delegates in Unity

Share the Post

In this tutorial, we will implement a Finite State Machine using C# Delegates in Unity. We will then demonstrate the use of this Finite State Machine (FSM) by applying it to control an NPC enemy game object in Unity.


Part 1 introduces a Finite State Machine and implements a generic Finite State Machine in C#. It uses the principles of inheritance and overloading to allow concrete implementations of application-specific States.

Part 2 uses the Finite State Machine created in part 1 and applies to a Unity project in a simple straightforward UI implementation of a Splash Screen. This is a very simple example implementation of the FSM created in Part 1.

Part 3 uses the same Finite State Machine and applies to a complex Unity project which handles multiple animation states of a 3D animated character. This is a slightly harder example implementation of the FSM created in Part 1.

This is Part 4 of my tutorials on Finite State Machine (FSM) in Unity. In this tutorial, we will not use inheritance to create different classes for individual States but use delegates to define the functionality of application states and instantiate the same State object. Thereafter we will demonstrate the use of the Finite State Machine by creating a simple key press based application in Unity.

In Part 5 of the tutorial, we will move further by demonstrating an enemy NPC behaviour, which handles multiple animation states of a 3D animated character, using the delegate based FSM.


What are C# Delegates?

C# delegates are a type that represents references to methods with a specific function signature. In short, delegates are references to methods that can be passed around as variables.

If you are familiar with C++ then delegates are equivalent to function pointer concept in C++.

An instantiated delegate can be associated with any method that has a compatible signature and return type. One can then call the method through the delegate instance. C# Delegates are used to pass methods as arguments to other methods. In other words, delegates are methods that you can use as variables, like strings, integers, objects etc. 

Based on C# syntax we can create a delegate with the delegate keyword, followed by a return type and the signature of the methods that can be delegated to it. For example, we can declare a delegate method with no argument as:

public delegate void NoArgDelegate();

Or a delegate with one argument (for this example let’s use string):

public delegate void OneArgDelegate(string val);

Or a delegate with two arguments (for this example let’s use an integer as first argument and string as the second argument):

public delegate void TwoArgDelegate(int val, string key);

Why Use C# Delegates?

Objects and variables can easily be sent as parameters into methods or constructor, however, methods are a bit more tricky. But every once in a while you might feel the need to send a method as a parameter to another method, and that’s when you’ll need delegates.

By defining a delegate, you are saying to the user of your class, “Please feel free to assign, any method that matches this signature, to the delegate and it will be called each time my delegate is called”, from Stackoverflow, Delegates are useful to offer to the user of your objects some ability to customize their behaviour. Most of the time, you can use other ways to achieve the same purpose. It is just one of the easiest way in some situations to get the thing done.

Typical use cases are events and observers. All the OnEventX delegate to the methods the user defines.

For a detailed read on C# delegates read the tutorial on What Are C# Delegates And How To Use Them.

Finite State Machine Using C# Delegates in Unity

Please go through Part 1 of the tutorial before proceeding.

From Part 1 we have the below State class.

    public class State
    {
        protected FSM m_fsm;
        /* The constructor of the State class
         * will require the parent FSM.
         * So we create a constructor 
         * with an instance of the FSM
         */
        public State(FSM fsm)
        {
            m_fsm = fsm;
        }

        /*!Virtual method for entry to the state.
         * This method is called whenever this 
         * state is entered. Derived classes
         * must implement this method and 
         * handle appropriately.
         */
        public virtual void Enter() { }

        /*!Virtual method for exit from the state.
         * This method is called whenever this 
         * state is exited. Derived classes
         * must implement this method and 
         * handle appropriately.
         */
        public virtual void Exit() { }

        /*!Virtual method that will be 
         * called in every Update call from Unity.
         * The call will be routed via the
         * FSM through the current state.
         */
        public virtual void Update() { }

        /*!Virtual method that will be 
         * called in every FixedUpdate call from 
         * Unity. The call will be routed via the
         * FSM through the current state.
         */
        public virtual void FixedUpdate() { }
    }

We won’t change this class as it provides the essence of our any State for an FSM. We will, however, derive a new NPCState class from this base State class, that will implement the delegates. This NPCState will represent all types of states for our NPC.

using Patterns;

public class NPCState : State
{
    public NPCState(FSM fsm) : base(fsm)
    {
    }
    // the delegate
    public delegate void StateDelegate();

    public StateDelegate OnEnterDelegate { get; set; } = null;
    public StateDelegate OnExitDelegate { get; set; } = null;
    public StateDelegate OnUpdateDelegate { get; set; } = null;
    public StateDelegate OnFixedUpdateDelegate { get; set; } = null;

    public override void Enter()
    {
        OnEnterDelegate?.Invoke();
    }

    public override void Exit()
    {
        OnExitDelegate?.Invoke();
    }

    public override void Update()
    {
        OnUpdateDelegate?.Invoke();
    }

    public override void FixedUpdate()
    {
        OnFixedUpdateDelegate?.Invoke();
    }
}

Look at the code above. We have used one type of delegate for all 4 sections, viz., Enter, Exit, Update and FixedUpdate. These delegates are named OnEnterDelegate, OnExitDelegate, OnUpdateDelegate and OnFixedUpdateDelegate.

Do note on the calling convention of a delegate. Did you notice the ? behind the name of the delegate? This is Null conditional check.

Null-conditional

Null-conditional operator lets you access members and elements only when the receiver is not-null, returning null result otherwise. If employees is a list then:

int? count = employees?.Length; // count is null if employees is null

The above is more less equivalent to:

int? count = (employees!= null) ? (int?)employees.Length : null;

Except that employees variable is only evaluated once.

Null-conditional delegate invocation

Delegate invocation can’t immediately follow the ? operator – too many syntactic ambiguities. However, you can call it via the Invoke method on the delegate. So for our case, we instead write simply by calling the Invoke method.

 OnFixedUpdateDelegate?.Invoke(); // for example

In general the calling of a delegate using the Null conditional will be

myDelegate?.Invoke(args); // where args are function arguments and myDelegate is the delegate.

Testing the Finite State Machine Using C# Delegates in Unity

We will now do no-fuss testing of our Finite State Machine using C# Delegates using a simple project. For this testing, we will use Key Presses to transition from one state to another state and display the state transition using Debug.Log in Unity.

Enemy NPC States

The state diagram for our enemy NPC is given below.

Let’s start with the implementation. Create a new scene in your Unity Project. Call the scene to be FSMUsingDelegates. Create an empty game object and name it EnemyNPC. Create a Cube and add it to the EnemyNPC (just to represent the NPC game object visually).

Create a new script called EnemyNPC.cs and add as a component to the EnemyNPC.

Double click and open the EnemyNPC.cs script in your favourite IDE.

Add the various state types as enum. For our tutorial we will have the IDLE, CHASE, ATTACK, DAMAGE and DIE states.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class EnemyNPC : MonoBehaviour
{
    public enum StateTypes
    {
        IDLE = 0,
        CHASE,
        ATTACK,
        DAMAGE,
        DIE
    }

    // Start is called before the first frame update
    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {
        
    }
}

We now add the NPCState that we created earlier. We will amend the original source code slightly for additional clarity and help.

#region States
public class NPCState : State
{
    protected EnemyNPC mNPC;
    private StateTypes mStateType;

    public StateTypes StateType {  get { return mStateType; } }

    public NPCState(FSM fsm, StateTypes type, EnemyNPC npc) : base(fsm)
    {
        mNPC = npc;
        mStateType = type;
    }

    public delegate void StateDelegate();

    public StateDelegate OnEnterDelegate { get; set; } = null;
    public StateDelegate OnExitDelegate { get; set; } = null;
    public StateDelegate OnUpdateDelegate { get; set; } = null;
    public StateDelegate OnFixedUpdateDelegate { get; set; } = null;

    public override void Enter()
    {
        OnEnterDelegate?.Invoke();
    }

    public override void Exit()
    {
        OnExitDelegate?.Invoke();
    }

    public override void Update()
    {
        OnUpdateDelegate?.Invoke();
    }

    public override void FixedUpdate()
    {
        OnFixedUpdateDelegate?.Invoke();
    }
}
#endregion

Note the slight amendment to the NPCState class. We have added the following to make it more friendly for our EnemyNPC class.

*******

protected EnemyNPC mNPC; // holds the reference to EnemyNPC object
private StateTypes mStateType; // the Statet type. This will be used as the key while adding to the FSM

public StateTypes StateType {  get { return mStateType; } }

public NPCState(FSM fsm, StateTypes type, EnemyNPC npc) : base(fsm)
{
    mNPC = npc;
    mStateType = type;
}

********

Now we have the FSM setup ready to be used. We will proceed with configuring our EnemyNPC game object.

************************

    #region NPC Data and Parameters
    public FSM mFsm;
    #endregion

    // Start is called before the first frame update
    void Start()
    {
        mFsm = new FSM();
        mFsm.Add((int)StateTypes.IDLE, new NPCState(mFsm, StateTypes.IDLE, this));
        mFsm.Add((int)StateTypes.CHASE, new NPCState(mFsm, StateTypes.CHASE, this));
        mFsm.Add((int)StateTypes.ATTACK, new NPCState(mFsm, StateTypes.ATTACK, this));
        mFsm.Add((int)StateTypes.DAMAGE, new NPCState(mFsm, StateTypes.DAMAGE, this));
        mFsm.Add((int)StateTypes.DIE, new NPCState(mFsm, StateTypes.DIE, this));

        Init_IdleState();
        Init_AttackState();
        Init_DieState();
        Init_DamageState();
        Init_ChaseState();

        mFsm.SetCurrentState(mFsm.GetState((int)StateTypes.IDLE));
    }

    void Update()
    {
        mFsm.Update();
    }

    void FixedUpdate()
    {
        mFsm.FixedUpdate();
    }

    #region Setup and initialize the various states.
    void Init_IdleState()
    {

    }
    void Init_AttackState()
    {

    }
    void Init_DieState()
    {

    }
    void Init_DamageState()
    {

    }
    void Init_ChaseState()
    {

    }
    #endregion

*****************

The above code will only create the various state type objects and add to the FSM. We shall now implement the various Init_* methods to set up the delegates for our different states. For this tutorial, we are going to do a simple key press and Debug.Log output based demonstration to test out our FMS using C# delegates.

IDLE State

We will simple add text messages into Debug.Log.

void Init_IdleState()
{
    NPCState state = (NPCState)mFsm.GetState((int)StateTypes.IDLE);

    // Add a text message to the delegates.
    state.OnEnterDelegate += delegate ()
    {
        Debug.Log("OnEnter - IDLE");
    };
    state.OnExitDelegate += delegate ()
    {
        Debug.Log("OnExit - IDLE");
    };

    state.OnUpdateDelegate += delegate ()
    {
        //Debug.Log("OnUpdate - IDLE");
    };
}

Notice the += operator we are using for the delegates. Like this we can add on multiple functions to our delegates. For more details refer to C# delegates using anonymous methods and delegate operator (C# reference).

Look at the state machine diagram above. We can see that from the IDLE state you can go to CHASE or DAMAGE states. Let’s create two key inputs to handle these two transitions. We will use the “c” and “d” keys to handle the transitions from IDLE to CHASE and DAMAGE respectively.

void Init_IdleState()
{
    NPCState state = (NPCState)mFsm.GetState((int)StateTypes.IDLE);

    // Add a text message to the OnEnter and OnExit delegates.
    state.OnEnterDelegate += delegate ()
    {
        Debug.Log("OnEnter - IDLE");
    };
    state.OnExitDelegate += delegate ()
    {
        Debug.Log("OnExit - IDLE");
    };

    state.OnUpdateDelegate += delegate ()
    {
        //Debug.Log("OnUpdate - IDLE");
        if(Input.GetKeyDown("c"))
        {
            SetState(StateTypes.CHASE);
        }
        else if(Input.GetKeyDown("d"))
        {
            SetState(StateTypes.DAMAGE);
        }
        else if (Input.GetKeyDown("a"))
        {
            SetState(StateTypes.ATTACK);
        }
    };
}

You can see that we have used the method called SetState with an input parameter as the StateTypes type. This is a helper method which is implemented as follows:

// Helper function to set the state
public void SetState(StateTypes type)
{
    mFsm.SetCurrentState(mFsm.GetState((int)type));
}

ATTACK State

Looking at the state machine diagram above we can see that similar to IDLE, here too from the ATTACK state you can go to CHASE or DAMAGE states. We will reuse the “c” and “d” keys to handle the transitions from ATTACK to CHASE and DAMAGE states respectively.

void Init_AttackState()
{
    NPCState state = (NPCState)mFsm.GetState((int)StateTypes.ATTACK);

    // Add a text message to the OnEnter and OnExit delegates.
    state.OnEnterDelegate += delegate ()
    {
        Debug.Log("OnEnter - ATTACK");
    };
    state.OnExitDelegate += delegate ()
    {
        Debug.Log("OnExit - ATTACK");
    };

    state.OnUpdateDelegate += delegate ()
    {
        //Debug.Log("OnUpdate - ATTACK");
        if (Input.GetKeyDown("c"))
        {
            SetState(StateTypes.CHASE);
        }
        else if (Input.GetKeyDown("d"))
        {
            SetState(StateTypes.DAMAGE);
        }
    };
}

CHASE State

Looking at the state machine diagram above we can see that from the CHASE state you can go to either IDLE, DAMAGE or ATACK states. We will use the “i”, “d” and “a” keys to handle the transitions from CHASE to IDLE, DAMAGE and ATTACK states respectively.

void Init_ChaseState()
{
    NPCState state = (NPCState)mFsm.GetState((int)StateTypes.CHASE);

    // Add a text message to the OnEnter and OnExit delegates.
    state.OnEnterDelegate += delegate ()
    {
        Debug.Log("OnEnter - CHASE");
    };
    state.OnExitDelegate += delegate ()
    {
        Debug.Log("OnExit - CHASE");
    };

    state.OnUpdateDelegate += delegate ()
    {
        //Debug.Log("OnUpdate - CHASE");
        if (Input.GetKeyDown("i"))
        {
            SetState(StateTypes.IDLE);
        }
        else if (Input.GetKeyDown("d"))
        {
            SetState(StateTypes.DAMAGE);
        }
        else if (Input.GetKeyDown("a"))
        {
            SetState(StateTypes.ATTACK);
        }
    };
}

DAMAGE State

Looking at the state machine diagram above we can see that from the DAMAGE state you can go to either IDLE, CHASE, ATTACK or DIE states. Let’s create two key inputs to handle these two transitions. We will use the “i”, “c” and “a” keys to handle the transitions from DAMAGE to IDLE, CHASE and ATTACK states respectively. For the transition to DIE state we will count the number of damages that the NPC takes. In our case, we will use the number assigned to mMaxNumDamages. By default, this value is assigned to 5. That means after 5 damage hits the NPC will die.

First, we add two variables. One public for the maximum number of damage hits before the NPC dies and the other the damage count.

    #region NPC Data and Parameters
    public FSM mFsm;
    public int mMaxNumDamages = 5;
    private int mDamageCount = 0;
    #endregion

Now, we implement the initialization of the damage state.

void Init_DamageState()
{
    NPCState state = (NPCState)mFsm.GetState((int)StateTypes.DAMAGE);

    // Add a text message to the OnEnter and OnExit delegates.
    state.OnEnterDelegate += delegate ()
    {
        mDamageCount++;
        Debug.Log("OnEnter - DAMAGE");
    };
    state.OnExitDelegate += delegate ()
    {
        Debug.Log("OnExit - DAMAGE");
    };

    state.OnUpdateDelegate += delegate ()
    {
        //Debug.Log("OnUpdate - DAMAGE");
        if(mDamageCount == mMaxNumDamages)
        {
            SetState(StateTypes.DIE);
            return;
        }

        if (Input.GetKeyDown("i"))
        {
            SetState(StateTypes.IDLE);
        }
        else if (Input.GetKeyDown("c"))
        {
            SetState(StateTypes.CHASE);
        }
        else if (Input.GetKeyDown("a"))
        {
            SetState(StateTypes.ATTACK);
        }
    };
}

DIE State

Finally, we implement the DIE state. This state is triggered when we reach the maximum number of damage hits to mMaxNumDamages. This state can be reached from DAMAGE state.

void Init_DieState()
{
    NPCState state = (NPCState)mFsm.GetState((int)StateTypes.DIE);

    // Add a text message to the OnEnter and OnExit delegates.
    state.OnEnterDelegate += delegate ()
    {
        Debug.Log("OnEnter - DIE");
    };
    state.OnExitDelegate += delegate ()
    {
        Debug.Log("OnExit - DIE");
    };

    state.OnUpdateDelegate += delegate ()
    {
        //Debug.Log("OnUpdate - DIE");
    };
}

We now make a few changes to SetState so that when we set the DIE state we delete the game object after a stated duration of time.

// Helper function to set the state
public void SetState(StateTypes type)
{
    mFsm.SetCurrentState(mFsm.GetState((int)type));
    if(type == StateTypes.DIE)
    {
        StartCoroutine(Coroutine_Die(1.0f));
    }
}

IEnumerator Coroutine_Die(float duration)
{
    yield return new WaitForSeconds(duration);
    Debug.Log("NPC died. Removing gameobject");
    Destroy(gameObject);
}

We click play and run the project. The game starts with IDLE state. At this stage, you can click on “c”, “a” or “d” to transit the state to CHASE, ATTACK or DAMAGE. When DAMAGE state is triggered there is an output showing the total damage taken so far. Then if the damage count reaches to mMaxNumDamages then the DIE state is triggered.

Once the DIE state is triggered the game object is deleted after 1 second. This is handled by the coroutine Coroutine_Die.

See below for the demonstration.

https://faramira.com/wp-content/uploads/2020/08/demo.mp4

This concludes Part 4 of the tutorial. In Part 5 of the tutorial, we will move further by demonstrating an enemy NPC behaviour, which handles multiple animation states of a 3D animated character, using the delegate based FSM created in this tutorial.

References

  1. https://en.wikipedia.org/wiki/Finite-state_machine
  2. https://gameprogrammingpatterns.com/state.html
  3. https://gamedevelopment.tutsplus.com/tutorials/finite-state-machines-theory-and-implementation–gamedev-11867

Leave a Reply

Your email address will not be published. Required fields are marked *