Enemy Behaviour With Finite State Machine Using C# Delegates in Unity

Share the Post

In this tutorial, we will implement enemy behaviour with Finite State Machine using C# delegates in Unity. This is a demonstration of C# delegate based FSM that we created in Part 4 of the tutorial.


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.

Part 4 uses delegates to define the functionality of the state and create the Finite State Machine. Thereafter we will demonstrate the use of the Finite State Machine by creating a simple key press based application in Unity.

This is Part 5 of the tutorial. Here 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 Part 4 of the tutorial.

For a better understanding please read Part 1 and Part 4 of the tutorial series before proceeding with this.

Download the entire Unity Package

Configure the NPC

Download and Import the Asset

We will reuse the project and the scene from our previous tutorial Part 3. For the enemy NPC we will use the following free asset from Unity Asset Store.

Once downloaded, import the asset into the project.

Create the NPC’s Animation Controller

Go to the folder called Animations inside the Resources folder.

Right-click on the Project window → Create → Animation Controller. Name it NPCAnim.

The asset comes with the following animations. We will use the Idle, Walk, Run, Skill, Attack, Damage, and Death animations.

Right click on the Animator window and create a new Blend Tree state called Movement.

Double click on Movement Blend Tree to edit.

Add two new Float parameters called PosX and PosZ.

Change the Blend Type to 2D Freeform Directional and choose the PosX and PosZ parameters.

Add the Idle, Walk and Run motions into the Blend Tree.

Add four more parameters. One Bool for Attack and three Trigger for Damage, Skill and Die.

Add the Die, Skill, Damage and Attack motions in the Animator.

Movement to Damage Transition

The movement to Damage Transition is triggered by the Damage parameter. Make sure you uncheck the Has Exit Time. Also, ensure that the Damage animation is not looping.

Movement to Attack Transition

The movement to Attack Transition is enabled by the Attack parameter set to true. Make sure you uncheck the Has Exit Time. Ensure that the Attack animation is looping by checking the Loop Time and Loop Pose checkboxes.

Movement to Skill Transition

The movement to Skill Transition is triggered by the Skill parameter. Make sure you uncheck the Has Exit Time. Ensure that the Attack animation is not looping by unchecking the Loop Time checkbox.

Damage to Movement and Skill to Movement Transitions

For Damage to Movement and Skill to Movement transitions ensure that Has Exit Time is enabled and the animations for Skill and Damage are not looping.

Attack to Movement Transition

For Attack to Movement transition disable Has Exit Time and make the transition condition set to Attack as False.

Any State to Die Transition

Any State to Die transition is triggered by the Die parameter. Make sure that the animation is not set to Has Exit Time and not looping.

Create Enemy Game Object

Create an empty game object and name it as EnemyNPC. Set it to position 0, 0, 0. Now drag and drop the Skeleton model from Assets->Fantasy Monster->Skeleton->Character into the EnemyNPC.

You will see that the Skeleton model is very small in scale in comparison with our Player game object.

Select the Skeleton@Skin game object and scale it by 13, 13, 13.

Change the position of the EnemyNPC to 3, 0, 0.

Now, select the EnemyNPC game object and add the Character Controller component to it. Change the Center value for Y to be 1. See figure below.

Enemy NPC States

The state diagram for our enemy NPC is given below.

Let’s start with the implementation.

Select the EnemyNPC game object and add a new Script component called EnemyNPC.cs. Double click and open the file in your favourite IDE.

Copy the code from the previous tutorial (Part 4) for the EnemyNPC. In our previous tutorial, we used key presses to handle transitions. Here will remove those key pressed based transition and implement a simple AI system for the enemy NPC.

Let’s start with the data that we will require for our enemy NPC.

    #region NPC data
    // The maximum speed at which the enemy NPC can move.
    public float mMaxSpeed = 3.0f;

    // The walking speed of the enemy NPC
    public float mWalkSpeed = 1.5f;

    // The maximum viweing distance of the enemy NPC
    public float mViewingDistance = 10.0f;

    // The maximum viewing angle of the enemy NPC.
    public float mViewingAngle = 60.0f;

    // The distance at which the enemy NPC will start attacking.
    public float mAttackDistance = 2.0f;

    // The turning rate of the enemy NPC.
    public float mTurnRate = 500.0f;

    // The tags for this NPC's enemy. Usually, it will be 
    // the player. But for other games this enemy NPC
    // can not only attack the player but also other NPCs.
    public string[] mEnemyTags;

    // The gravity. 
    public float Gravity = -30.0f;

    // The transform where the head is. This will 
    // determine the view of the enemy NPC based on 
    // it's head (where eyes are located)
    public Transform mEyeLookAt;

    // The distance to the nearest enemy of the NPC.
    // In this demo, we only have our player. So
    // this is the distance from the enemy NPC to the
    // Player.
    [HideInInspector]
    public float mDistanceToNearestEnemy;

    // The nearest enemy of the enemy NPC.
    // This is in case we have more than
    // one player in the scene. It could also 
    // mean other NPCs that are enemy to
    // this NPC.
    [HideInInspector]
    public GameObject mNearestEnemy;

    // The reference to the animator.
    Animator mAnimator;

    // The reference to the character controller.
    CharacterController mCharacterController;

    // The total damage count.
    int mDamageCount = 0;

    // The velocity vector.
    private Vector3 mVelocity = new Vector3(0.0f, 0.0f, 0.0f);

    // The Finite State Machine.
    public FSM mFsm;

    // The maximum damage count before the 
    // enemy NPC dies.
    public int mMaxNumDamages = 5;
    #endregion

The code above is self explanatory. These are the various variables that will be used by the Script during it’s execution.

Select the EnemyNPC game object and find the Head Transform node. Drag and drop it to the mEyeLookAt variable.

This will be used to calculate the LookAt direction for the enemy NPC. Do note that the forward vector for the Head is Up rather than Forward.

So, we create a function that returns the forward (look at) direction for the EnemyNPC.

public Vector3 GetEyeForwardVector()
{
    // The Up vector (green coloured vector)
    // is actually the forward vector for Head transform.
    return mEyeLookAt.up;
}

The Start Method

void Start()
{
    // Instead of here we could also have set as
    // public and then drag and drop in the editor.
    mAnimator = transform.GetChild(0).GetComponent<Animator>();
    mCharacterController = gameObject.GetComponent<CharacterController>();

    if (!mEyeLookAt)
    {
        mEyeLookAt = transform;
    }

    // The below codes are from previous tutorialPart 4.
    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));
}

In the Start method we initialize the mAnimator and mCharacterController variables. We then proceed with initializing the Finite State Machine as described in our previous tutorial.

There are no changes to the Update and FixedUpdate methods. Our FSM will do most of the work. So, the Update and FixedUpdate methods remain the same for the Script.

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

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

The Enemy Tag for our Enemy NPC

We will now set the values for mEnemyTags. This will take all the tag names of game objects that are this EnemyNPC’s enemy. For us, it will be the main player.

So, first of all, we set the tag for our Player to be Player.

Then we select the EnemyNPC and set the value of the mEnemyTags as shown below. For our tutorial, we only have our player as the enemy of our enemy NPC. So, the size of the mEnemyTags will be 1 and the value will be Player.

Move Method

We will now implement the Move method. This is for our NPC to be able to move.

public virtual void Move(float speed)
{
    Vector3 forward = transform.TransformDirection(Vector3.forward).normalized;

    mAnimator.SetFloat("PosZ", speed / mMaxSpeed);

    mVelocity = forward * speed;
    mVelocity.y += Gravity * Time.deltaTime;

    mCharacterController.Move(mVelocity * Time.deltaTime);

    if (mCharacterController.isGrounded &amp;&amp; mVelocity.y < 0)
        mVelocity.y = 0f;
}

This is a simple movement towards the EnemyNPC’s forward direction.

MoveTowards Method

MoveTowards method allows the NPC to move towards a certain point at a predefined speed.

public bool MoveTowards(Vector3 tpos, float speed)
{
    float dist = Distance(gameObject, tpos);
    if (dist > 1.5f)
    {
        Vector3 mpos = transform.position;

        Vector3 currentDirection = transform.forward;
        Vector3 desiredDirection = (tpos - mpos).normalized;

        Vector3 forward = Vector3.Scale(desiredDirection, new Vector3(1, 0, 1)).normalized;
        transform.rotation = Quaternion.RotateTowards(transform.rotation,
            Quaternion.LookRotation(forward), mTurnRate * Time.deltaTime);

        Move(speed);

        return true; // still moving
    }
    return false; //complete
}

GetNearestEnemyInSight Method

This method finds the nearest enemy for the NPC. In our case it will find the nearest player. There is only one player so it will always be the same game object. However, in a multiplayer game or in a game where the NPC has multiple enemy tags then there could be many enemy game objects for the NPC at the same time. In that case the following method will return the nearest enemy.

It also uses the parameter called useVieweingAngle which when used will use the Head’s direction to determine if the EnemyNPC can see the player.

public GameObject GetNearestEnemyInSight(out float distance, float viewableDistance, bool useVieweingAngle = false)
{
    distance = viewableDistance;
    GameObject nearest = null;
    for (int t = 0; t < mEnemyTags.Length; ++t)
    {
        GameObject[] gos = GameObject.FindGameObjectsWithTag(mEnemyTags[t]);
        for (int i = 0; i < gos.Length; ++i)
        {
            GameObject player = gos[i];

            Vector3 diff = player.transform.position - transform.position;
            float curDistance = diff.magnitude;
            if (curDistance < distance)
            {
                diff.y = 0.0f;

                if (useVieweingAngle)
                {
                    float angleH = Vector3.Angle(diff, GetEyeForwardVector());
                    if (angleH <= mViewingAngle)
                    {
                        distance = curDistance;
                        nearest = player;
                    }
                }
                else
                {
                    distance = curDistance;
                    nearest = player;
                }
            }
        }
    }
    return nearest;
}

At the same time, we also implement a helper method that determines the distance between a game object and a point.

public static float Distance(GameObject obj, Vector3 pos)
{
    return (obj.transform.position - pos).magnitude;
}

PlayAnimation and StopAnimation Methods

We now implement two more helper methods that will enable/disable animation sequences. These two methods are PlayAnimation and StopAnimation. These two methods set the transition for a certain animation to be played or stopped.

public void PlayAnimation(StateTypes type)
{
    switch (type)
    {
        case StateTypes.ATTACK:
            {
                mAnimator.SetBool("Attack", true);
                break;
            }
        case StateTypes.DIE:
            {
                mAnimator.SetTrigger("Die");
                break;
            }
        case StateTypes.DAMAGE:
            {
                mAnimator.SetTrigger("Damage");
                break;
            }
    }
}
public void StopAnimation(StateTypes type)
{
    switch (type)
    {
        case StateTypes.ATTACK:
            {
                mAnimator.SetBool("Attack", false);
                break;
            }
        case StateTypes.DIE:
            {
                // trigger so no need to do anything.
                break;
            }
        case StateTypes.DAMAGE:
            {
                // trigger so no need to do anything.
                break;
            }
        case StateTypes.IDLE:
            {
                break;
            }
        case StateTypes.CHASE:
            {
                mAnimator.SetFloat("PosZ", 0.0f);
                mAnimator.SetFloat("PosX", 0.0f);
                break;
            }
    }
}

We are now ready to implement the Finite State Machine state initialization methods that will allow us to implement the behaviour of our EnemyNPC.

Init_IdleState Method

For the implementation of the IDLE state we will remove or comment off the codes we wrote for key press enabled transitions and write new codes for changing animations.

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 ()
    {
        StopAnimation(StateTypes.IDLE);
        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);
        //}
        mNearestEnemy = GetNearestEnemyInSight(out mDistanceToNearestEnemy, mViewingDistance);
        if (mNearestEnemy)
        {
            if (mDistanceToNearestEnemy > mAttackDistance)
            {
                SetState(StateTypes.CHASE);
                return;
            }
            SetState(StateTypes.ATTACK);
            return;
        }
        PlayAnimation(StateTypes.IDLE);
    };
}

As for the logic, we find the nearest enemy (in our case the player) if within range. If the nearest enemy is valid we check if the distance is greater than the attack distance. If so then we make the NPC chase the player. This is done by setting the state to CHASE. If the distance is lesser than the attack distance then we set the state to be ATTACK.

Init_AttackState Method

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");
        StopAnimation(StateTypes.ATTACK);
    };

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

        if (mNearestEnemy)
        {
            if (IsAlive())
            {
                if (mDistanceToNearestEnemy < mAttackDistance)
                {
                    PlayAnimation(StateTypes.ATTACK);
                }
                else if (mDistanceToNearestEnemy > mAttackDistance &amp;&amp; mDistanceToNearestEnemy < mViewingDistance)
                {
                    SetState(StateTypes.CHASE);
                }
            }
            else
            {
                SetState(StateTypes.IDLE);
            }
            return;
        }
        if (!mNearestEnemy || mDistanceToNearestEnemy > mViewingDistance)
        {
            SetState(StateTypes.IDLE);
            return;
        }
    };
}

Init_ChaseState Method

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");
        StopAnimation(StateTypes.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);
        //}
        mNearestEnemy = GetNearestEnemyInSight(out mDistanceToNearestEnemy, mViewingDistance);

        if (!mNearestEnemy/* || !isMoving*/)
        {
            SetState(StateTypes.IDLE);
            return;
        }

        if (mDistanceToNearestEnemy < mAttackDistance)
        {
            SetState(StateTypes.ATTACK);
            return;
        }

        MoveTowards(mNearestEnemy.transform.position, mWalkSpeed);
        PlayAnimation(StateTypes.CHASE);
    };
}

Init_DamageState Method

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, Total damage taken: " + mDamageCount);
    };
    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);
        //}

        PlayAnimation(StateTypes.DAMAGE);
        SetState(StateTypes.IDLE);
    };
}

Init_DieState Method

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");
        PlayAnimation(StateTypes.DIE);
    };
}

We cannot test the DIE and DAMAGE states yet because we are not shooting the enemy.

To test out, set the Player position to be at 20,0,20 and click play. See below the video!

Note that we cannot trigger the DAMAGE and subsequently the DIE states. This is because we did not implement the Player to shoot the enemy.

In our next tutorial, we will implement

  • the Bullets for the Player to shoot,
  • the DAMAGE and DIE states for the EnemyNPC,
  • the muzzle flash and explosion effects, and
  • the sound effects to make the scene more realistic.

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 *