Learn To Create Enemy AI Systems With A Few Lines Of Code In Unity Game Engine

Table of Contents

Learn To Create Enemy AI Systems With A Few Lines Of Code In Unity Game Engine

Reading Time: 42 minutes
Level: Beginner – Intermediate

Help Others Learn Game Development

There are different types of enemy AI that you can create in Unity, from the very basic enemies that move between two points all the way to machine learning where your enemies are learning from the events in the game and behaving accordingly.

In this post we are going to learn about AI in Unity by creating basic and intermediate enemy AI behaviour.

Download Assets And Complete Project For This Tutorial

To follow along with this tutorial please download the assets and the starter project by clicking on the green Download assets button above.
 
In the downloaded folder you will find the assets for the tutorial, the finished project, and the starter project I prepared for you to follow this tutorial.
 

Important Information Before We Start

One of the labels for this tutorial is beginner, however this is not a tutorial for complete beginners.

I expect you to know how to create basic games in Unity, but you are a beginner when it comes to AI programming in Unity. So it is mandatory that you know how to code in C# and how to use Unity and its interface.

If you don’t know any of these things, you can learn how to code in C# in my C# tutorial series starting with variables, and then you can move on to create your first game in Unity with my Rainy Knifes tutorial.

Starting With Basic Enemy AI - Shooting

One of the most common AI feature is shooting. So let’s take a look at our first example by opening the scene named 1 – Basic Enemy AI Shooting inside the Assets -> Scenes folder.
 
I’ve already prepared all the elements we need for this example, such as the enemy, bullet, and the enemy shooter script.
 
Inside the Assets -> Scripts folder, open the SpiderShooter script in Visual Studio.
 
First, we are going to create a basic AI shooting where the spider enemy will shoot a bullet ever X amount of seconds.
 
Above the Start function declare the following variables:
 
				
					[SerializeField]
    private GameObject spiderBullet;

    [SerializeField]
    private Transform bulletSpawnPos;

    [SerializeField]
    private float minShootWaitTime = 1f, maxShootWaitTime = 3f;

    private float waitTime;
				
			
First we need the bullet that the spider will shoot, that is why we have declared the spiderBullet variable. I’ve already created the bullet and saved it in the Prefabs folder and we will attach it to the appropriate slot in a few moments.
 
The bulletSpawnPos is the position where we will spawn the bullet, and for that we will create an empty object, position it where the spider bullet is going to be spawned and the attach it in the appropriate slot in the Inspector tab.
 
The min and max shoot wait time are self explanatory. We are going to randomize the shooting of the bullet and the wait time is going to be a random range between the min and max shoot wait time. And we will use the waitTime variable to keep track when to shoot the bullet.
 
Before we do all of this, we need to attach the bulletPrefab and the bulletSpawnPos variable. Starting with the bulletPrefab, in the Assets -> Prefabs folder, drag the Spider Shooter Bullet prefab in the Spider Bullet slot for the SpiderShooter script in the Inspector tab:
 
Img 1
Next, create an empty game object and make it a child of the Spider Shooter object. Name the child object Bullet Spawn Position and set the following values for the Position in the Transform component:
 
Img 2
This will position the Bullet Spawn Position object where the mouth of the spider shooter are, and when we shoot the bullet it will look like the bullet is coming out of the spider’s mouth.
 
The last step is to attach the Bullet Spawn Position object in the appropriate slot in the SpiderShooter script:
 
Img 3

Going back in the SpiderShooter script, we are going to create the shooting functionality by adding the following lines of code:

				
					void Shoot()
    {
        Instantiate(spiderBullet, bulletSpawnPos.position, Quaternion.identity);
    }
				
			

The Shoot function simply uses the Instantiate function to create a new copy out of the spiderBullet game object. It will spawn it at the bulletSpawnPos variable position, and it will set the rotation values to 0 for X, Y, and Z using Quaternion.identity.

Inside the Update we will create a timer that will shoot the bullet every X seconds:

				
					private void Update()
    {
        if (Time.time > waitTime)
        {
            waitTime = Time.time + Random.Range(minShootWaitTime, maxShootWaitTime);
            Shoot();
        }
    }
				
			
Time.time on line 3, is the amount of time passed since the game started. This value always increases every second we play the game, and we can use it to create effective timers.
 
So, if Time.time is greater than waitTime, on line 5 we will first set a new value for the waitTime by using the current value of Time.time and adding to it the random value returned between min and max shoot wait time.
 
Let’s assume that the current value of Time.time is 5, and the returned random value between min and max shoot wait time is 3, this means that the new value of waitTime is 8, and given the fact that during that calculation the current value of Time.time is 5, we will need to wait another 3 seconds before we shoot a new bullet.
 
And to shoot the bullet, we simply call the Shoot function that we created and explained what it does.
 
Before we test this out, we can also delay the shooting of the bullet by seting a new value for waitTime in the Start function:
 
				
					private void Start()
    {
        waitTime = Time.time + Random.Range(minShootWaitTime, maxShootWaitTime);
    }
				
			
The reason why I set a new value for waitTime before we start the game is because the current value of waitTime is zero(0), which means as soon as we run the game the spider will shoot a bullet, and to delay that we set a new value for waitTime.
 
Let’s run the game to test it out:
 

As you can see, after every X amount of seconds the spider is shooting the bullet.

If you don’t like the wait time between each shoot, you can change the values for min and max shoot wait time because we added SerializeField in their declaration which means we can change their values in the Inspector tab:

Img 4
Lastly, I will leave the first version of the EnemyShooter script below for you to have as a reference to compare your code to mine, or if you want to copy paste the code in your project:
 
				
					using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class SpiderShooter : MonoBehaviour
{

    [SerializeField]
    private GameObject spiderBullet;

    [SerializeField]
    private Transform bulletSpawnPos;

    [SerializeField]
    private float minShootWaitTime = 1f, maxShootWaitTime = 3f;

    private float waitTime;

    private void Start()
    {
        waitTime = Time.time + Random.Range(minShootWaitTime, maxShootWaitTime);
    }

    private void Update()
    {

        if (Time.time > waitTime)
        {
            waitTime = Time.time + Random.Range(minShootWaitTime, maxShootWaitTime);
            Shoot();
        }

    }

    void Shoot()
    {
        Instantiate(spiderBullet, bulletSpawnPos.position, Quaternion.identity);
    }

}
				
			


Optimizing Enemy AI Shooting - Object Pooling Technique

While the shooting functionality works, it can lead to our game being slow if we have too much shooting enemies in the game.

The reason for that is because we are using the Instantiate function which creates new objects every time, plus we are not disposing the bullets that we already created and this can lead to many game objects being in the game, not doing anything or having any functionality yet taking our game resources and this can make our game slower.

To fix this problem, we use a programming technique called pooling. The idea of pooling is to create a pool of objects, in our case a pool of bullets, and when we need a new bullet, we will reuse one of the bullets stored in the pool.

If by any chance all the bullets in the bullet are not available for use e.g. they are currently being used, then we will create a new bullet and store it in the pool and repeat the process.

To create this system, first we need to add new variables in the SpiderShooter script. Above the Start function add the following lines:

				
					[SerializeField]
    private List<GameObject> bullets;

    private bool canShoot;
    private int bulletIndex;

    [SerializeField]
    private int initialBulletCount = 2;
				
			

First we create a new list that will store game objects. A list is like an array, with the difference that a list is flexible, meaning we can add new and remove old elements from that list.

Between the <> we type the type of object we want to store in the list, in our case a GameObject. This can be modified in case we have a Bullet script for example, and we only want to store game objects that have the Bullet script attached on them, then, instead of typing:

				
					[SerializeField]
    private List<GameObject> bullets;
				
			
We would type:
 
				
					[SerializeField]
    private List<Bullet> bullets;
				
			
The canShoot variable will be used to control the shooting so that we don’t shoot two bullets at the same time instead of only one.
 
bulletIndex variable will represent the index of the bullet in the list that we will use.
 
And initialBulletCount value will determine how many bullets we will create and store in the list when the game starts.
 
From this, you can conclude that the list is actually the pool holding the bullets, and since a list is like an array, we can use indexes to access elements in the list.
 
When the game starts, the first thing we need to do is create the initial bullets and store them in the pool. In the Start function, add the following lines of code:
 
				
					private void Start()
    {
        GameObject newBullet;

        for (int i = 0; i < initialBulletCount; i++)
        {
            newBullet = Instantiate(spiderBullet);
            bullets.Add(newBullet);
            newBullet.SetActive(false);
        }

        waitTime = Time.time + Random.Range(minShootWaitTime, maxShootWaitTime);
    }
				
			

First we create the newBullet variable and we don’t set a value for it, which means it is equal to null. We need the newBullet variable so that we can add the newly created bullet in the list.

Of course, since this is programming, there are always multiple ways how you can achieve a certain result. We can rewrite this code so that we don’t have to create the newBullet variable at all:

				
					private void Start()
    {
        for (int i = 0; i < initialBulletCount; i++)
        {
            bullets.Add(Instantiate(spiderBullet));
            bullets[i].SetActive(false);
        }

        waitTime = Time.time + Random.Range(minShootWaitTime, maxShootWaitTime);
    }
				
			

We can use Instantiate function as a parameter in the Add function from the list, because the Instantiate function returns the game object it has created, and the Add function of the list stores the object in the list.

To deactivate the newly created bullet, we can use i variable declared in the for loop and access the bullet we just created to deactivate it.

The reason why we deactivate the bullets as soon as we create them is because if don’t do that, they will be spawned in the level from the very start which is not something that we want.

You can test that by removing

				
					bullets[i].SetActive(false);
				
			

from the code and see the outcome. Rewrite the Shoot function so that it uses the bullets from the bullets list instead of instantiating new ones:

				
					void Shoot()
    {
        canShoot = true;
        bulletIndex = 0;

        while (canShoot)
        {
            if (!bullets[bulletIndex].activeInHierarchy)
            {
                bullets[bulletIndex].SetActive(true);
                bullets[bulletIndex].transform.position = bulletSpawnPos.position;

                canShoot = false;
                break;
            }
            else
            {
                bulletIndex++;
            }

            if (bulletIndex == bullets.Count)
            {
                
                bullets.Add(Instantiate(spiderBullet, bulletSpawnPos.position, Quaternion.identity));

                canShoot = false;
                break;
            }
        }
    }
				
			

We are going to use a while loop to loop through the list and search for a bullet that is not active in the scene e.g. it used SetActive function and passed false as the parameter.

Because of that, every time we call the Shoot function first we need to set the value of canShoot to true. We also set the value of bulletIndex to zero(0) because we will start searching from the first element in the list.

The activeInHierarchy property of the game object returns true if the game object is active in the scene e.g. game, and it returns false if the game object is not active in the scene.

You will notice that we used an exclamation mark in front of the activeInHierarchy property, and the exclamation mark will make what’s after it, the opposite, meaning if activeInHierarchy returns true, then the exclamation mark will make it the opposite which is false, and if activeInHierarchy returns false, the exclamation mark will make it the opposite which is true.

So essentially we are searching for a game object, in our case a bullet, that is NOT active in the hierarchy so that we can activate it and use it. And this is the whole point of pooling technique because we are reusing game objects instead of creating new ones which saves performance.

We are using the bulletIndex to access the specific index in the list, and since we set the starting value of bulletIndex to 0 on line 4, we will first test if the element at index 0 is not active in the hierarchy.

To activate a game object, we simply call SetActive and pass true as the parameter and it will make the game object active in the game again.

Since we are simulating the effect of a bullet, we need to reposition the bullet we just activated so that it falls from the bulletSpawnPos, and this will make it look like the spider is shooting new bullets.

When we finish with that, we need to set canShoot to false, so that we don’t spawn more than one bullet, and we use break to exit outside the while loop.

When the code reaches the break statement, it will simply stop executing the loop, and all the code that is below the break statement will not get executed.

In our case, since we are using the canShoot variable to control the while loop, we can remove the break statements, but I put them in the code for this example just to explain what they are doing and that you can use them for that purpose.

In case we don’t find any bullets that are not active in the hierarchy, then we will create a new bullet and store it in the bullets list. This way, we will only create new bullets if all current bullets are active and being used, and this is very hard to happen when we get to a certain amount of bullets in the game.

Before we test out the game, one thing to note is that I added SerializeField above the bullets list declaration, I did this so that we can see directly in the Inspector tab when the bullets are created and added to the list, so when we test the game make sure that you pay attention to that.

Now run the game and let’s test it out:

When started the game we had only two spider bullets in the bullets list, and the more the spider enemy shoot, the more bullets were created in the list.

The reason for this is, we need to deactivate the bullet objects. Inside the Assets -> Scripts folder, create a new C# script and name it SpiderBullet. Open the SpiderBullet script in Visual Studio and add the following lines of code:

				
					private void OnTriggerEnter2D(Collider2D collision)
    {
        if (collision.CompareTag("Ground") || collision.CompareTag("Player"))
        {
            gameObject.SetActive(false);
        }
    }
				
			

In OnTriggerEnter2D we are testing if the bullet collides with the ground, or if the bullet collides with the player object, and if that happens we will deactivate the bullet.

Of course, in a real game, you would not use hard code string values instead you would have a more efficient way to compare strings to each other but I am not going to go into that in this tutorial.

Make sure that you attach the BulletScript to the Spider Shooter Bullet prefab inside the Assets -> Prefabs folder.

As for the Ground Holder game object, I’ve set his tag to Ground:

Img 5
but for your project always double check if you set the tags correctly when you testing tags in your code.
 
Let’s test the game now:
 

The result we have now is the same we had in the beginning when we used Instantiate, but the difference now is that the spider enemy is shooting the same way, but instead of creating new bullets every time, we are reusing the ones we already have.

Before we move forward, I will leave the new version of the SpiderShooter script as a reference:

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

public class SpiderShooter : MonoBehaviour
{
    [SerializeField]
    private GameObject spiderBullet;

    [SerializeField]
    private Transform bulletSpawnPos;

    [SerializeField]
    private float minShootWaitTime = 1f, maxShootWaitTime = 3f;

    private float waitTime;

    [SerializeField]
    private List<GameObject> bullets;

    private bool canShoot;
    private int bulletIndex;

    [SerializeField]
    private int initialBulletCount = 2;

    private void Start()
    {
        for (int i = 0; i < initialBulletCount; i++)
        {
            bullets.Add(Instantiate(spiderBullet));
            bullets[i].SetActive(false);
        }

        waitTime = Time.time + Random.Range(minShootWaitTime, maxShootWaitTime);
    }

    private void Update()
    {
        if (Time.time > waitTime)
        {
            waitTime = Time.time + Random.Range(minShootWaitTime, maxShootWaitTime);
            Shoot();
        }
    }

    void Shoot()
    {
        canShoot = true;
        bulletIndex = 0;

        while (canShoot)
        {
            if (!bullets[bulletIndex].activeInHierarchy)
            {
                bullets[bulletIndex].SetActive(true);
                bullets[bulletIndex].transform.position = bulletSpawnPos.position;

                canShoot = false;
            }
            else
            {
                bulletIndex++;
            }

            if (bulletIndex == bullets.Count)
            {
                
                bullets.Add(Instantiate(spiderBullet, bulletSpawnPos.position, Quaternion.identity));

                canShoot = false;
            }
        }
    }

} // class
				
			


Triggering AI Actions With Colliders

For the first example the spider enemy is shooting with the help of a timer. But what if we don’t want a functionality like that in our game. Let’s say we want to trigger the enemy to attack if the player is passing by.

We can do that in couple of ways, one of them is using a collider. Attach a Box Collider 2D on the Spider Shooter game object in the Hierarchy tab, check the Is Trigger checkbox and set the following values for the size and position of the collider:

Img 6

Since we are not going to use the timer functionality, we can remove the Update function and all the code that is inside. In the Start function we will only leave the code that will create the initial bullets when the game starts:

				
					 private void Start()
    {
        for (int i = 0; i < initialBulletCount; i++)
        {
            bullets.Add(Instantiate(spiderBullet));
            bullets[i].SetActive(false);
        }
    }
				
			

The Shoot function is going to stay the same. To trigger the shooting in the code, we are going to use OnTriggerEnter2D function:

				
					 private void OnTriggerEnter2D(Collider2D collision)
    {
        if (collision.CompareTag("Player"))
        {
            Shoot();
        }
    }
				
			

Since we checked the Is Trigger checkbox for the Box Collider 2D that is attached on the Spider Shooter, we can use OnTriggerEnter2D to detect collision between the player and the spider enemy, when they collide, the spider enemy will shoot.

I have already prepared the player game object and I’ve created a prefab out of it. In the Assets -> Prefabs folder, drag the Player object in the Hierarchy and run the game to test it:

As soon as the player entered the collider of the spider the spider started to shoot. One thing to keep in mind here is that we are using OnTriggerEnter2D to detect collision and shoot, depending on what you want to do this might not be the solution you are looking for.

Because if the player enters the collider and stays inside, then the spider will shoot only once:

You can fix this by using OnTriggerStay2D instead of OnTriggerEnter2D. The difference between the two functions is that the OnTriggerEnter2D registers a collision only when the object enters the trigger, which happens only once. Because for the function to register another collision, the object needs to exit the trigger, then enter it again.
 
But OnTriggerStay2D registers collision when the object enters the collider and as long as the object is staying within the bounds of the collider it will register.
 
Remove the OnTriggerEnter2D and add the OnTriggerStay2D function:
 
				
					private void OnTriggerStay2D(Collider2D collision)
    {
        if (collision.CompareTag("Player"))
        {
            Shoot();
        }
    }
				
			
Run the game and let’s test it out:
 

First, when the player entered the collider the spider shooter started shooting a lot of bullets, this is something we need to fix. Second, only when the player is moving while he is within the bounds of the collider does the spider start shooting.

“But teacher you said that OnTriggerStay2D registers collision for as long as the player stays inside the bounds.”

Yes I did handsome stranger. The issue here is, we need to attach a Rigidbody2D component on the spider, and we need to set the Sleeping Mode option to Never Sleep:

Img 7
The Sleeping Mode option set to Never Sleep will keep the collider detection trigger always active, which means that now as long as the player is within the bounds of the collider it will detect collision.
 
Before we test it out now, don’t forget to set the Gravity Scale to 0 because we don’t want gravity to affect the spider shooter since he is a static enemy and doesn’t move.
 
Run the game and let’s test it out now:
 

The collision now works as intended, but we still have the issue where the spider is shooting like crazy, which not something that we want.

We can fix this by implementing the same timer technique we used in the first example.

Inside the OnTriggerStay2D add the following lines of code:

				
					private void OnTriggerStay2D(Collider2D collision)
    {
        if (collision.CompareTag("Player"))
        {
            if (Time.time > waitTime)
            {
                waitTime = Time.time + Random.Range(minShootWaitTime, maxShootWaitTime);
                Shoot();
            }
        }
    }
				
			
Now we detect collision with the player object, we check if Time.time is greater than waitTime and only then we shoot. Of course, we reset the waitTime value so that we wait again and don’t shoot more than one bullet at the time.
 
Run the game and let’s test it out now:
 

Now we have a working logic that will trigger after X amount of seconds for as long as the player, or any other target game object, stays within the bounds of the collider.

The last trigger option, which we mentioned, that we can check is OnTriggerExit2D:

				
					private void OnTriggerExit2D(Collider2D collision)
    {
        if (collision.CompareTag("Player"))
        {
            Shoot();
        }
    }
				
			
OnTriggerExit2D will detect collision only when the game object exits the collider bounds. When the target game object enters the collider bounds, nothing will happen, but as soon as he exist the bounds the collider will detect that and it will fire the function in the code:
 

Of course, you would use all three trigger detection functions in different ways depending on your needs, I am only showing you the options that you have at your disposal.

As a reference I will leave the new version of the SpiderShooter script below:

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

public class SpiderShooter : MonoBehaviour
{
    [SerializeField]
    private GameObject spiderBullet;

    [SerializeField]
    private Transform bulletSpawnPos;

    [SerializeField]
    private float minShootWaitTime = 1f, maxShootWaitTime = 3f;

    private float waitTime;

    [SerializeField]
    private List<GameObject> bullets;

    private bool canShoot;
    private int bulletIndex;

    [SerializeField]
    private int initialBulletCount = 2;

    private void Start()
    {
        for (int i = 0; i < initialBulletCount; i++)
        {
            bullets.Add(Instantiate(spiderBullet));
            bullets[i].SetActive(false);
        }
    }

    void Shoot()
    {
        canShoot = true;
        bulletIndex = 0;

        while (canShoot)
        {
            if (!bullets[bulletIndex].activeInHierarchy)
            {
                bullets[bulletIndex].SetActive(true);
                bullets[bulletIndex].transform.position = bulletSpawnPos.position;

                canShoot = false;
            }
            else
            {
                bulletIndex++;
            }

            if (bulletIndex == bullets.Count)
            {
                
                bullets.Add(Instantiate(spiderBullet, bulletSpawnPos.position, Quaternion.identity));

                canShoot = false;
            }
        }
    }

    private void OnTriggerEnter2D(Collider2D collision)
    {
        if (collision.CompareTag("Player"))
        {
            Shoot();
        }
    }

    private void OnTriggerStay2D(Collider2D collision)
    {
        if (collision.CompareTag("Player"))
        {
            if (Time.time > waitTime)
            {
                waitTime = Time.time + Random.Range(minShootWaitTime, maxShootWaitTime);
                Shoot();
            }
        }
    }

    private void OnTriggerExit2D(Collider2D collision)
    {
        if (collision.CompareTag("Player"))
        {
            Shoot();
        }
    }

} // class
				
			


Triggering AI Actions With Raycasts

Another way how we can create AI behavior is using raycasts. The idea of a raycast is to create an invisible ray in the shape of a line, circle, or a box, and when the target game object touches that ray, collision will be detected and you can perform actions based on that collision.

We are going to reuse the same script with the same shooting functionality, but we are going to change the way how we detect shooting, so you can remove the functions that detect collision between the enemy and other objects.

Now, to create a ray we use the Physics2D class, and it goes like this:

				
					private void Update()
    {
        if (Physics2D.Raycast(transform.position, Vector2.down, 10f))
        {
            
        }
    }
				
			
The code on line 3 will create a ray in the shape of a line. The first parameter is the starting position, I’ve used the current position of the spider as the starting position.
 
The second parameter is the direction where the ray will go, Vector2.down is a shortcut for writing Vector2(0, -1), this has to do with the normalization of the vector that you can read more about by clicking here.
 
The third parameter is the length of that ray. We can visually represent the length of the ray using Debug.DrawRay.
 
In the Update function below the current code that creates the ray, add the following line:
 
				
					Debug.DrawRay(transform.position, Vector3.down * 10f, Color.red);
				
			

The DrawRay function doesn’t have the length parameter to determine the length of the ray that we will draw, because of that we can multiply the direction with the length of the ray, which we did for the second parameter: Vector3.down * 10f, and this will draw a ray in the down direction with the length of 10. The color parameter is the color of the ray that will be drawn on the screen.

Let’s run the game and see this in action:

The red line you saw in the preview above is the ray that is drawn with the help of DrawRay function.
 
From the structure of the code you already figured our that the Raycast function will return a bool value, meaning it will return true if the ray colliders with an object or false if the ray doesn’t collide with the object.
 
If we try to shoot by adding:
 
				
					if (Physics2D.Raycast(transform.position, Vector2.down, 10f))
{
    Shoot();
}
				
			
This will shoot infinitely because the ray will hit the ground object and detect collision, but we only want to detect collision with the player object. For that we can use layer masks.
 
Above the Start function declare the following variable:
 
				
					[SerializeField]
    private LayerMask collisionLayer;
				
			
In the Raycast function add the collisionLayer as the last parameter:
 
				
					if (Physics2D.Raycast(transform.position, Vector2.down, 10f, collisionLayer))
{
    Shoot();
}
				
			
The idea of the collisionLayer variable is that the raycast will only detect collisions with game objects that are on a specific layer. Add a new layer and name it Player:
 
Img 8

Now change the layer for the player object to Player layer:

Img 9
Now select the Spider Shooter object and in the Inspector tab for the SpiderShooter script, select the Player layer for the Collision Layer variable we created in the script:
 
Img 10

Now the raycast will only detect collisions with game objects that are on the Player layer. Let’s run the game to test it:

The collision works but again we have the same problem we had with OnTriggerStay2D which can be fixed in the same way by adding a timer functionality.
 
We already did that a couple of times during this tutorial so I am not going to demonstrate it again as you can do that by yourself, plus I will leave you with the fixed version of the script down below for your reference:
 
				
					using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class SpiderShooter : MonoBehaviour
{
    [SerializeField]
    private GameObject spiderBullet;

    [SerializeField]
    private Transform bulletSpawnPos;

    [SerializeField]
    private float minShootWaitTime = 1f, maxShootWaitTime = 3f;

    private float waitTime;

    [SerializeField]
    private List<GameObject> bullets;

    private bool canShoot;
    private int bulletIndex;

    [SerializeField]
    private int initialBulletCount = 2;

    [SerializeField]
    private LayerMask collisionLayer;

    private void Start()
    {
        for (int i = 0; i < initialBulletCount; i++)
        {
            bullets.Add(Instantiate(spiderBullet));
            bullets[i].SetActive(false);
        }
    }

    private void Update()
    {
        if (Physics2D.Raycast(transform.position, Vector2.down, 10f, collisionLayer))
        {
            if (Time.time > waitTime)
            {
                waitTime = Time.time + Random.Range(minShootWaitTime, maxShootWaitTime);
                Shoot();
            }
        }

        Debug.DrawRay(transform.position, Vector3.down * 10f, Color.red);
        
    }

    void Shoot()
    {
        canShoot = true;
        bulletIndex = 0;

        while (canShoot)
        {
            if (!bullets[bulletIndex].activeInHierarchy)
            {
                bullets[bulletIndex].SetActive(true);
                bullets[bulletIndex].transform.position = bulletSpawnPos.position;

                canShoot = false;
            }
            else
            {
                bulletIndex++;
            }

            if (bulletIndex == bullets.Count)
            {
                
                bullets.Add(Instantiate(spiderBullet, bulletSpawnPos.position, Quaternion.identity));

                canShoot = false;
            }
        }
    }

} // class
				
			
Now when it comes to raycasts they are a wide topic, because we have a raycast that creates a line, we also have a box shaped raycast and a circle shaped raycast.
 
They all work in the same way but they are shaped differently. You can research more about raycast and you will find examples how to use them, I am not going to cover every form of a raycast in this tutorial because it will go off topic since we are interested in creating enemy AI behaviour.
 

Shooting In Player's Direction

So far, all the AI examples we saw make the enemy shoot in one direction. But what if we want the enemy to shoot in the player’s direction, for example you have an enemy with a gun and you want it to shoot the player no matter where the player is in the level.

For that, we need to make the enemy rotate towards the player’s direction first, and then fire a bullet in that direction.

We are going to use the same shooting functionality, but you can remove the current code from the Update function and the LayerMask variable declaration since we don’t need raycasting for this example.

What we do need is a reference to the player’s Transform component because want the enemy to rotate towards the player.

Above the Start function declare the following variable:

				
					private Transform playerTransform;
				
			
In the Awake function, we will get the reference to the player’s Transform component:
 
				
					private void Awake()
    {
        playerTransform = GameObject.FindWithTag("Player").transform;
    }
				
			
Since we are using FindWithTag function, make sure that the player object is tagged with Player tag.
 
To make the spider enemy face the player’s direction, we need two more variables. Below the playerTransform variable, declare the following variables:
 
				
					private Vector3 direction;
    private float angle;
				
			
First we need to calculate the direction where the player is facing, for that we are going to use the direction variable, then we need to calculate the angle by which we need to rotate the enemy to make it face the player’s direction, for that we will use the angle variable.
 
Below the Shoot function, we are going to create a function that is going to calculate the direction and the angle, and make the enemy face the player:
 
				
					void FacePlayersDirection()
    {
        direction = playerTransform.position - transform.position;
        angle = Mathf.Atan2(direction.y, direction.x) * Mathf.Rad2Deg;
        transform.rotation = Quaternion.AngleAxis(angle + 90f, Vector3.forward);
    }
				
			
To calculate the direction of the player, we simply subtract from the player’s current position the current position of the enemy.
 
To calculate the angle we use the Atan2 function from the Mathf library. Atan2 function will return an angle between two points in radians, that is why we multiply the returned value with Rad2Deg which will convert radians into degrees. This is how we will get the angle between the two points.
 
And to rotate the enemy towards the player, we use the AngleAxis function from the Quaternion class. AngleAxis will rotate around the given axis by the given degrees.
 
The first parameter are the amount of degrees by which we will rotate, and the second parameter is the axis around which we will rotate. Since this is a 2D to make the spider rotate towards the player’s direction we use the Z axis which is what Vector3.forward represents.
 
And for the degrees we use the calculated angle and we added 90 degrees to it to make it rotate towards the player’s direction.
 
Make sure that you call the FacePlayersDirection in the Update function:
 
				
					private void Update()
    {
        FacePlayersDirection();
    }
				
			
Now run the game and let’s test it out:
 
Wherever the player goes, the spider enemy is rotating in that direction. Now to shoot the bullet in the direction the spider is facing we need to make a couple of changes.
 
First, in the Assets -> Prefabs folder, select the Spider Shooter Bullet, and in the Inspector tab locate the Rigidbody 2D component and set the Gravity Scale value to 0:
 
Img 11

If we want the bullet to move towards the direction where it is fired, then we need to turn off the gravity affect it, otherwise the gravity will pull the bullet down.

Next, open the SpiderBullet script, and below the class declaration add the following lines of code:

				
					[SerializeField]
    private float moveSpeed = 7f;

    [SerializeField]
    private Rigidbody2D myBody;
				
			

You can already assume what we are going to do with both of these variables. But before we proceed with that, we need to attach the Rigidbody2D component to the appropriate slot in the Inspector tab.

You can drag the Spider Shooter Bullet object in the slot and it will register its Rigidbody2D component:

Img 12
To shoot the bullet, we need to create a function:
 
				
					public void ShootBullet(Vector3 direction)
    {
        myBody.velocity = direction * moveSpeed;
    }
				
			
This function is going to shoot the bullet in the given direction multiplied with the moveSpeed. The moveSpeed variable has the SerializeField property so we can change the speed in the Inspector tab whenever we want.
 
We also need to stop the movement when the bullet gets deactivated inside the OnDisable:
 
				
					private void OnDisable()
    {
        myBody.velocity = Vector2.zero;
    }
				
			
The reason for this is when we deactivate the game object, that will not affect the movement speed he already has, it will just deactivate it, and when we activate the object again it will continue to use the speed the object had in the moment of its deactivation, and this can mess up our shooting system.
 
Now open the SpiderShoot script. Inside the FacePlayerDirection function add the code that will shoot the bullet after X amount of seconds:
 
				
					 void FacePlayersDirection()
    {
        direction = playerTransform.position - transform.position;
        angle = Mathf.Atan2(direction.y, direction.x) * Mathf.Rad2Deg;
        transform.rotation = Quaternion.AngleAxis(angle + 90f, Vector3.forward);
        
        if (Time.time > waitTime)
        {
            waitTime = Time.time + Random.Range(minShootWaitTime, maxShootWaitTime);
            Shoot();
        }
    }
				
			

As you can see, we are using the same approach we used so far. Of course, you can also trigger the shooting like we showed in the previous examples.

Now to shoot the bullet, we need to make a couple of changes. First, we are going to change the declaration of the bullets list from:

				
					[SerializeField]
    private List<GameObject> bullets;
				
			

to

				
					[SerializeField]
    private List<SpiderBullet> bullets;
				
			

The reason for this is because we are going to use the Rigidbody2D component of the bullet to make it move e.g. to shoot the bullet. We already created the ShootBullet function inside the SpiderBullet script, and in order to access that function we either need to use GetComponent, which can be performance heavy, or we can store a reference to the SpiderBullet class itself and simply access the function from the stored variable which is more efficient.

Before we proceed, inside the Start function we need to modify the code that is storing the bullet game objects in the list:

				
					private void Start()
    {
        for (int i = 0; i < initialBulletCount; i++)
        {
            // while instantiating the bullet game object also get the SpiderBullet component
            bullets.Add(Instantiate(spiderBullet).GetComponent<SpiderBullet>());
            bullets[i].gameObject.SetActive(false);
        }

        waitTime = Time.time + Random.Range(minShootWaitTime, maxShootWaitTime);
    }
				
			
Because we are storing SpiderBullet references in the list, when we create the bullet game object we need to use GetComponent to get a reference to its script and store it in the array.
 
We also set a new value for the waitTime variable to prevent the bullet shooting as soon as the game starts.
 
We also need to modify the Shoot function so that it uses the ShootBullet function from the SpiderBullet script when we want to shoot the bullet:
 
				
					 void Shoot()
    {
        canShoot = true;
        bulletIndex = 0;

        while (canShoot)
        {
            if (!bullets[bulletIndex].gameObject.activeInHierarchy)
            {
                bullets[bulletIndex].gameObject.SetActive(true);

                // set the rotation of the bullet to the rotation of the spider(parent game object)
                bullets[bulletIndex].transform.rotation = transform.rotation;
                bullets[bulletIndex].transform.position = bulletSpawnPos.position;

                // call the shoot bullet function to shoot the bullet
                bullets[bulletIndex].ShootBullet(transform.up);

                canShoot = false;
            }
            else
            {
                bulletIndex++;
            }

            if (bulletIndex == bullets.Count)
            {
                // while 
                bullets.Add(Instantiate(spiderBullet, bulletSpawnPos.position, transform.rotation).GetComponent<SpiderBullet>());

                // access the bullet we just created by subtracting 1 from
                // the total bullet count in the list
                bullets[bullets.Count - 1].ShootBullet(transform.up);

                canShoot = false;
            }
        }
    }
				
			
In the previous examples when we spawned the bullet, we used Quaternion.identity for its rotation, but this time we are using transform.rotation which is the rotation of the parent game object e.g. the spider.
 
We need to use the spider’s rotation so that the bullet is also facing the player when it gets instantiated or activated again.
 
To shoot the bullet, we simply call its ShootBullet function passing transform.up as the parameter.
 
One new line of code we added in the Shoot function is the code on line 33. If we don’t find any deactivated bullets in the game, we will create a new one. And in order to shoot the new bullet we need to access it by subtracting 1 from the total count of the bullets in the list.
 
Save the new changes we made to the script and run the game to test it out:
 
Now we have totally different AI system where the enemy is following the player’s movement and shooting bullets towards his direction. As I already mentioned, you can change the speed of the bullets to make them faster and with that make the game more challenging.
 
I will leave the new modified SpiderShooter script, as well as the SpiderBullet script down below for your reference:
 
				
					using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class SpiderShooter : MonoBehaviour
{
    [SerializeField]
    private GameObject spiderBullet;

    [SerializeField]
    private Transform bulletSpawnPos;

    [SerializeField]
    private float minShootWaitTime = 1f, maxShootWaitTime = 3f;

    private float waitTime;

    [SerializeField]
    private List<SpiderBullet> bullets;

    private bool canShoot;
    private int bulletIndex;

    [SerializeField]
    private int initialBulletCount = 2;

    private Transform playerTransform;

    private Vector3 direction;
    private float angle;

    private void Awake()
    {
        playerTransform = GameObject.FindWithTag("Player").transform;
    }

    private void Start()
    {
        for (int i = 0; i < initialBulletCount; i++)
        {
            // while instantiating the bullet game object also get the SpiderBullet component
            bullets.Add(Instantiate(spiderBullet).GetComponent<SpiderBullet>());
            bullets[i].gameObject.SetActive(false);
        }

        waitTime = Time.time + Random.Range(minShootWaitTime, maxShootWaitTime);
    }

    private void Update()
    {
        FacePlayersDirection();
    }

    void Shoot()
    {
        canShoot = true;
        bulletIndex = 0;

        while (canShoot)
        {
            if (!bullets[bulletIndex].gameObject.activeInHierarchy)
            {
                bullets[bulletIndex].gameObject.SetActive(true);

                // set the rotation of the bullet to the rotation of the spider(parent game object)
                bullets[bulletIndex].transform.rotation = transform.rotation;
                bullets[bulletIndex].transform.position = bulletSpawnPos.position;

                // call the shoot bullet function to shoot the bullet
                bullets[bulletIndex].ShootBullet(transform.up);

                canShoot = false;
            }
            else
            {
                bulletIndex++;
            }

            if (bulletIndex == bullets.Count)
            {
                // while 
                bullets.Add(Instantiate(spiderBullet, bulletSpawnPos.position, transform.rotation).GetComponent<SpiderBullet>());

                // access the bullet we just created by subtracting 1 from
                // the total bullet count in the list
                bullets[bullets.Count - 1].ShootBullet(transform.up);

                canShoot = false;
            }
        }
    }

    void FacePlayersDirection()
    {
        direction = playerTransform.position - transform.position;
        angle = Mathf.Atan2(direction.y, direction.x) * Mathf.Rad2Deg;
        transform.rotation = Quaternion.AngleAxis(angle + 90f, Vector3.forward);
        
        if (Time.time > waitTime)
        {
            waitTime = Time.time + Random.Range(minShootWaitTime, maxShootWaitTime);
            Shoot();
        }
    }

} // class
				
			
				
					using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class SpiderBullet : MonoBehaviour
{

    [SerializeField]
    private float moveSpeed = 7f;

    [SerializeField]
    private Rigidbody2D myBody;

    private void OnDisable()
    {
        myBody.velocity = Vector2.zero;
    }

    public void ShootBullet(Vector3 direction)
    {
        myBody.velocity = direction * moveSpeed;
    }

    private void OnTriggerEnter2D(Collider2D collision)
    {
        if (collision.CompareTag("Ground") || collision.CompareTag("Player"))
        {
            gameObject.SetActive(false);
        }
    }
}

				
			


Enemy Jumping AI

Moving forward, in the Assets -> Scenes folder open the scene named 2 – Basic Enemy AI Jump Attack. Inside the scene you will see a small spider in the middle, which is going to be our enemy for this example, and you see the player object.

I have prepared the animations of the spider as well as the script and I added some code that will animate him so that we don’t have to do that because that’s not the goal of this tutorial.

The idea of the Spider Jumper is to make him jump and try to kill the player while he is attempting to jump over the spider.

If you checked out the SpiderJumper script that I already prepared you will notice from the variables I’ve prepared that we are going to use a timer to make the spider jump.

But first, let us create the Jump function:

				
					void Jump()
    {
        if (canJump)
        {
            canJump = false;
            myBody.velocity = new Vector2(0f, Random.Range(minJumpForce, maxJumpForce));
        }
    }
				
			
You can declare the Jump function below the HandleAnimations function. As you can see we are using the same approach as with SpiderShooter.
 
When we jump, we set canJump to false to prevent double jump, and then we add velocity to the rigidbody’s Y axis to push it upwards.
 
You are probably wondering why didn’t we just used a timer like we did with SpiderShooter, and the reason is that we need to detect if the Spider Jumper is on the ground as well as if a certain amount of time has passed.
 
Because of that we have another function that is going to handle the jumping:
 
				
					void HandleJumping()
    {
        if (Time.time > jumpTimer)
        {
            jumpTimer = Time.time + Random.Range(minWaitTime, maxWaitTime);
            Jump();
        }

        if (myBody.velocity.magnitude == 0)
            canJump = true;

    }
				
			
Inside HandleJumping is where we are actually going to jump. If Time.time is greater than jumpTimer, then we are allowed to jump, but we also need to test if the spider is on the ground as we already mentioned which we are doing on line 9.
 
If the magnitude of the rigidbody’s velocity is 0, that means that the spider is not moving. If you don’t know what is magnitude and how it works, you can learn about that by clicking here.
 
The last step is to call the HandleJumping and HandleAnimation in the Update function:
 
				
					private void Update()
    {
        HandleJumping();
        HandleAnimations();
    }
				
			
Now run the game and let’s test it out:
 
You are probably wondering how the enemy jumping AI example is different from the enemy shooting AI example that we first introduced.
 
Well, I am glad you asked that handsome stranger.
 
This example shows you that depending on the type of the enemy, you can use your own logic to implement the enemy AI. The first enemy was shooting, which is common in a lot of games, for that enemy you implement the shooting logic.
 
The second enemy was jumping and that way preventing the player from jumping over him. The AI logic we used for this enemy is similar to the first, but in the game it looks so much different.
 
Of course, you can also implement the trigger and raycast logic for the Spider Jumper same as what we did for the Spider Shooter, but again this comes back to what are you trying to achieve in your game and what is the outcome you are looking for.
 
I will leave the full version of the SpiderJumper script down below for your reference:
 
				
					using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class SpiderJumper : MonoBehaviour
{

    private Rigidbody2D myBody;
    private Animator anim;

    [SerializeField]
    private float minJumpForce = 5f, maxJumpForce = 12f;

    [SerializeField]
    private float minWaitTime = 1.5f, maxWaitTime = 3.5f;

    private float jumpTimer;

    private bool canJump;

    private void Awake()
    {
        anim = GetComponent<Animator>();
        myBody = GetComponent<Rigidbody2D>();
    }

    private void Start()
    {
        jumpTimer = Time.time + Random.Range(minWaitTime, maxWaitTime);
    }

    private void Update()
    {
        HandleJumping();
        HandleAnimations();
    }

    void HandleAnimations()
    {
        if (myBody.velocity.magnitude == 0)
            anim.SetBool("Jump", false);
        else
            anim.SetBool("Jump", true);
    }

    void Jump()
    {
        if (canJump)
        {
            canJump = false;
            myBody.velocity = new Vector2(0f, Random.Range(minJumpForce, maxJumpForce));
        }
    }

    void HandleJumping()
    {
        if (Time.time > jumpTimer)
        {
            jumpTimer = Time.time + Random.Range(minWaitTime, maxWaitTime);
            Jump();
        }

        if (myBody.velocity.magnitude == 0)
            canJump = true;
    }

} // class
				
			


Enemy AI Movement

All examples we saw so far are using static enemies e.g. enemies that don’t move and only shoot or perform another form of attack.

For this example we are going to see how we can make the enemy move around the level.

Inside the Assets -> Scenes folder, open the scene named 3 – Basic And Intermediate Enemy AI Point A To Point B Movement.

Inside you will see a prepared level and a prepared enemy. I have also prepared a script called GuardPointToPoint and attached it on the enemy object.

When you open the GuardPointToPoint script in visual studio you will see that I’ve already prepared a few variables and one function that will animate the enemy.

To make the movement work, we need to add a few more variables. Above the Awake function, add the following lines of code:

				
					[SerializeField]
    private Transform[] movementPoints;

    private Vector2 currentMovementPoint;

    private int currentMovementPointIndex, previousMovementPointIndex;

    [SerializeField]
    private float moveSpeed = 2f;
				
			
The movementPoints variable declared on line 2 is an array of Transforms which will be the movement positions laid out in the level where the enemy can move.
 
The currentMovementPoint is the position where we are currently moving towards.
 
The currentMovementPointIndex is going to store the index of the current position from the movementPoints array. In regards to previousMovementPointIndex we are going to compare it to the currentMovementPointIndex every time we want to change the movement position to make sure that we don’t get the same movement position.
 
And the moveSpeed variable is self explanatory.
 
First we are going to create a function that is going to handle the movement of the enemy:
 
				
					void MoveToTarget()
    {
        transform.position =
            Vector2.MoveTowards(transform.position, currentMovementPoint, Time.deltaTime * moveSpeed);

        if (Vector2.Distance(transform.position, currentMovementPoint) < 0.1f)
        {
            
        }

        AnimateMovement(true);
    }
				
			
We are using Vector2.MoveTowards to make the enemy move towards the currentMovementPoint e.g. the current destination the enemy is moving towards.
 
The function requires three parameters. The first parameter is the origin e.g. the starting position, the second parameter is the target position e.g. where we are going to, and the third parameter is the speed by which we are going to get to the target position.
 
Next, we use the Vector2.Distance function to calculate the distance between two points. And we are calculating the distance between the current position of the enemy and the currentMovementPoint e.g. our destination.
 
If that value is less than 0.1 that means the enemy has reached the target point and now we should change the currentMovementPoint to a new position in the game.
 
To do this, we need to create a new function:
 
				
					void SetMovementPointTarget()
    {
        while (true)
        {

            currentMovementPointIndex = Random.Range(0, movementPoints.Length);

            if (currentMovementPointIndex != previousMovementPointIndex)
            {
                previousMovementPointIndex = currentMovementPointIndex;
                currentMovementPoint = movementPoints[currentMovementPointIndex].position;
                break;
            }
        }
    }
				
			
Again, we are using the while loop but this time we passed true as the parameter which means the loop will run forever. However, we have a condition where we will break out of the loop.
 
That condition is if the currentMovementPointIndex is not equal to the previousMovementPointIndex, which means we don’t want to get the same movement position as the previous one.
 
Let’s say the value of previousMovementPointIndex is 3, and we try to randomize the value of currentMovementPointIndex on line 6 above, if the Random.Range returns the value of 3, then we would get the same position, because of that we have the condition test on line 8.
 
If that condition is not true, then the loop will run again, randomizing the value of currentMovementPoint again, and it will continue to do so until the condition on line 8 is true which means the currentMovementPointIndex is not equal to previousMovementPointIndex.
 
When that happens we will set the previousMovementPointIndex to be equal to currentMovementPointIndex so that we can test again for the same condition on line 8 and make sure we don’t get the same movement position.
 
Next, we set the currentMovementPoint to position of the element at currentMovementPointIndex in the movementPoints array. And after that we use the break keyword to break out of the while loop.
 
To make this work, we need to call the SetMovementPointTarget in the Start function to set the first initial movement point:
 
				
					private void Start()
    {
        SetMovementPointTarget();
    }
				
			
And we need to call the same function inside the MoveToTarget when we test if the distance between the current enemy position and the currentMovementPoint is less than 0.1 so that we can set a new movement point when we reach the previous one:
 
				
					void MoveToTarget()
    {
        transform.position =
            Vector2.MoveTowards(transform.position, currentMovementPoint, Time.deltaTime * moveSpeed);

        if (Vector2.Distance(transform.position, currentMovementPoint) < 0.1f)
        {
           // set the new movement point 
           SetMovementPointTarget();
        }

        AnimateMovement(true);
    }
				
			

One more thing we need to do is add the code that will handle the facing direction depending on where the enemy is going:

				
					void HandleFacingDirection()
    {
        tempScale = transform.localScale;

        if (transform.position.x > currentMovementPoint.x)
        {
            tempScale.x = Mathf.Abs(tempScale.x);
        }
        else if (transform.position.x < currentMovementPoint.x)
        {
            tempScale.x = -Mathf.Abs(tempScale.x);
        }

        transform.localScale = tempScale;
    }
				
			

The logic is simple, based on the position X of the enemy and the current movement point, we will change the facing direction.

When the enemy’s position X is greater than the position X of the current movement point that means that the player is on the right side and he is going towards the left side. In that case we set the Scale X to a positive number, because by default how the enemy sprite is created he is facing the left side.

And if the position X of the enemy is less than the position X of the current movement point, then we set the Scale X to negative value to change the facing direction of the enemy.

To make this work, we are going to call the HandleFacingDirection in the Update function:

				
					private void Update()
    {
        MoveToTarget();
        HandleFacingDirection();
    }
				
			
Before we can test this out, we need to create empty game objects and position them in the level where we want the enemy to move:
 
Img 13
When we are done with that, we need to add the movement points in the movement points array we created in the GuardPointToPoint script:
 
Img 14

I’ve attached the points from the Hierarchy tab in the Movement Points slots in the GuardPointToPoint script, but you can also tag the movement points and get a reference to them from code by using their tag.

Run the game and let’s test it out:

As soon as the enemy reaches the end of the current movement point a new one is calculated and the enemy starts moving towards that movement point.
 
What’s really smart about this set up is that we can have as many movement points as we want, we just need to add them in the movement points array and then the script will one movement point at random and make the enemy move towards it.
 
I will leave the full GuardPointToPoint script down below as a reference:
 
				
					using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class GuardPointToPoint : MonoBehaviour
{

    private Animator anim;

    private Vector3 tempScale;

    [SerializeField]
    private Transform[] movementPoints;

    private Vector2 currentMovementPoint;

    private int currentMovementPointIndex, previousMovementPointIndex;

    [SerializeField]
    private float moveSpeed = 2f;

    private void Awake()
    {
        anim = GetComponent<Animator>();
    }

    private void Start()
    {
        SetMovementPointTarget();
    }

    private void Update()
    {
        MoveToTarget();
        HandleFacingDirection();
    }

    void AnimateMovement(bool walk)
    {
        anim.SetBool("Walk", walk);
    }

    void MoveToTarget()
    {
        transform.position =
            Vector2.MoveTowards(transform.position, currentMovementPoint, Time.deltaTime * moveSpeed);

        if (Vector2.Distance(transform.position, currentMovementPoint) < 0.1f)
        {
            SetMovementPointTarget();
        }

        AnimateMovement();
    }

    void SetMovementPointTarget()
    {
        while (true)
        {

            currentMovementPointIndex = Random.Range(0, movementPoints.Length);

            if (currentMovementPointIndex != previousMovementPointIndex)
            {
                previousMovementPointIndex = currentMovementPointIndex;
                currentMovementPoint = movementPoints[currentMovementPointIndex].position;
                break;
            }
        }
    }

    void HandleFacingDirection()
    {
        tempScale = transform.localScale;

        if (transform.position.x > currentMovementPoint.x)
        {
            tempScale.x = Mathf.Abs(tempScale.x);
        }
        else if (transform.position.x < currentMovementPoint.x)
        {
            tempScale.x = -Mathf.Abs(tempScale.x);
        }

        transform.localScale = tempScale;
    }

} // class

				
			


Moving Towards Player Target

In the example we created, the enemy is moving from one point to another and this behaviour can be good if you want to have a patrolling enemy, or if you have a part of your level where the player needs to pass and you have an enemy that moves in that part of the level and so on.
 
But how can we make the enemy patrol between points, and when he detects the player he moves towards the player?
 
There are, of course, multiple ways how we can achieve this. One of the ways is to detect the player using a collider, and when that happens switch the moving target towards the player object.
 
In the Hierarchy tab select the Guard enemy game object, and change the size of its collider and also make the collider a trigger:
 
Img 15
Next, in the GuardPointToPoint script, above the Awake function add the following variable:
 
				
					private bool chasePlayer;
				
			

In the OnTriggerEnter2D function, we are going to detect when we collide the player and make the enemy move towards the player, and in the OnTriggerExit2D function we will detect when the player leaves the bounds of the enemy’s collider and we will stop chasing him:

				
					private void OnTriggerEnter2D(Collider2D collision)
    {
        if (collision.CompareTag("Player"))
        {
            chasePlayer = true;
            currentMovementPoint = collision.gameObject.transform.position;
        }
    }

    private void OnTriggerExit2D(Collider2D collision)
    {
        if (collision.CompareTag("Player"))
        {
            chasePlayer = false;
            SetMovementPointTarget();
        }
    }
				
			

In OnTriggerEnter2D we compare the tag of the game object the enemy has collided with, and if it’s equal to the Player tag, then we will set the currentMovementPoint to the position of the player.

In OnTriggerExit2D we do the same thing, except that this time we call the SetMovementPointTarget function to set a new movement point for the enemy.

As for the chasePlayer variable, we are going to use to determine if the enemy should move towards the player game object or towards one of the movement points laid out in the level, and we are going to do that in the Update function:

				
					private void Update()
    {
        if (chasePlayer)
            MoveToPlayer();
        else
            MoveToTarget();

        HandleFacingDirection();
    }
				
			
Run the game and let’s test it out:
 

I’ve moved the player outside the bounds of the camera on purpose so that we can see what is happening from the Scene tab. As soon as the enemy detected the player with its collider, it started moving towards him.

Of course, in this example, I only coded the movement AI, but depending on your game, the enemy will deal damage to the player when it reaches its destination.

You can do this by calling the attack animation, or you can create a child game object for the enemy and attach a collider and a damage script on it, and when the child object collides with the player it will deal damage to him and so on.

I will leave the new modified GuardPointToPoint script down below as a reference:

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

public class GuardPointToPoint : MonoBehaviour
{

    private Animator anim;

    private Vector3 tempScale;

    [SerializeField]
    private Transform[] movementPoints;

    private Vector2 currentMovementPoint;

    private int currentMovementPointIndex, previousMovementPointIndex;

    [SerializeField]
    private float moveSpeed = 2f;

    private bool chasePlayer;

    private void Awake()
    {
        anim = GetComponent<Animator>();
    }

    private void Start()
    {
        SetMovementPointTarget();
    }

    private void Update()
    {
        if (chasePlayer)
            MoveToPlayer();
        else
            MoveToTarget();

        HandleFacingDirection();
    }

    void AnimateMovement(bool walk)
    {
        anim.SetBool("Walk", walk);
    }

    void MoveToTarget()
    {

        transform.position =
            Vector2.MoveTowards(transform.position, currentMovementPoint, Time.deltaTime * moveSpeed);

        if (Vector2.Distance(transform.position, currentMovementPoint) < 0.1f)
        {
            SetMovementPointTarget();
        }

        AnimateMovement(true);
    }

    void SetMovementPointTarget()
    {
        while (true)
        {

            currentMovementPointIndex = Random.Range(0, movementPoints.Length);

            if (currentMovementPointIndex != previousMovementPointIndex)
            {
                previousMovementPointIndex = currentMovementPointIndex;
                currentMovementPoint = movementPoints[currentMovementPointIndex].position;
                break;
            }
        }
    }

    void HandleFacingDirection()
    {
        tempScale = transform.localScale;

        if (transform.position.x > currentMovementPoint.x)
        {
            tempScale.x = Mathf.Abs(tempScale.x);
        }
        else if (transform.position.x < currentMovementPoint.x)
        {
            tempScale.x = -Mathf.Abs(tempScale.x);
        }

        transform.localScale = tempScale;
    }

    void MoveToPlayer()
    {
        transform.position =
            Vector2.MoveTowards(transform.position, currentMovementPoint, Time.deltaTime * moveSpeed);

        if (Vector2.Distance(transform.position, currentMovementPoint) < 0.1f)
        {
            AnimateMovement(false);
        }
        else
        {
            AnimateMovement(true);
        }
        
    }

    private void OnTriggerEnter2D(Collider2D collision)
    {
        if (collision.CompareTag("Player"))
        {
            chasePlayer = true;
            currentMovementPoint = collision.gameObject.transform.position;
        }
    }

    private void OnTriggerExit2D(Collider2D collision)
    {
        if (collision.CompareTag("Player"))
        {
            chasePlayer = false;
            SetMovementPointTarget();
        }
    }

} // class

				
			


Creating Enemy AI Behaviour With Raycasts

Another way how we can create enemy AI behaviour is using raycasts. We already saw raycasts and we used them to detect the collision with the player.
 
We can apply the same principles in this case, but instead of making the enemy attack the player, we are going to make the enemy move.
 
First, in the Assets -> Scenes folder, open the scene named 4 – Intermediate Enemy AI Raycast Movement.
 
Inside I’ve already prepared the enemy and the platform he will be moving on. I also created EnemyRaycast script and attached it on the enemy.
 
As always, we need to add the variables we need to make the raycast movement logic work. Open the EnemyRaycast script and above the Start function add the following variables:
 
				
					[SerializeField]
    private float moveSpeed = 2.5f;

    private Vector3 tempPos;
    private Vector3 tempScale;

    private bool moveLeft;

    [SerializeField]
    private LayerMask groundLayer;

    private Transform groundCheckPos;

    private RaycastHit2D groundHit;
				
			

When it comes to the movement, the variables that we will use and how we will use them is very similar no matter if the movement is done using physics, Transform component, raycasts and so on.

Because of that, from the name of the variables you can already assume for what we are going to use every variable that we declared.

First, change the Start function to Awake, and add the following lines of code:

				
					private void Awake()
    {
        groundCheckPos = transform.GetChild(0).transform;

        moveLeft = Random.Range(0, 2) > 0 ? true : false;
    }
				
			
Since we are using raycasting, we will need the origin position where the ray is will start, for that we are using the groundCheckPos variable.
 
And we get a reference to the groundCheckPositionVariable from the child game object that we will attach to the enemy object.
 
For that we used the GetChild function from the transform, which will get the child at the specified index, and since we only have one child game object for the enemy we passed 0 as the parameter.
 
As for the moveLeft variable, we will use it to determine the movement direction of the enemy. And when the game starts we will randomize it so that sometimes the enemy will start by moving to the left side, and sometimes it will start by moving to the right side.
 
To move the enemy we are going to create a function that will handle that for us:
 
				
					void HandleMovement()
    {
        tempPos = transform.position;
        tempScale = transform.localScale;

        if (moveLeft)
        {
            tempPos.x -= moveSpeed * Time.deltaTime;
            tempScale.x = -Mathf.Abs(tempScale.x);
        }
        else
        {
            tempPos.x += moveSpeed * Time.deltaTime;
            tempScale.x = Mathf.Abs(tempScale.x);
        }

        transform.position = tempPos;
        transform.localScale = tempScale;
    }
				
			
First we get the current position and the scale of the enemy, then if we are moving to the left side, which we control with the moveLeft variable, then we will subtract from the enemy’s current position moveSpeed multiplied with Time.deltaTime to smooth out the movement.
 
We will also change the Scale of the enemy to make it face the moving direction. Since the left side is the negative side in Unity, we set the Scale X value to negative.
 
And if we are moving to the right side are doing the opposite of what we did for the left side. Then we assign back the modified values to the transform.position and transform.localScale.
 
The idea with the raycasting is to create a ray that will check if the enemy is on the ground, if the ray doesn’t detect that the enemy is on the ground, it will change the movement direction from left to right and vice-versa.
 
To do this, we are going to create a function:
 
				
					void CheckForGround()
    {
        groundHit = Physics2D.Raycast(groundCheckPos.position, Vector2.down, 0.5f, groundLayer);

        if (!groundHit)
            moveLeft = !moveLeft;

        Debug.DrawRay(groundCheckPos.position,
            Vector2.down * 0.5f, Color.red);
    }
				
			
We are using the Raycast function from the Physics2D class to cast a ray. The first parameter in the function is the starting point of the ray, the second parameter is the direction where the ray will go, the third parameter is the length of the ray and the fourth parameter is the layer mask on which we are checking the collision.
 
We are storing the returned value in the groundHit variable that we declared, and then we are checking if the the groundHit is null. There are two ways to perform this check, one of them is this:
 
				
					if (!groundHit)
{
}
				
			
and the other is this:
 
				
					if (groundHit == null)
{
}
				
			

Both checks perform one and the same test. This means if the ray doesn’t collide with the ground anymore, then change the moving direction and we do that by setting the moveLeft variable to the opposite of its current value.

We know that the exclamation mark makes what’s after it the opposite, so if the current value of moveLeft is true, and when we type:

				
					moveLeft = !moveLeft;
				
			
that means that we are setting the moveLeft to the opposite of the value true, which is false.
 
At the end, on line 8 we are using Debug.DrawRay so that we can see the ray in the game when we test it.
 
Before we move in the editor, make sure that you call the HandleMovement and CheckForGround functions in the Update function:
 
				
					private void Update()
    {
        HandleMovement();
        CheckForGround();
    }
				
			

Going in the Unity editor, there are couple of things we need to do. First, create an empty child object for the Zombie Enemy and set its position to the following values:

Img 16
Next, create a new layer and name it Ground, then select the Zombie Enemy game object, and for the Ground Layer in the Enemy Raycast script select the Ground:
 
Img 17

I’ve already set the Layer for the Platform game object to Ground, so you don’t need to do that. Now run the game and let’s test it out:

Again, depending on the needs of your game and what type of enemies you have, you can always tweak the logic to your own needs and make the enemy detect and attack the player using raycasting, or make the enemy shoot using raycasting as we already saw, and so on.
 
I will leave the full EnemyRaycast script below as a reference:
 
				
					using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class EnemyRaycast : MonoBehaviour
{
    [SerializeField]
    private float moveSpeed = 2.5f;

    private Vector3 tempPos;
    private Vector3 tempScale;

    private bool moveLeft;

    [SerializeField]
    private LayerMask groundLayer;

    private Transform groundCheckPos;

    private RaycastHit2D groundHit;

    private void Awake()
    {
        groundCheckPos = transform.GetChild(0).transform;

        moveLeft = Random.Range(0, 2) > 0 ? true : false;
    }

    private void Update()
    {
        HandleMovement();
        CheckForGround();
    }

    void HandleMovement()
    {
        tempPos = transform.position;
        tempScale = transform.localScale;

        if (moveLeft)
        {
            tempPos.x -= moveSpeed * Time.deltaTime;
            tempScale.x = -Mathf.Abs(tempScale.x);
        }
        else
        {
            tempPos.x += moveSpeed * Time.deltaTime;
            tempScale.x = Mathf.Abs(tempScale.x);
        }

        transform.position = tempPos;
        transform.localScale = tempScale;
    }

    void CheckForGround()
    {
        groundHit = Physics2D.Raycast(groundCheckPos.position, Vector2.down, 0.5f, groundLayer);

        if (!groundHit)
            moveLeft = !moveLeft;

        Debug.DrawRay(groundCheckPos.position,
            Vector2.down * 0.5f, Color.red);
    }
}
				
			


NavMesh Enemy AI

So far we covered 2D enemy AI examples. Now we are moving to 3D and we are going to see how to create 3D AI enemy behaviour.
 
Before we begin, I want to say that everything what we did so far is also applicable to 3D games as well. Of course, we would need to do a little tweaking to the scripts to make the game object move in 3D space, but the point is that we can use the exact same techniques.
 

In this part of the tutorial however, we are going to use a different way of creating enemy AI with the help of Unity’s NavMesh system.

First, in the Assets -> Scenes folder, open the 5 – Intermediate Enemy AI Navigation Movement scene.

If you see the level in 2D view, you can switch it to 3D view by clicking on the 2D button right below the Scene tab:
 
Img 18
So what is a NavMesh?
 
NavMesh is Unity’s navigation system that allows us to “bake” the game level, or level parts, which essentially represents the process of calculating the level surface to detect where are the walkable areas for that level.
 
Of course, there are multiple parameters that we can tweak in order to make sure that every higher surface on the level is not walkable, or to detect where are the wholes or gaps in the level where the player can’t walk on and so on.
 
To bake the level, we need to open the Navigation tab which is located under Window -> AI -> Navigation:
 
Img 19

To bake the Ground we have in the level, first we need to make it Navigation Static. This means that we tell Unity that this object can be navigated.

To do this, select the ground object in the Hierarchy tab, and in the Inspector tab click on the drop down list where it says Static, and from the list select Navigation Static:

Img 20

Next, in the Navigation tab, click on the Bake tab, and then click the Bake button:

Img 21

When you finish, you will notice that the Ground object has a blue color on it, if you don’t see it, make sure that you opened the Navigation tab and that the Show NavMesh checkbox is checked:

Img 22

Before we can make our player game object move, we need to attach the Nav Mesh Agent component on him. Select the player game object in the Hierarchy, and in the Inspector tab click on the Add Component button and filter for nav mesh agent and attach it on the player:

Img 23
Now open the EnemyNavigation script, and above the class declaration add the following line:
 
				
					using UnityEngine.AI;
				
			
In order to access the NavMesh system, we need to import the AI library from Unity.
 
Moving forward, above the Start function, add the following lines:
 
				
					private Animator anim;

    private NavMeshAgent navAgent;

    [SerializeField]
    private Transform destination;
				
			
To make the player object move, we need a reference to his NavMeshAgent, and the destination variable is going to be the position where we are going.
 
In the Awake function get a reference to the variables and set the destination where the NavMeshAgent will move:
 
				
					private void Awake()
    {
        anim = GetComponent<Animator>();
        navAgent = GetComponent<NavMeshAgent>();

        navAgent.SetDestination(destination.position);
    }
				
			

To make the agent move, we only need to call its SetDestination function and pass the destination position, and since we have baked the level, the agent will calculate the shortest path it takes for it to reach the destination and move towards it.

Let us also animate the player’s movement:

				
					private void Update()
    {
        AnimatePlayer();
    }

    void AnimatePlayer()
    {
        if (navAgent.velocity.magnitude > 0)
        {
            anim.SetBool("Walk", true);
        }
        else
            anim.SetBool("Walk", false);
    }
				
			
You will notice that the nav agent has a velocity property on line 8. We can use that to test the speed of the agent. If the magnitude of its velocity is greater than 0, that means the agent is moving, and if its equal to zero then the agent is not moving. We can use this to animate the player’s movement.
 
Before we test our game, go inside the editor, create an empty game object, name it Destination and position it anywhere in the level, but keep it within the bounds of the Ground game object so that the nav agent will be able to reach that position.
 
Next, select the player game object, and drag the Destination game object in the Destination empty field for the Enemy Navigation script:
 
Img 24
Let us now run the game and test it out:
 
Since we set the destination for the agent in the Awake function, as soon as we run the game, the agent started moving towards the destination. And since the agent was moving, we also animated the player with the help of the velocity property of the agent.
 
As soon as the agent reached the destination it stopped moving, which is the default behaviour of the agent component. If we want the agent to move to another destination then we need to call SetDestination function and provide the new destination.
 

Changing Agent's Destinations In The Level

To make the nav agent move from one point to another point we can use a similar technique like we did in the Guard enemy example.
 
For that, we need to remove the destination variable, and instead of it, declare the following variables above the Awake function:
 
				
					[SerializeField]
    private Transform[] destinationPoints;

    private Vector3 currentDestination;

    private int destinationIndex;
				
			
In the Awake function we are also going to change the code that will set the destination for the nav agent:
 
				
					private void Awake()
    {
        anim = GetComponent<Animator>();
        navAgent = GetComponent<NavMeshAgent>();

        // get the current desination and make the agent
        // go towards that destination
        currentDestination = destinationPoints[Random.Range(0, destinationPoints.Length)].position;
        navAgent.SetDestination(currentDestination);
    }
				
			
To set a new destination for the agent, we are going to create a function similar to the one we created in the Guard enemy example:
 
				
					void SetNewDestination()
    {
        while (true)
        {
            destinationIndex = Random.Range(0, destinationPoints.Length);

            if (currentDestination != destinationPoints[destinationIndex].position)
            {
                currentDestination = destinationPoints[destinationIndex].position;
                navAgent.SetDestination(currentDestination);
                break;
            }

        }
    }
				
			
Again, using the power of the while loop we are going to pick a random destination from the destinationPoints array, if the random destination is not equal to the current destination, that means we have a new destination for the agent, and then we will break out of the while loop.
 
But before we can set a new destination, we need to make sure that the agent has reached its current destination:
 
				
					void CheckIfAgentReachedDestination()
    {
        // agent is not moving
        if (navAgent.velocity.magnitude == 0f)
        {
            SetNewDestination();
        }
    }        
				
			
We are using the magnitude of the agent’s velocity to check if the agent is moving, if the magnitude is equal to 0, the agent is not moving which means he reached his destination.
 
Before we go in the editor to create the destination points, call the CheckIfAgentReachedDestination function in the Update function:
 
				
					private void Update()
    {
        AnimatePlayer();
        CheckIfAgentReachedDestination();
    }
				
			
In the editor, make sure that you create a few empty game objects and lay them out in the scene but within the bounds of the level so that the agent will be able to reach them.
 
When you are done with that, attach those empty game objects in the Destination Points array in the Inspector tab for the Enemy Navigation script:
 
Img 25

Run the game and let’s test it out:

Every time the agent reaches his current destination, we set a new one for him. Now there are multiple ways how we can check if the agent reached his destination.

In our current example we are using the magnitude of the agent’s velocity. We can also check the remaining distance the agent has left:

				
					void CheckIfAgentReachedDestination()
    {
        if (navAgent.remainingDistance <= navAgent.stoppingDistance)
        {
            SetNewDestination();
        }
    }
				
			

The remainingDistance will returna float value representing the distance that is remaining until the agent reaches its destination. The stoppingDistance represents the distance between the agent and its destination when the agent will stop moving.

We can set that value in the Nav Mesh Agent component:

Img 26

The current value is set to 0, this means the that stopping distance from the destination is 0, but if we set the value to 5 for example, this would mean that the nav agent will stop when the distance between him and the destination is equal to 5.

We can also use the pathPending property to test if the path is currently in the process of being computed e.g. we provided the path and the agent is calculating how to reach it, and we can use hasPath to test if the agent currently has a path that’s in the process e.g. the agent has a destination to be reached:

				
					void CheckIfAgentReachedDestination()
    {
        // agent is not moving
        if (!navAgent.pathPending)
        {
            if (navAgent.remainingDistance <= navAgent.stoppingDistance)
            {
                if (!navAgent.hasPath || navAgent.velocity.sqrMagnitude == 0f)
                {
                    SetNewDestination();
                }
            }
        }
    }
				
			

The reason why you might want to perform these tests is because a distance check isn’t always accurate since the steering behavior portion of the NavMeshAgent may actually still be working even if the distance is less than the stopping distance, so keep that in mind.

I will leave the full version of the EnemyNavigation script below as a reference:

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

public class EnemyNavigation : MonoBehaviour
{

    private Animator anim;

    private NavMeshAgent navAgent;

    [SerializeField]
    private Transform[] destinationPoints;

    private Vector3 currentDestination;

    private int destinationIndex;

    private void Awake()
    {
        anim = GetComponent<Animator>();
        navAgent = GetComponent<NavMeshAgent>();

        // get the current desination and make the agent
        // go towards that destination
        currentDestination = destinationPoints[Random.Range(0, destinationPoints.Length)].position;
        navAgent.SetDestination(currentDestination);
    }

    private void Update()
    {
        AnimatePlayer();
        CheckIfAgentReachedDestination();
    }

    void AnimatePlayer()
    {
        if (navAgent.velocity.magnitude > 0)
        {
            anim.SetBool("Walk", true);
        }
        else
            anim.SetBool("Walk", false);
    }

    void SetNewDestination()
    {
        while (true)
        {
            destinationIndex = Random.Range(0, destinationPoints.Length);

            if (currentDestination != destinationPoints[destinationIndex].position)
            {
                currentDestination = destinationPoints[destinationIndex].position;
                navAgent.SetDestination(currentDestination);
                break;
            }

        }
    }

    void CheckIfAgentReachedDestination()
    {
        // agent is not moving
        //if (navAgent.velocity.magnitude == 0f)
        //{
        //    SetNewDestination();
        //}

        //if (navAgent.remainingDistance <= navAgent.stoppingDistance)
        //{
        //    SetNewDestination();
        //}

        if (!navAgent.pathPending)
        {
            if (navAgent.remainingDistance <= navAgent.stoppingDistance)
            {
                if (!navAgent.hasPath || navAgent.velocity.sqrMagnitude == 0f)
                {
                    SetNewDestination();
                }
            }
        }
    }

} // class

				
			


Setting A Random Destination Within The Baked Area

We can also generate a random destination for the agent within the baked area of the level.

Remove all the current code from the EnemyNavigation script except for the Animator variable and the AnimatePlayer function, and then add the following variables:

				
					    private NavMeshAgent navAgent;

    private NavMeshHit navHit;

    private Vector3 currentDestination;

    [SerializeField]
    private float maxWalkDistance = 50f;
				
			

The variables are pretty much the same except for the NavMeshHit which represents result information for any queries we perform on the NavMesh. In the Awake function we are going to get the reference we need:

				
					private void Awake()
    {
        anim = GetComponent<Animator>();
        navAgent = GetComponent<NavMeshAgent>();

        SetNewDestination();
    }
				
			

Again we are going to use SetNewDestination function to set a new destination for the nav agent, but this time we are going to take a different approach:

				
					void SetNewDestination()
    {
        while (true)
        {
            NavMesh.SamplePosition(((Random.insideUnitSphere * maxWalkDistance) + transform.position),
                out navHit, maxWalkDistance, -1);

            if (currentDestination != navHit.position)
            {
                currentDestination = navHit.position;
                navAgent.SetDestination(currentDestination);
                break;
            }
        }
    }
				
			

The SamplePosition function of the NavMesh class will find the nearest point based on the NavMesh within a specified range.

The first parameter represents the sourcePosition e.g. the starting position from which we are going to get a random point.

To calculate that position, we are using Random.insideUnitySphere which will return a random point inside or on a sphere with a radius of 1.0. We multiply that value with the maxWalkDistance variable because that is the max amount of distance where the agent can walk, and then we add the current position of the enemy to that value.

This means, that we are going to search for a point within the nav mesh baked area from the position of the agent e.g. the enemy, with the radius of insideUnitySphere multiplied with the maxWalkDistance.

Then we pass the navHit variable where the SamplePosition will store the information that will be returned. The out keyword used before the navHit variable means that we are passing the navHit variable as a reference to this function.

If you don’t know what is a reference and what does that mean, you can learn about that concept by clicking here.

The next parameter represents the distance from the sourcePosition parameter in which we are going to search for the random point.

And the last parameter is a LayerMask variable that represents the layer of the game object on which we perform this check. By passing -1 we are including all layers, but if you want to check for the nav point only on a specific nav area, you can use layers for that.

After that we are checking if the currentDestination is not equal to the new random destination we generated with the SamplePosition function, and we are using the navHit variable for that.
 
As I already said, the navHit variable will have all the information in regards to the SamplePosition function, so we can access the random position by calling the position property of the navHit variable.
 
If we have a new position, then we will set the currentDestination to that position and pass that value to the SetDestination function of the nav agent.
 
We are going to reuse the function from the previous example where we checked if the agent has reached its destination:
 
				
					void CheckIfAgentReachedDestination()
    { 
        if (!navAgent.pathPending)
        {
            if (navAgent.remainingDistance <= navAgent.stoppingDistance)
            {
                if (!navAgent.hasPath || navAgent.velocity.sqrMagnitude == 0f)
                {
                    SetNewDestination();
                }
            }
        }
    }
				
			
And as a last step, we need to call the CheckIfAgentReachedDestination in the Update function:
 
				
					private void Update()
    {
        AnimatePlayer();
        CheckIfAgentReachedDestination();
    }
				
			

Run the game and test it out:

I will leave the new version of the EnemyNavigation script below as a reference:

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

public class EnemyNavigation : MonoBehaviour
{

    private Animator anim;

    private NavMeshAgent navAgent;

    private NavMeshHit navHit;

    private Vector3 currentDestination;

    [SerializeField]
    private float maxWalkDistance = 50f;

    private void Awake()
    {
        anim = GetComponent<Animator>();
        navAgent = GetComponent<NavMeshAgent>();

        SetNewDestination();
    }

    private void Update()
    {
        AnimatePlayer();
        CheckIfAgentReachedDestination();
    }

    void AnimatePlayer()
    {
        if (navAgent.velocity.magnitude > 0)
        {
            anim.SetBool("Walk", true);
        }
        else
            anim.SetBool("Walk", false);
    }

    void SetNewDestination()
    {
        while (true)
        {
            NavMesh.SamplePosition(((Random.insideUnitSphere * maxWalkDistance) + transform.position),
                out navHit, maxWalkDistance, -1);

            if (currentDestination != navHit.position)
            {
                currentDestination = navHit.position;
                navAgent.SetDestination(currentDestination);
                break;
            }
        }
    }

    void CheckIfAgentReachedDestination()
    { 
        if (!navAgent.pathPending)
        {
            if (navAgent.remainingDistance <= navAgent.stoppingDistance)
            {
                if (!navAgent.hasPath || navAgent.velocity.sqrMagnitude == 0f)
                {
                    SetNewDestination();
                }
            }
        }
    }

} // class

				
			


Nav Mesh Agent Speed, Rotation, Stopping Distance And Other Settings

While testing the examples we created so far, you probably noticed that the enemy object was rotating in a weird way, and you also noticed that he was moving in a certain speed.

But we didn’t define any of those settings, so how come is the enemy moving without us doing anything on that part?

Well, as you can already assume the movement and everything related to it is controlled and performed by the Nav Mesh Agent.

We can, of course, edit those settings. We can change the speed of the agent, we can also change the speed by which he will rotate, we can even change the stopping distance length from the agents destination and we talked briefly about it.

All of these settings are located on the Nav Mesh Agent component itself:

Img 27

The first option on the nav agent represents the agent’s type. The agent type is controller in the Agents tab in the Navigation settings:

Img 28

As you can see the agent type has properties that we can change, and these properties affect how the nav mesh is built e.g. how the navigation area will be baked.

We can click on the + button to create a new agent type:

Img 29

We can now create a new agent type by changing the values for the settings.

The name represents the agent type name.

Radius is the radius of the agent type. If we create an agent type for a lion, then we would set the radius to a larger value because the lion is a large animal.

The height represents the height of the agent type, so if we have a tall game character like a dragon for example, we would set that value to a higher number.

Step height represents the height of the each step the agent will make. If we have a dragon in the game, then the step height would be pretty large.

And the max slope represents how well the agent will climb angled surfaces. Depending on the type of the agent you will adjust this value accordingly.

I am going to create a sample dragon agent type:

Img 30

Now that we have two agent types, we can click on the drop down list for the Agent Type property on the Nav Mesh Agent and select our desired type:

Img 31
Of course, we would need to rebake the level based on the type of the agent we have selected to enable that particular agent to walk and navigate in the level, so keep that in mind.
 
In regards to other options they are straight forward and self explanatory. You can even hover over every individual option and see the explanation for what it is used.
 
I am going to change the value of the angular speed property which will make the agent rotate faster:
 
Img 32

Now when the enemy turns to go in another direction in the game it will look more natural.

Run the game and let’s test it out:

You can play with the other settings on your own such as the Speed which will make the agent move faster, or the Acceleration which will make the agent’s starting speed faster and so on.
 

Navigating Between Obstacles

So far we only made the agent move in a empty level so to say. With this I mean that we didn’t have any obstacles in the level like walls.
 
Because of that, in this part of the tutorial we are going to take a look at how can we make the nav agent navigate between obstacles in the level
 
I am going to reuse the script we created in the Changing The Agent’s Destinations In The Level part of the tutorial, so make sure that you copy the whole script.
 
Now going in the editor, in the Hierarchy tab you will see a game object called Obstacles. The game object is not active in the game so make sure that you activate it and when you do you will see the obstacle child objects laid out in the level:
 
Img 33

Before we can bake the level with the obstacles, we need to make the obstacles Navigation Static. Select every obstacle in the Hierarchy tab, and in the Inspector tab click on the Static drop down list and make them Navigation Static:

Img 34
Now that we made the obstacles Navigation Static, we can go in the Navigation -> Bake tab and click the Bake button. When you do that, you will notice that around the obstacle objects there is no blue color when you have the Show NavMesh checkbox checked:
 
Img 35

This means that the obstacles are counted as such, and because of that they are not counted as a walkable area in the level, that is why we don’t see the blue color around them.

Of course, by changing the settings values, like the Step Height settings which determines the height the agent can climb on:

Img 36

and after that when we bake the level we will see that the obstacles are now counted as a walkable area because their height is less than the Step Height value so now the nav mesh system thinks the player can walk on the obstacles:

Img 37

Another way how we can denote that an object is an obstacle is by attaching the Nav Mesh Obstacle component.

Before we do that, select all obstacle child objects, and click the Static drop down list and make sure that the objects are not marked as Navigation Static:

Img 38
Since the obstacle child objects are not Navigation Static anymore, make sure that you rebake the level again so that they are not counted as obstacles:
 
Img 39
Now select all the obstacle objects again, and in the Inspector tab click on Add Component button and filter for nav mesh obstacle:
 
Img 40

To make the obstacle objects counted as obstacles by the nav mesh system, select all the obstacle objects and in the Nav Mesh Obstacle component check the Carve checkbox:

Img 41

When you do that, you will notice now that around every obstacle in the game there is no blue color:

Img 42

By changing the size property in the Nav Mesh Obstacle component you will also change how large of an obstacle the nav mesh system think a particular game object is:

Img 43

Before we test this out, make sure that you lay out the destination points near the obstacles so that you can see how the nav agent is avoids the obstacles.

Now run the game and let’s test it out:

Of course, the larger you set the size of the Nav Mesh Obstacle component for a specific obstacle object, the further away from the obstacle the player will be as he is not able to walk on that area.

Moving Towards Player With Nav Mesh Agent

To make the nav agent move towards the player we just need to feed it the player’s position. And to do that, we first need to detect the player’s presence near the enemy.
As you can assume, this can be done in multiple ways. We can use colliders, raycasts, or even calculate the distance between the player and the enemy and if the player is within a certain distance we can make the enemy chase the player. I am going to use colliders for this example to detect the player’s presence.
 
Select the enemy object in the Hierarchy tab, and attach a sphere collider on him. Check the Is Trigger checkbox and set the following values for the Center and Radius property of the sphere collider:
 
Img 44
I am also going to change the Stopping Distance to 2 for the Nav Mesh Agent component:
 
Img 45

This will make the nav agent stop near the player’s position instead of stopping exactly at the same position where the player is.

For this example I am going to use the same version of the EnemyNavigation script as we did in the previous example e.g. the EnemyNavigation that uses destination points in the level to move.

Above the Awake function add the following variable:

				
					private bool moveToPlayer;
				
			

Next, we need to create the functions that will detect when the player enters the sphere collider and when he exits the sphere collider:

				
					private void OnTriggerEnter(Collider other)
    {
        if (other.CompareTag("Player"))
        {
            moveToPlayer = true;
            navAgent.SetDestination(other.transform.position);
        }
    }

    private void OnTriggerExit(Collider other)
    {
        if (other.CompareTag("Player"))
        {
            moveToPlayer = false;
            SetNewDestination();
        }
    }
				
			

In OnTriggerEnter we are testing if we collided with the player. If that is true, then we will set the destination for the nav agent to the player’s position and we will set the moveToPlayer variable to true.

In OnTriggerExit, we are doing the opposite. We set the moveToPlayer value to false, and we call SetNewDestination to get a new moving destination in the game.

Lastly, we need to modify the CheckIfAgentReachedDestination function so that the enemy has one behavior when it is going towards the player, and another behaviour when it is going from one destination point to another:

				
					void CheckIfAgentReachedDestination()
    {
        if (!navAgent.pathPending)
        {
            if (navAgent.remainingDistance <= navAgent.stoppingDistance)
            {
                if (!navAgent.hasPath || navAgent.velocity.sqrMagnitude == 0f)
                {
                    if (moveToPlayer)
                    {
                        Debug.Log("Attack the player");
                    }
                    else
                    {
                        SetNewDestination();
                    }
                }
            }
        }
    }
				
			
When we reach the player’s destination I only added the Debug.Log statement so that we can see that the enemy has indeed reached the player. When you implement this in your game, depending on your project, you will add your own logic what you want the enemy to do with the player when it reaches its position.
 
Going back in the editor, from the Assets -> Prefabs folder drag the Lian You game object, which we will use as a player in this example, in the level:
 
Img 46

Now let’s run and test the game:

As soon as the player was within the sphere collider’s bounds, we detected the collision and the nav agent started moving towards the player.

When he reached his destination we saw that he didn’t stop exactly at the position of the player, instead he stopped near the player because of the Stopping Distance value.

2 thoughts on “Learn To Create Enemy AI Systems With A Few Lines Of Code In Unity Game Engine”

Leave a Comment