CS 491 Lecture 13 – Navigation
TODO
- Work on your third person camera game.
- Break your spring!
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 touch
ing 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:
- I should only check for a win when a tank changes its color. Checking more often is unnecessary.
- 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:
- Unity provides a way to query for all the objects in your current scene with a given tag:
GameObject.FindGameObjectsWithTag("TAG")
. - As we saw last lab, you can ask a game object with a TankController component for its team:
TankController tankController = tankObject.GetComponent<TankController>(); print(tankController.team);
- When you’ve determined the winner, set the label of the UI Text object with something like this:
message.text = "Team Pink wins!";
The Text object and instance variable
message
has already been wired up.
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:
-
Mathf.Infinity
-
Vector3.Distance
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?