3D Enemy AI In Unity: Combat System & Attack Script

Tutorial To Unity Enemy AI Combat System Script & Health Bars

This simple enemy AI tutorial is part of bigger Game Development serie. To make this lesson work and to understand it, you should read previous parts.

In previous part we made really good, functional drag and drop RPG inventory system with picking and dropping objects. In this lesson we will go through implementing an AI enemy.

AI has two meanings, one related to machine learning, and other to behavior of NPCs, especially enemies and monsters.

Both are really interesting concepts but this guide is directed towards beginners so we will cover the latter: how to implement fighting system like in typical role playing games and MMORPGs.

Friendly reminder that this is not supposed to be final version of our game but rather a prototype or a concept that you can use, improve, change, fix, or disregard completely according to your needs.

This chapter explains how to create a combat system with the following qualities:

  • Doesn’t take much code or effort at all.
  • It’s simple to understand and extend.
  • Contains enemy behavior that attacks players in range and tries to catch them. If they are too fast then it comes back to starting location.
  • 100% compatible with all our character animations and its model.
  • Healthbars above enemy and player.
  • When we are being attacked, the HP amount is reduced from healthbar.

And it looks like that. Let’s start.


Setting The Player Character Object

For enemy you can use any model and animations you want, even trolls and orcs. However for the sake of simplicity we will use our character’s model and animations.

Setting Animator

In this guide we’ve decided that our main source of animations is Mixamo. So first, get attacking animation either from Mixamo or from other place.

We will be using melee combat animation, if you want wands, bows, guns or fists then you can but make sure to change things accordingly wherever suitable.

Adding Node

So select our player in hierarchy, click Animator window, then drag the animation file from project window into Animator. It should create another node next to our Idle, Running and Jumping.

Connecting Nodes

Connect your nodes in this way:

And create Attacking parameter if you haven’t yet. Now we need to specify under which requirements transition links (arrows) will fire.

Adjusting Transitions Conditions

Attacking to itself:

Idle to Attacking:

Attacking to Idle:

Running to Attacking:

Attacking to Running:

You may need to also add condition “attacking == false” where sensible if fighting animation isn’t playing sometimes or at all. That’s because if it’s not playing then it means other transition fired instead.

Adding Health Bar

Using canvas elements such as health bar above our moving characters can be tricky. I absolutely dislike the way it’s done but its most straight-forward option so let’s do it.

Adding Slider

Add canvas right under our main (the top parent) player object on first spot (and index zero). Inside canvas add slider UI element.

Customizing Slider

Remove the part used for drawing and you have this:

Now change background to red and filling to green.

Canvas Render Mode

First set canvas render mode to world space, and drag our camera there.

Position Slider

Set position of canvas to “0, 0, 0” and do the same to slider. Now double click on slider in our hierarchy and it will focus it in our scene. Try to locate it above player head. BTW, you should be moving the slider object, not the canvas object.


Setting The AI Enemy Object

Now if you have slider in proper place above character then duplicate it. Because we will make enemy from it.

Adjusting Enemy

Rename it to “Enemy”. Remove from Enemy all character scripts (not components) you had such as movement, rotation, animation or camera (again, not components).

Our Existing Collider

By now we should have Box Collider with similar settings:

Size and center don’t have to be the same, it depends on your model. Default settings should be fine, if you did it properly in one of my previous chapters.

What is important here is that Is Trigger is unchecked (so it returns false). This way it will work all the time and not only when triggered. So we won’t fall through floor ;).

Add Sphere Collider

Add Sphere Collider.

These settings are good. Radius determines how far enemy will see our player. If player enters sphere, enemy starts following him. If player leaves sphere, enemy goes back. Is Trigger must be checked.


Coding The Combat System Scripts

Go to Animator, select attacking node and in the right panel click this button:

Name the script OnAttack.

OnAttack

Edit the script. First variables:

Slider targetSlider;
Animator classAnimator;

int frame = 0;

Then OnStateEnter:

// OnStateEnter is called when a transition starts and the state machine starts to evaluate this state
override public void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
{
    if (classAnimator != animator) ;
    {
        classAnimator = animator;

        if (classAnimator.gameObject.name == "Player")
        {
            targetSlider = GameObject.Find("Enemy").transform.GetChild(0).GetChild(0).gameObject.GetComponent<Slider>();
        }
        else if (classAnimator.gameObject.name == "Enemy")
        {
            targetSlider = GameObject.Find("Player").transform.GetChild(0).GetChild(0).gameObject.GetComponent<Slider>();
        }
    }
}

This is simple. If classAnimator has no reference to animator component yet, then it will assign it. And then if the calling object is player, it will assign enemy’s slider as reference. If calling object is enemy, it will assign player’s slider as reference.

That’s because if calling object is enemy, it means he’s the attacking object. So we need to get a reference to whoever is fighting now with us (in this simple sample it’s player) or rather to his health bar (slider). OnStateUpdate:

//OnStateUpdate is called on each Update frame between OnStateEnter and OnStateExit callbacks
override public void OnStateUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
{
    frame += 1;
    if (frame == 40)
    {
        targetSlider.value -= 10;
    }
}

Remember how we made default value of integer frame = 0? This method is called every frame so to detect in which moment we have been attacked by the sword we need to count the frames and remove HP after the attack.

If we don’t do this and put it after the animation is done, then we will have to wait till whole animation is finished and it will look like we had lags. That’s because the HP won’t be reduced when sword touches us but when enemy gets ready for next attack. So you can do it this way. OnStateExit:

// OnStateExit is called when a transition ends and the state machine finishes evaluating this state
override public void OnStateExit(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
{
    frame = 0;
}

After animation is done we reset the frame counter.


Artificial Intelligence Enemy Behavior

Variables:

public Animator animator;
public CharacterController characterController;

GameObject player;
Vector3 startPosition;
Coroutine attack;

bool shouldChase = false;

Detecting collisions:

void Start()
{
    startPosition = transform.position;
    characterController.detectCollisions = false;
}

void OnTriggerStay(Collider collider)
{
    if (collider.gameObject.name == "Player")
    {
        shouldChase = true;
        player = collider.gameObject;
    }
}

void OnTriggerExit(Collider collider)
{
    if (collider.gameObject.name == "Player")
    {
        shouldChase = false;
    }
}

First of all we need to store starting position so we can later return to it. Then we need to add OnTrigger methods that check if player entered our sphere. Update:

void LateUpdate()
{
    if (shouldChase & Vector3.Distance(transform.position, player.transform.position) > 1.5f)
    {
        transform.LookAt(new Vector3(player.transform.position.x, transform.position.y, player.transform.position.z));
        transform.position = Vector3.MoveTowards(transform.position, player.transform.position, 2 * Time.deltaTime);

        animator.SetBool("Running", true);
        animator.SetBool("Attacking", false);
    }
    else if (shouldChase & Vector3.Distance(transform.position, player.transform.position) < 1.5f)
    {
        animator.SetBool("Running", false);

        animator.SetBool("Attacking", true);
    }

    if (!shouldChase && Vector3.Distance(transform.position, startPosition) > 2.5f)
    {
        transform.LookAt(new Vector3(startPosition.x, transform.position.y, startPosition.z));
        transform.position = Vector3.MoveTowards(transform.position, startPosition, 2 * Time.deltaTime);
    }
    else if (!shouldChase && Vector3.Distance(transform.position, startPosition) < 2.5f)
    {
        animator.SetBool("Running", false);
        animator.SetBool("Attacking", false);
    }
}

We have four if statements:

  1. If player entered collision and distance from enemy to player is bigger than 1.5f (otherwise he wouldn’t stop near target but rather walk on him infinitely).
    Then we move towards player, we look at him, and we set parameters.
  2. If player entered collision and distance from enemy to player is smaller than 1.5f.
    Full stop. Enemy reached our player so we turn off running animation. We also don’t move towards player anymore.
  3. If player left collider and distance between enemy and his starting position is bigger than 2.5f.
    Then we look at our spawn and run back to it.
  4. If player is not in range and we returned to spawn point.
    Then turn off all animations except Idle.

And that’s it. The very basic, simple enemy AI, health bars and combat system that is waiting for you to extend it!


Alternatives To Counting Frames

Frame counter is one of many ways to perform an action during certain frame. And so is adding Behaviour script. Other alternatives to Behaviour script can be:

  • Coroutines.
  • Asynchronous methods.
  • Animation events.

All have pluses and minuses. In Unity there are always many solutions to single problem. With time you will develop your own neat, clean designing style, so don’t worry about amount of options.

Leave a Reply