CS 491 Lecture 12 – Properties and Projectiles
TODO
- Watch Nav Meshes. We’ll implement these next class and you will need them in the next homework assignment. On a 1/4 sheet, write down your plan for the environment of your third-person controller. What agents will automatically target objects in your scene? What will they target? What obstacles will they need to avoid?
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?