Enemy Behaviour Using C# Delegates in Unity

Enemy Behaviour Using C# Delegates in Unity

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.

This tutorial first appeared on Faramira.

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.

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.

No alt text provided for this image

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.

No alt text provided for this image

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

No alt text provided for this image

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

No alt text provided for this image

Double click on Movement Blend Tree to edit.

Add two new Float parameters called PosX and PosZ.

No alt text provided for this image

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

No alt text provided for this image

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

No alt text provided for this image

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

No alt text provided for this image

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

No alt text provided for this image

Movement to Damage Transition

No alt text provided for this image

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

No alt text provided for this image

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

No alt text provided for this image

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.

No alt text provided for this image
No alt text provided for this image

Attack to Movement Transition

No alt text provided for this image

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

Any State to Die Transition

No alt text provided for this image

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 the 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 tin in scale in comparison with our Player game object.

No alt text provided for this image

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

No alt text provided for this image

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

No alt text provided for this image

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.

No alt text provided for this image

Enemy NPC States

The state diagram for our enemy NPC is given below.

No alt text provided for this image

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 its execution.

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

No alt text provided for this image
No alt text provided for this image


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.

No alt text provided for this image
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.

No alt text provided for this image

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 on the codes we wrote for keypress 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.

要查看或添加评论,请登录

Shamim Akhtar的更多文章

  • Implement Mazes in Unity

    Implement Mazes in Unity

    This tutorial will show how to implement mazes in Unity by applying the backtracking algorithm with an explicit stack…

  • Reusable Finite State Machine using C++

    Reusable Finite State Machine using C++

    This tutorial will implement a reusable Finite State Machine using C++. We will then illustrate the concept by applying…

  • Finite State Machine Using C# Delegates in Unity

    Finite State Machine Using C# Delegates in Unity

    In this tutorial, we will implement a Finite State Machine using C# Delegates in Unity. We will then demonstrate the…

  • Applying a Finite State Machine Using C# in Unity to Implement a Splash Screen

    Applying a Finite State Machine Using C# in Unity to Implement a Splash Screen

    This tutorial first appeared in Faramira In this tutorial, we will learn how to apply a Finite State Machines using C#…

  • CPF Simulator

    CPF Simulator

    Recently, as an experiment, I had written a CPF Simulator to compare and contrast the various amounts that you can…

    2 条评论
  • Solving 8 puzzle problem using A* star search

    Solving 8 puzzle problem using A* star search

    In this tutorial, we will solve the 8 puzzle problem using A* (star) search algorithm. We will approach the solution by…

  • Implementing a Finite State Machine Using C# in Unity — Part 1

    Implementing a Finite State Machine Using C# in Unity — Part 1

    In this tutorial, we will learn about Finite State Machines and implement a Finite State Machine using C# in Unity. We…

  • Artists Own the Industry Now!

    Artists Own the Industry Now!

    Do you remember the last time an artist or a song that really captured your mind and your heart? What was that…

  • The Myth of the “Tortured Artist”

    The Myth of the “Tortured Artist”

    The Myth of the “Tortured Artist”. Throughout history, one common thread in the mystery and allure of the artist is the…

    1 条评论
  • Forex Trading – 3 Embarrassing Truth Revealed

    Forex Trading – 3 Embarrassing Truth Revealed

    This article appeared first on faramira.com.

社区洞察

其他会员也浏览了