跳转至

9 Basic AI & Navigation

Introduction to the Arena

This chapter is the first installment in the three-part adventure arc of veggie combat! Can you survive as one tank against an army of mad veggie gladiators armed with cutlery???

You begin with a starter project containing an arena filled with veggie spectators eagerly awaiting the combatants. By the end of this chapter, you’ll have added some point-and-click navigation and basic enemy AI to the scene.

Are you afraid of an unlimited army of veggie gladiators? Not when you’re driving a tank!

img

First, you’ll need to learn how to add a basic navigation AI to your games through the Unity Navigation System. Through this system, you can create agents that are able to navigate the geometry of a scene through the generation of navigation meshes — or simply NavMeshes.

This movement system is the perfect choice for a variety of games where the dominant mechanic is not the coordination of the player, but rather the strategy of exploring and interacting with a complex world of objects. Top-down adventure games and turn-based strategy games are both excellent examples of mechanics where the Unity Navigation system is a great fit.

Navigation in Unity is automatically handled by intelligent agents that find the shortest paths on NavMeshes. NavMeshes are data structures composed of polygons, where there are no barriers between points within a given polygon. Paths across a map are found by stepping between the polygons of the NavMesh structures. Overall, the NavMesh simplifies the geometry of objects in the scene to less varied surfaces that face upwards.

NavMesh Agents calculate the shortest possible paths over the NavMesh surface to reach a destination from their current position. Unity runs the A* shortest path algorithm to search over the NavMesh graph for shortest paths and remove less efficient routes quickly.

img

Building a NavMesh

Building a NavMesh is done through a process called NavMesh Baking, where all the geometry of the scene is analyzed and GameObjects and terrains are processed to make a simplified surface that can be transversed by a NavMesh Agent. Shortest paths are calculated over this surface using algorithms such as the A* pathfinding algorithm.

Open the starter project for this chapter in Unity. Then, open the RW / Scenes / Arena scene. You’ll begin this game by converting your tank into an agent that can navigate around the arena:

  1. From the top menu, choose Window ▸ AI ▸ Navigation to make a navigation pane appear in the right side panel.
  2. In the Hierarchy, select the Scene ▸ Arena-Floor GameObject of the Arena scene.
  3. Click the Navigation pane and make sure the Navigation Static indicator under the Navigation ▸ Object tab is checked. This ensures the arena is included in the NavMesh baking process.

img

  1. Switch to the Navigation ▸ Bake tab and click the Bake button to generate a static NavMesh describing the floor of the arena.

img

The bake settings adjust how the surface is created to match the size of a NavMesh Agent, which can make it easier or harder to reach surfaces or climb inclines. Agent Radius defines how close the agent center can get to an edge of a surface such as a wall or a ledge. The Agent Height can be adjusted to make it easier to reach the spaces raised off the ground. The Max Slope can be adjusted to make it possible to navigate up steeper angles and ramps in the surfaces. Finally, the Step Height defines whether the agent can step over an obstruction.

Note: What is “Baking”? When you pre-run calculations in the Unity Editor and save the results to disk, this process is called baking. Unity loads the baked data at runtime, and having this precalculated data reduces the performance cost at runtime. You’ll similarly hear of “Baking Lighting,” which is another way to perform a one-time calculation for lighting effects rather than constantly recalculating while the game is running.

In the Project pane, your RW / Scenes folder now includes an Arena / NavMesh asset for the generated mesh.

Look back at the Scene window, and make sure the NavMesh Display / Show NavMesh is enabled. You’ll see the areas defined by the NavMesh in blue. If the blue NavMesh isn’t displaying, check that the Gizmos button is enabled at the top of the scene window. Notice the pit in the center of the arena is not part of the navigable area. The steep cliff of the pit in the center of the map defines an edge to your NavMesh, while the walls of the arena form the other boundaries.

Configuring a player NavMesh Agent

The NavMesh Agent component enables characters to utilize the baked NavMesh to find pathways to a target and avoid obstacles. It’s time to make your first moving NavMesh Agent to be guided by point-and-click mouse input of the new Unity Input System.

Find and select the Tank prefab in the RW / Prefabs folder, and from the Inspector view select Open Prefab to view the tank.

Then, select Add Component and choose NavMesh Agent. This is the fundamental unit that will drive the AI of the tank to be able to navigate your mesh.

img

Take a quick look at the NavMesh Agent component in the inspector, which configures how pathfinding for the agent behaves and how the agent will avoid obstacles. The Speedcontrols how fast the agent will travel along its path, while the Angular Speed determines how quickly the agent will rotate at turns. Acceleration determines how quickly the agent will speed up, while the Stopping Distance configures how close to the target the agent will stop. The Obstacle Avoidance parameters will determine how far from obstacles the agent needs to be in any path.

Adjust the Steering section to increase the Speed to 7 and Angular Speed to 720. These options make the tank more responsive and faster to turn to new positions.

The tank prefab still needs another script to adapt input from the player to actions taken by the NavMesh Agent. Add another component — the Player Input — from the new Unity Input System. Set the Actions to a pre-defined Input System action called MyActions.

img

Last, add the predefined component called Player Controller. This script needs additional logic to describe how the input system should trigger the moves of the tank around the arena floor. Player Controller should also provide actions such as firing projectiles that you must implement.

Find RW / Prefabs / ForkProjectile in the project and drag this to the Projectile field in the Player Controller component. This will be your ammo.

img

Now for some animation!

Game logic

Look at the Hierarchy view of the tank prefab. Your tank prefab is actually constructed of a hierarchy of different 3D models — from the wheels all the way to the cannon. As an animating effect, you’ll allow the cannon to freely move and target enemies to fire the projectile. You’ll implement the PlayerController script logic next to allow this.

Setting a destination from Input System

The first thing you need to do is to get the tank to move. The goal is to convert a position of the mouse to a 3D location to direct the tank as a navigation target.

Open the PlayerController script and find the OnMove method. OnMove is the expected name for the action handler to be called any time there’s a mouse click, as defined by the MyActions Input System profile.

Add the following logic to OnMove:

RaycastHit hit; // 1
Debug.Log("Try to move to a new position");

// 2
if (Physics.Raycast(Camera.main.ScreenPointToRay(
                        Mouse.current.position.ReadValue()),
                    out hit,
                    100))
{
    agent.destination = hit.point;  // 3
    Debug.Log("Moving to a new position");
}

Going through this:

  1. You define a RaycastHit object which you’ll use to store a point that you want the tank to move towards.
  2. Camera.main.ScreenPointToRay is a method provided by the Unity API that takes a 2D coordinate (x, y) of the where the mouse is on the screen, and converts this to a ray from the camera position forward into the 3D scene. The Physics.Raycast method traces the path of the ray until it encounters an object in the scene where it calculates and returns the 3D coordinates (x, y, z) of this point in the RaycastHit object.
  3. You set agent.destination to this point. When you do this, the NavMesh Agent will begin to find a path and move towards it.

img

Essentially, the 3D coordinate becomes the new destination for the tank. The NavMesh Agent AI will then use the A* algorithm to find the shortest path on the baked NavMesh to reach the target.

Save your script and go back to the Unity Editor. Click Play on the scene to give the movement a try. Click anywhere in the arena and watch the tank head in that direction!

img

What’s really neat is that if you click a point on the other side of the pit, the tank will move around it to reach the new destination. Remember that the pit is not part of the NavMesh, so the tank can’t go over that part of the scene!

Setting aim from Input System

Return to the PlayerController script and find the OnFire method stub. Fill in the logic for the projectile firing with this:

// 1. On a mouse click, get the mouse current position
float x = Mouse.current.position.x.ReadValue();
float y = Mouse.current.position.y.ReadValue();

// 2. Ray trace to identify the location clicked.
Ray ray = Camera.main.ScreenPointToRay(new Vector3(x, y, 
              Camera.main.nearClipPlane));

// 3. Raycast to hit the surface, turn turret to face.
RaycastHit hit;
if (Physics.Raycast(ray, out hit))
{
    Vector3 target = hit.point;
    target.y = cannonHorizontal.transform.position.y;
    cannonHorizontal.transform.LookAt(target);
}

// 4. Find the direction forward of the cannon
Vector3 forward = cannonVertical.transform.forward;
Vector3 velocity = forward * launchVelocity;
Vector3 velocityHand =
    new Vector3(velocity.z, velocity.y, velocity.x);

// 5. Instantiate a projectile and send it forward
Transform cannon = cannonVertical.transform;
GameObject fork =
    Instantiate(projectile, cannon.position, cannon.rotation);
fork.GetComponent<Rigidbody>().AddForce(velocity);

Here’s what’s happening:

  1. You first identify the x and y positions where the mouse was clicked.
  2. To find what is being clicked, you convert from screen coordinates to a ray along the camera direction to find hits.
  3. You use the transform LookAt method to determine how the turret needs to rotate to aim at this target.
  4. You use the turret cannon barrel to determine the angle the projectile needs to launch from and the vector of a velocity to apply to the projectile as movement.
  5. You finally instantiate your prefab of the projectile, and send it shooting forward with the velocity.

Save your script and play the game again. This time, try right-clicking to launch your projectile. Forks away!

img

Note: If you look at the top level of the Hierarchy as you play the game, you’ll notice that each fork you fire is added as a separate GameObject to the scene. A more optimized way to handle this would be to use Object Pooling. You’ll learn more about this in the coming chapters.

Enemy AI and NavMesh Agents

Next, you’ll incorporate NavMesh Agents to provide automated movements for some enemies.

Find the RW / Prefabs / Warrior_Carrot prefab in the Project view, and drag it into your scene. Adjust the position so that the warrior carrot is at X:0, Y:0, Z:10, standing right next to the pit.

If you play the scene, the enemy carrot will animate in an idle pose — but it won’t do much because it doesn’t have any other attributes. So now you’ll make this enemy chase after the tank!

Attached to the Warrior_Carrot is both a configured NavMesh Agent component and a script called Enemy Controller.

To get started, open up the Enemy Controller script in your editor. The enemy has three possible states: Ready to chase the player, Attacking the player or Dead:

// 1. States
enum States { Ready, Attack, Dead };

You’ll find the Update method with some empty blocks of code for handling these states. Implement the chasing the player action first by adding the following to the state == States.Ready block:

// 1. Set the destination as the player
agent.SetDestination(player.transform.position);
characterAnimator.SetFloat("Speed", agent.velocity.magnitude);

// 2. Stop when close and animate an attack
if (agent.remainingDistance < 5.0f)
{
    agent.isStopped = true;
    characterAnimator.SetBool("Attack", true);

    state = States.Attack;
    timeRemaining = 1f;  
} 
else
{
    // 3. Stop attacking and allow movement
    agent.isStopped = false;
    characterAnimator.SetBool("Attack", false);
}

Here’s what you’re doing:

  1. First, you set the destination for the Nav Mesh Agent component to the current player position.
  2. When the enemy approaches the player (within a distance of 5 in this case), you stop moving the NavMesh Agent and enable an animation for the enemy attack motions by changing the state to Attack.
  3. The last section will exit the attack animation state if the player moves away and enables the enemy to repeat the chase.

Now, complete the action by finishing the state == States.Attack block:

timeRemaining -= Time.deltaTime;
if (timeRemaining < 0)
{
    state = States.Ready;
    if (Vector3.Distance(player.transform.position, 
            gameObject.transform.position) < 5.0f)
    {
        player.GetComponent<PlayerController>().
                    DamagePlayer();
    }
}

This keeps the enemy attacking the player for a brief amount of time before beginning to chase again.

Save your script, and play your scene!

img

Wow, those Carrot Warriors are pretty vicious! It’s a good job you’re in a tank!

The last bit of enemy logic is to allow triggering colliders to eliminate the enemies. These colliders could be the projectiles or even the tank itself. Open the EnemyController script again and go to the stub for OnTriggerEnter and add this to the if (state != States.Dead) block:

characterAnimator.SetBool("Death", true);
Destroy(gameObject, 5);
state = States.Dead;
agent.isStopped = true;

Save the script and run the scene again, and now your tank can fire at and run over the enemy carrot warrior. How satisfying!

img

Game state

To fully finish the game design, you’ll implement waves of Carrot Warriors to battle against. You’ll also track the player’s health as the attack happens.

Create an empty GameObject named Game in your Arena scene. Add the script called Game State to this GameObject using Add Component.

Open the script in your editor, and you’ll see this contains a simple state machine with the four states: Countdown, Fight, Battle and Lose. The GameState script updates the GUI to provide a countdown and a health bar that reflects your tank damage. It also defines the start of the game and — when the player loses — its end.

The UpdateGUI method provides feedback on the Canvas overlay:

void UpdateGUI()
{
    switch (state)
    {
        case States.Countdown:
            int timer = (int) Math.Ceiling(timeRemaining);
            MessageBar.text = timer.ToString();
            break;
        case States.Fight:
            MessageBar.text = "Fight!";
            break;
        case States.Battle:
            MessageBar.text = "";
            break;
        case States.Lose:
            MessageBar.text = "You Lose!";
            break;
    }

    HealthBar.sizeDelta = new Vector2(735 * player.GetComponent<PlayerController>().GetPercentHealth(), 65);
}

Depending on the value of state, the MessageBar will update to display new information.

You need to first assign the relevant components to the public fields.

Go back to the Unity Editor and view the Game State script component in the Inspector. Now, attach each of the GUI objects to the appropriate fields in Game State: Drag the Canvas / Text GameObject from the Hierarchy to the Message Bar field of Game State in the Inspector, then drag the Canvas / Frame / Bar health bar to the Health Bar field of Game State.

img

Now, if the player’s health falls to zero, Game State enters the losing state and updates the GUI with the relevant message.

Play the game, and you’ll see a countdown. Given enough time, the single enemy player will take away all your health and the game will display the “You Lose!” message.

One enemy is not a real challenge, though, so it’s now time to implement the spawning of waves of the carrot army!

Adding the enemy waves

The enemy waves spawn at the gates of the arena. Each of the Scene / GateWallGameObjects has a component GateSpawner that can instantiate a crowd of Warrior_Carrotenemies. You need to connect each of these into the Game State to provide spawn locations for the enemies.

Before that, you need to disable any NavMesh Agents until the enemies are spawned at the gates.

Open the EnemyController script, and add this to the bottom of the Awake method:

agent.enabled = false;

This will ensure the agents don’t show up until you ask them to. This happens in the Enablemethod.

Save the script then head back to the Unity editor. Now, select the Game GameObject and view the Game State component script in the Inspector view. Click the Lock button in the upper right to keep this component in view.

img

Click the + button under the Spawners list four times and drag each of the four Scene/GateWall GameObjects of the scene into the list to add each as a potential spawning location.

Next, you need to assign the parent for all these enemies. This is simple — just assign Enemiesto the Enemies field in the Game State script.

Once you’ve completed mapping the fields, you can delete the sole warrior carrot you added earlier from the scene hierarchy. He’s served his purpose.

Now click Play to battle it out in the arena against an army of carrots!

img

This concludes the project on the arena.

Common pitfalls with NavMesh Agents

While NavMesh Agents provide out-of-the-box navigation, they do have limitations:

  1. Unity can be quite picky if a NavMesh Agent isn’t on the NavMesh or within a minimum distance. This will produce errors in the console and navigation functions won’t work. The best workaround when adding characters to a NavMesh is to reposition them onto the NavMesh before activation.
  2. If an agent appears to get stuck or blocked by small inclines and walls, increase the NavMesh Agent size or adjust the allowed angle for navigation to enable the characters to overcome the heights you want them to.

Key points

  1. The Unity Navigation System provides advanced pathfinding intelligence.
  2. Window ▸ AI ▸ Navigation enables the Unity Navigation panel.
  3. Bake a NavMesh for a selected GameObject to create a navigation geometry.
  4. GameObjects can be marked Navigation Static to allow them to be baked into the NavMesh.
  5. A component called the Nav Mesh Agent must be added to a GameObject to provide pathfinding and obstacle-avoiding intelligence.
  6. You can create complex behaviors using a NavMesh Agent by calling its API from your MonoBehaviour scripts.
  7. The Navigation System is a great way to build enemy AI that can intelligently march around a field, chase after targets and avoid obstacles.

Where to go from here?

In this chapter, you learned about using the Unity Navigation System to implement point-and-click navigation and some basic enemy AI.

You can experiment with the features to dig deeper into the subject. For example, you can add some obstacles with the NavMesh Obstacle component. When these exist as moving objects, the NavMesh Agents will attempt to avoid them, and when they are stationary, these objects will carve out regions of the NavMesh.

For dynamically changing NavMeshes and more advanced uses, the AI Navigation packageprovides additional tools.

You can also learn more about the Unity Navigation System and pathfinding with the raywenderlich.com tutorial Pathfinding with NavMesh: Getting Started tutorial.