teaching machines

CS 491 Lecture 11 – Third Person

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

TODO

Lab

Today we’ll start a game with the tentative title Our Color vs. Their Color. In it, little tanks will drive around a landscape trying to collect shot of their color. After picking up the shot (by colliding with it), they can shoot it at a tank of the opposing color. If hit, the hit tank changes to the shooter’s color. The game is over when all tanks are of one color. This game is not violent because changing one’s color is not painful.

This lab focuses on moving tanks around and making the camera follow them. Next lab we’ll examine shooting shot, switching colors, and implement some navigation and crude AI.

Terrain

Quickly add a terrain. Spend barely any time on this step. We just need a frame of reference.

Tank

In Blender, compose a tank-like prototype. (I made a box with a turret.) Do all scaling, rotation, and translating in edit mode so that the actual vertex locations are set. Applying these transformations in object mode has confusing effects when the model is imported into Unity.

Import this tank model into Unity. It’s a prototype made in five minutes.

Tank Parent

When turning models into Game Objects, you can often save yourself some headache from fighting transformations and changes to the model by making the model a child of a very normal empty. Go ahead and make such an empty and call it Tank Parent or something similarly meaningful. (Empty Tank?)

Drag the tank model onto it as a child object.

Add to the empty a box or sphere collider, a rigidbody, and a PlayerController script. By putting all these components on the more stable empty, it’s easier to replace the tank model with something else later. All we change is the loosely-coupled child object.

Drag the empty into the project assets to make it a prefab. Later on we will make many instances of it.

Fore and Aft Movement

In the PlayerController script, we want to move the tank on user input. Since the object has a rigidbody, we must use it to move it around. Objects without rigidbodys are moved via their Transform component. If objects with rigidbodies are moved via their Transform, a hole will appear below your chair and suck you into oblivion. Don’t ever forget this.

There are several ways to make an object move via its rigidbody. AddForce and AddTorque are commonly used. I personally cannot stand the effects of AddForce. In pressing movement keys, I build up in excess a bunch of forces, and the momentum keeps the object moving long after I’ve let go of the keys. Such systems feel either unresponsive or hyper-sensitive to me.

The good news is that we can have more direct control by assigning to the rigidbody’s velocity property:

rigidbody.velocity = new Vector3(...);

Let’s get the tank moving forward and backward when the player hits W and S, respectively.

Declare a private instance variable for the rigidbody component. A superclass already has this field declared, so we get a warning when we declare a second. One way to avoid that warning is by using the new keyword in the declaration:

private new Rigidbody rigidbody;

In Start, use GetComponent to assign this reference.

Method Update is called once per frame. It’s normally where we listen for user input. However, it runs at an inconsistent rate—usually as fast as the CPU and GPU allow. The physics engine, on the other hand, updates at a fixed rate. For our objects to behave normally, we must update rigidbodies in the FixedUpdate method.

Inside FixedUpdate, use Input.GetAxis("Vertical") to get the current “pressure” reading on the W and S keys. The result is a value in [-1, 1]. Apply this pressure to the tank’s forward vector (available via its Transform). Use this scaled vector to set the rigidbody’s velocity.

Playtest. Does the tank move forward and backward?

Speeding Up

Probably the tank moves slowly. Let’s fix that. Add a public float named speed to the PlayerController. Assign to it some non-zero value in the Inspector. Apply it to the vector used to the set the velocity.

Playtest. Does the tank move more quickly?

You may find that the tank moves too easily. I tweaked the mass and froze some of the axes of rotation on the rigidbody to eliminate unwanted behavior.

Heading Movement

In addition to moving forward and backward, we also want the tank to turn left or right when we press A or D. These keys will rotate, not strafe. If you’d rather use the more conventional Q and E, feel free to add an input axis in Edit / Project Settings / Input.

First, get the torque force using Input.GetAxis("Horizontal"). (If you made a new axis, use its name.) Then use the rigidbody’s AddTorque method to apply rotational force…around which axis?

Playtest. You may want to adjust the angular drag on the rigidbody.

Sync Prefab

If you made any changes to the tank empty game object, it will be out of sync with its prefab. Select the empty and hit Apply to propagate any changed values up to the prefab.

Third-person Camera Position

Now it’s time to link our camera to trail behind our tank object and give a third-person perspective to our game. When the tank rotates, we want the camera to rotate. When the tank moves, we want the camera to move. One way to do this would be to make the camera a child of the the tank. However, later on we’ll allow the camera to latch on to some other tank, so let’s implement this without a parent/child relationship.

Add a CameraController script to your camera object. Declare in it a public Transform for the camera’s target. In the Inspector, drop the tank into this field.

In the Update method, we want to position the camera behind and above the target. The behind direction is the inverse of the target’s forward direction: -target.transform.forward. The above direction is the y-axis: Vector3.up. The anchor point is target.transform.position. Combine these Vector3s to position the camera in a third-person perspective.

Playtest. Does the camera move?

Likely the camera is too close to the tank. Let’s increase the distance behind and above the tank. Add two public floats: distanceAbove and distanceBehind. Use these values to scale the offset vectors.

LookAt

The previous step just positioned the camera. We also want the camera to pivot around its origin to look at the target. There’s a very handy LookAt method in the transform component. Use it to rotate the camera toward its target.

Playtest.

Switching Camera Target

Let’s allow the player to explicitly switch which tank it trails behind by clicking on a different one. (Later, when a tank gets shot it becomes a tank of the opposing color, and we will also want the camera to move to a different tank.)

In the CameraController’s Update method add a conditional to see if the left mouse button is down. Place it before the existing transform manipulation code, as we may be updating the target referenced in that code.

If the mouse is down, we want to do something like this:

shoot ray from eye through mouse position into scene
if ray hits a tank
  make hit tank the camera's target

What’s this look like in C#? The first step is to generate a ray from the eye into the scene. The Camera class can help:

Ray ray = camera.ScreenPointToRay(Input.mousePosition);

You’ll need a reference to the camera object for this to work.

Next we need an out variable to hold the raycast’s hit information.

RaycastHit hit;

This will act as a second return value to the Raycast method:

Physics.Raycast(ray, out hit)

The regular return value is a boolean indicating whether or not anything was hit. Add a conditional for some code to only be run when there’s a hit. In the then block, print out the name of the hit object:

print(hit.collider.gameObject.name);

Playtest. Does the name of the object you click on pop up in the status bar?

Layer Masks

We only want to know about clicks on tanks. We can add more parameters to the Raycast call to specify in which layers we’d like to focus our attention.

First, select the tank empty prefab. Add a Layer named Tank and place the empty and its child in this layer.

Notice that you are limited to 32 layers. Why might this be?

To restrict our raycast to just the tank layer, we must find the layer’s number and use it to shift a bit into the appropriate slot in the so-called layer mask. Since Raycast has a bunch of default parameters, we must also add an explicit actual parameter for the ray’s length:

Physics.Raycast(ray, out hit, Mathf.Infinity, 1 << LayerMask.NameToLayer("Tank"))

Locking On

Now that we only detect clicks on tanks, let’s update the camera’s target to be the transform of the tank the player has clicked on.

Add a couple of other tank instances to the scene.

Playtest. Can you alter targets?

Moving Just One Tank

Currently when you try to move a tank, they all move. Let’s only move the currently selected one.

Select the tank prefab in the assets and uncheck the PlayerController script. This will disable the script but not remove it.

Re-enable the PlayerController script on just the one tank game object that the camera is initially locked onto.

When another tank is targeted, disable the PlayerController on the old tank and enable it on the new one:

target.GetComponent<PlayerController>().enabled = false;
hit.collider.gameObject.GetComponent<PlayerController>().enabled = true;

Playtest. Does just one tank move?

Smooth Transitions

You probably noticed that the transition is a little abrupt. Let’s smooth it out with an animation! We’ll use a coroutine, a process that runs alongside the normal game loop. Coroutines that blend between two states look something like this:

IEnumerator Transition(Transform newTarget) {
  // calculate start state
  // calculate end state

  float startTime = Time.time;
  float targetTime = howLongYouWantTheAnimationToTake;
  float elapsedSeconds = 0.0f;

  do {
    elapsedSeconds = Time.time - startTime;
    float proportion = Mathf.Clamp01(elapsedSeconds / targetTime);

    // state = Lerp(start state, end state, proportion);

    yield return null; // wait a frame before next tick
  }

  target = newTarget;
} while (elapsed < targetTime);

Let’s start by blending between target and newTarget‘s position. Here we calculate the starting state (where the camera is currently) and ending state (where we want the camera to be behind and above the new tank):

Vector3 startPosition = transform.position;
Vector3 endPosition = sameCalculationAsYouHaveInUpdateButForNewTarget;

Now, inside the loop, we linearly interpolate (lerp) between the two:

transform.position = Vector3.Lerp(startPosition, endPosition, proportion);

To get this coroutine started, head back up to Update. Add a private isTransitioning variable and issue this call:

isTransitioning = true;
StartCoroutine(Transition(hit.collider.gameObject.transform));

Since the coroutine adjusts our camera, we want to skip the other code in Update that also adjust the camera if we are currently transitioning:

if (!isTransitiong) {
  transform.position = ...
  transform.LookAt(...);
}

At the end of the Transition coroutine, we can safely update target and say we’re done transitioning:

target = newTarget;
isTransitioning = false;

Playtest. How do things look?

Smooth LookAt

The coroutine so far just blends between positions. We also want to blend the rotation. After lerping the position but before updating the target, add another lerp pattern, this time blending between rotation Quaternions:

Quaternion startRotation = transform.rotation;
Quaternion endRotation = Quaternion.LookRotation(newTarget.position - transform.position);

startTime = Time.time;
targetTime = 0.5f;
elapsed = 0.0f;

do {
  elapsed = Time.time - startTime;
  float proportion = Mathf.Clamp01(elapsed / targetTime);
  transform.rotation = Quaternion.Lerp(startRotation, endRotation, proportion);
  yield return null;
} while (elapsed < targetTime);

Playtest. Is switching tanks nice and gentle?