Tag Archives: ai

Navigation with the Nav Mesh Part 4: Patrolling and Chasing

 

Before you start
This tutorial won’t make any sense if you haven’t first done Part 1, Part 2, and Part 3.

And as with the rest of this series, you can grab a download of the full code as it should be at the end of this part of the tutorial at the bottom of this tutorial, so you can compare your own project.

Now that we’ve covered the basics of using the Nav Mesh, Nav Mesh Agent, and Nav Mesh Obstacle, let’s make better use of those tools to create something more game-like: an enemy.

We’ll create a simple enemy that uses the Nav Mesh for navigation, and has two types of behaviour – patrolling and chasing the player.

Create an Enemy

We’ll use our existing agent as our player, and create a new one to use as an enemy:

  1. Duplicate the Agent GameObject, and rename the copy ‘Enemy’ (Hint: Ctrl-D duplicates the selected object).
  2. Move the enemy far away from the player’s position.
  3. Change the enemy’s material and/or shape to make it distinct from the player (I changed mine to a cube and added a new orange material).
  4. Remove the Agent script (NOT the Nav Mesh Agent component) from the enemy (we’ll replace it with a different script).
  5. Rename the original Agent object to ‘Player’ for clarity.

To start with, we will make the enemy chase the player, as this is actually simple to implement. All we need to do is set the enemy’s Nav Mesh Agent destination to the player’s position, and update it periodically.

Enemy Script

We will build our enemy script in multiple stages, so you can either add in each piece of code as you read, or you can wait until the end of this section, where the whole script is listed.

  1. Create a new C# script and call it ‘Enemy’.
  2. Replace all the code in the script with the following:

using UnityEngine;
using UnityEngine.AI;
public class Enemy : MonoBehaviour {

    NavMeshAgent agent;
    [SerializeField] float decisionDelay = 3f;
    [SerializeField] Transform objectToChase;

    void Start () {
        agent = GetComponent<NavMeshAgent>();
        InvokeRepeating("SetDestination", 1.5f, decisionDelay);
    }

    void SetDestination() {
        agent.SetDestination(objectToChase.position);
    }
}

That code simply tells the enemy to move towards the object it is following, and repeats this via InvokeRepeating every 3 seconds (this is a long time between updating the destination, but it is much easier to see the behaviour in action with a long delay).

Now setup the enemy object:

  1. Drag the Player GameObject into the empty Object to Chase field in the Enemy’s Inspector window.
  2. Set the enemy’s Nav Mesh Agent Speed, Acceleration, and Angular speed settings slower than the play (or they will catch the player too easily!).

Test the scene. The enemy will wait 1.5 seconds and then start moving towards the player’s position. The enemy updates their destination (to the player’s current position) every 3 seconds. Try adjusting the Enemy’s Decision Delay and also their Nav Mesh Agent settings to get a good result. You don’t want them to easily reach the player, but you also don’t want them to be too slow.

At the moment the enemy constantly chases the player. This is OK for a simple demo, but let’s make it more interesting.

Catching the Player

Let’s implement a rudimentary way for the enemy to catch the player. This will make the project feel a bit more game-like. We will restart the scene when the player and enemy touch, so add this code to the enemy script (and make sure to save the script):

Firstly, add the following to the top of the Enemy script:

using UnityEngine.SceneManagement;

Then add the following method in the script body:

private void OnCollisionEnter(Collision collision)
{
    if (collision.transform.name == "Player")
    {
        SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex);
    }
}

That code requires a Rigidbody component on the enemy in order to cause the collision, so in Unity, add a Rigidbody component to the Enemy GameObject.

BUG NOTE:

Unity has a bug that causes lighting to break when reloading a scene (this only affects the Unity editor – not actual game builds). If the scene goes dark when the enemy catches the player and restarts the scene, do the following to work around the bug:

  1. Go to the Unity menu and choose Window > Lighting > Settings.
  2. Select the Scene tab.
  3. At the bottom of the window, untick the Auto Generate button.
  4. Click the Generate Lighting button.

Test the scene, and now the enemy will chase and catch the player, causing the scene to restart. f you want, add some extra code to the collision so something more interesting happens when the enemy catches the player – try adding some sound or making a message display on the screen.

Patrolling

In many games, enemies patrol. Our enemy will patrol between a few waypoints, and will switch to chasing the player if the player gets close. If the player then moves far enough away from the enemy, the enemy will resume patrolling.

Waypoints

Let’s create a simple waypoint script to make the patrol waypoints visible in the Scene window, which will make it easier to place them and visualise the enemy’s movement. Create a new C# script called Waypoint, and replace all the code inside with the following:

using UnityEngine;

public class Waypoint : MonoBehaviour {

    private void OnDrawGizmos()
    {
        Gizmos.color = Color.red;
        Gizmos.DrawWireCube(transform.position, new Vector3(1.5f, 1.5f, 1.5f));
    }
}

Any object with the Waypoint script will have a cube drawn at its position in the Scene window, which will be useful when setting up the enemy waypoints.

Now create all the waypoints in the scene:

  1. Create a new GameObject in the scene called Waypoint.
  2. Add the Waypoint script to the Waypoint GameObject.
  3. Create a prefab from the Waypoint GameObject by dragging the object into the Assets folder (create a Prefabs subfolder).
  4. Place the Waypoint GameObject near to the Enemy (this is the first waypoint).
  5. Create four more Waypoint objects by duplicating the existing one or by dragging the prefab into the scene.
  6. Rename the Waypoints sequentially (e.g. ‘Waypoint 1’, ‘Waypoint 2’, etc.) so you can easily place them in the right order.
  7. Place the waypoints so that they form a path for the enemy to travel along. In our code we will make the path a loop, so that the enemy returns to the first waypoint after reaching the last one.
  8. To ensure the waypoints are actually on the ground, set the Y position of all of them to 0.

Here are the waypoints in my project (the red cubes):

The enemy needs to know about the waypoints in order to move between them, so make the following changes to the Enemy script. Firstly, add a new array variable to hold all the waypoint positions:

[SerializeField] Transform[] waypoints;

Select the Enemy object in the Hierarchy and then go over to the Inspector window. In the Waypoints area, you need to enter the number of waypoints you have in the Size field, like this:

Now add references to the waypoints in the enemy script by dragging the waypoint objects from the Hierarchy into the empty fields in the Inspector, like so (do them in the correct order!):

State

The enemy needs to know when  to chase the player and when to patrol. For this simple example, we will use an enum variable to determine the enemy’s state, and determine behaviour based on its value.

First, we need to create an enum type and add it to the enemy script. Place the following code in the script. I usually put this sort of thing up with the variables:

enum EnemyStates {
    Patrolling,
    Chasing
}

Now, add the following variable to the script:

[SerializeField] EnemyStates currentState;

If you’ve not used enums before, you can think of them like a custom variable type that you specify the possible values for. In this case, a variable of type EnemyStates can only have one of two values: EnemyStates.Patrolling or EnemyStates.Chasing. The first value in the list is the default, meaning our enemy will start off in the Patrolling state until something changes that.

Patrolling Movement

Let’s get back to the patrolling movement. We’ll keep it simple. The enemy, when patrolling, will move to the next waypoint on their route, and when they reach the final waypoint, will return to the first one.

Add a new variable to keep track of which waypoint the enemy is currently walking toward:

int currentWaypoint = 0;

Add the following Update() method to the script:

void Update()
{
    if(currentState == EnemyStates.Patrolling)
    {
        if(Vector3.Distance(transform.position, waypoints[currentWaypoint].position) < 0.6f)
        {
            currentWaypoint++;
            if (currentWaypoint == waypoints.Length)
            {
                currentWaypoint = 0;
            }
        }
        agent.SetDestination(waypoints[currentWaypoint].position);
    }
}

That should be self-explanatory, but in a nutshell it updates the destination to the next waypoint when the agent gets close to the current target waypoint.

We also need to make a change to our chasing code, as the way it is done now would make the enemy try to patrol and chase at the same time! We need to make the chasing conditional upon the current state, so change the SetDestination() method to the following:

void SetDestination() {
    if(currentState == EnemyStates.Chasing) agent.SetDestination(objectToChase.position);
}

Finally, add this as the last line of the Start() method to initiate the patrolling:

if(currentState == EnemyStates.Patrolling) agent.SetDestination(waypoints[currentWaypoint].position);

The enemy script would not win any awards for great code, but it’s good enough to demonstrate the simple behaviour we need.

Test the level to make sure the patrolling is working. If the enemy moves to the first waypoint and then stops, try changing the 0.6f value in the Update method to something higher. You can also change the enemy’s current state to chasing in the Inspector, and the enemy will stop patrolling and chase the player instead. Try changing it back and forth. Great, now there are two states, but the enemy needs a way to decide which state to be in.

Since we’re more interested in the navigation, we’ll cheat with the AI here. We will have a couple of simple rules to switch the enemy behaviour:

  • If the player is less than 10 units away from the enemy, the enemy will chase.
  • If the player is 10 or more units away from the enemy, the enemy will patrol.

We’ll pretend that the enemy can only hear or see the player when they are close.

Add this code to the Update() method before the code that is already in there:

if(Vector3.Distance(transform.position, objectToChase.position) >10f)
{
    currentState = EnemyStates.Patrolling;
}
else
{
    currentState = EnemyStates.Chasing;
}

On every Update(), the enemy will check how close the player is, then change state if necessary. You can now test the scene and make sure the enemy chases the player when the player gets too close. Try changing some of the values to make it feel right. The player needs to be faster than the enemy (so the player can run away), and the distance the enemy can detect the player needs to be enough to prevent the player escaping immediately. You may also want to adjust how long it takes the enemy to update their destination when chasing the player.

Tip: You can add a gizmo to the script to visualise how far away the enemy can detect the player. Add something like this:
private void OnDrawGizmos()
{
    Gizmos.DrawWireSphere(transform.position, 5f);
}

The value of 5 here is the radius, which should be half the distance used in the code that checks the distance between the player and enemy. The gizmo will appear as a wire sphere around the enemy in the Scene view.

Done

That’s everything for Part 4. You should now have an enemy that switches between chasing the player and patrolling. The way this works and feels will depend a lot on the various values you use for everything, and there’s no better way to get it right than trial and error.

Download: Here is a download for the project as it should be as of the end of this tutorial: Part 4 download.

Here’s the full Enemy script including all the pieces from above:

using UnityEngine;
using UnityEngine.AI;
using UnityEngine.SceneManagement;

public class Enemy : MonoBehaviour {
    NavMeshAgent agent;
    [SerializeField] float decisionDelay = 3f;
    [SerializeField] Transform objectToChase;
    [SerializeField] Transform[] waypoints;
    int currentWaypoint = 0;
    
    enum EnemyStates
    {
        Patrolling,
        Chasing
    }

    [SerializeField] EnemyStates currentState;

    void Start () {
        agent = GetComponent<NavMeshAgent>();
        InvokeRepeating("SetDestination", 0.5f, decisionDelay);
        if(currentState == EnemyStates.Patrolling) agent.SetDestination(waypoints[currentWaypoint].position);
    }

    void Update()
    {
        if(Vector3.Distance(transform.position, objectToChase.position) > 10f)
        {
            currentState = EnemyStates.Patrolling;
        }
        else
        {
            currentState = EnemyStates.Chasing;
        }
        if(currentState == EnemyStates.Patrolling)
        {
            if(Vector3.Distance(transform.position, waypoints[currentWaypoint].position) <= 0.6f)
            {
                currentWaypoint++;
                if (currentWaypoint == waypoints.Length)
                {
                    currentWaypoint = 0;
                }
            }
            agent.SetDestination(waypoints[currentWaypoint].position);
        }
    }

    void SetDestination() {
        if(currentState == EnemyStates.Chasing) agent.SetDestination(objectToChase.position);
    }

    private void OnCollisionEnter(Collision collision)
    {
        if (collision.transform.name == "Player")
        {
             SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex);
        }
    }

    private void OnDrawGizmos()
    {
        Gizmos.DrawWireSphere(transform.position, 5f);
    }
}

Giving Enemies the Power of Sight!

Enemies are more challenging when they have some intelligence. A great start to building enemy AI is to grant enemies the power of sight!

Sight is really a test that there is clear space between the enemy’s eyes and the player. With that knowledge it’s simple to work out a solution.

enemy_sight

One enemy has line of sight, the other doesn’t

You can download the sample project at the bottom of this article to see it in action (and to get all the juicy code).

Defining Vision

To simulate vision we need to distil it down to fundamentals. Vision has:

  • a range (you can’t read newspaper print from a kilometre away)
  • a direction (you can’t see something directly behind you when you are facing forward)
  • line of sight (you can’t see through solid objects).

For a 2D game you can represent sight like so:

enemy_sight_2

The elements of enemy vision

  • The green circle represents the maximum distance the enemy can see.
  • The dotted green lines represent the enemy’s field of view – it can only see things within that ‘cone’.
  • The blue line represents the enemy’s line of sight to the player (to test if anything blocks that view).

Coding Vision

Now that we have defined what the enemy can see (i.e. anything within their range, in the direction they are facing, and not blocked by other objects) we can work on the code.

I chose to break down this problem into three steps, where each step relies on the previous one:

IF the player is within the enemy's vision range

    IF the enemy is facing the player's direction

        IF nothing is in between the player and the enemy

            THEN the player can be seen!

  • If the player is not within range there is no need to check anything else (e.g. it doesn’t matter if you’re looking right at a newspaper if it’s too far away to read).
  • If the enemy is not looking in the player’s direction it doesn’t matter how close the player is (e.g. if he’s behind the enemy the enemy can’t see him.
  • Finally, if the player is behind a wall the enemy can’t see him even if the player is within range and in the enemy’s field of view.

Now to code those steps one-by-one.

Is the Player within the Enemy’s Vision Range?

This is easy. Simply give the enemy a very large circle (or sphere in 3D space) trigger collider, and if the player is within this collider he is within visual range. Typically this circle/sphere would be many times larger than the enemy itself, giving it vision across a large part of the screen:

The enemy's visual range is determined by a large circle trigger collider

The enemy’s visual range is determined by a large circle trigger collider

In the example linked at the bottom of this article I have a bool variable [code]]czoyMDpcImlzUGxheWVySW5TaWdodFJhbmdlXCI7e1smKiZdfQ==[[/code], which is set to true in the enemy’s [code]]czoxNzpcIk9uVHJpZ2dlclN0YXkyRCgpXCI7e1smKiZdfQ==[[/code] method, and set to false in the corresponding [code]]czoxNzpcIk9uVHJpZ2dlckV4aXQyRCgpXCI7e1smKiZdfQ==[[/code] method. The enemy script always knows if the player is (theoretically) within visual range.

I use a special collision layer for my vision collider so that it doesn’t get triggered by all the items in the level, since we only care about whether the enemy can see the player or not. This collision layer only collides with the player (set via the collision matrix in the physics settings).

Is the Enemy Facing the Player’s Direction?

I have set an angle of 65 degrees in my example code. This will ensure that the enemy can only see the player if the player is within 65 degrees of the centre of the enemy’s line of sight (which I have set as a transform directly in front – to the left in screen space – of the enemy).

The line of sight transform is just a way to draw an imaginary line facing directly forward from the enemy’s eyes. We then draw another imaginary line from the enemy’s eyes to the player, creating an angle like so:

enemy_sight_4

Measuring the angle of the player from the enemy’s forward direction to see if he’s in the field of view

If that angle is < 65 degrees we consider the player to be within the enemy’s line of sight. The angle is calculated via [code]]czoxMzpcIlZlY3RvcjIuQW5nbGVcIjt7WyYqJl19[[/code], which takes the two vectors as parameters and returns the angle between them:

Vector2 directionToPlayer = player.position - transform.position; // represents the direction from the enemy to the player   
Vector2 lineOfSight = lineOfSightEnd.position - transform.position; // the centre of the enemy's field of view, the direction of looking directly ahead
// calculate the angle formed between the player's position and the centre of the enemy's line of sight
float angle = Vector2.Angle(directionToPlayer, lineOfSight);

Does anything block the enemy’s view of the player?

The final check sends a raycast (an imaginary line that detects colliders) from the enemy to the player and see if it hits anything else on its journey. If it hits something other than the player (e.g. a wall or a crate) then we determine that the enemy’s line of sight to the player is blocked and the player can’t be seen; otherwise the player is seen.

RaycastHit2D[] hits = Physics2D.RaycastAll(transform.position, player.position - transform.position, distanceToPlayer);

[code]]czoyMDpcIlBoeXNpY3MyRC5SYXljYXN0QWxsXCI7e1smKiZdfQ==[[/code] returns an array of all the colliders the ray touches on its travel from the enemy’s eyes to the player. If you look at the code in the sample project, we ignore any objects that are actually the enemy. If the ray hits anything else that is not the player we know that this object must be between the player and the enemy because the ray only goes as far as the player.

The third parameter of [code]]czoyMDpcIlBoeXNpY3MyRC5SYXljYXN0QWxsXCI7e1smKiZdfQ==[[/code] lets us specify how far to shoot the ray, and therefore the ray can’t detect items further away than [code]]czoxNjpcImRpc3RhbmNlVG9QbGF5ZXJcIjt7WyYqJl19[[/code] in this example.

This technique works well, but has a potential flaw depending on the context. The ray is sent from the enemy’s eyes to the player’s location (which will typically be the centre of the player’s transform). This means the player’s centre must be visible/not blocked in order for the enemy to see him. If the player’s body is behind a crate, but his head is exposed, the enemy still can’t see him.

You could remedy this by using multiple rays to detect different points on the player’s body, then work out a system that suits your particular game (e.g. the enemy must be able to see two out of three points on the player’s body to consider the player visible).

Working with it

That’s how it works. Download the sample to see all of the code, and refer to the code comments and the theory in the article if you get lost. Of course questions and comments are welcome too.

In the example project I change the enemy’s sprite colour to red when he can see the player. You can see some debug lines in the Scene window as well, indicating where the enemy is looking. I recently did a post about using debug lines if you want a bit of background on that technique.

Here are a few ways you could adapt this method to suit your game:

  • Change the enemy’s field of view to make it more narrow or wider
  • Add multiple visible points to the player and/or use multiple rays from the enemy to create more nuanced vision
  • Remove the vision circle trigger collider and always consider the player in visual range (i.e. make the enemy’s visual range effectively infinite)
  • Ignore the field of view angle totally if you want enemies to see in all directions simultaneously (e.g. a turret)
  • Expand the check for obstacles to ignore transparent objects (e.g. if the player is behind a glass window the enemy can still see him).