teaching machines

CS 491 Lecture 13 – Navigation

March 17, 2016 by . Filed under gamedev3, lectures, spring 2016.

TODO

Lab

Today we’ll finish up our tank coloring game at long last. We’ll add a win condition (all the tanks are the same color) and let Unity’s navigation AI system control all the tanks but the one the player has claimed.

Grab Base

We’re all in various stages of completion of the previous labs. I encourage you to finish those on your own at another time. So that we may move forward on the navigation, please download my reference implementation of the game so far from Bitbucket. (Click Downloads, save and unpack the ZIP, and actively open the expanded directory from within Unity. I had some trouble with the imported WAV files on OS X, which I fixed by touching the files and reimporting. Call me over if you experience this.) In it I’ve implemented shot pickup, firing, and color flipping. The scene has four spawners and 10 tanks.

Win Logic

Before our game gets too involved, let’s add and test a method to check for a win state. Where should such a method go? The software developer inside of me thinks two things:

  1. I should only check for a win when a tank changes its color. Checking more often is unnecessary.
  2. Knowledge of the entire game universe is needed to determine a win. This is not knowledge that a single tank should have.

It seems to me that the CameraController is a reasonable place to add a public CheckForWin method. Its singleton status makes it a natural spot for embedding universal knowledge. When a tank flips its color in TankController.Flip, we can ask the CameraController to do its checking.

Add a public CameraController.CheckForWin method and call it from TankController.Flip. Getting at the camera from TankController can be done in several ways: through a public reference, through GameObject.Find, or through Camera.main. The last of these is probably the fastest and easiest, but the result is the Main Camera object’s Camera component. You’ll have to use GetComponent to get its sibling CameraController.

For the implementation of CheckForWin, we need an algorithm that checks to see if all the tanks are the same color. Write that algorithm, bearing in mind the following:

Test your method by disabling all but two tanks, one for each team. Make sure these are close together and near a spawner.

NavMesh

Now we’re ready to start adding some AI navigation to our game. The first thing we need is a NavMesh, which you should have learned is Unity’s accelerated data structure for marking up the navigability of a game area. Generate one of these for our terrain by visiting Window / Navigation. Select the terrain, make sure Navigation Static is checked and that the area is Walkable, and click Bake to precompute the data structure.

NavMeshAgent

With a NavMesh in place, we need agents that will try to traverse it. Every tank that isn’t controlled by the player will instead be driven by a NavMeshAgent.

Select the Tank Parent prefab. Add a NavMeshAgent component.

PlayerController vs. NavMeshAgent vs. Rigidbody

Currently our player-owned tank has both a NavMeshAgent and a PlayerController component. These aren’t compatible. Either the player controls the tank or the computer does. We need to make sure only one of them is enabled at a time.

Further, NavMeshAgents don’t play well if they have non-kinematic rigidbodies. Recall that a non-kinematic rigidbody is one where the transform is calculated by scripts and not the physics engine. The navigation system bypasses the regular physics engine and therefore needs the rigidbody to be kinematic.

Address both these concerns by adding some hooks into PlayerController. When enabled, we want to make the rigidbody non-kinematic and disable the NavMeshAgent. When disabled, the rigidbody becomes kinematic and the NavMeshAgent becomes enabled. Use the callback methods OnEnable and OnDisable to make this automatic. Rigidbodies have a boolean isKinematic property and NavMeshAgents have a boolean enabled property.

Race to the Bottom

Let’s make these tanks drive on their own now. We’ll start by just having them all rush to the origin. Back in TankController, grab a private reference to the NavMeshAgent, just as we’ve done for Rigidbody.

In Update, set the agent’s destination to (0, 0, 0). But only do this if the agent is actually enabled:

if agent is enabled
  set agent's destination to (0, 0, 0)

Re-enable the other tanks. Playtest. Do they converge?

Shotget

These thanks are a little too 0-hungry to do much vanquishing of the enemy. We need a better strategy. Instead of just blindly heading to the origin, let’s pursue something like this:

if the tank has no shot
  identify target by searching for shot nearby
else
  identify target by searching for an opposing tank nearby

if target was found
  set agent's destination to target
else
  set agent's destination to origin

Flesh out this logic in C# inside your Update function.

Searching for nearby objects can be done with the help of the Physics class. Just as we used Raycast to identify what tank we clicked on, we can use OverlapSphere to see which objects appear within a given radius around a location:

Collider[] colliders = Physics.OverlapSphere(POSITION, RADIUS, 1 << LayerMask.NameToLayer("LAYER"));

Layers Shot and Tank have already been defined and set on the prefabs.

Use this method to locate any nearby shot. Traverse the colliders and find the closest one. You might find the following Unity constructs helpful:

Nearest Enemy

Once we’ve got a shot in barrel, we want to go searching for an enemy. Add code to do this.

This can be done very similarly to finding a nearby shot, but we want to make sure we only pursue tanks of the opposing color. OverlapSphere is going to give us a list of all the tanks in the area, so we’ll need to check the team property to ensure we don’t go chasing a friend.

AutoFire

If we have a shot and we’ve located a nearby enemy, we want to shoot it and flip its color. Under what conditions should you fire? Experiment.

Jitter

If no target was found, a tank will head to the origin. When many do this, the results are kind of comical. The first time, anyway. We can add some randomness to this fallback target location using Random.insideUnitCircle:

Vector2 xz = Random.insideUnitCircle * SOME_JITTER_RADIUS;

If we use xz to set the agent’s destination, the tanks don’t look nearly so much like enslaved sheep.

Playtest. Do the tanks “graze” more naturally?

Post-Flip Camera Migration

Now that the enemy is able to fire, what happens when the player gets hit? We must transfer the camera to another tank on the same team. (In the game that I have packaged up, the player/camera is always aligned with Team A, which is pink.)

In TankController.Flip, you will find this code already in place:

if (team == Team.B && GetComponent<PlayerController>().enabled) {
  Camera.main.GetComponent<CameraController>().LockOntoNearest();
}

If the flipped tank has just joined team B but is owned by the player, we ask the camera to switch.

Find CameraController.LockOntoNearest and identify the nearest tank on team A. We don’t want OverlapSphere here, because we don’t want to restrict ourselves to a certain area. We must find a tank, however far away it is. What method should we use to query for all tanks?

Playtest. Does the camera switch?