teaching machines

CS 491 Lecture 12 – Properties and Projectiles

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

TODO

Lab

Today we pick up where we left off in Our Color vs. Their Color, the sequel to Our Animal vs. Their Animal. If you didn’t finish the camera switching coroutines from last time, start there. Ask plenty of questions if things are behaving strangely! From there we’ll add shot that can be picked up and fired at tanks of the opposing color.

Shot

First let’s add a sphere game object. Name it Shot, give it a rigidbody component, and turn it into a prefab. Delete the instance from the scene.

Give the prefab a ShotController script, which we’ll leave blank for the time being.

Spawner

Where does this shot come from? We don’t want each tank to carry an endless supply, because infinite bullets ruin games. Instead, let’s have the shot will fall from the sky. Add a ShotSpawner empty to the scene. Place it somewhere near your default tank so you can quickly test things out without having to first drive over to spawner.

Give it a ShotSpawnerController script which will continuously emit shot. Give it a public game object named shotPrefab. Drop in the shot prefab in the Inspector.

Let’s create a coroutine to do the emitting:

IEnumerator Emit() {
  while (true) {
    Instantiate(shotPrefab, transform.position, Quaternion.identity);
    yield return new WaitForSeconds(timeBetweenDrops);
  }
}

Fire off the coroutine in Start with StartCoroutine.

Playtest. Does shot fall?

Team Enum

To track what side a tank is on and from which side a fired shot comes from, let’s add an enum named Team. Create a new C# script, but delete all the default text and replace it with this simple enum:

public enum Team {
  A, B, NEUTRAL
}

Team Materials

Let’s color each tank and shot according to the team to which it belongs. Create three materials, one for team A, one for team B, and one neutral color, used for shot that hasn’t been claimed.

Coloring Tanks

Let’s use our work from the previous two steps to get tanks colored according to their team. Add a TankController script to the tank parent prefab. Give it a public Team variable and a public Material array with two elements. The Team variable will be assigned per instance, but we can safely assign the materials on the prefab.

With the prefab selected, drop in materials A and B into the array in the Inspector.

Add an UpdateColor method that colors the tank according to its current team. Color is determined by the material, which is a property of the MeshRenderer component that’s part of the tank model object, which is a child of the parent. So, we must do some querying to get at what we need:

Transform child = transform.Find("REPLACE WITH NAME OF THE CHILD TANK OBJECT");
child.GetComponent<MeshRenderer>().material = materials[team == Team.A ? 0 : 1];

Call UpdateColor in Start to assign each tank its initial color. Find some of your tank instances in the hierarchy and alter their team in the Inspector.

Also add a public Flip method. If the tank is of team A, switch it to B. And vice versa. Update the color after switching. We’ll call this method later when a tank gets hit by shot from the other team.

Playtest. Do you see tanks of opposing colors?

Tank’s Shot Property

Let’s handle picking up the shot now. For this, we’re going to use a feature of C# that is worth knowing: properties. Properties let you expose state of an object in a controlled way. Outside clients feel like they’re directly accessing instance variables through natural syntax, but in reality they are calling special accessor methods that give you complete control over that state. In essence, properties let methods masquerade as variables. There may not even be an instance variable behind the property! The value might be derived from some other data.

The state that we’ll make a property for is the shot that a tank may or may not be holding. We first add a backing store for this shot:

private GameObject _shot;

Note that it’s private. Now let’s add a public property to manage access to this state:

public GameObject shot {
  get {
    return _shot;
  }
  set {
    // we'll expand this soon
  }
}  

Whenever someone expresses tank.shot, this will behave like a call to tank.getShot(). Whenever someone expresses tank.shot = rhs, this will behave like a call to tank.setShot(rhs). If we ever want direct access to the state, we just use _shot.

Shot’s Team Property

Let’s also add a team property to the shot. We’ll use this to color the shot according to its current team. First, add a Material array with three elements to the shot prefab, and drop in materials A, B, and neutral. Add an UpdateColor method:

void UpdateColor() {
  MeshRenderer renderer = GetComponent<MeshRenderer>();
  if (team == Team.A) {
    renderer.material = materials[0];
  } else if (team == Team.B) {
    renderer.material = materials[1];
  } else {
    renderer.material = materials[2];
  }
}

Now add a team property, whose setter triggers an update of the color:

private Team _team;
public Team team {
  get {
    return _team;
  }

  set {
    if (value != _team) {
      _team = value;
      UpdateColor();
    }
  }
}

Claiming Shot

Initially, let’s have a tank claim a shot when it collides with it. We could handle this collision in either TankController or ShotController. Let’s do it in ShotController. Since shots will collide with many things (ground, obstacles), give the tank parent prefab tag Tank. Then we can add a discriminating OnCollisionEnter method:

void OnCollisionEnter(Collision collision) {
  if (collision.gameObject.tag == "Tank") {
    // assign this shot to the TankController
  }
}

Replace the comment with an assignment to the tank’s shot property.

Then, back in TankController, expand the set to slurp up the shot only if it doesn’t already own one:

set {
  if (_shot == null) {
    _shot = value;
    // assign this shot's team to tank's team
  } else if (value == null) {
    _shot = null;
  }
}

Playtest. When you run into a shot, does it change color?

Picking Up Shot

Instead of just leaving the shot on the ground, let’s pick it up. In the setter for the shot’s team, if the team is not NEUTRAL, use SetActive(false) on the game object to hide the shot that just got picked up.

Playtest. Do the claimed shots disappear?

Firing Shot

Let’s give the tank the ability to fire the shot it’s holding. Add a public Fire method that looks something like this:

if tank has a shot
  make shot active again
  set it's position to be tank's + tank's forward * some scale
  add forward force to shot's rigidbody
  shot = null

Then, in PlayerController, let’s trigger this when the Fire1 button is down. By default, Fire1 is the left mouse button or left Control key. We listen for user input in the Update method:

if (Input.GetButtonDown("Fire1")) {
  // get TankController and fire
}

Playtest. Can you fire the shot?

Flipping Colors

When a tank gets hit by a fired shot, let’s flip its color. Head back to ShotController.OnCollisionEnter. Instead of blindly assigning the shot’s owner, let’s expand the logic a bit:

if collided with tank
  if shot is neutral
    assign shot to tank
  else if shot's team != tank's team
    flip tank's color
    destroy shot

Playtest. Do tanks switch color when hit?

Neutralizing Misses

When a shot hits the ground or any other non-tank object, let’s neutralize it so it becomes fair game for future pickups. In OnCollisionEnter, let’s add a case for hitting non-tanks:

if collided with tank
  ...
else
  set shot's team to NEUTRAL

Halo

It’d be nice if you could look at a tank and see if it currently carried a shot. Let’s add a reddish glow to tank’s packing heat.

Unity provides a halo effect that we can use, but there appear to be some bugs or just unsupported features that make it difficult to enable or disable dynamically. If we add a Halo component, we should be able to say something like this:

tank.GetComponent<Halo>().enabled = true;

But I can’t get this to work. Instead, let’s add a child to the tank parent prefab. Position it right at (0, 0, 0). Give it a Halo component and switch the color to something dangerous. Deactivate the child object in the Inspector by checking the box in the upper-left corner.

In TankController, add a private game object named halo. In Start, grab a reference to it by searching for it:

halo = transform.Find("Halo").gameObject;

In the shot property’s setter, activate this object when a shot is picked up, and deactivate it when the shot goes null.

Playtest. Do the halos appear when you pick up a shot and disappear when you fire?