Chamillard-C-Unity-Book Part 2
Chamillard-C-Unity-Book Part 2
Now that we've identified the fields and methods for our Fish class (we don't need any properties for
this class), we can generate our UML and move on to the Write Test Cases step.
Unity Mice, Keyboards, and Gamepads 175
Our first test case is the same one we used to test our “game” in Section 7.6:
Test Case 1
Checking Teddy Bear and Spawn Behavior
Step 1. Input: None.
Expected Result: Game runs with the following characteristics:
Each teddy bear dies (is removed from the game) after approximately 10 seconds
New teddy bears appear at random intervals of approximately 1 to 2 seconds
New teddy bears are randomly selected from the 3 teddy bear colors
New teddy bears are spawned at random screen locations
Test Case 2
Checking Fish Movement
Step 1. Input: Left arrow key
Expected Result: Fish moves and faces left
Step 2. Input: Up arrow key
Expected Result: Fish moves up and keeps facing left
Step 3. Input: Right arrow key
Expected Result: Fish moves right and face right
Step 4. Input: Down arrow key
Expected Result: Fish moves down and keeps facing right
Step 5. Input: Left and Up arrow keys simultaneously
Expected Result: Fish moves up and left and faces left
Step 6. Input: Right and Down arrow keys simultaneously
Expected Result: Fish moves down and right and faces right
Finally, we want to make sure the collisions between the fish and a teddy bear work properly:
Test Case 3
Checking Fish/Teddy Bear Collisions
Step 1. Input: Collide with teddy bear with head of fish
Expected Result: Teddy bear removed from game
Step 2. Input: Collide with teddy bear with top, bottom, or tail of fish
Expected Result: Teddy bear bounces off fish
To start, copy the entire folder for the “game” from Section 7.6. (from the code available on the Burning
Teddy web site) into a new folder. This saves us from redoing the work we did there. Run the game to
make sure everything still works fine; Test Case 1 should pass. Let's work on getting Test Case 2 to pass
next.
Use your operating system to copy the fish sprite sheet (from the code available on the Burning Teddy
web site) into the Sprites folder for the project. The fish sprite sheet contains two images (facing right
and facing left). These aren't two frames for an animation, they're the images we want to display based
on which direction the fish is facing. Select the fish sprite in the sprites folder in the Project window and
change the Sprite Mode in the Inspector to Multiple. Use the Sprite Editor to slice the sprite into two
separate images (don't forget to click the Apply button before closing the Sprite Editor).
Drag the fish sprite (not fish_0 or fish_1, the sprite with the arrow to the left of it) from the Sprites
folder in the Project window into the Hierarchy window and change the name of the new game object to
Fish. Add a Box Collider 2D component and edit the collider to tightly match the body of the fish; the
fins should be mostly outside the collider. You can check that the collider fits both fish_0 and fish_1
properly by clicking the small circle next to the Sprite field in the Sprite Renderer component and
selecting the sprite you want to check.
Add a new Fish script to the Scripts folder in the Project window and drag the new script onto the Fish
game object in the Hierarchy window. Open the Fish script in Visual Studio and paste the code from the
TeddyBear script from the Section 8.3. solution (from the code available on the Burning Teddy web
site). Change the name of the class to Fish, delete the prefabExplosion field, delete the code in the
Unity Mice, Keyboards, and Gamepads 177
Updatemethod that blows the game object up on space bar (no exploding fish here!), and change all the
comments from teddy bear to fish.
Select the Fish game object in the Hierarchy window and set the Move Units Per Second field in the
script in the Inspector to 5. If you run the game now, you should be able to drive the fish around with the
keyboard, but it won't face left when you move to the left. You probably also noticed that the teddy
bears already bounce off the fish – that's the Unity physics system at work!
Okay, let's get the fish to face left and right correctly. Add Sprite fields marked with
[SerializeField] (so we can populate them in the Inspector) to hold the facing right
(facingRightSprite) and facing left (facingLeftSprite) sprites. Add a field called spriteRenderer
to hold a reference to the SpriteRenderer component. Add code to the Start method to save the
SpriteRenderer component in the spriteRenderer field and to set spriteRenderer.sprite to
facingRightSprite.
Now we need to change the code in our Update method that moves the fish based on Horizontal input:
Our code is a little more complicated because we need to know the direction of the Horizontal input so
we can set the sprite for the sprite renderer properly.
Go to the Unity editor and select the Fish game object in the Hierarchy window. Drag the fish_0 sprite
from the Project window onto the Right Facing Sprite field in the script in the Inspector and drag the
fish_1 sprite from the Project window onto the Left Facing Sprite field in the script in the Inspector. At
this point, Test Case 2 should pass when you run it.
All we have left is making it so the fish can eat teddy bears with its head. Remember that we'll do that in
the OnCollisionEnter2D method. To make sure we get the method header correct, we'll copy the entire
method from the example in the documentation, add a comment above the method, and delete all the
code in the method body (the code between the { and the }). We'll fill in the method body soon, but we
need to figure out what we need to put there first.
Let's talk about our general idea for detecting collisions with the fish's head, then figure out the details.
One way to do this would be to use a bounding box around the fish's head; see Figure 8.7.
178 Chapter 8
When we detect a collision with a teddy bear, if the collider for the teddy bear intersects with the
bounding box at the fish's head, we'll eat (destroy) the teddy bear. When a collision occurs, we'll have to
first move the fish head bounding box to the appropriate side of the fish (the left if the fish is facing left
and the right if the fish is facing right). How do we actually get the collider for the teddy bear, though?
If we look at the method header for the OnCollisionEnter2D method, we see there's a Collision2D
object as the only parameter. The Collision2D documentation shows that we can access the collider
property of that object to get the teddy bear's collider. The collider is a Collider2D object, which has a
bounds property of type Bounds for the “world space bounding area of the collider.” Bounds objects are
bounding boxes (see Figure 8.8.), so if we have Bounds objects for the teddy bear collider and the fish
head, we can check to see if they intersect to determine whether or not the collision occurred at the head
of the fish.
So how did we know about all these different classes and properties? We didn't! We just started at the
OnCollisionEnter2D documentation and used the links to explore what information was available in
each of the classes and properties to figure out what would help us solve our current problem. You'll
find yourself doing this all the time as a game developer, even an experienced game developer!
Unity Mice, Keyboards, and Gamepads 179
It looks like we have all the pieces we need, so let's start implementing the actual code. We'll start by
adding several fields to the Fish class:
The headBoundingBox field holds the bounding box for the fish's head. The constant tells us how much
of the total fish collider represents the head of the fish; this will be useful when we create the bounding
box. We'll use the headBoundingBoxLocation field to move the bounding box to the correct side of the
fish on a collision. We save the x offset we need to apply from the center of the collider to move the
bounding box the correct amount to get it to the front of the fish, and we save a reference to the
BoxCollider2D component of the Fish so we don't have to look that up every time we have a collision.
For the headBoundingBoxLocation field, we could create a new Vector3 object every time in the
OnCollisionEnter2D method instead. Those objects would simply be used once and then take up
memory until the garbage collector runs, though, so we avoid that approach. We can't actually control
when the garbage collector runs, and it can run at very inopportune times, so if we avoid creating
garbage we delay the need to collect garbage. For many games and platforms this won't matter at all, but
a good general rule of thumb is to avoid repeatedly creating “short lifetime” objects if possible.
The next thing we do is add code to the Start method to initialize the fields we just added:
We obviously had some math to do here! For the calculation of the headBoundingBoxXOffset, we drew
a picture to help us figure out the required equation. We draw pictures all the time as we figure out how
to calculate stuff, though they're usually not quite as neat as Figure 8.9! By the way, we already have the
diff variable from our earlier work in the method saving the collider dimension value.
180 Chapter 8
Because we'll be changing the center field of headBoundingBox to move it to the correct side of the
fish (yes, we read the documentation again), we need half the width of the bounding box for the fish's
head; that's what we're calculating using (diff.x * HeadPercentageOfCollider) / 2. If we subtract
that value from colliderHalfWidth, we have exactly the amount we need to subtract (if we're facing
left) from the center of the fish collider (fishCollider.transform.position.x) to put the head
bounding box on the left side of the fish. If the fish is facing right, we'll simply add the x offset instead
of subtracting it. Whew!
The second line of code creates a new Vector3 for the location of the head bounding box. We set the
location as though the fish is facing right, but we'll change both the x and y components of this vector
when we're in the OnCollisionEnter2D method so the bounding box location is consistent with the
current location and orientation of the fish.
The final line of code calls the Bounds constructor to create a new object for our headBoundingBox
field. The constructor takes 2 arguments: a Vector3 for the center of the bounding box and a Vector3
for the size (width, height, and depth) of the bounding box.
Don't worry, the code we need in the OnCollisionEnter2D method is much less “math intensive!”
Here's our implementation of that method:
/// <summary>
/// Checks whether or not to eat a teddy bear
/// </summary>
/// <param name="coll">collision info</param>
void OnCollisionEnter2D(Collision2D coll)
{
// move head bounding box to correct location
headBoundingBoxLocation.y = fishCollider.transform.position.y;
if (spriteRenderer.sprite == leftFacingSprite)
{
headBoundingBoxLocation.x = fishCollider.transform.position.x -
headBoundingBoxXOfsset;
}
else
{
Unity Mice, Keyboards, and Gamepads 181
headBoundingBoxLocation.x = fishCollider.transform.position.x +
headBoundingBoxXOfsset;
}
headBoundingBox.center = headBoundingBoxLocation;
When we did our initial implementation of the OnCollisionEnter2D method, we discovered we weren't
destroying any of the teddy bears, even if we collided them with the head of the fish. We decided we
wanted to make the teddy bears move much more slowly so we could debug the problem; at that point,
we marked the minImpulseForce and maxImpulseForce variables in the TeddyBear class with
[SerializeField] so we could manipulate those values in the Inspector. We actually set both to 0 for
debugging – it's easy to run into a stationary teddy bear22! By the way, the bug was that we calculated
the new headBoundingBoxLocation correctly but forgot to set the headBoundingBox center field to it.
When we ran the game, we could eat teddy bears with the head of the fish, but we had to be pretty
precise with the collision. From a gameplay perspective, that was really irritating, so we actually made
the headBoundingBox stick out from the front of the collider a small amount and also made it slightly
taller than the actual collider (see the code accompanying the chapter). All the collisions still worked
properly, but the change made it easier to actually eat teddy bears, making the game much more fun to
play.
There are of course a number of other ways to solve the problem of determining whether or not the
collision occurs at the fish's head. One alternative we considered was to have a normal collider for the
80% of the fish that's not the head and a trigger collider (a collider with the Is Trigger box checked) for
the 20% of the fish that's the head. We decided not to use that approach because every time the fish
changed its horizontal direction, we'd need to move the collider and the trigger collider to the back and
front of the Fish game object. We prefer to use our approach, where we only have to move the head
bounding box to the front of the fish when a collision occurs, but we wanted to remind you that there are
other solutions to this problem.
We just have a little cleanup to do before we move on to the Test the Code step. Drag the Fish game
object from the Hierarchy window into the Prefabs folder in the Project window to make a Fish prefab.
Although that doesn't really help us in this game, if we built multiple scenes it would make it much
easier to spawn a Fish at the start of each scene.
22If you set them both to 0, you can also actually "herd" the teddy bears around with the fish. Not that you'd have fun doing
something like that ...
182 Chapter 8
Test Case 1
Checking Teddy Bear and Spawn Behavior
Step 1. Input: None.
Expected Result: Game runs with the following characteristics:
Each teddy bear dies (is removed from the game) after approximately 10 seconds
New teddy bears appear at random intervals of approximately 1 to 2 seconds
New teddy bears are randomly selected from the 3 teddy bear colors
New teddy bears are spawned at random screen locations
Test Case 2
Checking Fish Movement
Step 1. Input: Left arrow key
Expected Result: Fish moves and faces left
Step 2. Input: Up arrow key
Expected Result: Fish moves up and keeps facing left
Step 3. Input: Right arrow key
Expected Result: Fish moves right and face right
Step 4. Input: Down arrow key
Expected Result: Fish moves down and keeps facing right
Step 5. Input: Left and Up arrow keys simultaneously
Expected Result: Fish moves up and left and faces left
Step 6. Input: Right and Down arrow keys simultaneously
Expected Result: Fish moves down and right and faces right
Test Case 3
Checking Fish/Teddy Bear Collisions
Step 1. Input: Collide with teddy bear with head of fish
Expected Result: Teddy bear removed from game
Step 2. Input: Collide with teddy bear with top, bottom, or tail of fish
Expected Result: Teddy bear bounces off fish
For example, suppose we had 12,000 students at a university and we wanted to store the GPAs for all of
them. We'll learn an effective way to read them in in the next chapter (though somebody's fingers will
hurt typing them in until we learn how to use files), but where do we put them all? With our current
knowledge we could declare 12,000 distinct variables, one for each GPA, but there's got to be a better
way! There is, of course: we can use an array to store all the GPAs. With an array, we only need to do a
little more work than we do when we declare any other kind of variable. Let's take a closer look.
This syntax looks a lot like what we used to create objects. That's for a very good reason – arrays in C#
ARE objects!
Problem Description: Declare and create the array variable required to store 10 GPAs.
With the above array variable creation, our array will consist of 10 “boxes” or elements. In C# arrays (as
in many other programming languages), the elements are numbered starting at 0, so the elements of our
184 Chapter 9
array will be numbered from 0 to 9. The number for a particular element is called the index of that
element. Each element in the array will be a float, which also seemed to be a clear choice given that
each element holds a GPA. No matter what data type we pick for the array elements, every single
element of the array will be of that type. In other words, we can't have some float elements, some int
elements, etc. in a single array.
A pictorial representation of gpas appears in Figure 9.1. The array consists of 10 elements, numbered
from 0 to 9, and each element holds a float. Note that if the elements of the array are a value type, then
each element of the array is initialized with the same initial value a variable of that data type would be
initialized to. For float, that initial value is 0.0.
So that's how we declare and create an array variable. And if we wanted to change this array variable to
hold 12,000 students instead of 10, all we'd have to do is change the 10 we put between the square
brackets above to 12000!
There's actually an alternate way to create an array object, where we also assign values to the array
elements when we create the object; the syntax for that is provided below. When we use this syntax, we
Arrays and Lists 185
provide the values for each of the array elements between curly braces, separated by commas. The first
value provided goes into element 0 of the array, the second value goes into element 1, and so on. Notice
that we don't have to explicitly say how many elements are in the array; C# simply creates an array
object with just enough elements to hold the values we provide.
Assignment
variableName[index] = value;
variableName[index] = float.Parse(Console.ReadLine());
Output
Console.WriteLine(variableName[index]);
23In fact, there are some operations we can perform on entire arrays. They won't really be useful for the problems we'll solve
in this book, though, so we won't bother covering them here.
186 Chapter 9
sum += variableName[index];
The key difference between array variables and the other kinds of variables we've used up to this point is
that we have to say which element of the array we want to use. The bottom line for all these accesses is
that we reference a particular element in the array by providing the array variable name followed by an
index between square brackets. For example, if we wanted to set the first element of our gpas array to
4.0, we could simply say
gpas[0] = 4.0;
Similarly, if we wanted to print out the 8th element of the gpas array, we could use
Remember, because we start counting at 0, the first element is at index 0 and the 8th GPA is at index 7.
We could also read a GPA into the 0th element using
Finally, if we were adding the last element in gpas to a variable called sum, for example, we'd use
sum += gpas[9];
This will be easier to understand if we work through an example together, so let’s build an array
containing 5 Card objects (remember the Card class from the Classes and Objects chapter?). Based on
the syntax for creating an array variable as described in section 9.1, we’d use:
This will give us an array of 5 elements, each of which will be a Card object. But wait a minute, you
say. Even though we’ve created the array object, don’t we need to actually create each of the card
objects contained in the array? Right you are! At this point, each element in the array is null because
that's what reference types get for their initial value.
Now, we hope you have one more question about arrays of objects. This would be a good question: How
do you access a property or call a method for one of the array elements? For example, how would we
print the Rank and Suit properties of the third card in the array?
It’s actually really easy. Remember that we refer to an element in the array by using the syntax
variableName[index]
Well, we do the same thing with our objects in the array. So we can access the Rank property for the
third card in the array using
hand[2].Rank
Because we know the hand array holds Card elements, we know that hand[2] is a Card object. That
means we can treat hand[2] like any other Card object, so we can access its properties and methods
using the standard dot notation. So we could print the rank and suit of the third card in the array using
Arrays of objects can be useful, and they’re pretty easy to use too. Just remember that you need to create
both the array AND the object for each element of the array and you’ll be fine.
namespace RefactoredCards
{
/// <summary>
/// An enumeration for card ranks
/// </summary>
public enum Rank
{
Ace,
Two,
Three,
Four,
Five,
Six,
Seven,
Eight,
188 Chapter 9
Nine,
Ten,
Jack,
Queen,
King
}
}
namespace RefactoredCards
{
/// <summary>
/// An enumeration for card suits
/// </summary>
public enum Suit
{
Clubs,
Diamonds,
Hearts,
Spades
}
}
Now that we have enumerations for card rank and suit, we change the Card class to use those
enumerations instead of string; the refactored class is shown below.
namespace RefactoredCards
{
/// <summary>
/// A playing card
/// </summary>
public class Card
{
#region Fields
Rank rank;
Suit suit;
bool faceUp;
#endregion
#region Constructors
/// <summary>
/// Constructs a card with the given rank and suit
/// </summary>
/// <param name="rank">rank</param>
/// <param name="suit">suit</param>
public Card(Rank rank, Suit suit)
{
Arrays and Lists 189
this.rank = rank;
this.suit = suit;
faceUp = false;
}
#endregion
#region Properties
/// <summary>
/// Gets the card rank
/// </summary>
public Rank Rank
{
get { return rank; }
}
/// <summary>
/// Gets the card suit
/// </summary>
public Suit Suit
{
get { return suit; }
}
/// <summary>
/// Gets whether or not the card is face up
/// </summary>
public bool FaceUp
{
get { return faceUp; }
}
#endregion
/// <summary>
/// Flips the card over
/// </summary>
public void FlipOver()
{
faceUp = !faceUp;
}
#endregion
}
}
We changed a number of things in the Card class. First, we changed the rank field to be a Rank rather
than a string and we changed the suit field to be a Suit rather than a string. This gives us all the
benefits of enumerations that we discussed previously for these fields.
190 Chapter 9
We also needed to change the parameters for the constructor to be a Rank and a Suit rather than the two
string parameters that we originally had. The last thing we did was change the return types for the
Rank and Suit properties. For example, we changed the first line of the Rank property from
to
This might now look a little confusing to you, but if you remember that the first Rank in the line above is
the data type for the property and the second Rank is the actual name of the property it should be clear.
Some beginning programmers might try to avoid confusing themselves by using something like
instead. This is a bad approach, though, because when we access the property using a Card object called
myCard (for example) we'd have to use myCard.CardRank instead of myCard.Rank; the CardRank name
is less intuitive. The bottom line is that making things a little clearer for the class developer makes
things harder for the consumer of the class, and our goal should always be to make our classes as easy to
use as possible.
Speaking of the consumer of the class, we're done refactoring the Card class itself so we should look at
how those changes affect consumers of the class. We still declare the array of Card objects the same
way we did before, so the first place we need to change is the code that actually fills the hand with cards.
Recall that we used to have
We used to pass two string arguments to the constructor, but we now need to pass a Rank argument
and a Suit argument to the constructor. What about our output? We leave it exactly the same as it was!
Here's the line of code for printing information about the third card:
When the Rank and Suit properties returned strings, it was easy to see how the above code would
print the concatenation of the three strings. It's less clear, however, why this works now that they
return Rank and Suit objects.
Because the properties are accessed as part of the argument to the call to Console.WriteLine, the
values of those properties are automatically converted to strings. This has been happening all along, of
course; when we include an int variable in a call to Console.WriteLine, the value of that variable is
automatically converted to a string. This just seemed like the right time to talk about it.
That concludes our discussion of the refactored Card class, so let's take a look at multi-dimensional
arrays.
variableName[rowNumber,columnNumber]
For example, to put a 1 into the upper left corner of the array (row 0, column 0), we'd say
grid[0,0] = 1;
Columns are numbered from left to right, and rows are numbered from top to bottom. We're not limited
to two-dimensional arrays either; we can essentially use as many array dimensions as we want.
9.6. Lists
Arrays are very useful for storing multiple values that are all the same data type, but there is one
significant limitation. We have to know the maximum number of elements that will be in the array when
we create the array object using new. This adds some complexity to processing the array in a number of
ways. If the array is only partially filled, for example, we need to keep track of how many elements are
currently in the array so we know where to add the next element in the array. If we end up filling the
array and needing more space because we misjudged how many elements we'd need to store, we'd need
to create a new (larger) array and copy all the elements from the old array to the new one. Arrays are
very powerful structures that are provided by most modern programming languages, but they have some
important limitations when we need to dynamically grow the arrays.
C# provides a set of built-in collection classes that help solve this problem. Basically, these classes let
us store a collection of elements, and they also provide lots of useful properties and methods for
manipulating those collections. We'll focus on the List class, which is in the
System.Collections.Generic namespace, but you can take a look at the complete listing of
192 Chapter 9
collections by
searching the documentation for the System.Collections.Generic and
System.Collections namespaces.
The List class is called a generic class because it's generic enough to hold elements of any type you
want. You will, however, have to specify what that type is when you create the new List object. In
other words, all the elements in a list need to be all the same data type just like they were in an array.
Let's make our hand of cards a List rather than an array. This is actually a really good idea, because
typically the number of cards in a hand changes a lot, and that's much easier to deal with using a List.
Here's how we initialize the hand:
This looks a lot like creating objects of any other type, except we have the <Card> part added both when
we declare the type (before the variable name) and when we create an object of the type (after the new).
Remember, the List class is a generic class, so we have to tell it the data type of the elements it will
hold. We do that between the < and the >, so the above code declares and creates a List object that will
hold Card elements.
As we said above, there are lots of useful properties and methods provided by the List class; some of
the most useful ones for beginning programmers are shown below (of course, the full list is available
from the documentation).
Methods
Add
Adds an element to the end of the list
Clear
Removes all the elements from the list
Contains
Determines whether a particular value is in the list
IndexOf
Returns the zero-based index of the first occurrence of a particular value
Remove
Removes the first occurrence of a particular value from the list
Property
Count
Gets the number of elements contained in the list
Given the above information, we now know how to fill the hand with the 5 cards we used before:
Although the list of members and properties above are useful, they don't tell us how to actually access a
specific element in the list. Under the hood, we do that by accessing the Item property of the list, but
rather than using the syntax we're used to using for properties, we use the square brackets just as we did
to access specific elements of an array. In other words,
This works exactly the same way on our List as it did when we were using an array. Pretty slick, huh?
Although we'll use the List class extensively throughout this book from now on, there are lots of
collection classes that you'll find useful as you pursue more advanced programming projects.
• Start with a TeddyBear game object, centered in the window, not moving
• On every right mouse click, add a Pickup game object where the mouse was clicked
• When the player left clicks the TeddyBear, the teddy starts collecting the pickups in the order in
which they were placed
• The TeddyBear collects a Pickup by colliding with it, but this only works for the Pickup the
Teddy has currently “targeted for collection”
• Once the last Pickup has been collected, the Teddy stops moving
• If the player adds more Pickups while the Teddy is moving, the Teddy picks them up as well
• If the player adds more Pickups while the Teddy is stopped, the player has to left click on the
Teddy again to start it collecting again
Wow, that problem seems quite a bit more complicated than we've solved so far, even at the ends of the
previous chapters. That's okay, though, because it will give us a chance to practice using Lists and will
help us enhance our Unity skills as well.
The required program behavior should be clear, even though we have lots of other work ahead of us.
Design a Solution
First of all, let's identify the active game objects in our game. It turns out that we only have two kinds:
the Teddy Bear and the Pickups. We'll definitely want a Pickup prefab because we'll be creating new
pickup objects in the scene when the player right clicks on the screen. Although we'll only have a single
Teddy Bear game object in the game, we'll create a TeddyBear prefab also.
What about scripts? We'll start by figuring out what scripts we need for the active game objects, then
identify any additional scripts we need. We can immediately tell that we'll need a TeddyBear script
because the Teddy Bear game object has specific behaviors it has to perform during the game (like
starting to collect pickups when the mouse is left clicked on it).
194 Chapter 9
Do we need a Pickup script? No, we don't. The Pickups in the game don't actually do anything, they just
sit there waiting to be picked up.
That's it for the active game objects. Do we need any higher-level scripts to run the game? That depends
on how we want to keep track of the Pickups currently waiting to be picked up. One approach would be
to hold those in a list in the TeddyBear script, in which case we don't need any more scripts.
We're going to take a different approach here. We'll write a TedTheCollector script, which we'll attach
to the Main Camera, to hold the list of Pickups in the game. From an object-oriented perspective, it
doesn't really make sense to have the Teddy Bear keep track of other kinds of game objects in the game;
instead, the Teddy Bear should just keep track of its own state and behavior. We'll find that we almost
always need a high-level “game manager script” that handles game-level kinds of things, and this is a
better object-oriented approach as well. We'll start following that approach in our solution to this
problem.
Let's design our TeddyBear script first, starting with the fields. We really only need to keep track of
whether or not the Teddy Bear is currently collecting Pickups because we only want to respond to left
clicks on the Teddy Bear is it's not currently collecting; that field should be a bool. For now, let’s
assume that’s the only field we need.
We'll need two methods (at this point) as well, but they're already provided in the default script. We'll
use the Start method to make sure the Teddy Bear is centered in the screen when the game starts and
we'll use the Update method to respond to left mouse clicks when they occur on the Teddy Bear. We'll
discover later on that we actually need another method, but we'll add that when we discover that we
need it.
The UML for the TeddyBear script is shown in Figure 9.5. Note that we don't need any properties for
this script since there won't be any consumers that need access to its state.
For the TedTheCollector script, we know we'll need a list of the Pickups that are currently in the game;
we'll make that a list of GameObjects. That's actually the only state information we need for this script.
Because the TedTheCollector script is managing the list of Pickups in the game, we need to keep the
default Update method so we can detect right mouse clicks and add a Pickup to the game when that
happens.
Arrays and Lists 195
We'll also, for the first time in the book, need some properties and methods (in Unity) that other classes
use rather than just having methods for “internal use.” Let's think about why we need those properties
and methods by thinking about how the game works.
When the TeddyBear Update method detects that the left mouse button has been left clicked on the
Teddy Bear game object the script is attached to, it needs to start moving toward the oldest Pickup in the
game. The TedTheCollector script is the class that has the list of Pickups, though, so it should expose a
TargetPickup property that returns the oldest Pickup in the game. The TeddyBear class can then access
that property to get its target when it's left clicked.
We also need to think about what happens when the Teddy Bear collides with the Pickup that it's
currently targeting. The Teddy Bear should get its next target (which it can get from the
TedTheCollector TargetPickup property), but the Pickup that has just been collected should also be
removed from the game. Again, the TedTheCollector script is the class that manages the Pickups in the
game, so it should expose a RemovePickup method that removes a given Pickup from the list of Pickups
and destroys that pickup to remove it from the game. The TeddyBear script can then call that method
when it collects a Pickup.
Test Case 1
Checking Game Behavior
Step 1. Input: Right Click
Expected Result: Pickup placed at click location
Step 2. Input: Left Click on Teddy Bear
Expected Result: Teddy Bear collects Pickup and stops
Step 3. Input: Left Click on Teddy Bear
Expected Result: No response (there's no pickup to collect)
Step 4. Input: Right Click
Expected Result: Pickup placed at click location
Step 5. Input: Right Click
Expected Result: Pickup placed at click location
196 Chapter 9
We have a lot of steps here, but this test case thoroughly tests the required game behavior. Notice that
Step 7 has a timing constraint on the input so we can make sure Pickups that are added while the Teddy
Bear is moving are collected properly.
We start by creating a new Unity 2D project, renaming SampleScene to Scene0, importing a teddy bear
sprite and a pickup sprite into a Sprites folder, and creating an empty Prefabs folder. Next, we'll build
the Pickup and TeddyBear prefabs, then move on to the scripting piece. We'll alternate between the
TeddyBear and TedTheCollector scripts as we implement the game functionality a little at a time.
Drag the teddy bear sprite from the Sprites folder in the Project window and drop it in the Hierarchy
window. Change the name of the game object in the Hierarchy window to TeddyBear. Create a new
Scripts folder in the Project window, then create a new C# Script called TeddyBear in the Scripts folder.
Drag the TeddyBear script onto the TeddyBear game object in the Hierarchy window, then drag the
TeddyBear game object from the Hierarchy window onto the Prefabs folder in the Project window. We
now have the TeddyBear prefab we need for the game.
Next, drag the pickup sprite from the Sprites folder in the Project window and drop it in the Hierarchy
window. Change the name of the game object in the Hierarchy window to Pickup. Drag the Pickup
game object from the Hierarchy window onto the Prefabs folder in the Project window. We now have
the Pickup prefab we need for the game. Since we don't want any Pickups in the scene when we start the
game, right-click the Pickup game object in the Hierarchy window and Delete it.
Create another C# script called TedTheCollector in the Scripts folder in the Project window and drag
that script onto the Main Camera.
Okay, we're ready to start working on our scripts. Because we know the fields, properties, and methods
we need in each script from our design work, we'll implement the fields and interface (the properties and
methods each script exposes) for each script, then implement the actual functionality iteratively. As we
do this, we'll create stubs for the TargetPickup property and the RemovePickup method in the
TedTheCollector class. A stub basically has the minimum amount of code required to make it compile.
As we iteratively write our code, we add the required functionality to our classes until we've “fleshed
out” all the stubs (and added any other fields, properties, and methods we discovered we needed).
Let's start with the TeddyBear script; our initial cut at the code is shown below:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// A collecting teddy bear
/// </summary>
Arrays and Lists 197
public class TeddyBear : MonoBehaviour
{
#region Fields
#endregion
#region Methods
/// <summary>
/// Start is called before the first frame update
/// </summary>
void Start()
{
// center teddy bear in screen
transform.position = Vector3.zero;
}
/// <summary>
/// Update is called once per frame
/// </summary>
void Update()
{
#endregion
}
We initialize the collecting field to false because the teddy bear isn't collecting when it's added to
the scene at run time.
In the Start method, we're making sure the Teddy Bear is centered in the screen and at z == 0; with the
camera aiming at the origin in world coordinates, putting the Teddy Bear at (0, 0) in x and y centers it in
the screen. Vector3.zero is a vector with 0 values for x, y, and z, so setting transform.position to
that vector does exactly what we need. Although our Teddy Bear prefab defaults to being at (0, 0, 0) in
x, y, and z, including the centering code in the Start method ensures that inadvertent position changes
in the Unity editor don't result in incorrect program behavior for the centered on startup requirement.
If you run the game now, you'll see that the game starts with the Teddy Bear centered on the screen. You
can even verify the centering by changing the location of the TeddyBear game object in the scene in the
Unity editor; when you run the game, the Teddy Bear is centered as required.
Let's move over to the TedTheCollector script; our starting point for that code is shown below:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// Game manager
/// </summary>
public class TedTheCollector : MonoBehaviour
198 Chapter 9
{
#region Fields
#endregion
#region Properties
/// <summary>
/// Gets the next target pickup for the teddy bear to collect
/// </summary>
/// <value>target pickup</value>
public GameObject TargetPickup
{
get { return null; }
}
#endregion
#region Methods
/// <summary>
/// Update is called once per frame
/// </summary>
void Update()
{
/// <summary>
/// Removes the given pickup from the game
/// </summary>
/// <param name="pickup">the pickup to remove</param>
public void RemovePickup(GameObject pickup)
{
#endregion
}
When we created the pickups field we also called the List constructor to create our new List object;
that way we don't have to create the List object the first time we want to add a Pickup to the field.
Finally, the TargetPickup property and the Update and RemovePickup methods are stubs as discussed
above.
Of course, the game behaves exactly the same as it did before when we run it because we're not doing
anything in this script yet.
Okay, we've been doing a lot of work so far, but if we were to run our test case we wouldn't even get
past Step 1! Let's fix that now by making it so a Pickup is added to the game on a right click. As we
discussed in the Design a Solution step, we'll do this in the TedTheCollector Update method. As soon
as we try to do that, though, we realize that we need another field in the TedTheCollector class:
Arrays and Lists 199
specifically, we need a field for the Pickup prefab so we can add a new instance of that prefab to the
game on a right click.
It's important that you realize that our design will almost always evolve as we learn more details during
the Write the Code step. This is a perfectly normal occurrence, and it doesn't mean we did our design
“wrong”; this is just how it works. You may hear some people claim that you should get your design
perfectly correct and complete before you start coding, but this is almost never feasible in practice.
That's why the development we demonstrate throughout the book shows a more typical sequence of
actions rather than an ideal, unrealistic process.
Add a prefabPickup field (as a GameObject) to the TedTheCollector class and mark it with
[SerializeField], go to the Unity editor, select the Main Camera in the Hierarchy window, and drag
the Pickup prefab from the Prefabs folder in the Project window onto the new field in the Inspector.
From the Unity scripting manual, the Input GetMouseButtonDown method “Returns true during the
frame the user pressed the given mouse button.” Furthermore, the documentation shows that we use 0
for the left button and 1 for the right button, so the Boolean expression for our if statement evaluates to
true on the first frame in which the right button is pressed. You should be able to see why we don't
want to place a pickup on every frame the right button is pressed – that would be a lot of pickups! – just
on the first frame.
Strictly speaking, this isn't actually a mouse click, which consists of a mouse button press followed by a
release of that mouse button. You'll probably actually run into situations in your game development
where you need to detect a “true mouse click”, but in this problem we'll just place the pickup on the first
frame in which the mouse button is pressed. That will actually feel more natural to the player, because it
wouldn't feel as responsive if the pickup were placed on the button release of a true mouse click instead.
The first line of code in the if body gets a copy of the mouse position so that the second line can change
the z coordinate just like we did when we were spawning teddy bears in Section 7.6. Recall that we
200 Chapter 9
needed to do that so that all our 2D objects are at z == 0 in the game world. The third line of code
converts the mouse position from screen coordinates to world coordinates.
The fourth line of code in the if body instantiates our Pickup prefab.
The fifth line of code in the if body sets the position of our new Pickup game object to the world
location of where the right mouse button was pressed, and the sixth line of code adds the new Pickup
game object to the list of pickups that the script maintains.
If you run the game now, you should be able to place pickups in the game by right-clicking the mouse.
Great, we're past Step 1 in our Test Case; let's get Step 2 working.
Now we need to make the TeddyBear game object start moving toward the oldest Pickup in the game
when the player left-clicks the teddy bear. Recall that the TedTheCollector class exposes the
TargetPickup property to support this, so let's implement that property now. You should realize that
we're deliberately making a mistake in the property, which we'll discover later in our coding; see if you
can figure out what the mistake is.
/// <summary>
/// Gets the next target pickup for the teddy bear to collect
/// </summary>
/// <value>target pickup</value>
public GameObject TargetPickup
{
get { return pickups[0]; }
}
The get accessor simply returns the first GameObject (a Pickup game object) in the list of pickups that
the class maintains; remember, the first item in the list is at index 0.
Because the TedTheCollector script recognizes right clicks in its Update method, our initial thought
was that the TeddyBear script would recognize left clicks in its Update method. This is a little more
complicated than right clicks, though, because we don't want to recognize any left click, we only want to
recognize left clicks that are actually on the TeddyBear game object.
It turns out that instead of using the Update method for this, we should use the OnMouseDown method
instead. That's because the OnMouseDown method is called when the user has pressed the mouse button
over a collider; the documentation means the left mouse button when it says “the mouse button”, so this
method will get called just when we need it to be.
Arrays and Lists 201
Go ahead and delete the TeddyBear Update method now since we won't need it after all. As reminder,
it's always a good idea to check the MonoBehaviour Messages section (excerpt above) to see if there's a
method that we can use for what we need.
Because our TeddyBear game object doesn't have a collider component yet (which we need for
OnMouseDown to be called), we'll add one now. Double-click the TeddyBear game object in the
Hierarchy window of the Unity editor; this zooms in on the game object in the Scene view. Click the
Add Component button in the Inspector and select Physics 2D > Box Collider 2D. As you can see in the
Scene view, the Box Collider 2D is too large because the sprite image has some border transparency.
Click the button to the right of Edit Collider in the Box Collider 2D component, drag the edges of the
collider in the Scene view to fit the TeddyBear game object more tightly, then click the button to the
right of Edit Collider in the Box Collider 2D component again. Click the small box next to Is Trigger in
the Box Collider 2D component.
That last step sets the collider to be a trigger rather than a collider from a physics perspective. This will
be important when the TeddyBear collides with Pickups in the game because we don't actually want the
TeddyBear and Pickup objects to respond to each other physically (don't be weird!), but we do want to
know when they collide.
While we're at it, we might as well add a Rigidbody 2D component to the TeddyBear as well. We're
going to just use the Unity physics engine to move the TeddyBear, and we'll need a Rigidbody 2D
component attached to it to do that. Click the Add Component button in the Inspector and select Physics
2D > Rigidbody 2D. Click the Overrides dropdown near the top of the Inspector and select Apply All to
apply the changes to the TeddyBear prefab.
202 Chapter 9
If you run the game now, the TeddyBear game object falls off the bottom of the screen. Select Edit >
Project Settings … from the menu bar at the top of the editor, select Physics 2D on the left, and set the Y
value for Gravity to 0. Run the game again to verify that you turned gravity off correctly.
The first thing we'll need to do in the OnMouseDown method is access the TedTheCollector
TargetPickup property – and we immediately have another problem! The TeddyBear class, which is
attached to the TeddyBear game object, doesn't have a reference to the TedTheCollector class, which
is attached to the Main Camera.
We've actually already learned all the pieces we need to solve this problem, we just have to put them
together in a new way. We know how to get access to the Main Camera using Camera.main and we
know how to get a component attached to a game object using the GetComponent generic method. That
means we can use the following to get the reference we need:
Camera.main.GetComponent<TedTheCollector>()
Because we need to use this reference every time we have a left click on the TeddyBear game object, for
efficiency we add a tedTheCollector field to the TeddyBear class and get the reference in the Start
method; we do the same thing for the reference to the Rigidbody 2D component as well.
Now we can get working on the OnMouseDown method. Because it's not provided in the template script
when we create a new C# script in Unity, we need to add the entire method (not just the body of the
method like we've been doing for Start or Update). How do we know what the method should like?
The documentation again, of course!
The easiest approach to use to make sure we get the method correct is to simply copy the entire method
from the example in the documentation, add a comment above the method, delete all the code in the
Arrays and Lists 203
method body (the code between the { and the }) , and add the code we need in the method body. Here's
what we end up with when we do that:
/// <summary>
/// OnMouseDown is called when the user has pressed the mouse button
/// over the collider
/// </summary>
void OnMouseDown()
{
// ignore mouse clicks if already collecting
if (!collecting)
{
// calculate direction to target pickup and start moving toward it
targetPickup = tedTheCollector.TargetPickup;
Vector2 direction = new Vector2(
targetPickup.transform.position.x - transform.position.x,
targetPickup.transform.position.y - transform.position.y);
direction.Normalize();
rigidbody2D.AddForce(direction * ImpulseForceMagnitude,
ForceMode2D.Impulse);
collecting = true;
}
}
The if statement makes sure we only respond to left mouse clicks on the TeddyBear game object if that
object isn't already collecting Pickups. The first line of code in the if body retrieves the target pickup the
teddy bear should go collect. We're saving that target pickup in a new field in the TeddyBear class for
reasons that will become clear soon.
The second line of code in the if body creates a Vector2 that points from the TeddyBear game object to
the Pickup game object it should go collect. The magnitude of that Vector2 is dependent on the distance
between the TeddyBear and the Pickup, though, so we want to normalize that vector so it points in the
same direction with a magnitude of 1; that's what the call to the Normalize method in the third line of
code does.
We added an ImpulseForceMagnitude constant at the top of the TeddyBear class, so the fourth line of
code in the if body adds an impulse force to the TeddyBear's rigidbody toward the pickup with a
magnitude of ImpulseForceMagnitude. By normalizing our vector and multiplying by our constant,
we're ensuring that the TeddyBear object always moves at the same speed toward its target Pickups
independent of the distance between the teddy bear and the target Pickup it's going to collect.
We also set the collecting flag to true because the Teddy Bear is now collecting.
Run Steps 1 and 2 of the test case and you'll see that the TeddyBear moves toward the Pickup, but then
keeps on going after it reaches it. That's because we haven't added the code we need to actually detect
when the TeddyBear reaches its target Pickup.
What we really need to do here is detect a collision between the TeddyBear and the target Pickup. To do
that in Unity, both game objects need to have a Collider2D component attached to them. At this point,
only the TeddyBear has a collider, so now we need to add one to the Pickup prefab.
204 Chapter 9
Drag a Pickup prefab from the Prefabs folder in the Project window onto the Scene view (don't put it on
top of the TeddyBear). Click the Add Component button in the Inspector and select Physics 2D > Circle
Collider 2D. As you can see in the Scene view, the Circle Collider 2D is too large because the sprite
image has some border transparency. Click the button to the right of Edit Collider in the Circle Collider
2D component, drag one of the boxes on the collider in the Scene view to fit the Pickup game object
more tightly, then click the button to the right of Edit Collider in the Circle Collider 2D component
again. Click the Overrides dropdown near the top of the Inspector and select Apply All to apply the
changes to the Pickup prefab. Delete the Pickup game object from the scene.
Now we need to have the TeddyBear script take the appropriate action when the collision we're looking
for occurs. Remember how we added the OnMouseDown method to respond to left clicks on the
TeddyBear? Well, we can also add an OnTriggerEnter2D method that automatically gets called when
the collider for the TeddyBear (remember, we made it a trigger) collides with another collider (a collider
for a Pickup game object). We need to add the entire method like we did for OnMouseDown; see below.
/// <summary>
/// Called when another object enters a trigger collider
/// attached to this object
/// </summary>
/// <param name="other">collider info</param>
void OnTriggerEnter2D(Collider2D other)
{
// only respond if the collision is with the target pickup
if (other.gameObject == targetPickup)
{
// remove collected pickup from game and go to the next one
tedTheCollector.RemovePickup(targetPickup);
rigidbody2D.velocity = Vector2.zero;
GoToNextPickup();
}
}
Remember, we're supposed to ignore any collisions we have with Pickups that aren't currently our target
Pickup; that's what the if statement checks for. The first line of code in the if body removes the target
pickup from the game (once we implement the body of the TedTheCollector RemovePickup method,
which we'll do soon). The second line of code stops the TeddyBear by setting its velocity to 0. We do
this so we get straight lines from one Pickup to the next as the TeddyBear collects them.
For the third line of code, we realized that the code we have in the if body of the OnMouseDown method
is identical to the code we need here. Rather than copying and pasting that code, we instead wrote a new
method called GoToNextPickup and had both the OnMouseDown method and the OnTriggerEnter2D
method call that new method. That way, we only have the required code in a single place.
Implementing the body of the TedTheCollector RemovePickup method is easy, because the List class
exposes a Remove method to remove a specific element from the list. Here are the lines of code we put in
the body of the method to remove the Pickup from both the list and the game:
pickups.Remove(pickup);
Destroy(pickup);
Arrays and Lists 205
Run Steps 1 and 2 of the test case. The good news is that the TeddyBear goes to the target Pickup and
stops and the Pickup is removed from the game. The bad news is that we get the error shown in the
Console window in Figure 9.9.
Oh dear (replace dear with your favorite expletive as necessary). The Argument is out of range error
(we'll discuss exceptions later in the book) occurs when we try to access an element in an array or list
with an index larger or smaller than the valid indexes for that array or list. The list of pickups is the only
array or list we have in our game, and we only access an element in that list with a specific index in the
TedTheCollector TargetPickup property, so let's use the debugger to see what's going on.
Open the TedTheCollector script in Visual Studio and put a breakpoint next to the get accessor in the
TargetPickup property by left-clicking in the gray column to the left of that line of code. If you do that
properly, a red circle will appear in the column at that location. Click the Attach to Unity button just
below the middle of the top menu bar to start debugging in Visual Studio. Run the game in the Unity
editor, wait until the play button turns blue, then right-click to place a pickup and left-click the teddy
bear to start it collecting.
When the game pauses at the breakpoint (Windows should give the Visual Studio window focus at that
point), hover the mouse over pickups in the get accessor. As you can see, the pickups list has 1
element at this point (Count == 1), so we should be fine for now. Press F5 to continue debugging and
wait in Visual Studio until the game stops at the breakpoint again. At this point, the teddy bear has
collected the pickup and is asking for the next target pickup to go to. If you hover the mouse over
pickups again you'll see that the pickups list is now empty.
Accessing the first element (at index 0) of an empty list has got to be trouble, because there is no first
element! Press F10 to step over the breakpoint (stepping over simply executes that line of code) and go
back to the Unity editor to see the error message. Stop running the game in the editor, stop debugging in
Visual Studio by selecting Debug > Stop Debugging (or clicking the red square) on the top menu bar,
and remove the breakpoint by clicking the red dot to the left of the get accessor in Visual Studio.
Remember we told you we were deliberately making a mistake when we implemented the
TargetPickup property; now we know what the mistake is! We need to make sure that we only return
the first pickup in the list if the list isn't empty. That's easy to do with an if statement, but what should
the property return if the list is empty? One very common technique is to return null from properties
and methods that usually return an object (in this case, a GameObject) but can't return a valid object
given the current state of the game (in this case, there are no more pickups to collect). That's what we'll
do here, so we change the body of the get accessor to:
206 Chapter 9
if (pickups.Count > 0)
{
return pickups[0];
}
else
{
return null;
}
Run Steps 1 and 2 from the test case again; now we get the error shown in Figure 9.10.
Amazing! We seem to just be going from one problem to another, but trust us, we're actually moving
forward with the game. This error gives us a little more detail than the previous one, because it tells us
the problem is in the TeddyBear GoToNextPickup method.
We can figure out the problem here without even using the debugger. The line of code that calculates the
direction vector from the teddy bear to the target pickup accesses the transform property of TeddyBear
targetPickup field, which holds the result of accessing the TedTheCollector TargetPickup property.
If the TargetPickup property returns null, then the targetPickup field is null, and we can't access
the transform property of an object that doesn't exist! To solve this problem, we change the body of the
GoToNextPickup method to:
If the TargetPickup property returns an actual GameObject, the code works just as before. If it returns
null, the teddy bear doesn't do anything; we do need to set the collecting flag to false, though,
because the Teddy Bear is no longer collecting at this point.
Finally, Steps 1 and 2 of the test case work the way they're supposed to! In fact, the entire test case
should work at this point, so let's Test the Code.
Our test case now passes, so strictly speaking, we're done. Unfortunately, there's actually still a bug in
the code that we should fix. Try placing two pickups close to each other, with the first pickup further
away than the second one, then click the teddy bear and watch what happens. Why does the teddy bear
pick up the first pickup but not the second one?
Before we fix the bug, we now have another test case we should include in our test plan, so we'll add
that now. By the way, this problem shows how hard it is to develop a set of test cases that will find every
possible bug in our code; in fact, except for the simplest programs it's impossible!
Test Case 2
Checking Close Pickup Behavior
Step 1. Input: Right Click
Expected Result: Pickup placed at click location
Step 2. Input: Right Click close to the first pickup on a line between the teddy bear and the first pickup
Expected Result: Pickup placed at click location
Step 3. Input: Left Click on Teddy Bear
Expected Result: Teddy Bear collects oldest Pickup, then collects other pickup, then stops
Let's use the debugger again to try to figure out what's going on. We know that the teddy bear actually
picks up each pickup in the TeddyBear OnTriggerEnter2D method, so that's a good place for us to set a
breakpoint.
Open up the TeddyBear script in Visual Studio and set a breakpoint on the first line of the
OnTriggerEnter2D method. In Visual Studio, Attach to Unity and start the game in the Unity editor.
The first time we hit the breakpoint is when the teddy bear collides with the non-target pickup; press F5
to continue. The second time we hit the breakpoint is when the teddy bear collides with the target
pickup, so we go into the if statement to remove that pickup and get the new target. Press F10 to step
over each line of code until the yellow line is on the call to the GoToNextPickup method, then press F11
to step into that method. Press F10 one more time and hover the mouse over targetPickup; as you can
see, we have a non-null target pickup. Press F5 to continue to the breakpoint in the OnTriggerEnter2D
method.
208 Chapter 9
Hmmm ... we never hit the breakpoint the third time, when we should be picking up the second pickup.
Stop the game in the Unity editor. If we go back to the MonoBehaviour OnTriggerEnter2D method, we
see the part of the description that says “... when another object enters a trigger collider ...” That seemed
just right to us when we decided to use the OnTriggerEnter2D method, but what if our next target
pickup is actually already in the trigger collider for the teddy bear because the pickups were so close to
each other? Our next target pickup never enters the trigger collider because it's already there!
It looks like the OnTriggerEnter2D method isn't the one we should be using after all. Luckily, if we
look a little further down in the documentation we find the OnTriggerStay2D method, which is called
“... each frame where another object is within a trigger collider...” This should work for us, because even
if our next target pickup is actually already in the trigger collider for the teddy bear when we set it as the
target, the OnTriggerStay2D method will get called. Change the TeddyBear OnTriggerEnter2D
method to an OnTriggerStay2D method instead and remove the breakpoint.
So why didn't we just tell you the correct method to use in the first place? Because we're walking you
through exactly how we solved this problem. Programmers make mistakes, and figuring out what those
mistakes are and how to solve them is an important skill for you to have. We spend a lot of time in this
book on the process of programming in addition to the mechanics of programming because the process
is just as, if not more, important.
As expected, both test cases complete successfully, so technically we're done with our problem solution.
We're not quite happy, though; read on.
It actually looks strange if the TeddyBear ignores a Pickup it collides with on its way to its target Pickup
because it passes under the ignored Pickup; it feels like it would look better for the TeddyBear to pass
over the ignored Pickup instead. Let's add one more explicit requirement to the Problem Description:
• The TeddyBear passes over Pickups it collides with that aren't the Pickup the Teddy has
currently “targeted for collection”
We can make a slight revision to Expected Result for Step 7 of our first test case to address the new
requirement:
Test Case 1
Checking Game Behavior
Step 1. Input: Right Click
Expected Result: Pickup placed at click location
Step 2. Input: Left Click on Teddy Bear
Expected Result: Teddy Bear collects Pickup and stops
Step 3. Input: Left Click on Teddy Bear
Expected Result: No response (there's no pickup to collect)
Step 4. Input: Right Click
Expected Result: Pickup placed at click location
Arrays and Lists 209
Our solution no longer passes the first test case because we changed the requirements after we were
done! We don't have to change anything in our design to fix this, though, so we can move directly to
making the changes here. We don't actually have to change our scripts at all, but we do have to make
some changes in the Unity editor.
To figure out what changes we need to make, we need to understand two more Unity features: Sorting
Layers and Order in Layer. In Unity, we can place objects in different sorting layers to control the order
in which the sprites are rendered in the scene. The template Unity 2D game project only contains a
single sorting layer called Default. If you select the TeddyBear and Pickup prefabs and look at the
Sorting Layer value in the Additional Settings section of their Sprite Renderer components, you'll see
that they're both set to Default; that means both of them will be drawn when the sprites in the Default
sorting layer are drawn.
One solution to our problem would be to add a new Sorting Layer to the project by selecting Edit >
Project Settings …, then selecting Tags & Layers on the left. Expanding the Sorting Layers section on
the right shows all the sorting layers in the game. To add a new sorting layer, we'd click the + at the
bottom right of the list of sorting layers. The sorting layers are rendered back to front from the top of the
list, so if we name our new sorting layer TeddyBear all the sprites in the Default sorting layer would be
rendered first, then all the sprites in the TeddyBear sorting layer would be rendered. If we went back to
our TeddyBear prefab and used the Sorting Layer dropdown in the Sprite Renderer component to assign
the TeddyBear sorting layer, our TeddyBear game object would be drawn in front of everything else
(including Pickup game objects, which are still in the Default sorting layer). That's one way to solve our
problem.
Just so you can see another reasonable solution, let's use Order in Layer instead. Order in Layer is used
to control the order in which sprites are rendered within a single Sorting Layer. If you select the
TeddyBear and Pickup prefabs and look at the Order in Layer value in the Additional Settings section of
their Sprite Renderer components, you'll see that they're both set to 0 (make sure they both have Sorting
Layer set to Default). Because sprites with a lower Order in Layer number are rendered behind sprites
with a higher Order in Layer number, we can simply set the Order in Layer value for the TeddyBear
prefab to 1 to solve our problem.
Everything now works fine in our solution, but remember that we made quite a few changes to our
design for the TeddyBear class as we uncovered details during the Write the Code step. In Visual
Studio, changing the code in a class automatically changes the UML diagram for that class if you've
210 Chapter 9
added a UML diagram for that class to your Visual Studio solution. We've provided the revised UML
for the class below so you can see the final design.
The ability to accomplish certain steps in our code multiple times (such as printing out the cards in a
hand that changes as the program runs) is the last control structure we need for our problem solutions. In
other words, we'd like to iterate (repeat) those steps in the algorithm. That's where the third and final
control structure comes in – the iteration control structure. Iteration control structures are often called
loops, because we can loop back to repeat steps in the algorithm.
Problem Description: Write an algorithm that will print out all the cards in a hand
We use indentation to show what happens inside the loop, just as we used indentation to show what
happened inside our selection algorithms.
Basically, our solution looks at each card in the hand and prints the information about that card. The step
inside the loop body – printing the info about a single card – is executed repeatedly. We already printed
out the cards in a hand in the previous chapter, but remember what our code looked like for five cards:
We actually had to know how many cards were in the hand and print out exactly that many cards. This is
a real problem, especially if the number of cards in the hand changes over time as it typically does.
Iteration lets us overcome this problem by letting us repeat the printing card information step precisely
the number of times we need to when we run the program, even if the number of times changes while
we're running the program.
212 Chapter 10
initializer, code that initializes the loop control variable (a variable whose value controls execution
of the loop)
condition, a Boolean expression that’s evaluated each time through the loop to see if we’ll keep
looping
iterator, an expression that says how to modify the loop control variable at the end of each time
through the loop (we often increment by one)
loop body, the code that’s executed each time through the loop
We do want to make a comment about the for loop before we try one out. C# will actually let us use a
variety of data types for the upper and lower bounds of our for loops. For the problems in this book,
though, we'll usually be using integer bounds.
All right, ready to try one? Let's actually implement the code for the algorithm in Example 10.1. We'll
assume that a hand variable has already been declared and instantiated as a List<Card> object and
already has some cards added to it (though the loop below works with 0 cards also).
First we'll add the start and end of the for loop:
Our initializer is the chunk of code that says int i = 0; i is our loop control variable, and it's
initialized to 0. The loop will stop when the condition evaluates to false (in other words, when i is no
longer less than hand.Count). Notice that we access the Count property of our hand list to determine
how many times to loop; that way, the loop works no matter how many cards are in the hand. So why
don't we use i <= hand.Count instead; if we use <, won't we skip the last card in the hand? No,
because the indexes of the list elements are zero-based, not one-based. The maximum index of an
element in the hand list is hand.Count – 1, so using i < hand.Count rather than i <= hand.Count is
the right choice.
When we use i++ as our iterator, we're saying to add 1 to i at the end of each time through the loop.
Now we modify the loop code to add the loop body, yielding
Iteration: For and Foreach Loops 213
// print cards in hand
for (int i = 0; i < hand.Count; i++)
{
Console.WriteLine(hand[i].Rank + " of " + hand[i].Suit);
}
The output when we run this code fragment (using our royal flush hand) will look like this:
Ten of Spades
Jack of Spade
Queen of Spades
King of Spades
Ace of Spades
The loop executes 5 times because there are 5 cards in the hand, and it prints out the information about
each card based on the value of i in the loop body on each iteration.
What? Think of it this way. The first time we enter the loop body, i is equal to 0, so we print out the
rank and suit for hand[0]. The second time we enter the loop body, i is equal to 1, so we print out the
rank and suit for hand[1]. It keeps working that way until the loop stops. This is really convenient,
because instead of hard-coding literals for our indexes like we had to do in the previous chapter, here we
can just use the loop control variable as our index. You'll see and write for loops like the one above
many, many times as you program games and other software applications, so make sure you understand
how they work.
Now you may have already realized this from the above example, but you should know that we don't
have to know how many times the for loop will execute when we write the program – we just have to
make sure we know how many times the for loop will execute before the program reaches that point in
its execution. Consider Example 10.2.
Problem Description: Write a program that will print the squares of the integers from 1 to n, where n is
provided by the user.
Although we do things slightly differently in this for loop – we start i at 1 rather than 0 and we use <= in
our condition rather than < – the ideas are exactly the same and the loop works just as you'd expect.
So that's it. When we know how many times to loop, either when we write the program or simply before
we get to the loop during program execution, we can easily use a for loop. In case you're wondering how
to test our code when it contains a for loop, we test that part of the code just like we tested the sequence
control structure; we just run it to see if it works.
Let's print the cards in our hand again, this time using a foreach loop:
Because each element in our list is a Card object, we use Card as the data type for the card variable.
The card variable will be assigned to each element of the list as we execute the loop body. In other
words, the first time through the loop, card will be set to hand[0], so we print out the rank and suit for
hand[0]. The second time through the loop, card will be set to hand[1], so we print out the rank and
suit for hand[1]. It works this way until we've iterated over all the elements in the hand list. Finally, we
needed to provide an array or collection over which we want to iterate; because we wanted to print all
the cards in the hand, we provided hand for the collection.
Iteration: For and Foreach Loops 215
One example is solutions in which we need to know the index of the element we're currently processing
in the loop body. In that case, we need to use a for loop so we can access the loop control variable inside
the loop body. For example, if we were trying to collect the locations of all the cards in the hand with a
specific characteristic (all the Jacks, say), a for loop would be the way to go.
A second example is when we want to execute the loop body a certain number of times but we're not
processing the elements of an array or collection. We've already seen an example of this in Example
10.2, where we printed a certain number of integers and their squares.
A third example is when we actually want to change the contents of a collection we’re iterating over.
We’re allowed to do this with for loops – we can remove each Jack as we find it, for example – but
we’re not allowed to change the contents of a collection we’re iterating over using a foreach loop.
Problem Description: Write a code fragment that will generate all the cards in a deck.
We know we have 4 different suits, with 13 different ranks in each suit, so this seems like the kind of
problem where nested for or foreach loops would be just the thing to use. Let's initialize the deck of
cards first using:
We're actually using a different overload of the List constructor that lets us specify the initial capacity
of the list when we create it. Why are we doing that?
Remember when we discussed how you'd have to grow an array if you needed to add more elements
than you initially allocated space for? It turns out that the List class actually uses an array as the
underlying structure to hold all the list elements. If we carefully read the documentation of the List Add
method, we find that adding elements to the list is usually very fast. If, however, the capacity of the list
needs to be increased to fit the new element, the Add method takes much longer to complete. Although
the Add method will automatically grow the capacity of the list as needed, we can avoid the extra time it
would take to do that by simply creating an initial list that has a capacity of 52 cards. That way, the
216 Chapter 10
capacity of the list doesn’t have to be increased as we add the cards to it. By the way, we can always
find out the current capacity of a list by accessing its Capacity property.
Now that we have a deck to hold all the cards, let's develop the code to fill the deck with cards.
Okay, let's look at the outer foreach loop first. This loop is set up to loop through each of the possible
suits in the Suit enumeration, but we're obviously using some C# features we haven't used before. At
least the first part is familiar; each element we look at will be a Suit, and the variable that will hold the
current element in the loop body is called suit. It's the specification of the array we want to iterate over
that looks different.
Recall that the Int32 structure is used to provide int-specific methods; in much the same way, the Enum
class is used to provide enum-specific methods (remember, our Suit enumeration is declared as an
enum). The GetValues method in the Enum class returns an array of the values defined by the
enumeration provided as the argument to the method.
The last piece of the puzzle, then, is understanding why we have to pass typeof(Suit) as the argument
to the GetValues method rather than Suit. The issue is that the argument to the method actually needs
to be a Type object that represents the type declaration of a particular type rather than the type itself.
Luckily, the typeof operation can be used to retrieve the Type object for a specific type (like Suit).
We know that feels pretty complicated given where you are in your programming knowledge at this
point. For now, just knowing the syntax you need to use to get an array of the values in an enumeration
is enough; you can develop a deeper understanding of typeof and Type as you do more advanced
programming later.
The inner foreach loop works in a similar way to iterate over the possible ranks for the cards. Now that
we understand the details about how each foreach loop is set up, we can move on to understanding how
the code will actually work when it executes.
When we reach the outer loop, the suit variable is set to Suit.Clubs since Clubs is the first value we
defined in the Suit enumeration. We then move to the loop body of the outer loop, which contains the
inner loop. In the inner loop, the rank variable is set to Rank.Ace. In the loop body for the inner loop,
Iteration: For and Foreach Loops 217
we create a new Ace of Clubs card and add it to the deck. We then execute the next iteration of the inner
loop, where we create a new Two of Clubs card and add it to the deck. We keep iterating through the
inner loop until we've covered all the possible card ranks, creating a new King of Clubs card and adding
it to the deck on the last iteration.
Once the inner loop is done, we execute the next iteration of the outer loop, where the suit variable is
set to Suit.Diamonds, the second value we defined in the Suit enumeration. We then generate all the
Diamond cards in the inner loop in the same way we generated the Clubs cards above, going from Ace
to King. Once the outer loop has executed for all the suits, the code is done and our deck contains all 52
cards.
Pretty cool, huh? The only thing you might find a little confusing is that the inner loop “starts over” each
time we get to it on each iteration of the outer loop. It's just like when we get to a loop that's not nested,
though. The program just starts the loop when it reaches it; it's just that we reach the inner loop 4 times
in the above code instead of only once.
Problem Description: Write an algorithm that will print out all the cards in a hand
and our code only changes in one place (we use the Length property of the array rather than the Count
property of the List):
There's obviously not much difference here at all. Rather than exposing a Count property like the List
class does to tell how many elements are in the list, arrays expose a Length property to tell the number
of elements there are in the array. By simply using the appropriate property, we easily get the same
behavior we got with a List earlier. We actually allocated the array to hold exactly 5 elements when we
created the array object, so we could have used 5 instead of hand.Length above, but then if we changed
the size of the array, we'd also have to change the bound in the for loop. When you’re using arrays, you
should get into the habit of using the Length property since that's a more robust approach. Note that we
218 Chapter 10
could have (and probably should have!) used a foreach loop instead of a for loop, but we wanted to talk
about the Count and Length properties and to explain why we should those properties instead of hard-
coded numbers in our for loops.
You should realize, though, that the above code will only work if we have exactly five cards in the hand
array. If there are some “empty slots” in the array that don’t hold cards (so those elements are null
instead), the Console.WriteLine will fail when we try to print the rank and suit for one of those empty
slots. That’s another reason we prefer using the List class when the size of our collection can change
over time.
Things also get a little trickier when we deal with 2D arrays, so let's look at another example.
Initialize sum to 0
Loop through the rows in the array
Loop through the columns in the array
Add current number to sum
Before converting our algorithm into code, we'll declare the 2D array that holds the numbers:
You should also assume we've filled up that array with values, probably using nested for loops.
Before we convert our algorithm into code, we need to figure out how to get the size of each dimension
in the array. We can't use the Length property, which tells us the total size of the array (12 elements in
this case), so we need something else. Luckily, array objects provide a GetLength method that will give
us the number of elements in a particular zero-based dimension. In other words, calling the GetLength
method with an argument of 0 will tell us how many rows there are in the array. Now we have enough
information to convert our algorithm into code:
This time we use nested for loops to walk all the dimensions of the array. When the above block of code
is done executing, the sum variable will hold the sum of all the integers in the array.
Iteration: For and Foreach Loops 219
First, we'll need 3 input axes to respond to the three different mouse button presses. We can certainly
add new axes as we did in Chapter 8 if we want to, but we can also simply rename the Fire1, Fire2, and
Fire3 axes we get by default because those axes respond to the mouse buttons we want to respond to.
The default axes also respond to keyboard keys, so if we want to ONLY respond to mouse buttons we
can remove the keyboard key responses from those axes. That's the approach we took in our solution.
Next, we'll need a script that checks for input on these axes on every frame so the teddy bears get blown
up as appropriate. Because this is really functionality at the game level rather than the object level, we'll
follow our typical approach of attaching the script to the Main Camera. We already have a
TeddyBearSpawner script attached to the Main Camera, but it's perfectly fine to have multiple scripts
attached to game objects. Although we could just add the new functionality to the TeddyBearSpawner
script, that would be a poor choice because blowing up teddy bears is most definitely NOT spawner
functionality! Instead, we'll create a new BlowingUpTeddies script that we'll attach to the Main Camera.
Here's our new script, with explanations included as appropriate:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// Blows up teddies in response to player input
/// </summary>
public class BlowingUpTeddies : MonoBehaviour
{
[SerializeField]
GameObject prefabExplosion;
We include the prefabExplosion field so we can instantiate a new Explosion game object when
necessary; the field is marked with [SerializeField] so we can populate it in the Inspector. Recall
that in Chapter 8 it was the TeddyBear class that had a prefabExplosion field. That's because for those
problems, the TeddyBear game object blew itself up based on player input. In contrast, in this game a
different class (BlowingUpTeddies) is blowing up the teddy bears, so the TeddyBear class doesn't need
to have any knowledge of the Explosion game objects.
[SerializeField]
Sprite yellowTeddySprite;
[SerializeField]
Sprite greenTeddySprite;
[SerializeField]
Sprite purpleTeddySprite;
We include these three Sprite fields so we can easily check the sprite color for each of the TeddyBear
game objects in the scene so we only blow up the TeddyBears that are the color currently being blown
up. The fields are marked with [SerializeField] so we can populate them in the Inspector.
220 Chapter 10
When we detect an input from one of the mouse buttons, we're going to need a list of all the game
objects in the scene so we can find and destroy the teddy bears of the appropriate color. Rather than
creating a new List object every time that happens, we'll use a field instead so we don't generate extra
List objects for the garbage collector to retrieve.
/// <summary>
/// Update is called once per frame
/// </summary>
void Update()
{
// FindObjectsOfType is slow, so only call it
// if at least one of the axes has input
if (Input.GetAxis("BlowUpYellowTeddies") > 0 ||
Input.GetAxis("BlowUpGreenTeddies") > 0 ||
Input.GetAxis("BlowUpPurpleTeddies") > 0)
{
gameObjects.Clear();
gameObjects.AddRange(Object.FindObjectsOfType<GameObject>());
}
We know from the Unity documentation that the FindObjectsOfType method is slow, so we don't want
to call it every frame, we only want to call it if we have input on one or more of the 3 input axes we
respond to. If we do have input on at least one of those axes, we clear the list of game objects in the
scene, then use the List AddRange method to add the current game objects in the scene to the list. Our
call to the Object FindObjectsOfType method returns an array of all the game objects in the scene, so
we pass that array as the argument to the AddRange method.
We use the if statements above to call a separate BlowUpTeddies method we wrote to blow up all the
teddy bears of a particular color (we'll discuss the details of that method soon). The second argument we
provide is obviously the list of game objects currently in the scene, but the first argument requires some
explanation. Let's leave the BlowingUpTeddies script briefly to discuss the first argument.
TeddyColor is an enumeration (like Suit and Rank). Recall that an enumeration essentially defines a
new data type with a specific set of values. Take a look at the TeddyColor code below:
Iteration: For and Foreach Loops 221
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// An enumeration of the teddy bear colors
/// </summary>
public enum TeddyColor
{
Green,
Purple,
Yellow
}
Variables and arguments of the TeddyColor type can only have one of three values:
TeddyColor.Green, TeddyColor.Purple, or TeddyColor.Yellow. We always need to precede the
value with the name of the enumeration. As you know, data types also specify operations that are valid
for variables of that data type. Although there are a variety of useful things we can do with enumerations
in C#, including converting back and forth between int and the enumeration type, in this book we'll be
using variables of enumeration types to simply store and compare values.
Before we move on, you might be wondering why we use enumerations at all. For the colors of the
teddy bears, for example, why don't we just use an int where 0 means green, 1 means purple, and 2
means yellow24? There are a number of reasons. First, it's much easier to read code where
TeddyColor.Purple means the color purple than trying to remember that 1 means purple – remember
our previous magic number discussion? You could certainly argue that we already know how to solve
the magic number problem, though; we could just declare constants as follows:
That certainly solves our magic number problem, but it leads to another problem. Let's say that we use
an int as an argument to the BlowUpTeddies method rather than a TeddyColor (as it's currently
defined). We know that an int can store any of 232 unique values, while in this case we only want the
variable to be able to hold 0, 1, or 2. Anything other than a 0, 1, or 2 would be an invalid value, so we'd
need to add extra code to make sure that whenever the BlowUpTeddies method is called that the first
argument is a valid value. This check will have to be done at run time, which of course costs us CPU
cycles that we could use for something else in our game. If we use the TeddyColor enumeration instead,
it's impossible to use an invalid value for that argument because the compiler will give us an error (and
the check happens at compile time, not run time).
As a reminder, enumerations give us a number of valuable things. They give us code that's easier to read
and understand, they enforce a set of specific values for variables and arguments of the enumeration
type, and they improve efficiency because all the checking happens at compile time. Enumerations are
great, and as you can see from TeddyColor, we can define our own enumerations when we need them.
24 The same points in the following discussion apply if we decided to use "green", "purple", and "yellow" strings
instead of numbers.
222 Chapter 10
/// <summary>
/// Blows up all the teddies of the given color
/// </summary>
/// <param name="color">color</param>
/// <param name="gameObjects">the game objects in the scene</param>
void BlowUpTeddies(TeddyColor color, List<GameObject> gameObjects)
{
// blow up teddies of the given color
for (int i = gameObjects.Count - 1; i >= 0; i--)
{
SpriteRenderer spriteRenderer =
gameObjects[i].GetComponent<SpriteRenderer>();
if (spriteRenderer != null)
{
Sprite sprite = spriteRenderer.sprite;
if ((color == TeddyColor.Green &&
sprite == greenTeddySprite) ||
(color == TeddyColor.Purple &&
sprite == purpleTeddySprite) ||
(color == TeddyColor.Yellow &&
sprite == yellowTeddySprite))
{
BlowUpTeddy(gameObjects[i]);
}
}
}
}
Our for loop works through the list of game objects in the scene from back to front because we may be
destroying objects in the list (teddy bears of the appropriate color) as we go. Although we won't actually
remove those objects from the list, it's a good idea to always go back to front in these situations.
You should never actually use a for loop that goes from the start to the end of a list if the body of the for
loop might remove an object from the list. The problem is that we would end up skipping over elements
of the list when elements are removed because the elements after the removed element are shifted down
in the list to fill the hole the removed element left in the list.
Here's a specific example. Say you're using a standard for loop that starts at 0 to iterate over a list of 4
elements, incrementing the loop control variable on each iteration. If you remove element 2 from the list
in the body of the for loop, element 3 shifts down to element 2 and element 4 shifts down to element 3.
We now finish the body of the for loop and increment the loop control variable from 2 to 3. That means
the loop body will now process element 3 (the old element 4) and will never process the old element 3
(which is now element 2). We skipped over the old element 3, which never got processed by the loop.
In the loop body above, we retrieve the SpriteRenderer component for the game object we're currently
processing and make sure it isn't null. In this particular case, we need to do this because the Main
Camera will be one of the game objects in the list; because we want to access the sprite property of the
sprite renderer for the TeddyBear game objects, we need to make sure we don't try to access the sprite
property of the Main Camera's (non-existent, and therefore null) sprite renderer. That's what the if
statement is for.
Iteration: For and Foreach Loops 223
If the sprite renderer isn't null, we access its sprite property. The complicated Boolean expression for
the inner if statement compares the sprite for the game object to the sprite for the color we're trying to
blow up. In more natural language, the Boolean expression checks if we're trying to blow up green teddy
bears and the current sprite is green, or we're trying to blow up purple teddy bears and the current sprite
is purple, or we're trying to blow up yellow teddy bears and the current sprite is yellow. If one of those is
true – remember, for || only one of the operands needs to be true for the Boolean expression to
evaluate to true – we call the BlowUpTeddy method we wrote:
/// <summary>
/// Blows up the given teddy
/// </summary>
/// <param name="teddy">the teddy to blow up</param>
void BlowUpTeddy(GameObject teddy)
{
Instantiate(prefabExplosion, teddy.transform.position,
Quaternion.identity);
Destroy(teddy);
}
The BlowUpTeddy method simply instantiates an Explosion game object at the teddy bear game object's
location then destroys the teddy bear game object.
Before our game will work, we need to populate the fields of the BlowingUpTeddies script. Select the
Main Camera in the Hierarchy window and drag the Explosion prefab onto the Prefab Explosion field of
the script component in the Inspector and drag the appropriate sprites onto the sprite fields.
Run the game to see that we can blow up the teddy bears by color properly.
Of course, there is a way to do that in Unity using tags. The general idea is that we can tag game objects
with a specific tag, then find game objects by their tag instead of by their type. Select the TeddyBear
prefab in the Prefabs folder in the Project window, then click the Open button near the top of the
Inspector. Click the dropdown next to the Tag field near the top of the Inspector to see the list of default
tags shown in Figure 10.1.
224 Chapter 10
As you can see, we can select one of the default tags provided by Unity or we can Add Tag... Select Add
Tag... and the Unity editor displays the list of user-defined tags in the Inspector. Click the + button at the
bottom right of that list to add a new tag to the list; add Green, Purple, and Yellow tags to the list.
Select the TeddyBear prefab in the Prefabs folder in the Project window, then click the Open Prefab
button near the top of the Inspector. Click the dropdown next to the Tag field near the top of the
Inspector to see the new list of tags shown in Figure 10.2. Set the Tag field for the TeddyBear prefab to
Yellow.
The next thing we need to do is make sure the TeddyBearSpawner SpawnBear method sets the tag
properly when it spawns a new TeddyBear game object. Here's the new chunk of code that selects a
random sprite and sets the sprite and tag appropriately:
We decided to rename our Sprite fields so we don't have to remember which sprite number is which
color (we had to populate those fields in the Inspector again because renaming them clears them as
well). As you can see, we can simply set the tag property for our new TeddyBear game object to give it
the appropriate tag based on the random sprite we selected.
/// <summary>
/// Update is called once per frame
/// </summary>
void Update()
{
// blow up teddies as appropriate
if (Input.GetAxis("BlowUpYellowTeddies") > 0)
{
BlowUpTeddies(TeddyColor.Yellow);
}
if (Input.GetAxis("BlowUpGreenTeddies") > 0)
{
BlowUpTeddies(TeddyColor.Green);
}
if (Input.GetAxis("BlowUpPurpleTeddies") > 0)
{
BlowUpTeddies(TeddyColor.Purple);
}
}
As you can see, we removed the code that retrieves all the game objects that are currently in the scene
and also removed the second argument in our call to the BlowUpTeddies method. Speaking of that
method:
/// <summary>
/// Blows up all the teddies of the given color
/// </summary>
/// <param name="color">color</param>
void BlowUpTeddies(TeddyColor color)
{
226 Chapter 10
We use the gameObjects field here to again hold a list of game objects in the scene, but that list only
contains the game objects with the tag we provide as the argument to the GameObject
FindGameObjectsWithTag method. We actually made sure our user-defined tags were identical to the
TeddyColor enumeration values. If we hadn't, we could certainly have used if statements to figure out
the appropriate tag to use for our argument, but this way we could just convert the color parameter to a
string using the ToString method and pass the resulting string as our argument.
You'll actually notice in the Unity editor that the game runs without error at this point until you press the
Play button to stop playing the game, at which point it says that you're trying to destroy a game object
that's already been destroyed. Our guess is that pressing the button to stop the game destroys all the
game objects in the scene but the code still responds to the left mouse press as though we're trying to
blow up yellow teddy bears. This seems like a reasonable hypothesis, especially since we don't get the
error message if we click the Maximize on Play button in the Game view, so we won't make any code
changes to try to handle this.
Now let's go through the entire problem-solving process for a problem that's most effectively solved
using the loops we've learned about in this chapter. Here's the problem description:
• Start with a TeddyBear game object, centered in the window, not moving
• On every right mouse click, add a Pickup game object where the mouse was clicked
• When the player left clicks the TeddyBear, the teddy starts collecting the pickups, starting with
the closest Pickup and targeting the closest Pickup each time it collects the Pickup currently
“targeted for collection”
• The TeddyBear collects a Pickup by colliding with it, but this only works for the Pickup the
Teddy has currently “targeted for collection”
• Once the last Pickup has been collected, the Teddy stops moving
• If the player adds more Pickups while the Teddy is moving, the Teddy picks them up as well
• If the player adds more Pickups while the Teddy is stopped, the player has to left click on the
Teddy again to start it collecting again
This is obviously our Ted the Collector game from Section 9.7., with the important change in the third
bullet that the TeddyBear collects the closest pickup rather than the oldest pickup as it collects the
pickups in the game.
Iteration: For and Foreach Loops 227
We've already solved a very similar problem, so the only question we have is what happens if the player
places a new Pickup that's closer to the Teddy than the Pickup it's currently moving to collect. Should
the Teddy change its target or should it keep heading toward the current targeted Pickup?
Having the Teddy keep heading toward the current targeted Pickup is the easier problem to solve – so
let's change the target, because we love a good coding challenge! It will also be more fun to keep
making the Teddy change course by placing new Pickups while it's collecting ...
Design a Solution
We still don't need a Pickup script, but we do need to make some changes to our TeddyBear and
TedTheCollector scripts. To figure out what those changes need to be, we need to think about what
should happen when the player places a new Pickup in the scene.
As before, the TedTheCollector script will add the new Pickup to the list of pickups that class
maintains. It's possible, though, that the new Pickup is closer to the Teddy Bear than the Pickup it
currently has targeted for collection. In that case, the Teddy Bear should change course to collect the
new Pickup instead.
The TeddyBear script is the appropriate class to decide whether or not the Teddy Bear should change
course, so we need to add a TeddyBear UpdateTarget method the TedTheCollector class calls when a
new Pickup is added to the scene. The new method has a parameter for the new Pickup game object so
the method can calculate the distance to the new Pickup and the distance to the Pickup that's currently
targeted for collection and decide whether or not to change course based on which Pickup is closer.
Figure 10.3. shows the UML for the TeddyBear class.
We also need to change the fields and properties for the TedTheCollector class. Because the
TedTheCollector class now needs to call a TeddyBear method, we'll save a reference to the TeddyBear
class so we don't have to look up that reference every time we add a new Pickup.
228 Chapter 10
We no longer need the TargetPickup property that returns the oldest Pickup in the game, because the
Teddy Bear no longer cares which Pickup is the oldest. The Teddy Bear does need to figure out which
Pickup should be its next target when it finishes collecting a Pickup, though. Because it selects the
closest Pickup as its next target, the TedTheCollector class should expose a Pickups property that
returns the list of the pickups in the game. The TeddyBear class can then walk that list to find the closest
Pickup.
You might think that we should just make the TedTheCollector pickups field public and let the
TeddyBear class access that field directly instead of implementing the new Pickups property. Although
you might see some Unity developers making fields public (usually instead of marking them with
[SerializeField] so they can be populated in the Inspector in the Unity editor), that's not the right
choice. Keeping our field private and exposing a public property is the correct object-oriented approach
to use.
We can still do all our testing in a single test case as shown below.
Test Case 1
Checking Game Behavior
Step 1. Input: Right Click
Expected Result: Pickup placed at click location
Step 2. Input: Left Click on Teddy Bear
Expected Result: Teddy Bear collects Pickup and stops
Step 3. Input: Left Click on Teddy Bear
Expected Result: No response (there's no pickup to collect)
Step 4. Input: Right Click
Expected Result: Pickup placed at click location
Step 5. Input: Right Click
Expected Result: Pickup placed at click location
Step 6. Input: Left Click on Teddy Bear
Expected Result: Teddy Bear collects closest Pickup then moves toward next pickup
Iteration: For and Foreach Loops 229
Step 7. Input: Right Click closer to the Teddy Bear than the currently targeted Pickup while Teddy Bear
is moving toward Pickup
Expected Result: Pickup placed at click location, Teddy Bear changes course to collect new Pickup, then
moves toward originally targeted Pickup
Step 8. Input: Right Click further away from the Teddy Bear than the currently targeted Pickup while
Teddy Bear is moving toward Pickup
Expected Result: Pickup placed at click location, Teddy Bear collects originally targeted Pickup, then
collects new Pickup, then stops
Notice that both Steps 7 and 8 have a timing constraint on the input so we can make sure Pickups that
are added while the Teddy Bear is moving are collected properly.
As we discussed in the Design a Solution step, we have a number of changes to make to both our
classes. Let's start with the TedTheCollector Pickups property:
/// <summary>
/// Gets the pickups currently in the scene
/// </summary>
/// <value>pickups</value>
public List<GameObject> Pickups
{
get { return pickups; }
}
Now we can move on to the TeddyBear GoToNextPickup method, where we change a single line of
code from
targetPickup = tedTheCollector.TargetPickup;
to
targetPickup = GetClosestPickup();
That of course means we decided to write a new GetClosestPickup method that finds the pickup in the
scene that's closest to the Teddy Bear:
/// <summary>
/// Gets the pickup in the scene that's closest to the teddy bear
/// If there are no pickups in the scene, returns null
/// </summary>
/// <returns>closest pickup</returns>
GameObject GetClosestPickup()
{
// initial setup
List<GameObject> pickups = tedTheCollector.Pickups;
GameObject closestPickup;
float closestDistance;
if (pickups.Count == 0)
{
return null;
230 Chapter 10
}
else
{
closestPickup = pickups[0];
closestDistance = GetDistance(closestPickup);
}
The chunk of code above returns null if there aren't any pickups in the scene just like the
TedTheCollector TargetPickup property used to do. The else body is only executed if there's at least
one pickup in the scene, so it sets closestPickup to the first pickup in the list. It also sets
closestDistance to the distance between that pickup and the teddy bear using a GetDistance method
we wrote (we'll discuss that method when we're done with this one).
The foreach loop iterates over all the pickups in the scene. The first thing we do in the body of the loop
is calculate the distance from the teddy bear to the pickup we're currently processing. If the distance to
that pickup is less than the distance to the closest pickup we've found so far, we just found a new closest
pickup. In that case, we save the new closestPickup and closestDistance. When the foreach loop
completes, we return the closest pickup we found in the scene.
You should note that some programmers take the reasonable position that every method should only
have a single return statement in it; a return statement is where we exit the method and return to the
caller of the method. Our method has two return statements instead: one if there are no pickups in the
scene and one after we've searched the list of pickups in the scene for the closest one. There's certainly a
way to write our method to have only a single return statement, but in our opinion that leads to less
efficient code that's harder to understand, so we decided to opt for efficiency and code clarity instead.
/// <summary>
/// Gets the distance between the teddy bear and the
/// provided pickup
/// </summary>
/// <returns>distance</returns>
/// <param name="pickup">pickup</param>
float GetDistance(GameObject pickup)
{
return Vector3.Distance(transform.position, pickup.transform.position);
}
Iteration: For and Foreach Loops 231
This method uses the Vector3 Distance method, which we found by reading the Vector3
documentation, to calculate and return the distance between the teddy bear's location and the pickup's
location. Although we could have simply used the Vector3 Distance method in the body of our
GetClosestPickup method instead, we felt that using this new method made that code easier to read
(and we'll use it again before we're all done).
If you execute the test case at this point, everything works fine through Step 6 but Step 7 fails. This is
understandable, since we haven't implemented or used the TeddyBear UpdateTarget method yet. The
good news, though, is that our GetClosestPickup method is working properly.
/// <summary>
/// Updates the pickup currently targeted for collection.
/// If the provided pickup is closer than the currently
/// targeted pickup, the provided pickup is set as the
/// new target. Otherwise, the targeted pickup isn't
/// changed.
/// </summary>
/// <param name="pickup">pickup</param>
public void UpdateTarget(GameObject pickup)
{
float targetDistance = GetDistance(targetPickup);
if (GetDistance(pickup) < targetDistance)
{
targetPickup = pickup;
GoToTargetPickup();
}
}
This code simply calculates the distance to our current target pickup and the distance to the pickup
parameter. If the pickup parameter is closer, we set that as the new target pickup (by setting the
targetPickup field) and start moving toward it. Notice that if the pickup parameter isn't closer we don't
need to do anything, we just keep moving toward our current target pickup.
When we were getting ready to write the code to calculate the direction to the target pickup and add the
impulse force to start the teddy bear moving in that direction, we realized that we already had code in
the GoToNextPickup method that does that. We couldn't just call the GoToNextPickup method, though,
because it also looks at all the pickups in the scene to find the closest one. That's definitely not the
functionality we need here!
Our solution was to pull the code we need out into a new GoToTargetPickup method and call our new
method from both the GoToNextPickup method and the UpdateTarget method:
/// <summary>
/// Starts the teddy bear moving toward the target pickup
/// </summary>
void GoToTargetPickup()
{
// calculate direction to target pickup and start moving toward it
Vector2 direction = new Vector2(
targetPickup.transform.position.x - transform.position.x,
232 Chapter 10
targetPickup.transform.position.y - transform.position.y);
direction.Normalize();
rigidbody2D.velocity = Vector2.zero;
rigidbody2D.AddForce(direction * ImpulseForceMagnitude,
ForceMode2D.Impulse);
}
Now we need to have the TedTheCollector script call the TeddyBear UpdateTarget method when a
new Pickup is added to the scene. As we said in the Design a Solution step, we've added a TeddyBear
field to our class for efficiency. We do need to do a little more work to populate that field.
Our general approach is to tag the TeddyBear with a new tag called TeddyBear, use the GameObject
FindWithTag method to find the TeddyBear game object, then find and save its TeddyBear component
in our field. We'll do all the coding in the Start method, which we'll need to add back in to the
TedTheCollector class because we removed it for the previous problem. Add the new TeddyBear tag
and set the tag for the TeddyBear prefab to the new tag. Remember, changing the prefab changes all
instances of that prefab in the scene, so the TeddyBear game object in the scene is also now tagged
(select it in the Hierarchy window if you'd like to confirm that).
/// <summary>
/// Start is called before the first frame update
/// </summary>
void Start()
{
// save reference for efficiency
GameObject teddyBearGameObject =
GameObject.FindWithTag("TeddyBear");
teddyBear = teddyBearGameObject.GetComponent<TeddyBear>();
}
The last thing we need to do is add the following code to the TedTheCollector Update method right
after we add a new pickup to the scene:
The test case passes when we execute it, so we'd like to declare victory and move on. Unfortunately, we
get the error message shown in Figure 10.5. whenever we add the first pickup to the scene (even after
the teddy bear has collected pickups, then stopped).
Iteration: For and Foreach Loops 233
Even though the game doesn't crash, it would be really ugly to leave this error in our code, so let's fix it
now.
The error message tells us that the error occurs at line 176 in the TeddyBear.cs file; that line is
in the GetDistance method. How do we know that's line 176? By default, Visual Studio shows us line
numbers, but if they're not showing up for you, you can show them by selecting Tools > Options from
the menu bar, expanding the Text Editor heading, selecting the All Languages heading, checking the
Line Numbers checkbox in the pane on the right, and clicking OK.
The error message also tells us that the error is a NullReferenceException. We get this error when we try
to access a field or property (or call a method) of a null object. We know that transform.position
can't be null because our TeddyBear game object has a Transform component, so we can form a
hypothesis that pickup is null.
To check our hypothesis, we need to look at the places where other code calls the GetDistance method.
To do this, right-click on the GetDistance method header and select Find All References (about
halfway down the popup). The resulting search results window is shown in Figure 10.6.
The search results list the method header and every call to the method, including the line number for
each line. We can even double-click one of the lines in the search results window and Visual Studio
takes us to that line in the code; when we do that for the first result, we go to line 85, the first line of
code in the UpdateTarget method body.
234 Chapter 10
Aha! We know the error occurs when we're adding the first pickup in the scene, which means the
targetPickup field is currently null because the Teddy Bear hasn't identified a target yet. That means
we're passing null in as the argument to the GetDistance method here, which is exactly what's
causing our problem. We can avoid this error by changing the UpdateTarget method to:
Now the Teddy Bear immediately starts collecting when we place the first Pickup! Why didn't we see
this problem before? Because our UpdateTarget method was crashing before it got to the call to the
GoToTargetPickup method. Now that we've fixed the crash, we get to that method call, which moves
the Teddy Bear toward the target pickup even though it's not collecting yet.
Sigh.
While we fix this problem, we can also clean up our UpdateTarget method a little. We have two places
that contain identical lines of code:
targetPickup = pickup;
GoToTargetPickup();
To fix this – because we know duplicated code is BAD – we'll pull these two lines of code into a
separate SetTarget method and call that method from both places. This is also a good idea because
we're about to make that code a little more complicated:
/// <summary>
/// Sets the target pickup to the provided pickup
/// </summary>
/// <param name="pickup">pickup</param>
void SetTarget(GameObject pickup)
{
Iteration: For and Foreach Loops 235
targetPickup = pickup;
if (collecting)
{
GoToTargetPickup();
}
}
Now we always set the targetPickup field (as we should) but only start moving toward the (new)
target pickup if the Teddy Bear is already collecting.
Unfortunately, we've now made it so Step 7 fails. The Teddy Bear keeps moving toward its original
targeted pickup, then keeps going past it without collecting it.
This might feel frustrating to you, but we WANT our test cases to expose errors in our code! It's much
better for us to discover these problems during development than it is to ship the game and then discover
them. It may not feel like it, but this is a good thing.
We again form a debugging hypothesis before we start digging around in the code. We know that the
Teddy Bear only collects a Pickup when it collides with it if that Pickup is set as its current target. The
fact that the Teddy Bear goes past its original target implies that the target pickup was changed (as it
should have been for Step 7 in the test case). We also know that the Teddy Bear didn't start moving
toward the new (closer) target pickup, which it only does if collecting is true.
If we go to the code, right-click the collecting field, and select Find references, we discover that the
field is never set in the code at all (except when it's initialized)! Let's fix that now.
First, we decide when collecting should be set to true. That should happen in the OnMouseDown
method just before we call the GoToNextPickup method.
Next, we decide when collecting should be set to false. That should happen in the GoToNextPickup
method if there are no more Pickups to be collected in the scene. We know that's the case if the
GetClosestPickup method returns null when we call it.
So why didn't we see this problem in our solution in the previous chapter? Because collecting is
initialized to false and the only Boolean expression that used collecting was checking if collecting
was false, so that Boolean expression always evaluated to true. That check is in the OnMouseDown
method, so it actually would send the Teddy Bear to the next Pickup even if it was currently collecting,
but because the rule was to go to the oldest Pickup in the scene, that never changed where the Teddy
Bear was going.
Changing the Array or List You're Iterating Over With a Foreach Loop
This isn't allowed, so your code will crash if you do this. If you need to remove elements from the array
or list you're iterating over while you're iterating you need to use a (back to front) for loop instead.
Chapter 11. Iteration: While Loops
In the previous chapter we introduced for and foreach loops; in both those loops, we know how many
times we expect the loop to iterate when we reach the loop during program execution. This chapter
covers a different form of iteration where we don't know how many times the loop will iterate when we
reach the loop. The decision about whether or not to keep looping for the loops discussed in this chapter
is based on some condition that typically isn't checking whether a counter has reached a particular value.
We've already seen the value of for and foreach loops in the previous chapter, so let's focus our attention
here on loops where we don't know how many times the loop will iterate. As usual, we'll start with an
example.
Problem Description: Write an algorithm that will ask for a GPA until a valid GPA is entered.
We use indentation to show what happens inside the loop, just as we used indentation to show what
happened inside our other selection and iteration algorithms. In this example, the third and fourth steps
are contained inside the loop (recall that the steps inside the loop are called the loop body).
Basically, our solution asks for a GPA. While the GPA is invalid (less than 0.0 or greater than 4.0), we
print an error message and ask for a new GPA. Eventually, the user will enter a valid GPA, stopping the
iteration of the last three steps in the algorithm. The associated CFG is shown in Figure 11.1.
The first thing we do is prompt for and get a GPA from the user. We then move to the node that “tests”
to see whether the GPA is invalid. Note that the test in this node is a Boolean expression like the one we
saw for the selection control structure. If the GPA is valid, we take the branch marked false, skipping the
loop body. If the GPA is invalid, however, we take the branch marked true, which lets us print an error
message and read in a new GPA. Notice the edge from the bottom node back up to the test node; after
we've read in the new GPA, we go back to the test node to check if the new GPA is invalid. Eventually,
the user will enter a valid GPA, the Boolean expression at the “test” node will evaluate to false, and
we'll take the left branch (the one marked false) out of the iteration control structure.
Boolean expression, this is just like the Boolean expressions we used in our if statements; it simply
evaluates to true or false. The Boolean expression is evaluated each time we get to the while part of
the loop. If it's true, we go into the loop and execute the loop body; if it's false, we exit the loop. The
Boolean expression for a while loop should generally contain at least one variable
loop body, the code that’s executed each time through the loop
Let's implement our algorithm from Example 11.1 using a while loop. Our algorithm is as follows:
First, we'll add the while part of the loop (since that's our new concept), then we'll fill in the rest.
Can you see how the value of gpa determines whether we keep looping or exit the loop? If the gpa is out
of range (either less than 0.0 or greater than 4.0), the Boolean expression evaluates to true and we
execute the loop body. If the gpa is in range, the Boolean expression evaluates to false and we end the
loop. Let's implement the rest of the algorithm:
Iteration: While Loops 239
// prompt for and get GPA
Console.Write("Enter a GPA (0.0-4.0): ");
float gpa = float.Parse(Console.ReadLine());
The first thing we do is get the GPA from the user, then while the GPA is invalid we print an error
message and get a new GPA from the user. How many times does the body of the while loop execute? It
depends on the user. If the user enters a valid GPA the first time, the loop body is never executed. If the
user enters an invalid GPA first, then enters a valid GPA, the loop body executes once. You get the idea.
There are three things we need to do with the variables in the Boolean expression for every while loop
we write, and we can remember those things with the acronym ITM. Before we get to the loop, we need
to Initialize the variables contained in the Boolean expression so that they have values before we get to
the line starting with while, which Tests the Boolean expression to see if the loop body should execute
or not. Finally, within the loop body, we need to Modify at least one of the variables in the Boolean
expression (give it a new value). So for every while loop, we need to make sure the variables in the
Boolean expression are Initialized, Tested, and Modified – ITM.
So what happens if we forget about ITM? If we don't initialize the variables in the Boolean expression
before the loop, the test in the while part is simply based on whatever happens to be in those memory
locations (and the chances of those being exactly the values we want are pretty slim). If we don't
properly test the Boolean expression, the decision about whether to loop or not won't be based on the
condition we really want to check. And if we don't modify at least one of the variables in the Boolean
expression inside the loop body, the loop will go on forever (commonly called an infinite loop)! Here's
why: if we get into the loop body, the Boolean expression in the while part must have been true. If we
don't change at least one of the variables inside the loop body, the Boolean expression is still true, so
we loop again, and again, and again ...
In the loop above, we initialize gpa (the variable in the Boolean expression of the while loop) when we
prompt for and get the GPA right before getting to the while loop. The GPA is then tested in the
Boolean expression of the while loop to see if it's valid. If the GPA is valid we don't execute the loop
body. If the GPA is invalid, though, we print an error message, modify the GPA by reading in a new
value from the user, and loop back around to the start of the loop.
The while loop lets us implement algorithms in which we don't know how many times we need to loop.
In fact, though, the while loop is so general that we can also use it to solve problems where we do know
how many times we need to loop. Let's revisit our “printing the squares” example from the previous
chapter, but this time we'll use a while loop to solve the problem instead of a for loop.
240 Chapter 11
Problem Description: Write a program that will print the squares of the integers from 1 to n, where n is
provided by the user.
The algorithm above loops through the body of the loop n times (from 1 to n), and each time it prints the
square of the current integer.
You should be able to easily see how similar this is to the for loop we used in the previous chapter to
solve the same problem. We still initialize i to 1 at the start of the loop, we still check if i is <= the
number the user entered on each iteration, and we still add 1 to i on each iteration. Although it's far
more common to use a for loop for this kind of problem, we did want to show you how a while loop can
be used for this problem as well.
In the previous chapter, we stated that testing for and foreach loops is like testing sequential code. That's
not true for while loops, however.
When we use a while loop in our program, “completely” testing the program becomes impossible. Think
about a single loop – to really test it, you'd have to execute the loop body zero times, execute the loop
body once, execute the loop body twice, execute the loop body three times, ... you see where this is
going, right? When the program contains while loops, we need to compromise. The best way to test such
programs is to execute each loop body zero times, one time, and multiple times. And of course we still
need to test the boundary values for each Boolean expression just like we did for the selection control
structure.
Let's talk about how we should test the while loop we used in Example 11.1 above. How do we execute
the loop body (the nodes that print an error message and get a new GPA) zero times? By entering a valid
Iteration: While Loops 241
GPA the first time we're asked. To execute the loop body one time, we enter an invalid GPA followed
by a valid one. To execute the loop body multiple times, we enter a few invalid GPAs followed by a
valid one. To test the boundary values, we want to input the following values for GPA: -0.1, 0.0, 4.0,
and 4.1. An example test plan is provided below.
Test Case 1
Loops: Loop Body 0 Times
Boundary Value: 0.0
Step 1. Input: 0.0 for GPA
Expected Result: Exit Program
Test Case 2
Loops: Loop Body 1 Time
Boundary Values: -0.1 and 4.0
Step 1. Input: -0.1 for GPA
Expected Result: Error message, reprompt
Step 2. Input: 4.0 for GPA
Expected Result: Exit Program
Test Case 3
Loops: Loop Body Multiple Times
Boundary Value: 4.1
Step 1. Input: -0.1 for GPA
Expected Result: Error message, reprompt
Step 2. Input: 4.1 for GPA
Expected Result: Error message, reprompt
Step 3. Input: 4.0 for GPA
Expected Result: Exit Program
These three test cases cover the boundary values and executing the loop body 0, 1, and multiple times.
Notice that the last step in each test cases says “Exit program.” Remember, each test case is for a
complete execution of a program. Our program is really quite useless at this point – it doesn’t even do
anything with the GPA once it has a valid one! – but it’s still a program.
In addition, we're actually using information about the structure of our solution rather than a problem
description to decide what the test cases should do. That makes these test cases white-box tests rather
than black-box tests, which in turn makes them unit tests rather than functional tests.
do
{
loop body
Boolean expression, the Boolean expression is evaluated each time we get to the while part of the
loop. If it’s true, we loop back around and execute the loopbody again; if it’s false, we exit the loop.
The Boolean expression for a do-while loop should typically contain at least one variable
loop body, the code that’s executed each time through the loop
Let's solve the problem from Example 11.1. one more time, this time using a do-while loop. Our
algorithm is:
Loop
Prompt for and get GPA
If the GPA is less than 0.0 or greater than 4.0
Print error message
While the GPA is less than 0.0 or greater than 4.0
This is a bit more awkward than using the while loop for this example, because we need the if statement
to make sure we print the error message only if the current GPA is invalid. Although there are examples
where a do-while loop provides a more elegant solution than a while loop, we personally find that we
almost never use do-while loops in practice. We did want to cover them here for completeness, though.
One quick comment about testing do-while loops. Because the loop body of a do-while loop is always
executed at least once, we can't test 0 iterations of a do-while loop; we can only test the loop body
executing 1 and multiple times.
Iteration: While Loops 243
All our work will be in the TeddyBearSpawner script because we only need to handle this problem
when we're spawning a new teddy bear. The big idea behind our approach is that we'll pick a random
location for the teddy bear we're spawning, then check to see if the collider for the new teddy bear would
collide with anything already in the game. If it wouldn't, we spawn the teddy bear at that location and
we're done. If it would collide with something, though, we randomly generate a new location and do the
check again. We keep doing this until we find a collision-free location for the new teddy bear or we
decide to give up on this spawn attempt.
We use the constant to make sure we don't end up in an infinite loop as we spawn a new teddy bear;
we'll discuss how we use that below. Because we need to check for a collision at least once, and possibly
several, times when we want to spawn a new teddy bear, saving the dimensions we'll use to check for
that collision saves us some processing time. We'll change the x and y components of the min and max
fields when we check for collisions, which saves us from creating two new Vector2 objects every time
we spawn a new bear.
Next, we add code to the TeddyBearSpawner Start method to retrieve and save those dimensions:
The first two lines of code above create a new teddy bear object so we can retrieve the teddy bear
collider. To generate the third and fourth lines of code, we found the size property in the
BoxCollider2D documentation (see Figure 11.2) and used the width (the x component) and the height
(the y component) to calculate the dimensions we need. The fifth line of code destroys the teddy bear we
created because we're done with it.
244 Chapter 11
Our final change is to the TeddyBearSpawner SpawnBear method. We replace the code we originally
had to generate a random location and create the new teddy bear object with the following code (with
discussion embedded as appropriate):
The first three lines of code select a random location in screen coordinates and the fourth line of code
converts those screen coordinates into world coordinates (we declared location as a field so we don't
need to create a new Vector3 object for it on each spawn). The final line of code calls a SetMinAndMax
method we wrote; we'll look at that method after we're done with this method, but the SetMinAndMax
method sets the min field to the coordinates of the upper left corner of a collision rectangle for a teddy
bear if it were placed at worldLocation and it sets the max field to the coordinates of the lower right
corner of a collision rectangle for a teddy bear if it were placed at worldLocation.
spawnTries++;
}
We use the spawnTries variable to keep track of how many times we've generated a location to see if
it's collision-free; that way, we can make sure we don't get stuck in an infinite loop if the game is
crowded with lots of teddy bears. We built our Boolean expression so the while loop would execute
while the current location would spawn the new teddy bear into a collision and we haven't tried the
maximum number of spawn tries yet. Figure 11.3 shows the documentation for the Physics2D
OverlapArea method we use for the first part of that expression.
As the documentation states, the method returns null if there's no collision for the given rectangle
corners, so if the method doesn't return null there is a collision for those corners. You might be
confused by the fact that we called the method with 2 arguments even though the documentation lists 5
parameters for the method. It turns out that C# has a way to specify optional parameters; the layerMask,
minDepth, and maxDepth parameters are optional for this method.
Most of the code above is identical to what we had in Chapter 7; the only difference is that we put it in
an if statement to ensure we only spawn the teddy bear in a collision-free location. Remember that the
while loop could have ended because we reached the max spawn tries, so we need to include the check
above before spawning the teddy bear.
That's all the code in the SpawnBear method; here's the SetMinAndMax method:
/// <summary>
/// Sets min and max for a teddy bear collision rectangle
/// </summary>
/// <param name="location">location of the teddy bear</param>
void SetMinAndMax(Vector3 location)
{
min.x = location.x - teddyBearColliderHalfWidth;
min.y = location.y + teddyBearColliderHalfHeight;
max.x = location.x + teddyBearColliderHalfWidth;
max.y = location.y - teddyBearColliderHalfHeight;
}
You should be able to see how the method sets the min field to the coordinates of the upper left corner of
a collision rectangle for a teddy bear if it were placed at location by calculating the x and y values for
that corner using the center of the teddy bear and the teddyBearColliderHalfWidth and
teddyBearColliderHalfHeight values we calculated in the Start method. We do similar processing
for the max field, which holds the coordinates of the lower right corner of a collision rectangle for a
teddy bear if it were placed at that location.
Develop a method that starts at a particular location in a list of strings, searching for the next occurrence
of "inactive". The search needs to examine every string in the list, returning the index of the next
occurrence of "inactive" or -1 if "inactive" doesn’t appear in the list.
Iteration: While Loops 247
This problem is actually based on a problem we needed to solve in one of our commercial games. In our
game, we need to search for a weapon launcher with a particular characteristic for our weapon selection
logic. Because we need to select the next launcher with that characteristic, starting from the currently
selected launcher, this is essentially the same problem we’re solving here.
To make sure we understand the problem, let’s think about all the possibilities for occurrences of
"inactive" in the list (this will help us develop our test cases as well). We already know what to do if
"inactive" doesn’t appear in the list, since the problem description tells us to return -1 in this case. It
also seems clear that if we find "inactive" later than the search start location in the list we should
return its index, and we should do the same thing if we find "inactive" before the search start location.
What if the string at the search start location is the only occurrence of "inactive" in the list? The
problem description says we need to look at every string in the list, so we need to return the search start
location if that’s the only occurrence of "inactive" in the list. By the way, this was exactly what we
needed in our commercial game as well, since if the currently selected weapon launcher was the only
launcher with the required characteristic, we wanted to keep it selected.
Design a Solution
We’re going to develop a single method to solve this problem, so we don’t need to worry about how a
set of objects will interact in our problem solution. We will, however, need to carefully figure out the
steps we need to follow in the method, so we’ll actually come up with an algorithm for the method
before we implement the code. We’ll do that at the beginning of our Write the Code step.
Let’s start by creating test cases for the four scenarios we thought about in the Understand the Problem
section.
We already know what to do if "inactive" doesn’t appear in the list, since the problem description tells
us to return -1 in this case. It also seems clear that if we find "inactive" later than the search start
location in the list we should return its index, and we should do the same thing if we find "inactive"
before the search start location. We also realized as we worked to Understand the Problem that we need
to return the search start location if that’s the only occurrence of "inactive" in the list.
We should also include test cases where we start the search at the very beginning of the list and at the
very end of the list. In some sense, these are like the boundary values we’ve tested in our selection and
while loop test cases.
Test Case 1
Inactive Not In List, Starting Search at Beginning of List
Step 1. Input: None. Hard-coded search start at index of 0 with list of "hey", "hi", "yo"
Expected Result: -1 for index
248 Chapter 11
Test Case 2
Inactive Later In List
Step 1. Input: None. Hard-coded search start at index of 1 with list of "hey", "hi", "yo", "inactive"
Expected Result: 3 for index
Test Case 3
Inactive Earlier In List
Step 1. Input: None. Hard-coded search start at index of 1 with list of "inactive", "hey", "hi", "yo"
Expected Result: 0 for index
Test Case 4
Only Inactive At Search Start Index
Step 1. Input: None. Hard-coded search start at index of 1 with list of "hey", "inactive", "hi", "yo"
Expected Result: 1 for index
Test Case 5
Starting Search at End Of List
Step 1. Input: None. Hard-coded search start at index of 3 with list of "hey", "inactive", "hi", "yo"
Expected Result: 1 for index
Because this is a fairly complicated problem, we’re going to develop a solid algorithm before we
implement that algorithm in C#.
As always, there are a number of ways we can solve this problem. Here’s one solution (that doesn’t
happen to use a while loop):
For each string from the search start index + 1 to the end of the list
If the string is "inactive", return its index
For each string from the start of the list to the search start index - 1
If the string is "inactive", return its index
If the string at the search start index is "inactive"
Return the search start index
Otherwise
Return -1
There are a couple problems with this solution. The largest problem is that we’ll have 3 if statements
that check for "inactive", which means we’ll end up duplicating essentially the same code in multiple
places in our method. Although that’s not really a huge deal for this problem, it would be worse if the
check was more complicated (like it is in our weapon selection problem). You should also be able to see
that the above algorithm might have us break out of our for loops when we find "inactive" in the list,
and some programmers object to this behavior in for loops. Let’s come up with a better algorithm that
actually uses a while loop.
We’ll start by figuring out what condition should be true for us to keep looping. This is actually a little
trickier than it might seem, because we should keep looping while we haven’t found "inactive" in the
list yet and while we haven’t looked at all the strings in the list yet. Instead of trying to specify that in a
Boolean expression, we’ll use a variable (often called a flag in this context) to keep track of that for us.
The start of our algorithm is therefore
Iteration: While Loops 249
Set search complete to false
While the search isn’t complete
<fill in more steps here>
We also need to know where to start our search. Because we only want to return the index of the string
at the search start location if it’s the only occurrence of "inactive" in the list, we should start our
search with the string at the location just past the search start location.
What happens if we find an occurrence of "inactive" inside our while loop? We should save its index
and set the search complete flag to true (since we just found what we were looking for). After the while
loop, we can simply return the saved index. Here’s our next cut at the algorithm:
You might be wondering why we initialized inactive index to -1. We did that so that if the body of the
while loop never changes that variable (which will be the case if "inactive" never appears in the list) it
will still be -1 when we return it at the end of the method.
The algorithm above actually has a bug that we’ll fix in a moment. If the search start location provided
to us is actually the last string in the list, we’ll end up trying to check a string at a location i that’s past
the end of the list.
We still have a little more work to do on our algorithm, because we haven’t made it so we move along
the list on each loop iteration, nor have we added logic to check when we’ve looked at every string in
the list. Let’s add that logic (and the logic to fix our bug) now:
The second if statement in our loop body sets our search complete flag to true; we know we started our
search at the search start index + 1, so if we’re looking at the string at the search start index, we just
checked the last string in the list and our search is complete.
In the otherwise part, we know we should keep searching the list so we add 1 to the current index so we
can look at the next string in the list on the next loop iteration. After adding 1 to i, we need to check to
see if it’s time to wrap around to the beginning of the list. We added that same wrapping code before the
loop when we initialize i to fix the bug we discussed above.
To add those algorithm steps in multiple places, we copied and pasted the steps in the algorithm.
Whenever you find yourself doing a copy and paste in an algorithm or in code, you should
IMMEDIATELY stop and decide whether or not you should create a new method for the duplicated
functionality. We haven’t really discussed how to write our own methods yet (coming soon, we
promise), so we’ll leave it this way here. We don’t like it, though!
You should work through the algorithm convincing yourself that it will work properly for the various
scenarios we considered in the Understand the Problem section. You should also convince yourself there
isn’t a much easier solution that just uses the index in a test for our while loop rather than using the
search complete flag; comparing the index to the search start location seems intuitive but isn’t really
much better. Here’s what that algorithm could look like:
The above algorithm is actually a couple steps shorter than the algorithm we developed, but we find it
less satisfying because the condition for the while loop is harder to understand and we have to duplicate
the check for "inactive" for the string at the search start index after the loop is done.
As we said, there are many ways to solve problems we come across in game development, and you
should certainly explore a different solution to this problem if you find an algorithm that seems more
intuitive to you. For the rest of this chapter, though, we’ll use the algorithm that uses the search
complete flag.
Because we’ve done such a thorough job coming up with our algorithm, converting the algorithm to the
required method is straightforward:
Iteration: While Loops 251
/// <summary>
/// Finds the index of the first occurrence of "inactive" in the list of
/// strings, starting at the provided index + 1. Checks all strings in the
/// list, wrapping around to the beginning of the list if necessary.
/// Returns -1 if "inactive" doesn't occur in the list
/// </summary>
/// <param name="startIndex">the start index</param>
/// <param name="strings">the list of strings</param>
/// <returns>the index of the "inactive" string or -1</returns>
static int FindInactiveStringLocation(int startIndex,
List<string> strings)
{
// initialize search variables
int inactiveIndex = -1;
int i = startIndex + 1;
if (i >= strings.Count)
{
i = 0;
}
bool searchComplete = false;
while (!searchComplete)
{
// check for and save inactive string info
if (strings[i] == "inactive")
{
inactiveIndex = i;
searchComplete = true;
}
return inactiveIndex;
}
Test Case 1
Inactive Not In List, Starting Search at Beginning of List
Step 1. Input: None. Hard-coded search start at index of 0 with list of "hey", "hi", "yo"
Expected Result: -1 for index
Test Case 3
Inactive Earlier In List
Step 1. Input: None. Hard-coded search start at index of 1 with list of "inactive", "hey", "hi", "yo"
Expected Result: 0 for index
Test Case 4
Only Inactive At Search Start Index
Step 1. Input: None. Hard-coded search start at index of 1 with list of "hey", "inactive", "hi", "yo"
Expected Result: 1 for index
Test Case 5
Starting Search at End Of List
Step 1. Input: None. Hard-coded search start at index of 3 with list of "hey", "inactive", "hi", "yo"
Expected Result: 1 for index
Our test plan seems complete, but remember we said we should execute the bodies of while loops 0, 1,
and multiple times in our testing. At this point all the test cases execute the loop body multiple times, so
let’s see if we can get test cases for 0 and 1 time.
To get the loop body to execute once, we can provide a list where "inactive" is in the location
immediately following the start location. Although we could add an additional test case for this, each
additional test case costs us time (and, in practice, money) to write and execute. Instead, let’s modify the
list we provide in Test Case 2 to cover this.
Test Case 2
Inactive Later In List
Step 1. Input: None. Hard-coded search start at index of 1 with list of "hey", "hi", "inactive"
Expected Result: 2 for index
How about a test case for making the loop body execute 0 times? It actually turns out that this is
impossible. Because we set the search complete flag to false before the loop and then test it when we
get to the loop, there’s no way for us to come up with a test case that skips the loop body based on the
search start index and list of strings we provide as input. So even though we should always try to
execute our while loop bodies 0, 1, and multiple times, there are going to be times when that just isn’t
possible.
Iteration: While Loops 253
To test the code, we embedded the method we wrote into a console application and had the Main method
in that application execute all the test cases for us. For example, here’s the code we wrote to execute
Test Case 1:
// Test Case 1
List<string> strings = new List<string>();
strings.Add("hey");
strings.Add("hi");
strings.Add("yo");
int index = FindInactiveStringLocation(0, strings);
if (index == -1)
{
Console.WriteLine("Test Case 1 passed");
}
else
{
Console.WriteLine("TEST CASE 1 FAILED!!");
}
Notice that the code for the test case provides the specific inputs for the test case, and also includes the
expected result for the test case (for Test Case 1, our expected result is -1 for the index). By structuring
our test code this way, we make it really easy to tell whether or not the test cases in the test plan passed
without having the tester try to evaluate each of the actual results to determine whether or not it matches
the expected result.
Go ahead and look at the code accompanying the book to see the code for the other test cases. When we
run our entire test plan, we get the following results shown in Figure 11.4.
As you can see, all our test cases passed, so we’re done solving this problem.
Boolean expression, the decision about whether to loop or not won't be based on the condition we really
want to check. We also need to make sure that at least one of the variables in the Boolean expression is
modified in the loop before we get back to the test again. If (when) you write a program containing an
infinite loop, check ITM.
In this chapter, our focus will be on designing and implementing a specific class rather than an entire
system of interacting classes. Developing complete systems is typically deferred to classes following the
first programming class in most game development and computer science programs, so we won’t tackle
formally designing and implementing full systems in this book25.
To make our discussions throughout this chapter more concrete, let’s work through the design and
implementation of a new class. Here’s a problem description:
Design and implement a class for a deck of cards. The class should implement standard operations that
we perform on a card deck.
Well, we’re probably going to need to figure out what’s required in a little more detail (Understand the
Problem) before we can start on our design.
For example, is this a standard deck of 52 cards with the typical ranks and suits? For this problem, we’ll
assume the answer is yes. Our bigger problem is figuring out what the standard operations are.
So what can we do with a deck of cards? We certainly need to be able to shuffle the deck, and we should
also provide the capability to cut the deck. What about dealing cards from the deck? This gets a little
trickier, because the way we deal cards is often dependent on the game we’re playing. Rather than trying
to make the deck use that information, we can simplify the deck and even keep it more general by
instead providing a method to take the top card from the deck. That makes the deck general enough to
use for any card game using the standard 52 card deck; other classes using the Deck class can handle the
details of how many cards get dealt to each player and so on.
Is that everything? Almost. Let’s think about how a deck is used in games in which the deck is used to
deal cards to each player, then gets used as the draw pile in the game. We can still take the top card from
the deck to draw a card from the pile, but at some point someone may take the last card from the draw
pile (making the deck empty). Would the next player try to get the top card from the empty draw pile?
We hope not (if you’re playing with someone who would try that, you should be playing for money)! In
25 The final chapter, though, does implement a complete game with multiple classes. We take a somewhat informal approach
to developing that system and defer more formal techniques (like use cases and UML sequence diagrams) to later
development efforts.
256 Chapter 12
real life we can tell the pile is empty by looking at it; we’re therefore going to need to provide a property
for our Deck class that tells if the deck is empty.
That leads us to the pictorial representation of the Deck class shown in Figure 12.1. Remember from our
discussion about classes and objects way back in Chapter 4 that we don’t let consumers of the class see
“inside” the object to use it. Instead, the consumer of the class uses object properties and methods to get
it to do what they need it to do without worrying about what fields are inside the object or even how the
properties and methods work. In other words, consumers of the class will be able to use everything
that’s provided on the outer part of the diagram below.
We’re now well on our way to our UML diagram. We’ve already identified the properties and methods
we’re going to want for our class, but we also need to know the fields (variables and constants used
throughout the class) we’re going to use, the data types of those fields, and the data types returned by
our properties and methods. Let’s start with our fields.
In our case, we only have one field: cards. Now, we could actually use 52 fields – one for each of the
cards in the deck – but that would be a very awkward approach to use, especially when we had to
process those fields with the properties and methods. What we’d really like is some object that could
hold all 52 cards. We’re obviously not the first C# programmers to need a class we could use for such an
object; in fact, we already know of several classes we could use. Specifically, either an array of Card
objects or a List of Card objects should work well for this. Let’s use a List, since the contents of the
deck change as we take cards from the deck.
Class Design and Implementation 257
Next we’ll look at our Empty property. The most reasonable thing to do is to have the property return
true if the deck is empty and false otherwise. We’ll worry about how to make that happen when we
Write the Code.
Now let’s look at our methods, starting with the Cut method. We’re going to need a single piece of
information from the code calling the method; specifically, the method will need to know where to cut
the deck. Let’s say that the place is specified as a number, where 0 would mean to cut at the top card, 1
would mean to cut at the second card, and so on. Should the method return anything? No, because only
the order of the cards in the deck is changed and the consumer of the class doesn’t (and shouldn’t) know
about the order of the cards in the deck.
What about our Shuffle method? It doesn’t need any information from the code that calls the method
because the deck just shuffles itself. Does the method need to return anything to the code that calls the
method? No, because all that happens is that the cards in the deck get shuffled into a different order, so
there’s nothing we need to return.
The TakeTopCard method won’t need any parameters either, because we’re simply going to take the top
card from the deck. The method does have to return something, though: the Card object from the top of
the deck.
Well, it looks like we’ve done all the design we need to create the UML diagram; that diagram is
provided in Figure 12.2.
We still have to Write the Code for our Deck class, of course, but we’ll defer that step until later. Just for
reference, Figure 12.3. shows what the documentation for the Deck class will look like when we’re all
done.
258 Chapter 12
namespace NamespaceName
{
documentation comment
[access modifier] [static] class ClassName
{
fields
constructors
properties
methods
}
}
Note: The square brackets ([ and ]) above are not part of the C# code, they’re just used to indicate which
parts of the given syntax are optional
We’ll discuss access modifiers in greater detail below, but for now, note that if we’re writing a class for
our programs or others to use, we use public as our access modifier. For the examples in this book, the
only other kind of class we’ll define is the application class, which doesn’t need an access modifier at
all.
Using the provided syntax, let’s work on implementing our Deck class in C#. Before we do that, though,
how do we actually add a new class to our project within the IDE? Right click the project name (it’s in
bold, near the top) in the Solution Explorer window and select Add > New Item… Now select Class in
the middle pane of the dialog, change the Name of the class to something reasonable (Deck for our
example), then click the Add button. You can see in the Solution Explorer window that the new class
has been added to your project.
Open the new Deck class by double-clicking it in the Solution Explorer window.
The namespace will be set to whatever we called our project when we created it (we called ours
DeckExample). We also need to include a class documentation comment, as we’ll always do. We’re
going to want to make this class accessible to others (public), so here’s how we start our class
definition:
using System;
using System.Collections.Generic;
namespace DeckExample
{
/// <summary>
/// A deck of cards
/// </summary>
public class Deck
{
Next, recall that a class has four parts: fields, constructors, properties, and methods. Let’s take a look at
fields first, then we’ll look at the other parts. Fields include both variables and constants. What are fields
for? They hold the state of the object.
Declaring the fields for our class is very similar to the way we’ve been declaring variables and constants
up to this point. We’ll discuss the details for fields that are variables, but the same concepts apply to
constants as well. For fields, there are two things we need to worry about that we haven’t had to deal
with for our variables and constants so far. Specifically, we need to decide on the visibility of the
variable, and we need to decide if the variable is a static variable or an instance variable. Here’s the
syntax for declaring a field (that’s a variable) in a class:
260 Chapter 12
access modifier, optional modifier that tells the visibility of the variable
static, optional modifier distinguishing between static and instance variables
dataType, the data type for the variable
variableName, the name of the variable
You’ll notice, of course, that the last two parts of the variable declaration – the data type and variable
name – are identical to the way we declared variables in the past. If the variable is in fact an object
rather than a value type, then we of course use a class name for the data type for that object.
The first new thing we see is the optional access modifier for each variable. This tells C# how
“accessible” the variable is; in other words, who can see and use it. We’ll have five choices for this:
public, private (the default in classes we define), protected, internal, and protected internal.
In this book, we’ll almost always make our fields private, and we suggest that you do the same. A
private variable is one that can only be looked at and modified by properties and methods contained in
the class that declares the variable. Making our variables private helps us keep our variables hidden
inside our objects so that objects can only interact with one another through the object properties and
methods. Because making our variable private is such a good idea, if we don’t provide an access
modifier at all the default for the variable in a class is private, so you only need to add an access
modifier if you want something other than private for that variable.
By the way, that’s why we’ve been making the (private by default) fields in our Unity scripts visible in
the Inspector by marking them with [SerializeField]. You’ll probably see some Unity developers
making the fields public instead of using [SerializeField] to make those fields show up in the
Inspector, but that’s a really bad approach to use because it breaks our information hiding.
Public fields can be accessed directly by methods and objects outside the class; we’ll try to avoid
public variables like the plague, but public constants are fine because consumers of the class can’t
mess them up. Protected fields can be accessed within the class and by subclasses of this class (when we
want to use inheritance, which we’ll discuss later in the book); we’ll only use protected fields if we’re
using inheritance in our problem solution. The last two access modifiers, internal and protected
internal, won’t be required for the problems we solve in this book (nor do we use them in any of the
commercial games we develop).
The next thing we need to decide for our fields is whether we want them to be static variables or
instance variables. If we want a static variable, we include the static keyword after the access
modifier; if we want an instance variable, we leave it off. So what’s the difference?
Let’s talk about instance variables first, since they’ll be the ones we use most commonly in this book.
Whenever we create an object from a class, that object gets its own copies of the instance variables, and
that object is the only one that can modify those instance variables (as long as we made them private).
Class Design and Implementation 261
If we create three Deck objects, for example, each of those objects will have its own list of the cards in
the deck.
We definitely want the cards variable in our deck to be private – you don’t want someone to be able
to change the order of the cards in the deck without shuffling or cutting the deck, right? We also want
this to be an instance variable, since each deck needs to have its own list of cards. So here’s what we
include in our class definition:
So what about static variables? A static variable is a single variable that gets “shared” by every object
that’s been created for that class. Say we want to give each deck of cards a unique ID. Not only is this a
classic example for static variables, we’ve actually had this precise need in one of our commercial
games for our game objects. The highest ID that’s been used so far isn’t information that really belongs
in one particular object of the class; it’s really class-wide information, since it applies to all objects of
that class. If we use a static variable, we can assign each new deck object a unique ID when we create it
(that ID would be stored in an instance variable for the new object), then increment the static variable to
make sure the next deck object we create gets a different ID. To make a variable a static variable instead
of an instance variable, all we have to do is to include static in our variable declaration:
And that’s all you need to know to declare fields in your classes.
Properties are typically used to provide access to the fields in the class; in other words, to provide access
to the state of the object. The three kinds of access we can provide for a property are read access, write
access, and read-write access. We read a property using a get accessor and we write a property using a
set accessor. If we want to provide both read and write access (typically called read-write access) to the
property, we simply provide both get and set accessors for the property. The syntax for defining
properties is provided below.
documentation comment
[access modifier] [static] dataType PropertyName
{
get { executable statements }
set { executable statements }
}
documentation comment, comment describing the property
access modifier, optional modifier that tells the visibility of the property
static, optional modifier distinguishing between static and instance properties
262 Chapter 12
dataType, the data type read and/or written for the property
PropertyName, the name of the property
executable statements, code to execute when reading or writing the property
Let’s start with a simple example that’s not related to the Deck class. Let’s say we have a Weapon class
that inflicts a certain amount of damage when the weapon is used in an attack. Consumers of the Weapon
class will need to be able to read the damage value to know how much damage to inflict. In addition, our
game may provide power-ups that actually increase the damage inflicted by the weapon either
temporarily or permanently. In either case, consumers of the class will need to be able to write the
damage value to make the change. The best way to handle this is to provide a Damage property as
follows (assuming we have an int damage field already declared):
/// <summary>
/// Gets and sets the damage inflicted by the weapon
/// </summary>
public int Damage
{
get { return damage; }
set { damage = value; }
}
Before we discuss how the property works, you've probably noticed that for the get and set accessors
we're breaking our rule that our open curly braces are always on a new line. Because it's very common
for those accessors to be a single line of code, it's common C# programming style to put the entire
accessor on a single line of code. For more complicated accessors (which we'll see soon), we follow our
standard curly brace placement style.
The get accessor returns the value of the damage field and the set accessor sets the damage field to the
value that’s provided. We provide the value by putting it on the right-hand side of the assignment
statement that’s setting the property; this is like passing a single argument into the property using special
syntax. Specifically, here’s the syntax we use to set a property:
objectName.PropertyName = value;
So if we set the Damage property of a Weapon object (let’s call it someWeapon) using
someWeapon.Damage = 42;
Class Design and Implementation 263
value is 42 inside the set accessor in the property code shown above and the internal damage field
inside someWeapon gets set to 42.
So why not just make our fields public instead of private – then we won’t need to use properties at
all? There are several reasons. First, when we make a field public, any consumer of the class can both
read and write the field. That might be fine in some cases, but in other cases (like the Empty property in
our Deck class) we don’t want to let them both read and write. It’s therefore better – and easier – to
always make our fields private and provide the properties necessary to let consumers access them as
appropriate.
Second, we can provide error-checking in the set accessor to make sure the consumer setting the
property isn’t setting it to an invalid value. For example, we can change our Damage property to make
sure we clamp the property to 0 if the consumer tries to set the Damage property to a negative number:
/// <summary>
/// Gets and sets the damage inflicted by the weapon
/// </summary>
public int Damage
{
get { return damage; }
set
{
if (value >= 0)
{
damage = value;
}
else
{
damage = 0;
}
}
}
Finally, we can expose properties that are intuitive to the consumer of the class but require a little extra
processing “under the hood.” Let’s say we wanted to provide the upper left corner of the collider for a
TeddyBear object:
/// <summary>
/// Gets the upper left corner for the collider
/// </summary>
public Vector2 ColliderUpperLeftCorner
{
get
{
BoxCollider2D collider = GetComponent<BoxCollider2D>();
return new Vector2(
collider.transform.position.x - collider.size.x / 2,
collider.transform.position.y - collider.size.y / 2);
}
}
264 Chapter 12
By including some extra processing logic in our get accessor, we can provide an intuitive property to
consumers of the class without exposing them to the details of the implementation. This is actually how
the Empty property in our Deck class will work also, so we’ll implement that property in a moment.
In some cases we might want the accessors of a read-write property to have different access modifiers;
for example, we might want the get accessor to be public and the set accessor to be private. In this
case, we set the access modifier for the entire property to public and add private before the set
keyword for the set accessor.
Okay, how can we tell if our deck is empty? Our deck is empty when it doesn’t have any more cards in
it; in other words, when the list of cards in the deck is empty. That observation leads us to the following
implementation of the property:
/// <summary>
/// Gets whether or not the deck is empty
/// </summary>
public bool Empty
{
get { return cards.Count == 0; }
}
A consumer of this property might use this property in the following way:
if (myDeck.Empty)
That means the get accessor needs to return a value to the consumer; we use return to make the
accessor return a value. The Boolean expression cards.Count == 0 evaluates to true if the list is
empty and false otherwise, so we can simply return the value of that expression as the value of the
property.
As discussed above, this property does some extra processing to make the result useful to the consumer
of the property while hiding the internal implementation details of the class. You should also note that
this property is read-only. We certainly want consumers of the class to be able to find out whether or not
the deck is empty, but we don’t want them to be able to make the deck empty by using write access to
the property.
documentation comment
[access modifier] [static] returnType MethodName(
dataType parameterName,
dataType parameterName,
. . . )
{
method body
}
The first part of the method (the part starting with the access modifier and ending just before the open
curly brace) is called the method header. Let’s take a closer look.
The access modifier for a method works exactly the same way as an access modifier for the fields and
properties in the class; we choose between public, protected, private, internal, and protected
internal to determine who can see and use this method. Remember that we said we should make all
the variables in our class private? Well, for methods, we’re going to want to make them public if
consumers of the class need to call them and private if they’re only used internally in our class (we’ve
already done this in our examples in previous chapters).
The next thing we need to decide for our methods is whether we want them to be static methods or
instance methods; this is the same idea as static variables and instance variables. If we decide that the
method is an instance method – the most common kind in this book and the most common kind you’ll
use in general – then each object will have its own copy of this method. If we decide that a method is a
static method, we include the word static in our method declaration. One quick caution – static
methods are NOT allowed to access instance variables. So if you need to get at an instance variable, you
need to use an instance method to do that.
Static methods can be very useful at times. For example, recall that when we use Console.WriteLine,
we’re using the static WriteLine method, so we don’t have to create a console object. However, static
methods generally represent a very small percent of the methods you’ll write, so think carefully before
deciding to make a method static.
The third thing we see in the method header is the return type for the method. This tells us which data
type will be returned by the method. For example, if we’re taking the top card from the deck, we want
our method to return a Card. If our method was calculating a speed for a bicycle, we’d want the method
266 Chapter 12
to return a float. There will be lots of times, however, when we don’t want to return anything at all
(we’re shuffling or cutting the deck, for example). In those cases, we pick void as our return type. This
simply means that this particular method doesn’t return anything.
Next, we pick our method name, which should be descriptive (just like our field and property names).
The final part we need to worry about in the method header is the list of parameters for this method.
Parameters seem to be one of the most confusing parts of programming for beginning programmers, but
there’s a straightforward way to think about them. If you understand that parameters are used to pass
information between the code that calls the method and the method itself you’re well on your way to
mastering them. By the way, that’s how we’ve been using parameters all along!
Finally, we include the method body, the part of the method that does the actual work for the method
when the method is called. The method body contains any local variables we need in the method. When
we have variables or constants that we only need in a particular method, we declare those variables and
constants at the beginning of the method body. These are called local variables (and constants) because
they can only be used inside the method in which they’re declared. Of course, the method body also
contains the executable statements that we want the method to carry out.
There’s actually a computer science term to describe where we can use a particular variable; it’s called
the variable’s scope. Variables that we declare as fields in a class are usable throughout the class, while
variables we declare within a method are only usable within the method. In fact, the loop control
variable we declare in a for loop is only usable within the body of the for loop, and in a foreach loop the
variable name we declare in the foreach part is only visible within the loop body.
Now, you’ll really have two kinds of methods in the classes you define: constructors and all the other
methods you need. Let’s look at constructors first.
Constructors are used when you create a new object from this class; whenever we’ve created objects in
our programs up to this point, we’ve been using the constructor to do so. For example, we should
provide a constructor for any objects we want to create from the Deck class. The easiest kind of
constructor to write is one that doesn’t do anything other than create the object:
/// <summary>
/// Constructor
/// </summary>
public Deck()
{
}
You should notice a few things here. We’ve made our constructor public because we want consumers
of the class to be able to create objects from this class; unless it’s a static class, it’s pretty useless if they
can’t create an instance of the class! We haven’t included a return type for this method, despite our
syntax description for methods above, because constructors are a special kind of method that doesn’t
require a return type. You can think of the constructor as returning a new object of the class, though. The
method name HAS to be the same as the class name for constructors; that’s how C# knows that it’s a
constructor rather than some other method. Finally, note that our parameter list above is empty. That
means we don’t have any arguments to pass when we call this constructor, though we can of course use
parameters for constructors when we need them.
Class Design and Implementation 267
But we probably wouldn’t want to have our constructor do nothing. Why not? Well, what if we created a
new deck object, then shuffled the deck and took the top card? What would we get for the top card? We
couldn’t take the top card, because the deck still doesn’t have any cards in it! We should definitely put
the cards in the deck when we create the deck object. Using the code we used in Chapter 10 to use
nested loops to fill a deck of cards, we can change our constructor to the following:
/// <summary>
/// Constructor
/// </summary>
public Deck()
{
// fill the deck with cards
foreach (Suit suit in Enum.GetValues(typeof(Suit)))
{
foreach (Rank rank in Enum.GetValues(typeof(Rank)))
{
cards.Add(new Card(rank, suit));
}
}
}
Recall that we already declared our cards variable as a field and created an empty list to hold the cards
when we declared the variable. The code above puts all the cards we need into that list.
As you know, our class can have multiple constructors. Why do we think you already know this?
Because we’ve already talked about method overloading, and since a constructor is just a special form of
method we can overload constructors also. The compiler figures out which constructor we want to use
based on the arguments that we provide in the call to the constructor.
You could argue that our constructor should also shuffle the deck, but have you ever noticed how the
cards are arranged in a new (physical) deck of cards you open? They’re actually arranged in order by
suit and then rank – it’s your job to shuffle the new deck before using it. We’re following the same
approach here.
One final comment about constructors before we move on. We've decided not to include the constructors
in our console apps in our UML diagrams because we'll always want to write at least one constructor for
each class we define, so there’s no need to add them to our diagrams. For our Unity games, the default is
that our scripts don't include constructors (remember, we use Instantiate instead of a constructor to
create new game objects), so we don't include constructors in our UML diagrams for them either.
Well, we’ve spent a lot of time talking about constructors, but there are a whole lot of other methods
too: all the other methods to do things with the class or the object. However, Figure 12.4. shows the
code as it currently stands.
using System;
using System.Collections.Generic;
namespace DeckExample
{
268 Chapter 12
/// <summary>
/// A deck of cards
/// </summary>
public class Deck
{
#region Fields
#endregion
#region Constructors
/// <summary>
/// Constructor
/// </summary>
public Deck()
{
// fill the deck with cards
foreach (Suit suit in Enum.GetValues(typeof(Suit)))
{
foreach (Rank rank in Enum.GetValues(typeof(Rank)))
{
cards.Add(new Card(rank, suit));
}
}
}
#endregion
#region Properties
/// <summary>
/// Gets whether or not the deck is empty
/// </summary>
public bool Empty
{
get { return cards.Count == 0; }
}
#endregion
}
}
Wait a minute, you say, after all that talk about making fields private why didn’t we make our cards
field private? As we said above, because it’s such good coding practice to do this, our fields
automatically default to being private; you’ll have to explicitly provide a different access modifier to
make them something else. Was that discussion a waste of time then? Not really, because you do need to
understand how the access modifiers work so you can use them properly.
Okay, let’s start working on our methods; recall that we have to implement the Cut, Shuffle, and
TakeTopCard methods. Let’s start with the easiest method – the TakeTopCard method. Here’s the code:
Class Design and Implementation 269
/// <summary>
/// Takes the top card from the deck. If the deck is empty, returns null
/// </summary>
/// <returns>the top card</returns>
public Card TakeTopCard()
{
if (!Empty)
{
Card topCard = cards[cards.Count - 1];
cards.RemoveAt(cards.Count - 1);
return topCard;
}
else
{
return null;
}
}
First, we check to make sure the consumer of the class isn’t trying to take the top card from an empty
deck. If they are, we return null. As you can see, we’re using the Empty property to check to see if the
deck is empty. We’ll actually talk about an alternative to returning null in this case when we get to the
chapter that discusses exceptions.
If the deck isn’t empty, we save the last card in the list – we’re thinking of the end of the list as the top
of the deck – into a local variable so we can return it from the method. We then remove the card from
the list of cards in the deck, since after someone takes the card from the deck it’s no longer in the deck.
Then we return the card we saved to the code that called the method. Note that we use return to
indicate what the method returns to the code that called the method just like we did in our Empty
property.
You might be thinking that it would be more intuitive to think of the front of the list of cards as the top
of the deck, since then we could use 0 in the above code instead of cards.Count - 1 to access and
remove the top card. The problem is that when we remove something from the front of the list, all the
other elements in the list get shifted down one place. When we remove something from the end of the
list, nothing else in the list needs to be moved. That may not seem like that big a deal to you, but think
about it this way. If we use the front of the list as the top of the deck and remove the top cards one at a
time until the deck is empty, we’ll have to do 51 * 50 * 49 * … * 1 shifts (that’s 51!, called 51 factorial).
If we use the back of the list as the top of the deck, we won’t have to do any shifts. Why spend all those
CPU cycles to do shifts when we don’t have to?
Especially in game development, we always want to be careful to be as efficient as possible. This is one
case where we can get much better efficiency by thinking carefully about how we approach our problem
solution.
Okay, let’s look at our first cut <grin> at the Cut method next:
/// <summary>
/// Cuts the deck of cards at the given location
/// </summary>
/// <param name="location">the location at which to cut the deck</param>
public void Cut(int location)
{
270 Chapter 12
int cutIndex = cards.Count - location;
List<Card> newCards = new List<Card>(cards.Count);
for (int i = cutIndex; i < cards.Count; i++)
{
newCards.Add(cards[i]);
}
for (int i = 0; i < cutIndex; i++)
{
newCards.Add(cards[i]);
}
cards = newCards;
}
Before we look at the details in the method body, let’s think of the problem this way. What we’re really
trying to do is move the stack of cards above the cut location to be below the stack of cards from the cut
location to the bottom of the deck. Now we’re getting somewhere – all we have to do is move the cards
above the cut location to the bottom of the deck.
We pass in the location at which we want to cut the deck as an argument in the method call since that’s
information the method needs to do its job. We’ll end up using lots of parameters for the methods we
write, so let’s talk a little bit more about how they actually work.
myDeck.Cut(5);
What really happens inside the method? The location parameter is matched up with the 5 when the
method is called, so any time location is referred to inside the method, it has the value 5. One of the
great things about methods is that we can reuse them as many times as we want, especially by using
different arguments when we call the method. So if we call the Cut method with this method call instead
myDeck.Cut(7);
we’ve reused the same code to cut the deck in a different place. Inside the method for this call, any time
location is referred to inside the method, it has the value 7.
We’ve been calling methods throughout the book, so you already know that we can use variables or
expressions as our arguments in the method calls. That means that both
and
myDeck.Cut(7 + 8);
Okay, back to the details of our method. Because the given location is interpreted as the number of cards
down from the top of the deck but the top of the deck is at the end of the list in our implementation, we
can’t use the provided location directly. Instead, we calculate the cutIndex to give us the list index that
Class Design and Implementation 271
corresponds to the given cut location. The next thing we do is create a new list of cards that will hold the
cards after they’ve been cut. We’re using one of the overloaded constructors for the List class; with this
constructor, we provide the capacity of the list. We know lists will actually grow in size as we need
them to – that’s one of the key benefits of lists over arrays – so why do we do that? It turns out that it
actually costs extra CPU time to grow a list when we need to, so providing the capacity of the list when
we construct it lets us avoid that CPU cost.
The next thing we do is copy the cards in the original list of cards from the cut index to the end of the
list (e.g., the top of the deck) into the beginning part (e.g., the bottom of the deck) of the new list of
cards. Then we copy the cards from the beginning of the original list of cards up to, but not including,
the cut index into the end of the new list of cards, essentially putting the bottom part of the old deck of
cards on top of the top part of the old deck of cards we’ve saved in the new list.
The last line of code sets our cards list to the newCards list we created and filled with the cut deck.
That’s what we want to do, of course, but what happens to the old list of cards that the cards variable
referred to before this assignment? It gets garbage collected since nothing refers to it any more.
Before moving on to our final method, you should realize that there’s a more intuitive way to implement
the Cut method. To figure out how to implement it, we need to read the List documentation to see if the
List class exposes any methods that could help us do this. Of course it does, so here’s the revised
method:
/// <summary>
/// Cuts the deck of cards at the given location
/// </summary>
/// <param name="location">the location at which to cut the deck</param>
public void Cut(int location)
{
int cutIndex = cards.Count - location;
Card[] newCards = new Card[cards.Count];
cards.CopyTo(cutIndex, newCards, 0, location);
cards.CopyTo(0, newCards, location, cutIndex);
cards.Clear();
cards.InsertRange(0, newCards);
}
You should look at the List documentation, specifically for the CopyTo and InsertRange methods, to
make sure you understand how the revised method works.
How did we know to do it this way? By reading the documentation! We read the description for each of
the List methods to see if there were any that could help us, and sure enough, there were. There are lots
of people who believe that reading documentation is for n00bs and that real programmers just whack the
code together as quickly as possible. That philosophy is dead wrong, though; we read documentation all
the time to make sure we’re writing clean, easy-to-understand code, and you should too.
On to our Shuffle method. Before we actually write the code, let’s come up with an approach we could
use to shuffle the deck. As you probably know, the point of shuffling a deck of cards is to randomize the
order of the cards in the deck. One approach we could use would be to really try to simulate a shuffle,
where we’d split the deck in half and interleave the cards from each half to re-form the deck. It’s been
mathematically shown that shuffling 7 times is reasonable to randomize the cards in a deck.
272 Chapter 12
Alternatively, since our goal is to get the cards into a random order, we can come up with a different
way to randomize the order of the cards.
Take a look at the following code. The general idea is to work our way through the list of cards
backwards, swapping each card with the card at a random location from the beginning of the list up to
the current location, inclusive. By the time we’ve worked our way through the entire list of cards, we
have a very good randomization of the card order. Notice the reference in the comment for the method;
this is the way Java implements a random shuffle of a list.
/// <summary>
/// Shuffles the deck
///
/// Reference:
/// http://download.oracle.com/javase/1.5.0/docs/api/java/util/
/// Collections.html#shuffle%28java.util.List%29
/// </summary>
public void Shuffle()
{
Random rand = new Random();
for (int i = cards.Count - 1; i > 0; i--)
{
int randomIndex = rand.Next(i + 1);
Card tempCard = cards[i];
cards[i] = cards[randomIndex];
cards[randomIndex] = tempCard;
}
}
And that finishes our work on the Deck class. Now let’s apply what we’ve learned about class design to
solve a complete problem.
If you download the code from the web site, you’ll see that we actually added a Print method to the
Deck class and wrote a small test program to test the constructor, property, and methods. You’ll often
find that your classes evolve, both to support unit testing those classes and because consumers of the
class need additional class functionality.
Design and implement a class to represent a single die. You also need to provide the standard die
operations for the class.
A couple questions immediately come to mind. First of all, how many sides should the die have? A
standard die has 6 sides, but you can purchase dice of many different numbers of sides (especially if you
want to roll to find out the damage the evil Orc has just inflicted <grin>). Let’s make the default die a
six-sided die, but also provide the capability to create a die of any integer number of sides.
Class Design and Implementation 273
We also need to ask what the standard die operations are. We should obviously be able to roll the die
(presumably, randomly selecting which side is on top), and we’ll also have to be able to find out which
side of the die is on top. Anything else? What about letting someone weight the die so some rolls are
more common than others? You’re kidding! We’re not going to let anyone cheat that way! Really,
rolling a die and seeing which side is on top are the only common die operations.
We will add one more operation, though, that tells how many sides the die has. In real life, you hardly
ever need to remind yourself how many sides the die has (especially with a standard die!), but in code if
the die has lots of sides you might need to remind yourself.
Design a Solution
Because we’re going to allow an arbitrary number of sides for a die, we’ll need a field in the Die class to
“remember” how many sides the die has. Because the number of sides is an integer, we’ll use an int for
that field. We’re also going to need to remember which side is currently on top; an int will work for
that field as well.
What about our properties and methods? Even though we don’t include constructors in our UML
diagram, we should probably realize that we need two constructors for the class. We’ll have one
constructor that doesn’t have any parameters and sets the number of sides to 6, and we’ll need another
constructor that lets the user specify with an argument how many sides the die will have. This makes it
easier for consumers of the class who are creating a standard die, while also providing the flexibility to
create a die with an arbitrary number of sides.
So which operations should be properties and which should be methods? Remember, we typically use
properties to provide access to the state of the object. That means we should expose a TopSide property
to let a consumer find out which side is on top and a NumSides property to let the consumer find out
how many sides the die has. You should immediately realize that both these properties should only
provide read (not write) access; if not, stop and think about that for a moment.
The TopSide get accessor will return an int to tell the consumer which side of the die is on top.
Similarly, the NumSides get accessor will return an int for the number of sides.
Because rolling the die is more complicated and really represents having the die do something (rather
than directly accessing object state), we’ll use a method to roll the die. The Roll method won’t need any
parameters, since all the information we need (the number of sides) is contained in the object. Should
the method return anything? There’s a tradeoff here. On one hand, rolling the die shouldn’t return
anything to the user, since it just changes the internal state of the die object. On the other hand, if we roll
the die, we’re probably interested in which side ends up on top, so we could return that information.
We’re going to design this so the Roll method doesn’t return anything; a consumer of the class would
therefore roll the die using the Roll method, then access the TopSide property to find out which side is
on top. This is actually consistent with how rolling a die works in the real world – first we roll the die,
then when it comes to rest we see which side is on top.
Given this design, we have the UML diagram shown in Figure 12.5.
274 Chapter 12
Since we’ve now finished our UML diagram, we’re ready to move on to our test plan.
We need to make sure our test plan covers both constructors, the TopSide and NumSides properties, and
the Roll method. We don’t have any selection or iteration constructs in any of this, so we could do all
the testing in a single test case, but let’s do one test case for the default six-sided die and another for a
256-sided die. For each of the test cases, we’ll get the top side and the number of sides right after
creating the die, then roll the die 3 times and print the resulting top side of the die after each roll. These
test cases are really unit test cases rather than functional test cases since we’re testing a single class.
Test Case 1
Checking 6-Sided Die
Step 1. Input: None. Hard-coded steps in the test case code
Expected Result:
Top Side: 1
Num Sides: 6
Top Side: appropriate
Top Side: appropriate
Top Side: appropriate
Notice that we have a new problem here because we’re trying to test a program that uses random
numbers. How are we going to tell whether or not the top side is correct after each roll? This is actually
a really hard problem to solve. At this point, we’ll just confirm that the rolls “look random”, though we
recognize this is an imperfect approach. We’ll discuss this further at the end of the chapter.
Test Case 2
Checking 256-Sided Die
Step 1. Input: None. Hard-coded steps in the test case code
Expected Result:
Top Side: 1
Num Sides: 256
Top Side: appropriate
Top Side: appropriate
Top Side: appropriate
Let’s write the constructor that doesn’t have any parameters and the TopSide and NumSides properties
and test those out before writing the Roll method. Our initial code is in Figure 12.6.
namespace DieProblem
{
/// <summary>
/// A die
/// </summary>
public class Die
{
#region Fields
int topSide;
int numSides;
#endregion
#region Constructors
/// <summary>
/// Constructor for six-sided die
/// </summary>
public Die()
{
numSides = 6;
topSide = 1;
}
#endregion
#region Properties
/// <summary>
/// Gets the current top side of the die
/// </summary>
public int TopSide
{
get { return topSide; }
}
/// <summary>
/// Gets the number of sides the die has
/// </summary>
276 Chapter 12
public int NumSides
{
get { return numSides; }
}
#endregion
}
}
Although the initial value we select for topSide is really arbitrary, we should still pick an initial value
to use in the constructor (ints actually default to 0 for their initial value, which would be invalid for a
standard die); we just happened to pick 1.
We need to write an application class for our test cases; we’ll start by simply adding the portions of Test
Case 1 that we can currently execute. Specifically, we won’t call the Roll method in the test case yet
because we haven’t written it yet!
Assuming you created the project in the IDE as a console application, we can simply add our test cases
to the Program class that was automatically generated by the IDE. Figure 12.7 show that class with Test
Case 1 partially implemented.
using System;
namespace DieProblem
{
/// <summary>
/// Tests the Die class
/// </summary>
internal class Program
{
/// <summary>
/// Executes test cases for the Die class
/// </summary>
/// <param name="args">command-line args</param>
static void Main(string[] args)
{
#region Test Case 1
#endregion
}
}
}
Class Design and Implementation 277
When we run the code, we get the output shown in Figure 12.8, which is exactly what we expected.
Next, we add the constructor that takes in the number of sides as a parameter; that code is provided
below.
/// <summary>
/// Constructor for a die with the given number of sides
/// </summary>
/// <param name="numSides">the number of sides</param>
public Die(int numSides)
{
this.numSides = numSides;
topSide = 1;
}
There’s actually something here that we only discussed briefly earlier in the book. You might think that
we could just use
numSides = numSides;
to set the field to the value of the parameter. Although this is syntactically correct, it doesn’t actually do
what you hoped it would. In fact, it sets the parameter’s value to the parameter’s value, which is
obviously a waste of time.
So how do we say we want to set the value of a field with the value of a parameter that has the same
name? By preceding the field’s name with the this keyword like this:
this.numSides = numSides;
The problem is that the parameter named numSides “hides” the field named numSides inside the
constructor. By preceding the field’s name with this, we’re indicating that we mean the field for this
278 Chapter 12
object, not the parameter. We won’t have to use the this keyword very often – most commonly in our
constructors – but it’s important that you understand how it works.
Now we can add code to our Program class to run the first part of Test Case 2. When we do that and run
the program, we get the output shown in Figure 12.9.
Our code seems to be working fine so far, but let’s work on the constructors a little more before moving
on.
/// <summary>
/// Constructor for six-sided die
/// </summary>
public Die()
{
numSides = 6;
topSide = 1;
}
/// <summary>
/// Constructor for a die with the given number of sides
/// </summary>
/// <param name="numSides">the number of sides</param>
public Die(int numSides)
{
this.numSides = numSides;
topSide = 1;
}
This isn’t very satisfying, because the code in the constructors looks remarkably similar (setting the
topSide field is even identical). We should be able to figure out a way to consolidate the constructor
logic in one place and reuse it for both the six-sided die and general die constructors. After all, the
Class Design and Implementation 279
ability to reuse methods – including constructors – is one of the reasons we use methods in the first
place!
We can, in fact, do this in a better way. Check out the new constructors:
/// <summary>
/// Constructor for six-sided die
/// </summary>
public Die(): this(6)
{
}
/// <summary>
/// Constructor for a die with the given number of sides
/// </summary>
/// <param name="numSides">the number of sides</param>
public Die(int numSides)
{
this.numSides = numSides;
topSide = 1;
}
How does the constructor for the default six-sided die work now? It actually calls the other constructor
with 6 as the argument, creating a die with 6 sides just as we want. Remember, we’ve used this before
to reference our fields; our modification simply shows another use of this to refer to (call) another
constructor for the class. Why is this a good idea? Because we’ve shortened the code and put all the
initialization that’s done at instantiation time into a single constructor. If we ever need to change that
code, we can do it in one place rather than in multiple places.
Before moving on, we need to re-test our code to make sure we haven’t broken anything by changing
the constructor for the six-sided die. Testing code after you’ve made internal changes that shouldn’t be
visible to consumers of the code is typically called regression testing because we’re making sure our
code hasn’t regressed – moved backward to a worse condition – as a result of our changes. Running our
test cases again shows that the code still works as expected, so we can move on.
/// <summary>
/// Rolls the die
/// </summary>
public void Roll()
{
Random rand = new Random();
topSide = rand.Next(numSides) + 1;
}
The first line of code creates a new Random object we can use to generate random numbers. The second
line of code generates a random number between 0 and numSides – 1, then adds 1 to that number to get
280 Chapter 12
the correct number for a face on the die. We don’t want a six-sided die to have its sides numbered from
0 to 5, do we? That’s why we need to add 1 to the random number – to shift the numbers so that the
sides are actually 1 to 6 as we’d expect.
That completes our Die class, so let’s move on to our complete test cases.
We added code to our Program class to roll the die 3 times in each of our test cases and print the results;
the output is shown in Figure 12.10.
This looks like everything is working fine, but this actually isn’t the best way to implement the code to
make our die roll randomly. We better go back to the code (again).
Why do we say we should do this differently? To find the answer, we need to look at the documentation
for the Random class. Our first step is to examine a little more closely how the Random constructor that
we’re using works; the documentation says that the constructor
“Initializes a new instance of the Random class, using a default seed value.” and, in the Remarks section,
“… the default seed value is derived from the system clock, which has finite resolution.”
This is actually fairly common. Random number generators have some base number that they use to
generate the sequence of numbers; this base number is called a seed. Using the current system time is
also a common approach. If we keep reading the Remarks section of documentation we find the
following statement:
“As a result, different Random objects that are created in close succession by a call to the default
constructor will have identical default seed values and, therefore, will produce identical sets of random
numbers. You can avoid this problem by using a single Random object to generate all random numbers.”
It feels like we’re getting closer. We’re creating a new instance of Random each time we call the Roll
method; because computers are screamingly fast these days, the documentation tells us that we could
Class Design and Implementation 281
end up using the same seed each time. Using the same seed yields the same sequence of numbers, so that
would be a big problem. Now all we have to do is fix it!
One reasonable solution is to use a single random number generator as a field rather than creating a new
random number generator each time we call the Roll method. We’ll create that object when we declare
the variable that holds it. Our new UML diagram is shown in Figure 12.11.
This isn’t exactly what the documentation suggests, though. The documentation says we should use a
single Random object to generate all random numbers. That’s what we do in our commercial games – use
a single Random object to generate all the random numbers in our entire game, not just in a single class –
but that’s a more complicated solution than we need to move forward here.
The complete (and revised) Die class code is provided in Figure 12.12.
using System;
using System.Collections.Generic;
namespace DieProblem
{
/// <summary>
/// A die
/// </summary>
public class Die
{
#region Fields
int topSide;
int numSides;
Random rand = new Random();
#endregion
282 Chapter 12
#region Constructors
/// <summary>
/// Constructor for six-sided die
/// </summary>
public Die(): this(6)
{
}
/// <summary>
/// Constructor for a die with the given number of sides
/// </summary>
/// <param name="numSides">the number of sides</param>
public Die(int numSides)
{
this.numSides = numSides;
topSide = 1;
}
#endregion
#region Properties
/// <summary>
/// Gets the current top side of the die
/// </summary>
public int TopSide
{
get { return topSide; }
}
/// <summary>
/// Gets the number of sides the die has
/// </summary>
public int NumSides
{
get { return numSides; }
}
#endregion
/// <summary>
/// Rolls the die
/// </summary>
public void Roll()
{
topSide = rand.Next(numSides) + 1;
}
#endregion
}
}
Figure 12.12. Die.cs
Class Design and Implementation 283
Now we run our test cases again; the results can be found in Figure 12.13.
It looks like our code still works with this change, which is good news! We should run the test cases a
few more times, though, to make sure we don’t keep getting the same sequence of numbers (we don’t).
There’s actually a reasonable way around our informal “see if the rolls look random” approach to testing
here. As discussed above, the Random class uses a seed as the starting point for the sequence of random
numbers it generates. If we create the Random class with the same seed every time we’ll get the same
sequence of random numbers. To be precise, we actually get pseudo-random numbers in the sequence –
if the sequence were truly random, we wouldn’t be able to predict the next number in the sequence, but
if we know the sequence of numbers previously generated using the same seed we can perfectly predict
the numbers in the sequence. Why do we care in this case? Because we could run the sequence once
with a given seed to see what the sequence of numbers is, then in our test case code if we use the same
seed we’ll know exactly what the roll results should be. We won’t bother with that approach here, but
we wanted you to be aware of it.
In Chapter 12 we said that classes have constructors and other methods. Recall that our constructors
don’t provide a return type, but all our other methods do. If the method doesn’t return anything we set
the return type to void, but if the method does need to return a value to the code that called the method
we set the return type to the data type of that value.
The methods we wrote in the previous chapter were all public since we wanted the consumers of the
classes to be able to use those methods. It’s often the case, however, that it also makes sense to have
private methods that do internal processing for the class. Private methods are useful inside the classes
that contain them, but there’s no reason for a consumer of the class to call those methods directly. That’s
why we make them private.
So how do we decide which methods to provide? If we were designing a system of interacting classes,
there are very useful Object-Oriented Analysis and Design techniques we can use to determine the
required methods. However, we’ll continue using a more informal process in this book.
Specifically, we’ll decide which methods to include in a class by thinking about the behavior that class
needs to provide. One of the things that really helps us do this is the realization that many of the classes
we design are actually used to model entities in the real world. Given that observation, we can think
about how the real world entity behaves and decide which of those behaviors will be useful in the
software class. We used this approach for our Deck class in the previous chapter, exposing Shuffle,
Cut, and TakeTopCard methods because shuffling, cutting, and taking the top card are things we do with
real decks of cards. That same approach led us to provide a Roll method for our Die class.
We’ll also regularly find that we need private methods to help with the internal processing required by
the class. We generally won’t realize we need those methods when we think about the behavior of the
real world object we’re modeling; instead, we’ll usually discover them when we Write the Code.
286 Chapter 13
Why do we need to figure out the information flow for our methods anyway? Because we're going to
need to use parameters for any information that comes into the method and we're going to need to return
whatever information needs to come out of the method26. Before we can actually write our methods, we
have to know what parameters and return types we need. Let's revisit the methods from our Deck class.
The constructor doesn't require any information to come in, since it just builds a standard deck of 52
cards with 4 suits and 13 ranks within each suit. We could come up with a more general constructor that
lets the code calling the method specify how many suits and ranks there should be, but we'd then also
need a new Card class that set the ranks and suits properly. We'll leave the constructor with no
information coming in through parameters. Like all constructors, this method returns an object of the
class.
How about the Shuffle method? We don't need any information to come in to the method because the
method just randomly shuffles the cards. We also don't pass any information out of the method, so this
method doesn't have any information flow at all. No parameters, and the return type is void.
The Cut method does need information to be passed in through a parameter; specifically, the method
needs to know the location at which to cut the deck. It doesn’t pass anything out of the method, though;
it simply changes the internal state of the deck by cutting it, so the return type should be void.
Finally, the TakeTopCard method doesn't need any information coming in to the method because the
deck knows the code calling the method is trying to remove the top card from the deck (rather than some
other card). It does need to return that card to the code calling the method, though. So there are no
parameters for this method, but the return type for the method is Card.
This figuring out the information flow stuff isn't new; in fact, we already had the above discussion in
Chapter 12 as we were developing our class diagram for the Deck class. We included it again here
because it's an important step we take when we design the methods in our class.
The method header for a method without any information flow is pretty straightforward – here's the
header for the Shuffle method:
26 In fact, if our parameters are objects we can also return information through those parameters. We'll address this when we
talk about passing objects as parameters.
More Class Design and Methods 287
public void Shuffle()
Methods that do have information flow require more complicated method headers. For example, the Cut
method has a method header that looks like
Because this method needs the location coming in to the method as an integer, we pass that information
with an int parameter. Whenever we have information flow into a method, we use a parameter for each
piece of information (in this case, one parameter for the location at which we should cut the deck).
Each parameter has a data type and a name. Both the data type and name for a parameter shouldn't really
require explanation, since they're very similar to the data types and names of variables and constants.
The TakeTopCard method doesn't need any information to come in to the method, but it does return the
top card from the method. That method header looks like
And that takes care of all the methods we have for the Deck class.
But how do we know how many parameters each method should have? And how do we know whether
or not the method needs to return a value? Believe it or not, we've already solved that problem! When
we did our information flow analysis, we captured the answer in the characteristics of each method in
our class diagram. Look again at the method headers we just generated, and you'll see how we easily
went from our class diagram to our method header. Don't skip the class diagram because you think your
problem solving will go faster without it; you're going to have to figure out the method headers no
matter what, and the class diagram is a great tool to help you do that.
Now that we have the method headers figured out, let's revisit the method bodies.
/// <summary>
/// Takes the top card from the deck. If the deck is empty, returns null
/// </summary>
/// <returns>the top card</returns>
public Card TakeTopCard()
{
if (!Empty)
{
Card topCard = cards[cards.Count - 1];
cards.RemoveAt(cards.Count - 1);
return topCard;
}
288 Chapter 13
else
{
return null;
}
}
First, we check to make sure the consumer of the class isn’t trying to take the top card from an empty
deck. If they are, we return null because there is no top card. If the deck isn’t empty, we save the last
card in the list – we’re thinking of the end of the list as the top of the deck – into a local variable so we
can return it from the method. We then remove the card from the list of cards in the deck, since after
someone takes the card from the deck it’s no longer in the deck. Then we return the card we saved to the
code that called the method.
/// <summary>
/// Cuts the deck of cards at the given location
/// </summary>
/// <param name="location">the location at which to cut the deck</param>
public void Cut(int location)
{
int cutIndex = cards.Count - location;
Card[] newCards = new Card[cards.Count];
cards.CopyTo(cutIndex, newCards, 0, location);
cards.CopyTo(0, newCards, location, cutIndex);
cards.Clear();
cards.InsertRange(0, newCards);
}
From the List documentation, we find that the CopyTo method “Copies a range of elements from the list
to a compatible one-dimensional array, starting at the specified index of the target array.” In other
words, our first call to the CopyTo method copies all the cards in the deck from the cut index to the end
(top) of the deck into the beginning of the newCards array. The first argument in the method call tells
where to start copying from and the last argument in the method call tells the number of elements to
copy. The second argument is the target array and the third argument is the starting index in the target
array. Our second call to the CopyTo method copies all the cards in the deck from the beginning
(bottom) of the deck up to the cut index into the newCards array starting just after the cards we already
added to that array. The last line of code in the method body simply copies all the cards in the array –
which now contains the cut deck – into our cards field, replacing the cards that used to be in that field.
/// <summary>
/// Shuffles the deck
///
/// Reference:
/// http://download.oracle.com/javase/1.5.0/docs/api/java/util/
/// Collections.html#shuffle%28java.util.List%29
/// </summary>
public void Shuffle()
{
Random rand = new Random();
for (int i = cards.Count - 1; i > 0; i--)
More Class Design and Methods 289
{
int randomIndex = rand.Next(i + 1);
Card tempCard = cards[i];
cards[i] = cards[randomIndex];
cards[randomIndex] = tempCard;
}
}
There’s nothing particularly complicated in this method body, though it is an interesting way to
randomize the order of the elements in the collection. That concludes our review of the methods in the
Deck class.
Perhaps the easiest way to understand how parameters work is to think of them this way: whenever we
associate an argument in the method call with a parameter in the method header, the value of that
argument is copied into a temporary variable with the parameter name inside the method. This approach
is called pass by value. Note that this only applies to passing value types as parameters; we’ll talk about
passing objects as parameters in the following section.
Let's illustrate this idea by looking at a code fragment that simply cuts a deck in two different locations
using the Cut method. Here's the code:
Recall that the Cut method has a single parameter called location. The first time we call the method,
any time location is referenced inside the method, we're really talking about the value of the
firstCutLocation variable. Why? Because in the method call, we said the parameter location
corresponds to the argument firstCutLocation (remember, it matches number, order, and type for
parameters), so the value of firstCutLocation gets copied into location. After the method finishes,
the deck will have been cut at the location given in firstCutLocation. Now we call the method again,
this time associating the location parameter with the secondCutLocation argument. That means that
the value of secondCutLocation gets copied into location. After the method completes this time, the
deck will have been cut at the location given in secondCutLocation.
We typically write methods to complete general kinds of actions on objects (e.g., cutting a deck). We
can then reuse these methods as many times as we want, and by using different arguments and different
variables on the left of the = sign each time (for methods that return something), we can make those
methods act on as many different values as we want. The ability to use the same action (method) many
290 Chapter 13
times is one of the things that makes methods so useful, and parameters are an essential part of making
this reuse possible.
So how does C# get around this problem? It turns out that, for objects, C# doesn't actually pass the entire
object as a value – it passes a reference to the object as the value instead. Think of this as passing the
memory address of the object rather than the actual object.
For example, say we have a StringBuilder object called message, with the value "Hi There!" that’s
stored in memory starting in memory location 40 as shown in Figure 13.1.
When the StringBuilder is passed as an argument, it’s actually the address of the StringBuilder
object (40) that’s passed rather than the object itself. The compiler can handle this without any trouble,
because it simply needs to set aside enough space for an address rather than for the object itself.
More Class Design and Methods 291
We get one additional benefit when we pass objects as arguments to a method. Within the method, we
can change the contents of the object (changing the contents of our StringBuilder object, for example)
without changing the address, so we can actually change a parameter within a method if it's an object.
It's kind of like changing the furniture in a house; the contents of the house change, but the address stays
the same. Say we had the following method (comments are omitted for the sake of brevity), which
simply replaces all occurrences of 'e' in the string builder object with 'o' instead:
void ChangeString(StringBuilder theString)
{
theString.Replace('e', 'o');
}
ChangeString(message);
and after the method runs, our message will now be "Hi Thoro!". That’s because we passed the
address of the object as an argument rather than the object itself.
When we change an object that was passed as an argument to a method inside that method, this change
is called a side effect. It's called a side effect because the fact that the object might be changed inside the
method isn't obvious from the return type of the method or the list of parameters. There are absolutely
many times when we want to change object parameters in a method, but be careful when you do this.
Errors caused by side effects are very difficult to find, so whenever you implement changes to object
parameters inside your method, do so with care.27
We said earlier that we could use literals rather than variables for arguments that were value types. We
can't do that for objects, of course, because there's no way to just use a literal value for an object (unless
we use null, which isn’t usually useful). We can, however, create a new object for the argument if we
don't happen to have an object already created. For example, in the constructor for our Deck class, we
used
to create new cards as arguments for the Add method in the List class.
Remember, when we pass objects as arguments, we really only pass a reference to the object rather than
the object itself. This also lets us change the object inside the method.
Design and implement a class to represent a hand of cards. You also need to provide the standard hand
operations for the class.
27There are ways to mark the parameters that could be changed through side effects to make it clearer that those side effects
may occur, but we don’t need them for the problems in this book.
292 Chapter 13
We seem to be seeing a trend in the problem descriptions: they keep saying we have to provide standard
operations for the class, and we have to figure out what those are! In lots of cases, we'd actually get a
better description of what we need to provide for the class, but this gives us good practice really
analyzing what's required.
We're obviously going to have to create Hand objects. We'll also need to provide some capability to
access the cards in the hand. Why would we want to do that? Because in reality, people typically look at
the cards in their hand to decide what to do. What else will we need? We're going to want to be able to
add a card to a hand because a card might be dealt into the hand or the player could draw a card into the
hand. We'll want the ability to remove a card from the hand, because players either play or discard cards
from their hand. Finally, we'll want to be able to tell whether or not the hand is empty.
Design a Solution
We're definitely going to need an instance variable in the Hand class to store the cards that are currently
in the hand; just as for the Deck class, a List will be quite useful for this variable.
Are there any properties that our hand should provide? What kinds of information would a consumer of
the Hand class want to know about the current state of a hand? At the very least, they’d probably want to
know how many cards are in the hand. Let’s therefore provide a Count property that provides that
information.28 Because consumers of the class shouldn’t be able to directly change the size of the hand
through the property, we’ll provide read (not write or read-write) access for this property.
We also said that a consumer should be able to tell whether or not the hand is empty. We know the
consumer could compare the Count property to 0 to determine whether or not the hand is empty, but as a
convenience we’ll also provide an Empty property that returns true if the hand is empty and false
otherwise. We’ll only provide read access for this property.
What about our methods? Our constructor won't need any parameters, because we'll simply have it
create a hand with no cards in it. We could also provide another constructor that takes in a list of cards
as a parameter and adds all those cards to the hand, but let's not do that. It's more intuitive to think of a
hand as starting empty, then we add one card at a time to build up the hand.
Providing access to each of the cards in the hand could be a little tricky. There's a very common pattern
that people use to “iterate” over things like a hand of cards; not surprisingly, it's called an iterator.
Providing an iterator would really require that we implement something called an interface in C#,
though, and implementing interfaces is a topic that we won’t cover in this book.
Instead, let's provide a method that will let someone using the Hand class access each of the cards in the
hand. This method, called GetCard, will let the user access a particular card in the hand (without
removing it from the hand). The code calling this method will need to provide the location of the card to
get as an int (starting with 0), and the method will return the card at that location.
28 You might think a property called Size would be better for the number of cards in the hand. Because the collection
classes expose the Count property to tell how many elements are in the collection, we opt to use the more standard name
here.
More Class Design and Methods 293
Providing this method (and the Count property) will make it very easy for someone to look at the cards
in the hand using a for loop. They can simply set up the loop to go from 0 to the size of the hand, then
get each of the cards inside the loop. By the way, do we need to add a new instance variable to store the
size of the hand? No! The list holding the cards in the hand gives us access to the Count property of that
list, so that list implicitly keeps track of the size of the hand for us. Cool.
We need to able to add a card to the hand, so let's figure out the information flow for an AddCard
method. We'll need a single piece of information – the card to be added to the hand – as a parameter for
this method. The method doesn’t return anything, so we'll pick a return type of void for the method.
Finally, we need to be able to remove a card from the hand. Our RemoveCard method will need a single
parameter telling the location of the card we want to remove. Like the TakeTopCard method in the Deck
class, this method will also return a value: the card being removed.
Now that we have our design completed, we're ready to move on to our test plan.
We need to make sure our test plan covers all the methods in our Hand class. The GetCard and
RemoveCard methods will both have selection constructs (making sure a valid location has been
provided), and we'll need to make sure we test both branches in those methods. We could do all our
testing in a single test case, but let's build a number of test cases that we can use to test our class as we
build it. Here's a reasonable set of test cases:
Test Case 1
Checking Constructor and Empty and Count Properties
Step 1. Input: None. Hard-coded steps in the test case code
Expected Result:
The hand is empty
Size: 0
294 Chapter 13
This test case simply checks that the object is created properly and that the Empty and Count properties
work properly when the hand is empty.
Test Case 2
Checking AddCard and GetCard methods and Empty and Count Properties
Branch: true branch in GetCard method
Step 1. Input: None. Hard-coded steps in the test case code
Expected Result:
Ace of Spades
Queen of Hearts
The hand is not empty
Size: 2
This test case makes sure the AddCard and GetCard methods work properly by adding an Ace of Spades
and Queen of Hearts to the hand and that the Empty and Count properties work properly when the hand
isn't empty.
Test Case 3
Checking GetCard method
Branch: false branch in GetCard method
Step 1. Input: None. Hard-coded steps in the test case code
Expected Result:
Location for getting a card was invalid
This test case makes sure the GetCard method returns null when we try to get a card at location 0 from
an empty hand.
Test Case 4
Checking RemoveCard method
Branches: true branch in RemoveCard method, false branch in RemoveCard method
Step 1. Input: None. Hard-coded steps in the test case code
Expected Result:
Jack of Clubs
Ace of Spades
Queen of Hearts
Location for removing a card was invalid
This test case makes sure the RemoveCard method works properly with both valid and invalid locations.
We add an Ace of Spades, Queen of Hearts, and Jack of Clubs to the hand, then remove the card at
location 2, then location 0, then location 0, then location 0 (which tries to remove a card from an empty
hand).
Our constructor doesn’t actually do anything, but we know we need a constructor for the classes we
write29. It actually turns out that if we don’t provide a constructor for a class, C# will automatically
generate a no-parameter constructor that doesn’t do anything.
So why did we explicitly write a no-parameter constructor that doesn’t do anything if C# will
automatically provide one? This is really a matter of taste; some programmers like to do this and others
don’t. We like to make the constructor explicit in the code, though, because we believe it makes the
code easier to understand.
Let’s write our properties next, because then we can run Test Case 1 to see how we’re doing with our
implementation. The Count property simply returns the number of elements in our cards list:
/// <summary>
/// Gets the number of cards in the hand
/// </summary>
public int Count
{
get { return cards.Count; }
}
and the Empty property tells whether or not the hand contains any cards (the same way we did in the
Deck class):
/// <summary>
/// Gets whether or not the hand is empty
/// </summary>
public bool Empty
{
get { return cards.Count == 0; }
}
Now we’ll add driver code for Test Case 1 to the Program class in our project; the code is provided in
Figure 13.3.
using System;
namespace HandProblem
{
/// <summary>
/// Tests the Hand class
/// </summary>
29 We’ll actually sometimes write static classes that don’t have a constructor, but those are the rare exception, not the
rule.
296 Chapter 13
internal class Program
{
/// <summary>
/// Tests the Hand class
/// </summary>
/// <param name="args">command-line arguments</param>
static void Main(string[] args)
{
Hand hand;
// Test Case 1
Console.WriteLine("Test Case 1");
Console.WriteLine("-----------");
hand = new Hand();
if (hand.Empty)
{
Console.WriteLine("The hand is empty");
}
else
{
Console.WriteLine("The hand is not empty");
}
Console.WriteLine("Size: " + hand.Count);
}
}
}
When we run this code, we get the results shown in Figure 13.4.
/// <summary>
More Class Design and Methods 297
/// Adds the given card to the hand
/// </summary>
/// <param name="card">the card to add</param>
public void AddCard(Card card)
{
cards.Add(card);
}
This is a very straightforward method that just adds the given card to the list of cards in the hand, so we
can proceed to the next method.
For the GetCard method, we need to handle the case where the code calling the method tries to get a
card at an invalid location: one that’s less than 0 or one that’s past the location of the last card in the
hand. In that case, the method returns null. This leads to the following code:
/// <summary>
/// Gets the card at the given location (leaving the card in the hand)
/// </summary>
/// <param name="location">the 0-based location of the card</param>
/// <returns>the card or null if the location is invalid</returns>
public Card GetCard(int location)
{
// check for valid location
if (location >= 0 && location < cards.Count)
{
return cards[location];
}
else
{
// invalid location
return null;
}
}
Now that we have the AddCard and GetCard methods implemented, we can add Test Cases 2 and 3 to
our test driver code and run those test cases. Here’s the code we add for Test Case 2:
// Test Case 2
Console.WriteLine("Test Case 2");
Console.WriteLine("-----------");
hand = new Hand();
hand.AddCard(new Card(Rank.Ace, Suit.Spades));
hand.AddCard(new Card(Rank.Queen, Suit.Hearts));
for (int i = 0; i < hand.Count; i++)
{
hand.GetCard(i).Print();
}
PrintEmpty(hand);
Console.WriteLine("Size: " + hand.Count);
Console.WriteLine();
298 Chapter 13
A couple comments before we look at the code for Test Case 3. We used a for loop to print each of the
cards in the hand. As we claimed when we were designing the class, the GetCard method and Count
property let us easily access each card in the hand.
You should also notice that we’re now calling a PrintEmpty method to print whether or not the hand is
empty. When we only had a single test case, we used the following code for that:
if (hand.Empty)
{
Console.WriteLine("The hand is empty");
}
else
{
Console.WriteLine("The hand is not empty");
}
When we started writing the driver code for Test Case 2, we realized we needed the exact same code for
this test case. One of the great benefits of methods is that they let us reuse code, and that benefit applies
to test driver code as well as the code we’d actually deliver for our game. So, we wrote the following
method in our Program class:
/// <summary>
/// Prints whether or not the given hand is empty
/// </summary>
/// <param name="hand">the hand to check</param>
static void PrintEmpty(Hand hand)
{
if (hand.Empty)
{
Console.WriteLine("The hand is empty");
}
else
{
Console.WriteLine("The hand is not empty");
}
}
We can now use this method for any of the test cases that need it without duplicating the code (notice
that we had to make this method static so our Main method, which is static, could call it). Our driver
code for Test Case 3 is:
// Test Case 3
Console.WriteLine("Test Case 3");
Console.WriteLine("-----------");
hand = new Hand();
Card card = hand.GetCard(0);
PrintLocationValidity(card, "getting a card");
Console.WriteLine();
Notice that we’re using a PrintLocationValidity method to print a message about the location. Take
a look at the code for this chapter to see how that method is implemented if you’re interested.
When we run all our test cases up to this point, we get the results shown in Figure 13.5.
More Class Design and Methods 299
Finally, we write the code for our RemoveCard method. Remember when we had to handle someone
trying to take the top card from an empty deck? Well, we also need to handle someone trying to remove
a card from an empty hand or trying to remove a card from a location outside the range of locations for
cards in the hand. It's not that bad, though; we can handle this the same way we did in the GetCard
method:
/// <summary>
/// Removes the card from the given location in the hand
/// </summary>
/// <param name="location">the 0-based location of the card</param>
/// <returns>the card or null if the location is invalid</returns>
public Card RemoveCard(int location)
{
// check for valid location
if (ValidLocation(location))
{
Card card = cards[location];
cards.RemoveAt(location);
return card;
}
else
{
// invalid location
return null;
}
}
Remember we said there will be times when we write private methods for internal processing in the
class? We actually realized as we were writing the RemoveCard method that we were writing the exact
same Boolean expression for the if statement as the one we used in the GetCard method. Rather than
having that duplicated code, we decided to move it into a new ValidLocation method that returns true
if the location is valid and false otherwise. That method is:
/// <summary>
300 Chapter 13
/// Tells whether or not the given location is valid
/// </summary>
/// <param name="location">the location</param>
/// <returns>true if the location is valid, false otherwise</returns>
bool ValidLocation(int location)
{
return location >= 0 && location < cards.Count;
}
We call the ValidLocation method from the RemoveCard method as shown above, and we also
changed the GetCard method to call the method as well. Not only does that put the validation logic in a
single place, it also makes it more obvious what the Boolean expression is checking in the if statements
in each of the methods.
Now that we have the RemoveCard method implemented, we can add Test Case 4 to our test driver code
and run that test case. Here’s the code we add for Test Case 4:
// Test Case 4
Console.WriteLine("Test Case 4");
Console.WriteLine("-----------");
hand = new Hand();
hand.AddCard(new Card(Rank.Ace, Suit.Spades));
hand.AddCard(new Card(Rank.Queen, Suit.Hearts));
hand.AddCard(new Card(Rank.Jack, Suit.Clubs));
hand.RemoveCard(2).Print();
hand.RemoveCard(0).Print();
hand.RemoveCard(0).Print();
card = hand.RemoveCard(0);
PrintLocationValidity(card, "removing a card");
When we run this code, we get the results shown in Figure 13.6, so we're done solving this problem.
More Class Design and Methods 301
That gives us some of the input part of a user interface (UI) we'd want to provide for a game, but it
doesn't give the user any text output (like for their current score) and it doesn't let the user provide text
input (like their Gamertag). In this chapter, we’ll learn how to implement those important user interface
components. As the title of this chapter suggests, text input and output is often referred to as text IO.
Of course, another important part of a game's UI lets the user interact with the menu system. That piece
of the UI requires some more advanced C# knowledge, though, so we'll put that off until we get to
Chapter 17.
Let's add a score display to the fish game we built in Chapter 8, where the player drives a fish around
eating teddy bears. First, we need to add a Text - TextMeshPro component to our scene. Right click in
the Hierarchy window and select UI > Text - TextMeshPro. When you do this, you’ll probably get a
TMP Importer popup asking you to import TMP essentials; just click the Import TMP Essentials button
to do so. Go ahead and close the popup. As you can see, you actually end up with a number of new
components, including a Canvas that the text is drawn on in the game. Change the name of the Text -
TextMeshPro component to ScoreText.
Select ScoreText in the Hierarchy window. In the Rect Transform component in the Inspector, change
the Pos X and Pos Y values in the Rect Transform component to move the text to be near the upper left
corner of the screen (we used -750 and 450 for these values). In the TextMeshPro - Text (UI) component
in the Inspector, change the Font Style to Bold by clicking the B button to the right of Font Style.
If you run the game now, you'll see that you can use the keyboard to drive the fish around eating teddy
bears, but the score display always says New Text. Let's fix that now.
Before we do that, though, we need to decide who should update the score display. Should we
implement another script that we attach to the main camera to do this for us? Should we have a teddy
Unity Text IO 303
bear update the score display when it's destroyed? Should we have the fish update the score display
when it destroys a teddy bear? As you can see, we have lots of options here.
We're going to add fields and processing to the Fish script to have it keep track of the current score and
to update the score display when it destroys a teddy bear. We decided to pursue this option because the
fish is the player's avatar (how often do you get to say that?), and it makes sense to have the player keep
track of their own score. Having each player keep track of their own score makes even more sense when
we implement multiplayer games, so it's reasonable to use the same approach in single player games as
well.
We'll start by adding three fields to the Fish script: a score field to keep track of the current score, a
bearPoints field that tells how many points each bear is worth, and a scoreText field that saves a
reference to the ScoreText component for efficiency. We mark the bearPoints and scoreText fields
with [SerializeField] so we can set them in the Inspector:
// score support
int score;
[SerializeField]
int bearPoints;
[SerializeField]
TextMeshProUGUI scoreText;
The TextMeshProUGUI class is actually contained in the TMPro namespace, so we need to include a
using directive for that namespace to get our changes to compile.
In the Unity editor, select the Fish game object in the Hierarchy window. Set the Bear Points value in
the Inspector to 10 and drag the ScoreText element from the Hierarchy window onto the Score Text
field in the Inspector. Click the Overrides dropdown near the top of the Inspector and click the Apply
All button to apply the changes to the prefab.
If you select the Fish prefab in the Project window, you'll see that the Bear Points value has been saved
in the prefab but the Score Text field still says None (Text Mesh Pro UGUI). That's because prefabs can
only refer to objects contained in the Project window and our Score Text component is only in the
Hierarchy window. That doesn't cause us any trouble at all for this particular problem, so we'll leave our
project as it is.
Next, we need to set the score text properly when the game starts; the appropriate place to do that is in
the Fish Start method, but before we can do that we need to know how to interact with a
TextMeshProUGUI object to change the text it displays. See the documentation below (which we
retrieved by searching for TextMeshProUGUI in a web browser, clicking the top result, and clicking the
TMP_Text.text link).30
30 Strangely, the TextMeshPro documentation isn't integrated into the Unity Scripting Reference; that's why we need to
search for TextMeshProUGUI in a browser.
304 Chapter 14
As you can see, we can simply set the text property to the string we want to display. Given that, we add
the following code to the Fish Start method:
When we run the game now, we get the output shown in Figure 14.2.
This is nicer than seeing “New Text” for our score display, but unfortunately our score stays at 0 no
matter how many fish we eat. The good news is that we only need to add two more lines of code, this
time in the Fish OnCollisionEnter2D method when we detect a collision between the fish's head and a
teddy bear:
Unity Text IO 305
// update score
score += bearPoints;
scoreText.text = "Score: " + score;
Run the game one more time and you'll see the score increase as you eat teddy bears. Awesome.
Unfortunately, although you might think we're done, check out Figure 14.3 to see what happens if we
change our aspect ratio to Free Aspect and run the game with Maximize on Play selected.
The score is definitely not in the correct place any more. Stop running the game to return to the editor.
Luckily, this is an easy problem to solve. Select the ScoreText game object in the Hierarchy window. In
the Inspector, click the circled area in the Rect Transform component as shown in Figure 14.4.
Clicking this button gives us access to a set of presets for anchoring our Text - TextMeshPro component
to a particular position on the Canvas. Select the circled preset as shown in Figure 14.5, which will
anchor the Text - TextMeshPro component to the upper left corner of the Canvas.
Click in the Inspector to close the anchor presets popup. Notice that this step changed the Pos X and Pos
Y values for the Rect Transform; we then “tweaked” those to be 150 and -80.
Run the game again and you'll see that the score text appears in the upper left corner whether or not we
have Maximize on Play selected.
There’s another important step we should take to make our game UI (User Interface) work well for
different resolutions. Select the Canvas in the Hierarchy window. In the Canvas Scaler component in the
Inspector, click the dropdown to the right of UI Scale Mode (it says Constant Pixel Size) and change it
to Scale With Screen Size instead. Type in the numbers for X and Y you want to use for your Reference
Resolution (we used 1920 and 1080). Now the UI elements on the canvas will stretch or shrink based on
the relationship between the current game resolution and your reference resolution.
Start by creating a new Unity Project, add a Canvas to the scene, add an Input Field – TextMeshPro to
the canvas, and rename the Input Field Gamertag. Select Gamertag in the Hierarchy window and add
Unity Text IO 307
text to the Text box in the TextMeshPro – Input Field component in the Inspector to say Enter Gamertag
...
At this point, the input field is displayed and the player can actually click on the text and change it using
the keyboard, but there's really no good way for them to know that! Let's change some visual
characteristics of the Input Field to make this more obvious to the player.
First, let's make the input field change color when the player hovers the mouse over it so they
understand they can do something. To do this, change the Normal Color of the Input Field to be darker
than the Highlighted Color. You can change the color by left clicking the bar to the right of Normal
Color; clicking the color wheel or entering R, G, and B values; then closing the popup. If you run the
game now, you'll see the Input Field color change appropriately when the mouse enters and exits the
Input Field.
Once the player sees the Input Field change color when they mouse over it, they're likely to actually
click the Input Field. At that point, the Input Field is selected; this is obvious because we now have a
blue background under the text (you can of course change that color as well by changing the Selected
Color value). The player can now type in their Gamertag.
You can see in the Inspector that the Input Field has a Character Limit value that defaults to 0, which
means there's no limit on the number of characters the player can enter. If they enter more characters
than will fit in the Text object, characters scroll off to the left (but are still included as part of the player's
input). You can limit the number of characters the player can input by changing the Character Limit
value.
Okay, so the player has entered their Gamertag into the Input Field. How do we actually get the text they
entered? The best way to do this would be to grab that text once they finish by pressing Enter or by
clicking somewhere else in the game window, but doing that requires some ideas we won't learn about
until Chapter 17. Instead, we'll demonstrate using a less efficient way that only uses concepts we've
already learned, but you should definitely use the Chapter 17 approach in your actual games. Think of
this demonstration as testing code rather than “ship it in the game” code.
Start by adding a new Scripts folder in the Project window and by adding a new PrintGamertag C#
Script to that folder. Open up the new script in Visual Studio and change it to the following:
using UnityEngine;
using TMPro;
We need to add a using directive for the TMPro namespace because the TMP_InputField class is in that
namespace.
/// <summary>
/// Prints the gamertag every second
/// </summary>
public class PrintGamertag : MonoBehaviour
{
// make visible in Inspector
[SerializeField]
TMP_InputField gamertagInputField;
308 Chapter 14
We use the secondsSinceLastOutput field to implement a simple timer that “goes off” approximately
every second.
/// <summary>
/// Update is called once per frame
/// </summary>
void Update()
{
// output gamertag every second
secondsSinceLastOutput += Time.deltaTime;
if (secondsSinceLastOutput > 1)
{
secondsSinceLastOutput = 0;
Debug.Log(gamertagInputField.text);
}
}
}
The first line of code adds the time it took for the previous frame to execute to the
secondsSinceLastOutput field. The if statement checks to see if it's been over a second since the last
output. If it has been, the body of the if statement resets the timer to 0 and outputs the current contents of
the TMP_InputField object by accessing the text field of that object.
Attach the script to the Main Camera in the scene and populate the Gamertag Text field by dragging the
GamertagText component from the Hierarchy window onto the field. If you run the game, you'll see the
current value of the Input Field displayed in the Console window approximately every second, and you
can see that value change as the player edits the contents of that Input Field.
There are many ways to make player text input more visually appealing and to process that input more
efficiently using Chapter 17 concepts, but this example should give you a good understanding of how
we can get text input from the player.
Display circles with integer radii from 1 to 5 in the game window, with each circle displaying its radius
and area.
As before, the problem description says to display the circles “in the game window”, but it doesn't say
where, so we'll place those circles as we see fit. Also, the problem description doesn't say where we
Unity Text IO 309
should have each circle display its radius and area, so we'll just have each circle display that information
centered itself.
Design a Solution
Because each circle will be responsible for displaying itself and its information once it's been placed in
the scene, it makes sense to modify the Circle script and implement the required functionality in the
Start method in that script.
Unfortunately, once we have graphical (rather than textual) output, it becomes harder to exactly specify
our expected results. For example, we don't know precisely where each of the circles will be placed in
the scene, so we won't indicate in our expected results where each circle will appear. We do know,
however, that there should be 5 circles with radii from 1 to 5, and we also know the area that should be
displayed for each circle. Here's our test case:
We'll include our modified Circle script below, and interleave our comments where things are
different.
Before we get to the code, though, let's look at the structure of our new Circle game object in the Unity
editor. We zoomed in on a Circle game object in the Hierarchy window to generate Figure 14.6.
The Circle game object has a number of child game objects. The ones we'll work with are the circle
sprite (so we can scale the sprite based on the radius) and the RadiusText and AreaText objects (so we
310 Chapter 14
can place them centered on the circle and set the displayed radius and area values). As you can see, for
our solution to this problem, each Circle game object has its own Canvas.
In case you're wondering how we built this object, we started by right clicking in the Hierarchy window
and selecting Create Empty. Next, we dragged the circle from the sprites folder onto our new game
object, which made the sprite a child of that game object. Next, we right clicked our game object,
selected UI > Text - TextMeshPro, and named the new Text - TextMeshPro element RadiusText. We
then repeated those steps to add the AreaText element. Finally, we dragged our game object into the
prefabs folder in the Project window and renamed the prefab Circle.
Okay, let's work our way through the Circle script, which we left attached to the Circle game object:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using TMPro;
We need to add a using directive for the TMPro namespace because the TextMeshProUGUI class is in that
namespace, and we use TextMeshProUGUI objects to display the radius and area information for each
circle.
/// <summary>
/// A circle
/// </summary>
public class Circle : MonoBehaviour
{
// make visible in the Inspector
[SerializeField]
int radius;
[SerializeField]
TextMeshProUGUI radiusText;
[SerializeField]
TextMeshProUGUI areaText;
As the comment above states, by marking the above three fields (variables) with [SerializeField],
we make them visible in the Inspector; see Figure 14.7.
Doing it this way is helpful because we can just change the radius field in the Inspector when we place
a circle game object. To populate the radiusText field, we drag the RadiusText object from the
Hierarchy window (Figure 14.6) onto the box for the field in the Inspector; populating the areaText
field uses the same drag-and-drop process.
Unity Text IO 311
You might wonder why we didn't use properties here instead. As a reminder, properties don't appear in
the Inspector, while fields marked with [SerializeField] do. Because we really wanted access to
these fields in the Inspector, we couldn't use properties to provide access to them.
float area;
The area field holds the calculated area for the circle; this field isn't marked with [SerializeField]
because we don't need (and shouldn't have) access to it in the Inspector.
We use the two constants above to offset the radius and area text from the center of the circle sprite.
/// <summary>
/// Start is called before the first frame update
/// </summary>
void Start()
{
// calculate area
// note that we're using UnityEngine Mathf instead of System Math
area = Mathf.PI * Mathf.Pow(radius, 2);
To understand the second line of code above, remember that Unity uses a component-based system. The
game object we created from the circle sprite has a SpriteRenderer component, that game object is a
child of the Circle game object, and the Circle script is attached to the Circle game object. The line of
code looks at all the children of the Circle game object, trying to find a SpriteRenderer component;
when it finds it (in the game object we created from the circle sprite), it puts that component into the
spriteRenderer variable. We need this reference to scale the circle game object based on the Circle's
radius.
The code above is almost identical to the scaling code from Chapter 4. The only difference is that we're
manipulating the Transform component for the Sprite Renderer component we retrieved above rather
than for the Circle object as we did in Chapter 4. We need to do it this way because we only want to
scale the sprite for the Circle object, we don't want to scale the text.
This code looks a little complicated, so let's think about the big picture before examining the details. We
need to know where the circle is located on the screen so we can shift the text to appear centered on the
circle. When we built our Circle game object, we placed our text appropriately so it would be in the
correct location for a circle placed in the center of the screen. To move the text to the correct location for
a circle that's NOT centered in the screen, we need to know how much the circle is offset in x and y
from the center of the screen. The code above calculates that offset for us.
The tricky part to this is that transforms specify the position, rotation, and scale of game objects in the
coordinate system for the world, but the text positions are specified in the coordinate system for the
screen. That makes perfect sense, because game objects “live” in the game world, but in Unity text
objects are part of the user interface (UI), so they “live” on the screen.
We actually need to use the Main Camera in the scene to do our coordinate conversions because the
location and other characteristics of the camera determine where something in the world is shown on the
screen. If this seems strange to you, think of zooming in or out with a digital camera. The “thing” you're
aiming at with the camera is at a specific location in the world, but changing the zoom settings on the
camera changes where that “thing” appears on the camera preview screen. The Camera.main part of the
first line of code gives us access to the Main Camera in the scene.
The rest of the first line of code calls the Camera WorldToScreenPoint method to convert the world
coordinates of our Circle game object to screen coordinates; remember, this is why we needed a
reference to our Main Camera. The argument we pass to the method is the position of the Circle game
object, which we get to by accessing the position property of the transform property of the Circle
game object. The WorldToScreenPoint method returns a Vector3 object (we learned that by reading
the documentation), so we store the result in a Vector3 variable called
circleCenterScreenCoordinates.
This should all feel a little familiar to you, of course. Back in Chapter 8, we retrieved the mouse position
in screen coordinates and had to convert that position to world coordinates so we could use the position
in world coordinates in our game. This is exactly the same idea, just going in the other direction.
Now that we have the location of the Circle game object converted to screen coordinates, we can
calculate offsets from the center of the screen in the x and y directions; that's what the second line of
code above does. The code calls the Vector3 constructor to create a new Vector3 object containing this
information and puts the new object into the circleOffsetFromCenter variable. For the constructor
we're calling, we provide x, y, and z components for the new object as arguments to the constructor.
Let's look at the first argument, the x component of the new vector. Unity gives us a very handy Screen
class that gives us access to information about the current display, including the width and height of the
display (in pixels). That helps us here because the screen coordinates are 0, 0 at the lower left corner of
the screen, so we know that the horizontal center of the screen is at Screen.width / 2. The fields in
the Screen class are static, so we access them using the class name rather than a specific Screen object.
To calculate the actual offset of the circle in the x direction, we subtract the horizontal center from the
horizontal center of the circle in screen coordinates. We do the subtraction in this order so that if the
circle is left of center the x offset is negative and if it's right of center the x offset is positive. We do a
Unity Text IO 313
similar calculation for the y offset, and of course we don't change the z component of the screen
coordinates when we're working in 2D.
The first line of code above calculates the correct location for the radius text based on the circle offset
from the center of the screen and the offset constants from the beginning of the script. The second line of
code then sets the actual position of the text, like we set the localScale field for the sprite renderer to
actually scale the circle sprite. The third and fourth lines of code do the same for the area text.
Finally, the two lines of code above seem pretty clear! As we learned earlier in this chapter, we can
change the string that's displayed by a TextMeshProUGUI object by accessing the text property of that
object; that's what those lines of code do.
The actual results from running our Unity game are provided in Figure 14.8. As you can see, our actual
results match our expected results. Whew!
314 Chapter 14
Figure 14.8. Test Case 1: Checking Radius and Area Info Results
Chapter 15. Unity Audio
At this point, we’ve included lots of the elements we need in good games. We know how to display
images and animations, and we know how to respond to user input so the player can interact with the
game world. There is, however, one more critical piece we need to include to provide an immersive play
experience for the player. That critical piece is audio – both music and sound effects – so this chapter
explains how to add audio to your games.
In this chapter, we’re going to add sound to our Fish and Bears game from the previous chapter. The
backgroundMusic sound will be used as background music in our game and the eat sound will be used
when the fish eats a bear. The remaining two sounds – bounce1 and bounce2 – will be played when a
bear bounces off the fish, but we want the game to randomly pick which one to play31. All the sounds
are included in the solution on the Burning Teddy web site.
You should know that Unity has a very robust audio system; we're just scratching the surface in this
chapter. You should explore the Audio section in the Unity manual to find out about all the other cool
stuff you can do with audio in your Unity games.
Adding the audio files to the Unity project is just as easy as adding sprites; all we need to do is copy the
files into our project. We like to store our audio in a separate folder, so right click on the Assets folder in
the Project window and create another new folder called Audio. Now go to your operating system and
copy the audio files into the Audio folder (which you'll find under the Assets folder wherever you saved
your Unity project). When you copy the audio files into the Audio folder, Unity automatically imports
them as Audio Clips in your project.
Select backgroundMusic in the Audio folder in the Project window; as you can see from the Inspector,
we have a variety of characteristics we can set for each Audio Clip.
The Load Type tells how Unity loads the audio asset at runtime. The general rule of thumb is to use
Decompress On Load (the default) for small files, Compressed In Memory for larger files, and
Streaming to decode on the fly as the file is read from the disk. We'll change our backgroundMusic
Audio Clip to use Compressed In Memory since this file is much larger than our sound effect files. Click
the tab with the computer icon (just to the right of the Default tab), click the check box next to “Override for
PC, Mac & Linux Standalone”, click the Load Type dropdown, select Compressed in Memory, and click
the Apply button. You should have something similar to Figure 15.1 at this point.
31The sound effects (but not the music) are actually from a commercial game our company was working on. Weird, we
know.
316 Chapter 15
The Compression Format controls how compressed the files are and, of course, the quality of those
sounds as well. Unity suggests using Vorbis (the default) for medium-length sound effects and music, so
we'll leave that alone here. For short sound effects, PCM or ADPCM provide much better compression
than Vorbis. Unity recommends ADPCM for sound effects that need to be played in large quantities;
that doesn't really apply to our eat and bounce sound effects, so we'll change those to use PCM.
By default, the Main Camera has an Audio Listener component attached to it. If you're doing fancy stuff
like having the direction a sound is coming from important to player immersion, you might decide to
remove the Audio Listener from the Main Camera and attach an Audio Listener to a different object
(most commonly, the player) instead. For our 2D game here, though, leaving the Audio Listener on the
Main Camera will work fine.
Now we need to add Audio Source components to the game objects that will play the audio in our game.
We'll start with the background music; it makes sense for the Main Camera to just play the background
music for the game. Yes, the Main Camera can make sounds and hear them as well. To see why this
makes sense, make a sound right now. Did you hear it? See, it works!
Select the Main Camera in the Hierarchy window, click the Add Component button at the bottom of the
Inspector, and select Audio > Audio Source. As you can see, there are lots of settings here to let you
Unity Audio 317
tune the way audio works in your game! We only need to tweak a couple things for our game. First, drag
backgroundMusic from the Audio folder in the Project window and drop it on the AudioClip field of the
Audio Source component in the Inspector. Click the check box for the Loop field so the background
music loops in the game. The Audio Component in the Inspector should look like Figure 15.2.
If you play the game now, you'll hear the background music. Sweet.
You should know that we've sometimes experienced strange Unity editor behavior where the audio
works, but if we leave the editor open for a while and then play the game again the audio no longer
works. Closing the Unity editor and then opening it up again consistently fixes this problem for us.
Select the Fish prefab in the prefabs folder in the Project window, click the Add Component button at
the bottom of the Inspector, and select Audio > Audio Source. Drag eat from the Audio folder in the
Project window and drop it on the AudioClip field of the Audio Source component in the Inspector.
Uncheck the check box for the Play On Awake field; we don't want the sound effect to play when we
add the Fish to the scene (which is what Play On Awake does), we want to play the sound effect from
the Fish script when the fish eats a teddy bear.
Okay, so how do we make an Audio Source play the clip that's associated with it? Check out Figure
15.3.
318 Chapter 15
The AudioSource class exposes a Play method that lets us do exactly what we need. The next question,
then, is how do we get access to the Audio Source component(s) that are attached to the Fish game
object that the Fish script is attached to?
It turns out that we can look them up (we'll show how soon), but for efficiency we don't actually want to
look them up every time we need to play a sound. Instead, we'll start by adding a field to the Fish script:
We can now populate that field in the Fish Start method using the following code:
The first line of code returns all the Audio Source components that are attached to the Fish game object.
At this point, there's only one Audio Source component attached to the Fish game object, so we could
have used the GetComponent method here instead, but looking forward we know we'll actually be
adding Audio Source components for the two bounce sounds as well, so we'll structure our code with
that in mind.
Unity Audio 319
The foreach loop walks through all the Audio Source components. The if statement checks if the name
of the clip for the current audio source is the clip for the eat sound. If it is, we save the audio source into
the field we declared above.
The last thing we need to do is actually play the eat sound when the fish eats a teddy bear. We already
do a number of actions in the Fish OnCollisionEnter2D method for that case (destroying the teddy
bear and adding points to the score), so we'll play the sound there as well:
If you play the game now, you'll hear the eat sound when the fish eats a teddy bear.
The last sound effect(s) we'll add to our game is for when a teddy bear bounces off the fish. Recall that
we said that we want the game to randomly pick between the bounce1 and bounce2 sound effects when
this happens.
Add two more Audio Source components to the Fish prefab, one for bounce1 and one for bounce2. Be
sure to uncheck the check box for the Play On Awake field for both components.
The next thing we'll do is add one more field to the Fish script (since we'll be playing these sound
effects in that script):
Next, we'll add code to the Fish Start method to add the Audio Sources for the bounce1 and bounce2
clips to our new bounce field. Here's the complete code for the audio source section of that method:
Finally, we need to change the Fish OnCollisionEnter2D method to play one of the bounce sound
effects when we've detected a collision between the fish and a teddy bear that's not at the front of the
fish. We'll add an else clause to the if statement that checks for a collision at the head of the fish, with
the following code in the else body:
For our index, we call the Random Range method, which will return either 0 or 1 (bounce.Count –
which we know is 2 in this case – is the exclusive upper bound of the random number that gets
generated). We then simply play the list element corresponding to the random index.
If you play the game now, you'll hear that sometimes bounce1 plays when a teddy bear bounces off the
fish and sometimes bounce2 plays when that happens.
That's it for this chapter! We've added background music and sound effects to our game, which makes it
a much more enjoyable experience for our players.
Chapter 16. Inheritance and Polymorphism
One of the defining characteristics that distinguishes object-oriented programming languages like C#
from other kinds of programming languages is the ability to use inheritance. Inheritance lets us structure
our programs so that we can reuse code that others have written, even when we're developing new
classes. Polymorphism is a related concept that’s made possible through the use of inheritance. This
chapter discusses both of these important object-oriented concepts and shows how we can use them in
C#.
Up to this point in the book, we've used composition to build classes that contain objects of other
classes. This is called a has-a relationship, since one class has (contains) objects of another class. In
contrast, we'll use inheritance to express an is-a relationship, where one class is a specialized version of
a more general class.
The core idea behind inheritance is that we can structure our system of classes as a set of parent and
child classes; these are also often called superclasses and subclasses. Why does this help us? Because a
child class inherits all the fields, properties, and behavior from the parent class. If that was all that
happens, we wouldn’t be so excited (yes, we know you’re excited, so don’t try to hide it)! But there’s
more, much more! You also get a free set of knives … oh, wait. There is more, but that’s not it. In
addition to inheriting everything from its parent class, a child class can both add fields and behavior that
aren’t in the parent class and change behavior that it inherited from the parent class. Let’s look at both of
those ideas.
In our Car and Dragster example above, we have the general car class with fields, properties, and
behaviors that are common to all cars. If we make the Car class the parent of the Dragster class, the
Dragster class inherits all those fields, properties, and behaviors. That makes perfect sense, because a
Dragster is a Car; in fact, the inheritance relationship is commonly called an is-a relationship for this
reason. But we also said above that a Dragster should be able to deploy a parachute, so we need to add
a behavior that we didn’t inherit from the Car parent class. How do we add a behavior to a class? The
same way we’ve always done it – by adding a method to the class. So we can just add a
DeployParachute method to the Dragster class to add that specialized behavior.
We capture the inheritance relationship between these two classes as shown in the UML in Figure 16.1;
specifically, the child class (Dragster) is connected to the parent class (Car) with a line that has an open
triangle at the parent class.
322 Chapter 16
Notice that we’ve added a field to the Dragster class as well to keep track of whether or not the
parachute is currently deployed. For child classes, we only list the new fields, properties, and methods;
we don't need to list all the fields, properties, and methods that are inherited from the parent class.
This is a pretty small class hierarchy – we can end up with numerous levels of inheritance between
parent and child classes in a larger program – but it does demonstrate an interesting characteristic of
class hierarchies in general. Specifically, we can observe that classes higher in the class hierarchy are
more general and classes lower in the class hierarchy are more specialized.
As a matter of fact, the root (top) of the C# class hierarchy is the Object class, the most general class of
all in a C# program. Because all reference types in C# inherit from the Object class, we typically don’t
include that class in our class diagrams.
We get a couple of important benefits by making the Car class the parent class of the Dragster class.
One benefit is that we only need to declare all the fields and define all the properties and methods for a
generic car in one place (the Car class). If we made the Dragster class a “stand-alone” class instead of a
child class, we’d have to copy all the fields, properties, and methods from the Car class into our
Dragster class (and add the new stuff as well). We’ve said before that copying and pasting code is
almost never a good idea, and it wouldn’t be a good idea here either.
Inheritance and Polymorphism 323
Even more importantly, if we use inheritance and we make changes to the Car class fields, properties, or
methods, the Dragster class automatically gets those changes without any effort on our part. For
example, if the Car class changes the way the Accelerate method works, calling the Accelerate
method on a Dragster object would automatically use the modified inherited method. If instead we
copied and pasted code from the Car class into the Dragster class, we’d have to remember that any
time we changed the Car class we’d have to manually change the Dragster class as well. That may not
seem so bad, but what if we also had RallyCar, FormulaOneCar, and StockCar classes as well? It
would be crazy to try to keep them all synchronized using copying and pasting, especially since
inheritance gives us a much more effective approach to use.
Well, we said we’d look at two different things we might do in a child class: add new behavior and
change inherited behavior. The example above covers the first one, so let’s move on to the second.
We change inherited behavior by overriding an inherited method. For this example, let’s use the class
hierarchy shown in Figure 16.2.
The FamilyMember class has a couple of fields that would apply to all family members. It also has a
HaveFun method defined as follows:
324 Chapter 16
/// <summary>
/// Makes the family member have fun
/// </summary>
public virtual void HaveFun()
{
Console.WriteLine("I'm writing code!");
}
This looks like the methods you’ve seen before, though we’ve added the virtual keyword to the
method header. This keyword indicates that child classes are allowed to override the method.
Now let’s say there’s a specialized family member who is a gamer. Shockingly, this gamer doesn’t have
fun by writing code (we don’t understand it either), they have fun by playing games. If we add the
following code to the Gamer class
/// <summary>
/// Makes the gamer have fun
/// </summary>
public override void HaveFun()
{
Console.WriteLine("I'm playing a game!");
}
then we’ve changed the inherited behavior to something else by overriding the inherited method. In fact,
we include the override keyword in the method header of the child class to explicitly indicate that
we’re doing this.
What does this do for us? If we call the HaveFun method on a Gamer object, we’ll get the playing games
message. We get the benefit of inheriting everything from the FamilyMember class while also getting the
flexibility to change some of the inherited behavior if we need to.
One final comment before we move on. Although in real life people inherit characteristics from both
their parents, in C# a child class can only have a single parent class. Although that might seem overly
restrictive to you, letting child classes have multiple parents (not surprisingly called multiple
inheritance) can lead to a number of serious problems, including something called the Diamond of
Death!
Now that we have the basic inheritance concepts down, let’s look at the syntax we use in C# to
implement inheritance in our games.
16.2. Inheritance in C#
Let’s run through all the syntax required to implement inheritance in C# for the class hierarchy shown in
Figure 16.3.
Inheritance and Polymorphism 325
We know from personal experience that it’s absolutely possible to be both a geek and a gamer, but let’s
ignore that possibility for this simple example.
We'll start by providing the implementation of the FamilyMember class. The code for that class is
provided in Figure 16.4.
using System;
namespace FamilyMemberExample
{
/// <summary>
/// A family member
/// </summary>
public class FamilyMember
{
#region Fields
int height;
int weight;
#endregion
#region Constructors
/// <summary>
/// Constructor
/// </summary>
326 Chapter 16
/// <param name="height">the height of the family member</param>
/// <param name="weight">the weight of the family member</param>
public FamilyMember(int height, int weight)
{
this.height = height;
this.weight = weight;
}
#endregion
#region Properties
/// <summary>
/// Gets and sets the height
/// </summary>
public int Height
{
get { return height; }
set { height = value; }
}
/// <summary>
/// Gets and sets the weight
/// </summary>
public int Weight
{
get { return weight; }
set { weight = value; }
}
#endregion
/// <summary>
/// Makes the family member have fun
/// </summary>
public virtual void HaveFun()
{
Console.WriteLine("I'm writing code!");
}
#endregion
}
}
We haven't really done anything special yet to account for child classes for this class, though we have
used the virtual keyword to indicate that child classes can override the HaveFun method. We’ve
basically just developed the class to represent a general family member.
Let's work on our Geek class next. We make a class the child of a parent class in C# using a colon (:) in
our class header. To make our Geek class a child class of the FamilyMember class, we use
Inheritance and Polymorphism 327
public class Geek : FamilyMember
for our class header. This tells the compiler that Geek is a child class of the FamilyMember class.
We also need to develop the constructor for the Geek class. The complete code for the Geek class can be
found in Figure 16.5.
namespace FamilyMemberExample
{
/// <summary>
/// A geek
/// </summary>
public class Geek : FamilyMember
{
#region Constructors
/// <summary>
/// Constructor
/// </summary>
/// <param name="height">the height of the family member</param>
/// <param name="weight">the weight of the family member</param>
public Geek(int height, int weight)
: base(height, weight)
{
}
#endregion
}
}
Notice that the constructor header contains something we’ve never seen before; specifically, it contains
: base(height, weight)
at the end of the header. This calls the constructor of the parent class for this class, which we want to do
because the parent class constructor initializes the FamilyMember instance variables for us.
Okay, let's move on to the Gamer class; the code is provided in Figure 16.6.
using System;
namespace FamilyMemberExample
{
/// <summary>
/// A gamer
/// </summary>
public class Gamer : FamilyMember
{
#region Constructors
/// <summary>
/// Constructor
328 Chapter 16
/// </summary>
/// <param name="height">the height of the family member</param>
/// <param name="weight">the weight of the family member</param>
public Gamer(int height, int weight)
: base(height, weight)
{
}
#endregion
/// <summary>
/// Makes the gamer have fun
/// </summary>
public override void HaveFun()
{
Console.WriteLine("I'm playing a game!");
}
#endregion
}
}
The constructor is very similar to the constructor we implemented for the Geek class, and the override of
the HaveFun method is as we described in the previous section.
16.3. Polymorphism
We’ve already seen some very nice capabilities that inheritance gives us, but there’s still another one we
should talk about: polymorphism. Polymorphism means that a particular method call can behave
differently – take multiple forms – based on the object on which it’s called. Let’s look at an example.
Let’s build a list of 3 family members where one is a FamilyMember, one is a Geek, and one is a Gamer.
Can we do this? Don’t lists have to have elements that are all the same type or class? Well – yes, but
because both Geek and Gamer are child classes of the FamilyMember class, we can declare our list as
follows:
This gives us a list of FamilyMember objects, and each of these objects can actually be either a
FamilyMember object, a Geek object, or a Gamer object. In other words, we have a single variable
(familyMembers) that refers to objects of different types, but we can do this because Geek and Gamer
are child classes of FamilyMember. Let’s add our family members to the list:
Okay, so now we have a list that’s a mix of various object types, but you’re still wondering why
polymorphism is useful, right? Here’s where it gets good. Let’s go through the list and call the HaveFun
method for each of the elements of the list:
We don’t have to know whether each element is a FamilyMember, a Geek, or a Gamer when we call the
HaveFun method; because of polymorphism, the program automatically calls the appropriate HaveFun
method for that item. The FamilyMember class uses the HaveFun method it defines, the Geek class
inherits that method so it uses that method as well, and the Gamer class overrides that method to give it
different behavior. The bottom line is that, through polymorphism, the program calls the appropriate
method at run time based on the type of the object it’s calling the method on.
Don't believe that it actually works that way? Check out the output in Figure 16.7.
The first object in the list is a FamilyMember object so the call to the HaveFun method executes the
method defined in that class and prints the coding message. The second object in the list is a Gamer
object so the call to the HaveFun method executes the HaveFun method defined in the Gamer class.
Finally, the third object in the list is a Geek object. The Geek class doesn’t define a HaveFun method, but
the Geek class inherits the HaveFun method from the FamilyMember class so it executes that method and
prints the coding message.
You might be asking yourself, though, how the Common Language Runtime decides which method to
use at run time. It does this using something called method resolution. Basically, it looks for a method
with the given header in the actual class for the object. If the method is there, that’s the one that will be
used. If it’s not, the CLR checks the parent class to see if the method is there. It uses the first method it
finds that matches the method header, working its way all the way up to the Object class if necessary
(more about that class soon). If the CLR couldn’t find the method given your class hierarchy, you’ll
actually get a compilation error, so the method has to appear in the object you’re using or in one of its
parent classes.
330 Chapter 16
Polymorphism is a very powerful capability that we get when we use inheritance. We’ve only scratched
the surface here, but as you move on to develop more complicated solutions using inheritance, you’re
sure to use polymorphism again as well.
We'll start with a basic bank account. We want to be able to make deposits to and withdrawals from the
account, so we need a BankAccount class that will accept deposits and provide withdrawals. We also
probably want the bank account object to be able to give us the current balance in the account, so we
need that as a property. Finally, to print a statement for the account at the end of each month, we'll have
to maintain lists of the deposits and withdrawals for the account so we can print those lists on the
statement as well.
It looks like the BankAccount class will need fields for the current balance, a list of deposits made to the
account, and a list of withdrawals made from the account. We’ll need a constructor (as always), and
we’ll need properties called Balance, Deposits, and Withdrawals. For the methods, we'll need
methods called MakeDeposit and MakeWithdrawal.
Note that we've decided to use list objects (rather than arrays) for the lists of deposits and withdrawals
since we don't know how long those lists will actually be. Using lists of decimal for the deposits and
withdrawals isn’t actually very realistic; we should really develop a Transaction class that tells what
kind of transaction it is (deposit or withdrawal), the amount of the transaction, and the date and time of
the transaction. We won’t do that here so we can concentrate on the inheritance aspects of our solution,
but if we were doing this “for real” we’d definitely store transactions, not just transaction amounts.
We haven't done anything that you haven't seen before yet, right? Here's where we get to the inheritance
stuff. We've already developed a general bank account class that gives us useful fields, properties, and
methods for all kinds of bank accounts, but we might need more. If we have a checking account, for
example, we need to keep track of checks that have cleared the bank and deduct them from the account
balance. And if we have a savings account, we need to keep track of the interest that has accrued for that
account. Rather than making two brand new classes with most of the same fields, properties, and
methods as our original bank account class, we'll make a checking account class and a savings account
class as child classes of the bank account class. They'll have all the fields, properties, and methods of the
bank account class – they inherit them from the parent class – but we also add new fields, properties,
and methods to our checking account and savings account classes.
The resulting UML diagram for our class hierarchy is shown in Figure 16.8.
Inheritance and Polymorphism 331
Just as for transactions, we’d really want to know more about checks than just the check amount; we’d
at least want to know the check number, date, and the payee for the check as well! We won’t develop a
Check class for this problem – remember, we’re concentrating on the inheritance stuff – but we would if
we wanted a complete solution.
Now let's actually implement the above class hierarchy using C#. We’ll start with the BankAccount
class; see Figure 16.9.
using System;
using System.Collections.Generic;
namespace BankAccounts
{
/// <summary>
/// A bank account that accepts deposits and withdrawals.
/// We can also access the current balance and lists of
/// deposits and withdrawals for the account
/// </summary>
public class BankAccount
{
#region Fields
decimal balance;
List<decimal> deposits = new List<decimal>();
List<decimal> withdrawals = new List<decimal>();
332 Chapter 16
#endregion
#region Constructors
/// <summary>
/// Constructor
/// </summary>
/// <param name="initialDeposit">the initial deposit opening the
/// account</param>
public BankAccount(decimal initialDeposit)
{
// set initial balance and add to list of deposits
balance = initialDeposit;
deposits.Add(initialDeposit);
}
#endregion
#region Properties
/// <summary>
/// Gets the balance in the account
/// </summary>
public decimal Balance
{
get { return balance; }
}
/// <summary>
/// Gets the list of deposits for the account
/// </summary>
public List<decimal> Deposits
{
get { return deposits; }
}
/// <summary>
/// Gets the list of withdrawals for the account
/// </summary>
public List<decimal> Withdrawals
{
get { return withdrawals; }
}
#endregion
/// <summary>
/// Adds the deposit to the account. Prints an error
/// message if the deposit is negative
/// </summary>
/// <param name="amount">the amount to deposit</param>
public void MakeDeposit(decimal amount)
{
// check for valid deposit
Inheritance and Polymorphism 333
if (amount > 0)
{
// increase balance and add deposit to deposits
balance += amount;
deposits.Add(amount);
}
else
{
// invalid deposit, print error message
Console.WriteLine(
"Deposits have to be larger than 0!");
}
}
/// <summary>
/// Deducts the withdrawal from the account. Prints an error
/// message if the withdrawal is larger than the account
/// balance
/// </summary>
/// <param name="amount">the amount to withdraw</param>
public void MakeWithdrawal(decimal amount)
{
// check for valid withdrawal
if (amount <= balance &&
amount > 0)
{
// deduct withdrawal and add withdrawal to withdrawals
balance -= amount;
withdrawals.Add(amount);
}
else
{
// invalid withdrawal, print error message
Console.WriteLine(
"Not enough money for withdrawal amount!");
}
}
#endregion
}
}
There’s nothing new here, so let’s start working on our SavingsAccount class. We're going to talk
about how child classes inherit fields, properties, and methods in the following sections, so for now we'll
just create the class and add the new field and the constructor.
namespace BankAccounts
{
/// <summary>
/// A savings account that pays interest
/// </summary>
public class SavingsAccount : BankAccount
{
334 Chapter 16
#region Fields
decimal interestRate;
#endregion
#region Constructors
/// <summary>
/// Constructor
/// </summary>
/// <param name="initialDeposit">the initial deposit opening the
/// account</param>
/// <param name="interestRate">the interest rate for the
/// account</param>
public SavingsAccount(decimal initialDeposit, decimal interestRate)
: base(initialDeposit)
{
this.interestRate = interestRate;
}
#endregion
}
}
The SavingsAccount constructor has two parameters: the initial deposit for the account and the interest
rate to be applied each time we accrue interest for the account.
There are a couple of subtleties here, though. For example, although the child class object has the fields,
it may not be able to directly reference them from its methods. For example, we'd get a compiler error if
we tried to include the following line in a CheckingAccount method that cashes a check:
balance -= amount;
Why do we get this error? Because private is the default access modifier for all the fields in our
BankAccount class (and any class we define), including the balance field. We've consistently made our
Inheritance and Polymorphism 335
fields private to preserve information hiding, but this causes problems when we want to use
inheritance.
So what should we do? Well, we could change the access modifier for the field to public, but this
would really break our information hiding since any user of the BankAccount class could then directly
access the field. Luckily, C# provides the protected access modifier to address this issue. Protected
fields can be accessed directly by child classes of the parent class. We're going to have to go back and
change our access modifier for the balance field to protected so our child classes can access that field:
We won't change the other fields, though, because we're not going to have to access them directly.
One last thing about fields in child classes. What happens if we create a new field in our child class that
has the same name as a field in the parent class? The new field hides the field in the parent class. This
can lead to some errors that are pretty hard to find, so you should only do this if you're positive you
really need to.
Even though we didn't explicitly create a Balance property in the SavingsAccount class, that class
inherits the property from the BankAccount class. This property works the same way in the
SavingsAccount class as it did in the BankAccount class, simply returning the balance in the account.
So properties and methods that are inherited from the parent class simply work the same way in the child
classes that inherit them.
Now that we understand how fields, properties, and methods work with inheritance, let's finish our
CheckingAccount and SavingsAccount classes (and make the few changes to the BankAccount class
that we’ve discussed as well). The complete code for the CheckingAccount class is provided in Figure
16.11; the SavingsAccount class code can be found in Figure 16.12.
using System;
using System.Collections.Generic;
namespace BankAccounts
{
/// <summary>
/// A checking account that lets us cash checks and access the list of
/// checks that have been cashed
/// </summary>
336 Chapter 16
public class CheckingAccount : BankAccount
{
#region Fields
#endregion
/// <summary>
/// Constructor
/// </summary>
/// <param name="initialDeposit">the initial deposit opening the
/// account</param>
public CheckingAccount(decimal initialDeposit)
: base(initialDeposit)
{
}
#endregion
#region Properties
/// <summary>
/// Gets the list of checks for the account
/// </summary>
public List<decimal> Checks
{
get { return checks; }
}
#endregion
/// <summary>
/// Cashes the check of the given amount. Prints an
/// error message if the check amount is larger than
/// the account balance
/// </summary>
/// <param name="amount">the amount of the check</param>
public void CashCheck(decimal amount)
{
// check for valid check amount
if (amount <= balance)
{
// deduct check and add check to checks
balance -= amount;
checks.Add(amount);
}
else
{
// invalid check, print error message
Console.WriteLine(
"Not enough money in account to cover check");
}
}
Inheritance and Polymorphism 337
#endregion
}
}
namespace BankAccounts
{
/// <summary>
/// A savings account that pays interest
/// </summary>
public class SavingsAccount : BankAccount
{
#region Fields
decimal interestRate;
#endregion
#region Constructors
/// <summary>
/// Constructor
/// </summary>
/// <param name="initialDeposit">the initial deposit opening the
/// account</param>
/// <param name="interestRate">the interest rate for the
/// account</param>
public SavingsAccount(decimal initialDeposit, decimal interestRate)
: base(initialDeposit)
{
this.interestRate = interestRate;
}
#endregion
/// <summary>
/// Adds accrued interest to the account balance
/// </summary>
public void AccrueInterest()
{
// calculate interest and add to balance
balance += balance * interestRate;
}
#endregion
}
}
Let's take a closer look at the ToString method that the BankAccount class inherits from the Object
class. Not surprisingly, the method converts an object of the class to a string. We can write a short
program that prints out the string for a BankAccount object:
using System;
namespace BankAccounts
{
/// <summary>
/// Demonstrates ToString method
/// </summary>
internal class Program
{
/// <summary>
/// Demonstrates ToString method
/// </summary>
/// <param name="args">command-line args</param>
static void Main(string[] args)
{
// create new bank account and print ToString results
BankAccount account = new BankAccount(100.00m);
Console.WriteLine("Object: " + account.ToString());
}
}
}
Object: BankAccounts.BankAccount
The default ToString method from the Object class prints the “fully qualified name of the type of the
Object.” Because our BankAccount class is declared in the BankAccounts namespace in our example,
BankAccounts.BankAccount is the output of the default method.
But if we really want to convert a bank account object to a string, wouldn't we want some more
meaningful information than the type for the object? Let's say that we'd like to print the balance when
we print a particular object. How do we do that? By overriding the ToString method. Let's add the
following method to the BankAccount class:
/// <summary>
/// Converts bank account to string
/// </summary>
/// <returns>the string</returns>
public override string ToString()
{
Inheritance and Polymorphism 339
return "Balance: " + balance;
}
Balance: 100.00
You’ll probably find yourself overriding the ToString method fairly regularly in practice. It's certainly
not required (we haven't done it up to this point), but it can be helpful for debugging and other display
purposes.
This gives us a list of BankAccount objects, and each of these objects can actually be either a bank
account object, a checking account object, or a savings account object. In other words, we have a single
variable (accounts) that refers to objects of different types, but we can do this because
CheckingAccount and SavingsAccount are child classes of BankAccount. Let’s add our accounts to
the list:
accounts.Add(new CheckingAccount(100.00m));
accounts.Add(new SavingsAccount(50.00m, 0.02m));
accounts.Add(new CheckingAccount(300.00m));
accounts.Add(new SavingsAccount(500.00m, 0.02m));
accounts.Add(new CheckingAccount(1000.00m));
accounts.Add(new SavingsAccount(50000.00m, 0.02m));
Now the bank decides to deposit $20.00 into every account as a sign of appreciation to its customers.
That means we need to go through the list and make a deposit of 20.00 into each account. Because of
polymorphism, we can simply use:
We don’t have to know whether each item is a checking account or a savings account when we call the
MakeDeposit method; because of polymorphism, the program automatically calls the appropriate
MakeDeposit method for that item. For both checking account and savings account objects, the
MakeDeposit method is inherited from the BankAccount class, but the method is called on checking
account or savings account objects. In addition, one or both of those classes could have overridden the
MakeDeposit method and the code would still work properly. The bottom line is that, through
polymorphism, the program takes care of those details for us so we don’t have to.
340 Chapter 16
Let's add the following code (which also uses polymorphism) to our program to print out each object:
C# automatically calls the ToString method when we call the Console WriteLine method with an
object, so the above code uses the ToString method we provided in the BankAccount class. Of course,
the CheckingAccount and SavingsAccount classes inherit the ToString method from the
BankAccount class, so the method is actually called on the checking account and savings account
objects in the list. The complete code for the polymorphism program we've developed in this section is
provided in Figure 16.13 and the output from the program is shown in Figure 16.14.
using System;
using System.Collections.Generic;
namespace BankAccounts
{
/// <summary>
/// Demonstrates polymorphism
/// </summary>
class Program
{
/// <summary>
/// Demonstrates polymorphism
/// </summary>
/// <param name="args">command-line arguments</param>
static void Main(string[] args)
{
// create list and add accounts
List<BankAccount> accounts = new List<BankAccount>();
accounts.Add(new CheckingAccount(100.00m));
accounts.Add(new SavingsAccount(50.00m, 0.02m));
accounts.Add(new CheckingAccount(300.00m));
accounts.Add(new SavingsAccount(500.00m, 0.02m));
accounts.Add(new CheckingAccount(1000.00m));
accounts.Add(new SavingsAccount(50000.00m, 0.02m));
All our scripts include the Start and Update methods in the template for the script. Figure 16.15. shows
an excerpt from the Messages section of the MonoBehaviour documentation from the Unity Scripting
Reference. As you can see at the bottom of the figure, the MonoBehaviour class has Start and Update
methods; those are the methods we inherit in our new scripts.
342 Chapter 16
Let’s build a complete, though small, game using the inheritance concepts we’ve learned in this chapter.
Here’s the problem description:
Design and implement a game where the player moves the mouse over different teddy bears to destroy
them. Green teddy bears will simply disappear (10 points), purple teddy bears will explode (25 points),
and yellow teddy bears will burn (50 points). The game should spawn a random teddy bear every
second.
This a pretty straightforward problem to understand. Although the problem description doesn’t say we
need to display the current score, that’s definitely something we’ll want to do as a standard component
of games with a score.
Design a Solution
Let’s think about blowing up and burning teddy bears first (we mean in the game, of course, not in real
life). In previous problems, when we needed an explosion we used an Explosion prefab with an attached
Explosion script that played the explosion animation. For our current problem, we’re going to need
both an explosion animation for the purple teddy bears and a fire animation for the yellow teddy bears.
We'll build prefabs for both of our required animations, but we won't actually need a script for the fire
animation; you'll see why when we get there.
What about the teddy bears? Well, we know we’ll need general teddy bear behavior – like moving and
bouncing off the edges of the window – for all the teddy bears, but each teddy bear has specialized
behavior as well. Wow, this looks like a great opportunity for us to apply our inheritance understanding!
And there was much rejoicing ...33
It also turns out that the exploding and burning teddy bears share behavior because each of them will
have an animation that they may be either starting or playing. Those observations lead to the class
hierarchy shown in Figure 16.16.
32 Unity doesn't actually use inheritance to provide these methods to us in our child classes, but conceptually it works that
way. If we implement one of those methods in the child class, that method is called when appropriate.
33 The appropriate response here is a very insincere “Yaay”
Inheritance and Polymorphism 343
We’ll start looking at the code soon, but there’s something you should notice about the TeddyBear and
AnimatedTeddyBear classes – they’re identified as abstract classes. What’s an abstract class? It’s a
class that serves as a parent class for one or more child classes but we can’t actually instantiate objects
of the class.
There’s actually a very good reason for making TeddyBear an abstract class. We want to make sure all
of the child classes include implementation of their required behavior (in the ProcessMouseOver
method) when the mouse goes over them, but we don’t know what the child classes have to do when the
mouse goes over them. Using an abstract class lets us handle that in an elegant way that we’ll discuss as
we go through the TeddyBear code. We'll also discuss why AnimatedTeddyBear is an abstract class
when we work through the code.
Because all our teddy bear types have a point value, we've included a pointValue field in the
TeddyBear class for all the child classes to inherit. The Start method will apply the impulse force
required to get the teddy bear moving (behavior we need for all the teddy bears) and the OnMouseEnter
method will call the child-specific ProcessMouseOver method when the mouse enters the collider for a
teddy bear.
The DisappearingTeddyBear class implements the ProcessMouseOver method to make the teddy bear
disappear.
344 Chapter 16
For the AnimatedTeddyBear class, we add a prefabAnimation field for the animation (for explosions
or fire) that will be played.
We need to make sure our test plan covers all three of the concrete (in other words, not abstract) teddy
bear classes: DisappearingTeddyBear, ExplodingTeddyBear, and BurningTeddyBear. In addition,
we need to make sure the overall game works properly, spawning teddy bears as appropriate and
keeping and displaying the score properly. The test cases listed below are designed to meet all those
testing needs.
Test Case 1
Checking Disappearing Teddy Bears
Step 1. Input: Hard-coded spawning of disappearing teddy bears
Expected Result: Moving green teddy bears that bounce off the walls and each other properly
Step 2. Input: Mouse over teddy bear
Expected Result: Teddy bear disappears, score increases by 10
This test case makes sure the DisappearingTeddyBear class is implemented properly.
Test Case 2
Checking Exploding Teddy Bears
Step 1. Input: Hard-coded spawning of exploding teddy bears
Expected Result: Moving purple teddy bears that bounce off the walls and each other properly
Step 2. Input: Mouse over teddy bear
Expected Result: Teddy bear explodes, score increases by 25
Step 3. Input: Mouse over teddy bear near collision with other teddy bear
Expected Result: Teddy bear explodes, score increases by 25, other teddy bear doesn’t collide with
explosion
This test case makes sure the ExplodingTeddyBear class is implemented properly.
Test Case 3
Checking Burning Teddy Bears
Step 1. Input: Hard-coded spawning of burning teddy bears
Expected Result: Moving yellow teddy bears that bounce off the walls and each other properly
Step 2. Input: Mouse over teddy bear
Expected Result: Teddy bear starts burning, score increases by 50
Step 3. Input: Mouse over teddy bear near collision with other teddy bear
Expected Result: Teddy bear starts burning, score increases by 50, teddy bears bounce off each other
properly
This test case makes sure the BurningTeddyBear class is implemented properly.
Inheritance and Polymorphism 345
Test Case 4
Checking Random Spawning
Step 1. Input: Hard-coded spawning of random teddy bears
Expected Result: Moving green, purple, and yellow teddy bears that bounce off the walls and each other
properly, with new teddy bear every second
Step 2. Input: Mouse over teddy bear
Expected Result: Teddy bear reacts properly, score increases properly
This test case makes sure the game is randomly spawning the teddy bears properly.
As usual, let’s work on our code a little at a time, moving through the steps of getting each test case to
pass before moving on to the next chunk of code. If we start on Test Case 1 (which makes sense), we
need to implement the TeddyBear and DisappearingTeddyBear classes. Since TeddyBear is the parent
class we’ll implement that class first.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// An abstract class for a teddy bear
/// </summary>
public abstract class TeddyBear : MonoBehaviour
{
As the UML diagram from our design indicates, we make the TeddyBear class an abstract class.
#region Fields
[SerializeField]
protected int pointValue;
#endregion
We mark the pointValue field with [SerializeField] so we can change it in the Inspector. Because
our DisappearingTeddyBear, ExplodingTeddyBear, and BurningTeddyBear classes are all child
classes (directly or indirectly) of the TeddyBear class, we'll be able to populate the field in the Inspector
for all those scripts. The field is protected so the child classes can access the field without exposing the
field to all the classes in the game.
/// <summary>
/// Start is called before the first frame update
/// </summary>
virtual protected void Start()
{
// apply impulse force to get teddy bear moving
const float MinImpulseForce = 3f;
const float MaxImpulseForce = 5f;
float angle = Random.Range(0, 2 * Mathf.PI);
346 Chapter 16
Vector2 direction = new Vector2(
Mathf.Cos(angle), Mathf.Sin(angle));
float magnitude = Random.Range(MinImpulseForce, MaxImpulseForce);
GetComponent<Rigidbody2D>().AddForce(
direction * magnitude,
ForceMode2D.Impulse);
}
The Start method applies the force to get the teddy bear moving; all the child classes inherit this
method, so they all start moving. We marked the Start method as virtual so that child classes
(specifically, the BurningTeddyBear class) can override the method as necessary. Also, by default the
Start method is private. We changed it to protected so the Start method in the BurningTeddyBear
class can call it to get the burning teddy bear moving.
How did we know at this point to mark the Start method virtual and protected as we were writing
the TeddyBear class? That's a great question, because when we're implementing a parent class we don't
necessarily know what child classes will be implemented and what they'll need access to.
Okay, confession time. We started with everything as private and when we discovered we needed
access to a method (to override it, call it, or both) as we implemented our child classes, we came back
and marked that method as appropriate. This is a really good approach to use because we didn't initially
know which method(s) needed to be protected (and virtual). We're showing and discussing the
(mostly) final TeddyBear code here rather than walking through all the iterations we went through as we
developed it.
/// <summary>
/// Called when the mouse enters the collider
/// </summary>
void OnMouseEnter()
{
ProcessMouseOver();
}
When the mouse enters the collider for any of the teddy bears, this method calls the ProcessMouseOver
method. We'll discuss that method next.
/// <summary>
/// Processing for when the mouse is over the teddy bear
/// </summary>
protected abstract void ProcessMouseOver();
#endregion
}
Here's something new. We're defining an abstract method by providing the method header (including the
abstract keyword) followed by a ; instead of a method body. What does this do for us? It forces any
child class to override the ProcessMouseOver method with an actual implementation that includes a
method body; that implementation is called a concrete method. Since most methods are actually
concrete, we usually just call them methods.
Inheritance and Polymorphism 347
There’s actually an exception to the override discussion above. A child class can choose not to override
the abstract method in the parent class, but in that case the child class also has to be abstract. Any class
in the class hierarchy that can actually be instantiated will need to inherit or contain concrete
implementations of all abstract methods in the hierarchy.
Why does forcing a child class to implement the ProcessMouseOver method help us? Remember, the
OnMouseEnter method calls the ProcessMouseOver method when the mouse enters the collider for the
teddy bear. Because we don't know what the child classes need to do when that happens, we have them
provide their processing in their overridden ProcessMouseOver method. That way we can include
detection of the mouse entering the teddy bear collider in the parent class, with each child class
providing the custom processing for that child class when that happens. This is so totally awesome that
you should take a deep breath or two before continuing.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// A teddy bear that disappears when the mouse passes over it
/// </summary>
public class DisappearingTeddyBear : TeddyBear
{
/// <summary>
/// Destroys the teddy bear
/// </summary>
protected override void ProcessMouseOver()
{
Destroy(gameObject);
}
#endregion
}
This method simply destroys the game object the script is attached to, which makes the game object
disappear when the mouse enters the collider for the game object.
Before we can run Test Case 1, we need to create a prefab for a disappearing teddy bear, including
Rigidbody 2D, Box Collider 2D, and DisappearingTeddyBear (the script) components. We also need
to set the Point Value field in the script to 10 in the Inspector. Check out the Unity project for this
problem if you want more details about that prefab.
We also need the game to start spawning disappearing teddy bears. We wrote a TeddyBearSpawner
script and attached it to the main camera to handle this for us (as we've done for previous games).
348 Chapter 16
Test Case 1
Checking Disappearing Teddy Bears
Step 1. Input: Hard-coded spawning of disappearing teddy bears
Expected Result: Moving green teddy bears that bounce off the walls and each other properly
Step 2. Input: Mouse over teddy bear
Expected Result: Teddy bear disappears, score increases by 10
Well heck. It's nice to see that the teddy bear disappears in Step 2 the way it's supposed to, but there's no
score display and nothing in the ProcessMouseOver method above to increase the score, so the test case
fails. Let's fix that now.
In our fish game in Chapter 14, we had our Fish script handle tracking and displaying the score. As we
mentioned then, this made sense because the fish is the player's avatar in that game and it makes sense to
have the player keep track of their own score. The player doesn't have an avatar in this game, though, so
we should use a different approach here.
We've also seen in our previous solutions that it sometimes makes sense to have a high-level “game
manager script” that handles game-level kinds of things (like in our Ted the Collector game). Since the
score display is a game-level function, we'll write a new TeddyBearDestruction script to handle this.
Like we did for our fish game, we need to add a Text – TextMeshPro element to our scene; here are the
instructions (again) for doing that. Right click in the Hierarchy window and select UI > Text –
TextMeshPro. As you can see, you actually end up with a number of new components, including a
Canvas that the text is drawn on in the game. Change the name of the Text – TextMeshPro element to
ScoreText.
Select ScoreText in the Hierarchy window, select the Anchor Preset for the top left corner, and change
the Pos X and Pos Y values in the Rect Transform component to position the text (we used 175 and -90
for these values). In the TextMeshPro – Text (UI) component, change the Font Style to Bold, the Font
Size to 24, and the Color to white.
/// <summary>
/// Game manager
/// </summary>
public class TeddyBearDestruction : MonoBehaviour
{
// score support
[SerializeField]
TextMeshUGUI scoreText;
int score = 0;
Inheritance and Polymorphism 349
/// <summary>
/// Start is called before the first frame update
/// </summary>
void Start()
{
// set initial score text
scoreText.text = "Score: " + score;
}
/// <summary>
/// Adds the given points to the score
/// </summary>
/// <param name="points">points to add</param>
public void AddPoints(int points)
{
score += points;
scoreText.text = "Score: " + score;
}
}
Next, we attach this script to the main camera. After doing so, drag the ScoreText element from the
Hierarchy window onto the Score Text field of the script in the Inspector.
Now we need a way for the DisappearingTeddyBear script to call the TeddyBearDestruction
AddPoints method. One good way to do that is to get a reference to the TeddyBearDestruction script
that's attached to the main camera. We should realize, though, that all of our teddy bear classes will need
a reference to this script. That means that it makes sense to put the field for this reference, and setting
that field to a value, in the TeddyBear class rather than in each of the child classes.
This really only requires that we make two changes. First, we add the required field to the TeddyBear
class:
// score support
protected TeddyBearDestruction teddyBearDestruction;
Notice that we make the field protected so the child classes can access the field to call the AddPoints
method.
Second, we give the field its value in the TeddyBear Start method:
// score support
teddyBearDestruction = Camera.main.GetComponent<TeddyBearDestruction>();
The last thing we need to do is add code to the DisappearingTeddyBear ProcessMouseOver method to
add points to the score:
teddyBearDestruction.AddPoints(pointValue);
When we run Test Case 1 again, the test case works as expected, so we can move on.
350 Chapter 16
To get Test Case 2 to pass, we need to implement the AnimatedTeddyBear and ExplodingTeddyBear
classes. Since AnimatedTeddyBear is the parent class we’ll implement that class first.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// A teddy bear with an animation
/// </summary>
public abstract class AnimatedTeddyBear : TeddyBear
{
This is the first time we’ve seen a child class that’s also abstract; that works fine, though. Remember
that we said that classes that will actually be instantiated need to contain concrete implementations of all
the abstract methods of all its parent classes in the class hierarchy, but it’s certainly okay to have
multiple abstract classes in that hierarchy.
#region Fields
[SerializeField]
protected GameObject prefabAnimation;
#endregion
}
We mark the prefabAnimation field with [SerializeField] so we can populate it in the Inspector
and we make it protected so child classes can access it as necessary.
Even though this is a very simple class, it inherits everything from the TeddyBear class and adds a field
that we know will be useful for the teddy bear classes that need to play an animation.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// An exploding teddy bear
/// </summary>
public class ExplodingTeddyBear : AnimatedTeddyBear
{
#region Protected methods
/// <summary>
/// Explodes the teddy bear
/// </summary>
protected override void ProcessMouseOver()
{
Inheritance and Polymorphism 351
teddyBearDestruction.AddPoints(pointValue);
Instantiate(prefabAnimation, transform.position, Quaternion.identity);
Destroy(gameObject);
}
#endregion
}
The method above overrides the abstract method from the TeddyBear class. Even though the TeddyBear
class isn’t the parent class (AnimatedTeddyBear is), TeddyBear is the grandparent of the
ExplodingTeddyBear class. Remember, concrete classes have to implement all abstract methods in the
classes above them in the class hierarchy.
The method adds the points for the exploding teddy bear, instantiates the prefabAnimation (which will
be the explosion prefab), and destroys the teddy bear game object.
Before we can run Test Case 2, we need to create a prefab for an exploding teddy bear, including
Rigidbody 2D, Box Collider 2D, and ExplodingTeddyBear (the script) components. We also need to
create a prefab for an explosion and populate the Point Value field in the script with 25 and the Prefab
Animation field in the script with the explosion prefab. Finally, we change the TeddyBearSpawner
script to only spawn exploding teddy bears.
Test Case 3
Checking Exploding Teddy Bears
Step 1. Input: Hard-coded spawning of exploding teddy bears
Expected Result: Moving purple teddy bears that bounce off the walls and each other properly
Step 2. Input: Mouse over teddy bear
Expected Result: Teddy bear explodes, score increases by 25
Step 3. Input: Mouse over teddy bear near collision with other teddy bear
Expected Result: Teddy bear explodes, score increases by 25, other teddy bear doesn’t collide with
explosion
To get Test Case 3 to pass, we need to implement the BurningTeddyBear class. Before we look at the
details of that class, we have some work to do in the Unity editor.
First, we need to create a prefab for a burning teddy bear, including Rigidbody 2D, Box Collider 2D,
and BurningTeddyBear (the script) components. We start by dragging the burningteddybear sprite from
the sprites folder in the Project window into the Hierarchy window and adding the components listed
above. Change the name of the game object to BurningTeddyBear.
Next, we create a prefab for the fire we need; we do this by following the same steps we did to create the
Explosion prefab, but you shouldn't include the first few frames of the fire sprite strip because we don't
need those “startup” frames in our looping fire animation. We don't actually need to attach a script to the
352 Chapter 16
fire prefab. We needed the Explosion script to destroy the Explosion game object once the explosion
animation finished, but our fire animation will just keep playing until the Fire object is destroyed.
After creating the Fire prefab, drag it into the Hierarchy window and drop it onto the BurningTeddyBear
game object in the Hierarchy window. This makes that object a child game object (not to be confused
with a child class) of the BurningTeddyBear game object; we used the same approach in Chapter 14 for
the circle problem. We want the fire to be a child game object so that it follows the teddy bear around as
it burns.
At this point, we need to make a couple of adjustments to the Fire child game object. First, we should
shift it up so the fire appears at the top of the teddy bear's head. To do this, select the Fire child game
object in the Hierarchy window and change the Y value in the Position field of the Transform
component to 0.28.
Also, you may have noticed that the fire appears in front of the teddy bear's head. It actually looks better
if the fire appears behind the teddy bear's head, so change the Order in Layer field of the Sprite Renderer
component to -1 (recall that we used Order in Layer in Chapter 9 as well).
Finally, we don't want the fire animation to be visible when the burning teddy bear is spawned, we only
want to make it visible once the teddy bear starts burning. Uncheck the check box just to the left of the
Sprite Renderer title at the top of the Sprite Renderer component. That disables the component so it
doesn't actually get drawn.
Apply the changes we made to the Fire prefab and save the BurningTeddyBear game object as a prefab.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// A burning teddy bear
/// </summary>
public class BurningTeddyBear : AnimatedTeddyBear
{
#region Fields
// burn support
Timer burnTimer;
const float BurnSeconds = 2f;
#endregion
We're going to have the teddy bear burn for 2 seconds before it's destroyed; we'll use the fields above to
support that.
#region Public methods
/// <summary>
/// Start is called before the first frame update
/// </summary>
Inheritance and Polymorphism 353
override protected void Start()
{
// create burn timer
burnTimer = gameObject.AddComponent<Timer>();
burnTimer.Duration = BurnSeconds;
Recall that we marked the Start method in the TeddyBear class to be virtual so we could override it
here. That overriding means that this Start method will get called instead of the one in the TeddyBear
class for game objects that have the BurningTeddyBear script attached to them.
We needed to override the method so we could create the burn timer. We check whether or not the burn
timer is finished in the Update method (coming soon), so we need to create it before then to avoid a
NullReferenceException when we access its Finished property.
We still need to start the teddy bear moving, though, so we also need to run the TeddyBear Start
method. That's what the last line of code above does. We use the base keyword when we need to call a
method in the parent class from the child class. Even though AnimatedTeddyBear is our immediate
parent, we can use the base keyword to call the TeddyBear Start method because AnimatedTeddyBear
doesn’te override that method.
/// <summary>
/// Update is called once per frame
/// </summary>
void Update()
{
// check for burn complete
if (burnTimer.Finished)
{
Destroy(gameObject);
}
}
#endregion
The Update method destroys the game object the script is attached to when the burn timer finishes. That
also destroys all child game objects, so both the BurningTeddyBear and the Fire game objects are
destroyed when that happens.
#region Protected methods
/// <summary>
/// Burns the teddy bear
/// </summary>
protected override void ProcessMouseOver()
{
teddyBearDestruction.AddPoints(pointValue);
#endregion
}
The ProcessMouseOver method above adds the points for the burning teddy bear to the score. It then
retrieves a reference to the SpriteRenderer component for the fire prefab (which the prefabAnimation
field is holding) and enables the sprite renderer so the fire animation is displayed. Finally, the method
starts the burn timer so the teddy bear will be destroyed once the burn timer is finished.
Finally, we populate the Point Value field in the script with 50 and the Prefab Animation field in the
script with the fire prefab (the child game object in the Hierarchy window, not the prefab from the
Project window), apply those changes to the BurningTeddyBear prefab, and change the
TeddyBearSpawner script to only spawn burning teddy bears.
Test Case 3
Checking Burning Teddy Bears
Step 1. Input: Hard-coded spawning of burning teddy bears
Expected Result: Moving yellow teddy bears that bounce off the walls and each other properly
Step 2. Input: Mouse over teddy bear
Expected Result: Teddy bear starts burning, score increases by 50
Step 3. Input: Mouse over teddy bear near collision with other teddy bear
Expected Result: Teddy bear starts burning, score increases by 50, teddy bears bounce off each other
properly
This test case also passes, but we actually discovered a problem while we were running the test case. If
we start a teddy bear burning, then pass the mouse over it while it's burning, we earn an additional 50
points (and we can do that multiple times). This isn't valid behavior, but the test case didn't detect it, so
our first step is to revise the test case appropriately.
Test Case 3
Checking Burning Teddy Bears
Step 1. Input: Hard-coded spawning of burning teddy bears
Expected Result: Moving yellow teddy bears that bounce off the walls and each other properly
Step 2. Input: Mouse over teddy bear
Expected Result: Teddy bear starts burning, score increases by 50
Step 3. Input: Mouse over teddy bear near collision with other teddy bear
Expected Result: Teddy bear starts burning, score increases by 50, teddy bears bounce off each other
properly
Step 4. Input: Mouse over teddy bear that's already burning
Expected Result: Teddy bear continues burning, score doesn't change
Inheritance and Polymorphism 355
The cleanest way to solve this problem is to check to make sure the burn timer isn't already running
before we do the processing in the ProcessMouseOver method. Here's the revised method:
/// <summary>
/// Burns the teddy bear
/// </summary>
protected override void ProcessMouseOver()
{
if (!burnTimer.Running)
{
teddyBearDestruction.AddPoints(pointValue);
Our revised Test Case 3 passes, so we can write the final piece of code for our problem solution.
To make our solution pass Test Case 4, the only thing we need to add is random spawning of teddy
bears to the game. The SpawnBear method from the TeddyBearSpawner script is shown below; this
method is run each time the spawn timer is finished.
/// <summary>
/// Spawns a new teddy bear at a random location
/// </summary>
void SpawnBear()
{
// generate random location
Vector3 location = new Vector3(Random.Range(minSpawnX, maxSpawnX),
Random.Range(minSpawnY, maxSpawnY),
-Camera.main.transform.position.z);
Vector3 worldLocation = Camera.main.ScreenToWorldPoint(location);
This code generates a random location, then randomly generates a 0, 1, or 2 – the Random Range method
we’re using here takes the inclusive lower bound and exclusive upper bound for the range of numbers –
and instantiates a DisappearingTeddyBear, ExplodingTeddyBear, or BurningTeddyBear game object
based on the generated number.
Test Case 4
Checking Random Spawning
Step 1. Input: Hard-coded spawning of random teddy bears
Expected Result: Moving green, purple, and yellow teddy bears that bounce off the walls and each other
properly, with new teddy bear every second
Step 2. Input: Mouse over teddy bear
Expected Result: Teddy bear reacts properly, score increases properly
That finishes off our solution to this problem. This is obviously a very simple game, but it gave us a
chance to really hone our skills using inheritance.
Delegates in C# are very useful in a number of ways. For example, they let us easily specify specific
behavior for instances of a more general class. In addition, they let us build a robust structure for
handling events that occur in our games. This chapter looks at how we can use C# delegates for those
two purposes. We'll also explore how to use different versions of UnityEvent, a set of built-in Unity
classes that lets us implement some Unity-specific event handling.
“A delegate is a type that defines a method signature. When you instantiate a delegate, you can
associate its instance with any method with a compatible signature. You can invoke (or call) the method
through the delegate instance.”
Hmmm, does that clarify everything for us? Perhaps not quite! Let’s look at delegates in a slightly
different way, then come back to this definition in a little while. We’ll start with an idea that you should
already have a firm grasp on: value and reference types.
As you know, variables that are declared as a reference type don’t hold the actual object for the
reference type; instead, they hold a reference to that object’s actual location in memory. So a reference
type variable holds the memory address for the object.
In some languages (like C and C++), a reference to a memory address is called a pointer. In fact, those
languages also support something called “function pointers.” As you might suspect, a function pointer is
a pointer to a function rather than to an object. Why does that help? Because it means we can pass
function pointers as arguments to methods (among other things), essentially passing behavior
specifications along so other methods can use them. But C and C++ aren’t C#, so why do we care?
Because, as the VS help says:
“Delegates are like C++ function pointers but are type safe.”
So we get the benefit of function pointers in C# as well (type safety helps make them even better, which
we’ll discuss soon). One more quote from the VS help:
“Delegates are used to pass methods as arguments to other methods. Event handlers are nothing more
than methods that are invoked through delegates.”
Those are precisely the two uses we mentioned in the introduction, so let’s look at each of them.
358 Chapter 17
When the mouse intersects the ball, the ball will do whatever the delegate says to do. You should be sure
to download the code from the web site so you can follow along.
The first thing we’ll do is define the delegate; you can find the following definition in the
BallBehavior.cs file in the Scripts folder of the Unity project:
/// <summary>
/// Delegate for ball behavior
/// </summary>
/// <param name="ball">the ball</param>
public delegate void BallBehavior(Ball ball);
As the first definition from VS help said, we’re defining the method signature for the delegate. When we
define a method to use for the delegate, we’ll have to make sure that the return type for the method is
void and that the method has a single Ball parameter. The access modifier and method names DON’T
have to match. In fact, the method name (BallBehavior) in the delegate specification is actually the
name of the type for the delegate (just as a class name for a class we define is the name of the type for
that class).
#region Fields
#endregion
Notice that the type of the variable is the delegate type we specified when we declared the delegate.
We need to expose a property so a consumer of the Ball class (for this example, a ball spawner) can
specify the ball behavior for an instance of the class:
#region Properties
/// <summary>
/// Sets the ball behavior
/// </summary>
/// <value>ball behavior</value>
public BallBehavior Behavior
{
set { behavior = value; }
}
Delegates and Event Handling 359
You should recall from previous chapters that when we want to change the sprite that's rendered for a
particular game object, we set the sprite for the SpriteRenderer component for that game object; we
can do a similar thing for color. Because we want balls with different behaviors to be different colors,
we also need to expose a Color property so the ball spawner can set the color for the ball to be the
correct color.
/// <summary>
/// Sets the ball color
/// </summary>
/// <value>ball color</value>
public Color Color
{
set
{
SpriteRenderer spriteRenderer =
GetComponent<SpriteRenderer>();
spriteRenderer.color = value;
}
}
In our SpriteRenderer examples in previous chapters, we saved the component in a field because we
expected to access that component multiple times over the life of the object. In our current example,
though, we only expect the color to be changed once (right after the Ball game object is created), so we
use a local variable in the set accessor instead.
As in our example from the previous chapter where we had different teddy bears with different
behaviors, we'll use the OnMouseEnter method to trigger the appropriate behavior when the mouse
intersects with the game object.
/// <summary>
/// Called when the mouse enters the collider
/// </summary>
void OnMouseEnter()
{
behavior(this);
}
This should remind you of our example in the previous chapter, where we had an abstract
ProcessMouseOver method that we called from the OnMouseEnter method. The key difference is that in
the previous chapter, different child classes implemented the ProcessMouseOver method to implement
their specific (different) behavior, leading to a class hierarchy of multiple classes. In our example here,
we have a single Ball class, and the different behaviors of the different Ball game objects are
determined by the BallBehavior delegate that was used to set the behavior field for each Ball game
object.
The Ball class has no idea what the behavior method does, because it was provided at run time when
the Behavior property was accessed to set that field, but we do know that the method being called
requires a single Ball argument. We provide this for that argument so that whatever the method does it
will do to this Ball.
360 Chapter 17
That’s all well and good, but it doesn’t actually demonstrate how we can effectively use delegates to
give balls different behavior. To see that, we need to create a script to actually spawn the balls.
Before we do that, though, we need to create a Ball prefab the spawner can spawn. Add a sprite for a
white ball to the project and drag the sprite into the Hierarchy window. Because the OnMouseEnter
method gets called based on when the mouse enters the collider for a game object, click the Add
Component button in the Inspector and select Physics 2D > Circle Collider 2D. Add the Ball script to
the Ball game object in the Hierarchy window, create a Prefabs folder in the Project window, and drag
the Ball game object from the Hierarchy window onto the Prefabs folder to create the prefab. Finally,
delete the Ball game object from the Hierarchy window.
As usual, we'll need to use a timer for our spawner, so go to your Operating System and copy the
Timer.cs file from one of the previous Unity projects into the Scripts folder for this example. Okay,
we're finally ready to start working on our BallSpawner script. Lots of the code in this script looks like
the TeddyBearSpawner script from the previous chapter, so we'll only look at the major differences
here.
Instead of picking a random teddy bear type to spawn, we'll pick a random color (and the associated
behavior) when it's time to spawn a new ball. We'll start by just spawning red balls, which will move
left, then add the other colors (and behaviors) after we get that working. We know, though, that one of
the things we need to do when we spawn a new ball is set the Behavior property with a BallBehavior
for the ball. Here's a method that moves the ball to the left:
/// <summary>
/// Moves the given ball to the left
/// </summary>
/// <param name="ball">ball to move</param>
void MoveLeft(Ball ball)
{
Vector3 position = ball.transform.position;
position.x -= BallMoveAmount;
ball.transform.position = position;
}
At this point in the book, you should be able to easily understand how the code in the method body
works (we declared a BallMoveAmount constant in our script). The important point about this method is
that it matches the requirements to be used as a BallBehavior delegate; specifically, it returns void and
has a single Ball parameter. Remember, the access modifier and method names DON’T have to match
our BallBehavior definition.
How do we use the MoveLeft method to create a red ball that moves to the left? Here's a SpawnBall
method that does that:
/// <summary>
/// Spawns a new ball at a random location
/// </summary>
void SpawnBall()
{
// generate random location
Vector3 location = new Vector3(Random.Range(minSpawnX, maxSpawnX),
Delegates and Event Handling 361
Random.Range(minSpawnY, maxSpawnY),
-Camera.main.transform.position.z);
Vector3 worldLocation = Camera.main.ScreenToWorldPoint(location);
The first block of code generates a random location for the ball; we've seen that code before, but the
second block of code is more interesting.
The first two lines of code in that block instantiate a new instance of the Ball prefab and get the Ball
script attached to that prefab so we can access the properties in the script. The third line of code changes
the color of the ball using the built-in Unity Color enumeration and the fifth line of code moves the new
Ball game object to the random location we generated in the first block of code.
The fourth line of code is the one that uses the delegate concepts we're exploring in this section. Recall
that setting the Behavior property requires that we include a BallBehavior delegate on the right of the
= because the type of the property is BallBehavior. Because our MoveLeft method matches the
requirements to be used as a BallBehavior delegate, we can set the Behavior property to the MoveLeft
method. And that's all we have to do to make this ball move to the left when the mouse intersects with
the collider for the ball!
Attach the BallSpawner script to the main camera in the scene, drag the Ball prefab from the Project
window onto the Prefab Ball value in the script in the Inspector, and run the game. You should see red
balls spawned periodically, and if you move the mouse over a ball you should see it move to the left.
All we have to do to finish this example is write MoveRight, MoveUp, and MoveDown methods to
implement the other 3 ball behaviors and change the second block of code in the SpawnBall method to
randomly pick between the 4 different ball colors (and behaviors). We'll assume you can easily write the
3 additional behavior methods; here's the revised second block of code for the SpawnBall method:
Now when you run the game, you should get all 4 different color balls with red balls moving left, green
balls moving right, blue balls moving up, and yellow balls moving down.
It takes a while for some new programmers to really understand how delegates work, but if you review
this section as needed you'll find that delegates (and events, coming up next!) are really powerful and
useful.
To see why this is a help, let’s refactor our fish game from Chapter 14. In that chapter, we had the Fish
game object keep track of and display the player's score. That made sense in the context of that example
(where our focus was on learning how to do text output in the game), but it's more common to actually
have a Heads Up Display (HUD) that displays information to the player. This becomes even more
appropriate as we display more information than score (like health, timers, and so on) to the player, so
let's learn how to do that now. Of course, we'll use an event and an event handler as part of our solution.
Starting from the Unity project from Chapter 14, create an empty game object by right clicking in the
Hierarchy pane and selecting Create Empty. Rename the new game object HUD, make sure the X and Y
values of the Transform component are both set to 0, and drag the Canvas and Event System onto the
HUD game object.
If you run the game now, you'll see that it still works the way it used to. The problem, though, is that the
Fish script has a reference to the ScoreText Text element inside the HUD. This is a problem because the
Fish script really shouldn't “know about” components within other game objects in the game.
One approach we could use to have the HUD handle displaying the score would be to write a HUD script
that exposes a static method that the Fish script calls to tell the HUD to add points to the score. There's a
real problem with this approach, though. For this approach to work, the Fish script has to know about
the HUD script and the methods it exposes. A better object-oriented design would have the Fish script
totally unaware of the existence of the HUD script. It is just a fish, after all, so it shouldn’t have to
understand how it fits into the larger game implementation!
Our design and implementation will have the Fish script invoke an event when the score changes
(without having to care about who might be listening) and have the HUD listen for that event and
Delegates and Event Handling 363
change the text output when it “hears” that the event occurred. With this approach, we have a solid
object-oriented design that works well (and uses delegates).
Let’s start by looking at the PointsAddedEvent.cs file. You should know that there are a number of ways
to structure events in C#; we’re showing you the way we’ve done it for all our events in our company
games when we needed to use C# events. Here’s the code:
/// <summary>
/// Delegate for handling the event
/// </summary>
/// <param name="points">points to add</param>
public delegate void PointsAddedEventHandler(int points);
/// <summary>
/// An event that indicates that points have been added
/// </summary>
public class PointsAddedEvent
{
// the event handlers registered to listen for the event
event PointsAddedEventHandler eventHandlers;
/// <summary>
/// Adds the given event handler as a listener
/// </summary>
/// <param name="listener">listener</param>
public void AddListener(PointsAddedEventHandler listener)
{
eventHandlers += listener;
}
/// <summary>
/// Invoke the event for all event handlers
/// </summary>
/// <param name="points">points to add</param>
public void Invoke(int points)
{
if (eventHandlers != null)
{
eventHandlers(points);
}
}
}
Up until this point, we’ve always only had one class in each file we create. In contrast, the
PointsAddedEvent.cs file contains both the delegate for handling the event and the class for the event
itself. We think it’s better to keep both of those in a single file, so that’s how we do it.
The delegate specification is similar to the delegate we discussed in the previous section, so it doesn’t
need any further explanation. The PointsAddedEvent class, however, merits a closer look.
The first thing we see in the class is a private eventHandlers field defined as the delegate type. It
probably seems strange, though, that the variable declaration also includes the event keyword. What
does that do?
364 Chapter 17
Marking the variable as an event indicates that it’s “a special kind of multicast delegate”.
That means that the eventHandlers field can actually hold multiple event handlers (kind of like a list of
the delegates, though simpler to interact with). This is useful when you might have multiple objects
listening for the same event to be invoked; using an event field ensures that all of the listeners will hear
the event.
The AddListener method lets an object that wants to listen for this event add a method to the set of
event handlers (delegates) that will be notified when the event is invoked. Using += simply adds the
listener parameter to the set of delegates that will be called when the event is invoked.
The Invoke method goes through the set of event handlers that have been added as listeners and calls
the delegate that was provided when that event handler was added as a listener. In other words, this part
works exactly like the MoveLeft, MoveRight, MoveUp, and MoveDown delegates getting called in the
Ball OnMouseEnter method. The only real difference is that each delegate isn’t held in a unique
variable like our behavior field in the Ball class; instead, our delegates here are bundled together in the
eventHandlers field in the PointsAddedEvent class.
So the general approach will be that an object that invokes the event (an instance of the Fish class, in
this case) will declare a field for a PointsAddedEvent object. Objects that want to listen for that event
(an instance of the HUD class, in this case) will call the AddListener method to provide a method
(delegate) that gets called when the event is invoked. When the object actually invokes the event, it calls
each of the delegates that have been added as listeners so they can do whatever they need to do to
process the event.
Okay, let’s look at how this is all implemented in the example code, starting with the Fish class. First,
we see a field for the event (note that we need to construct a new instance of the event class to use it):
We also expose a method that lets other objects add listeners for this event:
/// <summary>
/// Adds the given event handler as a listener
/// </summary>
/// <param name="listener">listener</param>
public void AddListener(PointsAddedEventHandler listener)
{
pointsAddedEvent.AddListener(listener);
}
This method makes it easy for other classes to add their event handlers as listeners.
Next, we need to create a HUD script that listens for the PointsAddedEvent so it can update the score
when appropriate. Given that statement, though, we immediately realize that the HUD script would need
to know about the Fish script so it could call the AddListener method in the Fish script. This is just as
bad as the Fish script having to know about the HUD script in the possible implementation approach we
rejected!
Delegates and Event Handling 365
This is unfortunately one of those areas where we need to slightly break the purity of our object-oriented
approach to make our game works in a reasonable way. We'll solve this problem by implementing a
static EventManager class that exposes an AddListener method for each of the events that can be
invoked by scripts in the game. When a script calls one of the EventManager AddListener methods,
the event manager will call the AddListener method on the script that actually invokes that event.
Essentially, the EventManager class implements a wrapper around all the AddListener methods
exposed by the scripts in the game. This is a win because each script only needs to know about the
single EventHandler class no matter how many events they need to listen for.
You could certainly argue that if we're not going to implement a “pure” object-oriented solution, we
should have just ignored all this event handling stuff and implemented an “ugly but simple” static
AddPoints method in the HUD script! That's not the right way to think about this, though. Imagine a
more complicated game where the HUD script needs to listen for events invoked by a number of different
scripts in the game. In our chosen solution, the HUD script still only needs to know about the
EventManager class, it doesn't need to know about all the other scripts that invoke the events it needs to
listen for. We've had to sacrifice a little purity in the name of practicality, but our solution scales up to
higher levels of script interaction complexity very cleanly, because all the mappings between listener
and event invoking scripts are contained in a single class: the EventManager class.
Okay, here's the EventManager class, which only needs to expose a single AddListener method for our
simple example:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// Manages connections between event listeners and event invokers
/// </summary>
public static class EventManager
{
/// <summary>
/// Adds the given event handler as a listener
/// Game objects tagged as Fish invoke PointsAddedEvent
/// </summary>
/// <param name="listener">listener</param>
public static void AddListener(PointsAddedEventHandler listener)
{
// add listener to all fish
GameObject[] fish = GameObject.FindGameObjectsWithTag("Fish");
foreach (GameObject currentFish in fish)
{
Fish script = currentFish.GetComponent<Fish>();
script.AddListener(listener);
}
}
}
First, the code finds all the game objects in the scene that have been tagged with the Fish tag; those are
the game objects that will have a Fish script attached to them. The code then retrieves the Fish script
366 Chapter 17
from each of those game objects and calls the AddListener method to add the provided event handler as
a listener.
In this example, there's obviously only a single fish, but in more complicated examples there may be
multiple game objects that invoke a particular event. For example, if we had teddy bears invoke the
event to add points when they've been eaten, we'd need to add the listener to each of those teddy bears.
Even though it's a bit of overkill for this example, finding all the game objects with the given tag is a
general approach that should always work.
Of course, we need to add the Fish tag to all the game objects that have a Fish script attached to them
for that approach to work. Add a Fish tag to the Fish prefab in the Prefabs folder, which automatically
tags all (in this case, one) instances of that prefab in the scene.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using TMPro;
/// <summary>
/// The HUD for the game
/// </summary>
public class HUD : MonoBehaviour
{
#region Fields
// score support
[SerializeField]
TextMeshProUGUI scoreText;
int score = 0;
#endregion
/// <summary>
/// Start is called before the first frame update
/// </summary>
void Start()
{
// add listener for PointsAddedEvent
EventManager.AddListener(HandlePointsAddedEvent);
/// <summary>
/// Handles the points added event by updating the displayed score
/// </summary>
/// <param name="points">points to add</param>
void HandlePointsAddedEvent(int points)
{
Delegates and Event Handling 367
score += points;
scoreText.text = "Score: " + score;
}
#endregion
}
The script saves a copy of the TextMeshProUGUI object used to display the score in a field so it doesn't
have to retrieve it every time points are added to the score. It also keeps track of the current score,
making sure the score starts at 0.
In the Start method, the code tells the EventManager to add the HandlePointsAddedEvent method as
a listener for the PointsAddedEvent. You should note that the HandlePointsAddedEvent method
matches the requirements to be used as a PointsAddedEventHandler delegate as defined in the
PointsAddedEvent.cs file we discussed above. The code also initializes the text value for the score text.
Finally, what does the HandlePointsAddedEvent method do when it “hears” the event? It simply adds
the provided points to the current score, then sets the text value of the TextMeshProUGUI object to
display the new score.
Be sure to attach the HUD script to the HUD game object or none of this will work! You also need to set
the Score Text value in the HUD script by dragging the ScoreText element from the Hierarchy pane
onto that value in the Inspector pane.
Finally, we finish our changes to the Fish script. Specifically, we remove the using statement for the
TMPro namespace (since we're no longer holding a TextMeshProUGUI object as a field), we remove the
score and scoreText fields, we remove the initialization of the score text from the Start method, and
in the OnCollisionEnter2D method we replace
// update score
score += bearPoints;
scoreText.text = "Score: " + score;
with
// update score
pointsAddedEvent.Invoke(bearPoints);
That last makes the Fish script invoke the PointsAddedEvent; because the
change
HandlePointsAddedEvent method in the HUD script was added as a listener for that event (through the
EventManager), the score text is updated in the HUD when the event is invoked.
Run the game again and you'll see that everything works properly. Although the game works the same
as it did in Chapter 14, it's a much better object-oriented design, and it also gave us a chance to learn a
powerful new C# programming capability. Sweet.
368 Chapter 17
For this example, we’ll refactor the teddy bear destruction game from Chapter 16. In our previous
solution, the TeddyBearDestruction script exposes an AddPoints method, which requires that all the
teddy bear classes know about this method. We’ll replace that structure with an event and an event
handler instead.
As usual, we start by implementing the event, but our event is identical to the one we used in the
previous section so there's no need to discuss that further here. The EventManager is more complicated
for this example, though, so let's look at that now.
Before we look at the details of our implementation, let's think about how the events will work in this
game. As we said in the previous section, a HUD is a pretty standard mechanism for displaying
information to the player, so we'll use the HUD game object (and the HUD script) from the previous
section as well. The additional complexity in our EventManager class comes from the invokers of the
PointsAddedEvent.
In our previous example, the Fish script was the only script that invoked the event and the fish was
already in the scene when the HUD script called the EventManager AddListener method to add a listener
for the event. The problem is more complicated here because the invokers of the event (all three kinds of
teddy bear) are actually spawned as the game progresses. We need the HUD script to listen for the events
that are invoked by the newly-spawned teddy bears as well as the ones that are already in the scene (if
there are any) when the HUD script calls the EventManager AddListener method. So there are actually
two things the EventManager class needs to do: when the HUD script calls the AddListener method is
needs to add the provided delegate from the HUD as a listener to each of the teddy bears that are already
in the scene, and when a new teddy bear is spawned it needs to add the HUD delegate as a listener for that
teddy bear as well.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// Manages connections between event listeners and event invokers
/// </summary>
public static class EventManager
{
#region Fields
#endregion
Delegates and Event Handling 369
The EventManager class in the previous section didn't have any fields, but to support the functionality
we need this time we need to store lists of both the event invokers and the delegates (event handlers) that
listen for the event.
/// <summary>
/// Adds the given script as an invoker
/// </summary>
/// <param name="invoker">invoker</param>
public static void AddInvoker(TeddyBear invoker)
{
// add invoker to list and add all listeners to invoker
invokers.Add(invoker);
foreach (PointsAddedEventHandler listener in listeners)
{
invoker.AddListener(listener);
}
}
When a new teddy bear is spawned, it will call the EventManager AddInvoker method shown above.
That method adds the provided TeddyBear script to the list of invokers, then adds all the listeners in the
listeners list as listeners for the PointsAddedEvent that the provided script could invoke. We haven't
added an AddListener method to the TeddyBear script yet, so we'll get compilation errors at this point,
but we'll fix that soon.
/// <summary>
/// Adds the given event handler as a listener
/// </summary>
/// <param name="listener">listener</param>
public static void AddListener(PointsAddedEventHandler listener)
{
// add listener to list and to all invokers
listeners.Add(listener);
foreach (TeddyBear teddyBear in invokers)
{
teddyBear.AddListener(listener);
}
}
#endregion
}
The AddListener method works much like it did in the previous section, but this time we don't need to
tag any of the game objects that have scripts that will invoke the event because the EventManager
doesn't have to find them all when a listener is added. Instead, each of those game objects will add
themselves as an invoker when they're spawned. We also add the provided event handler to the
listeners list so the AddInvoker method can add the event handler as a listener to a newly-spawned
teddy bear.
Next, we add the required code to our TeddyBear script. This is very similar to the code we added to our
Fish script in the previous section, with a new field:
370 Chapter 17
// events fired by the class
protected PointsAddedEvent pointsAddedEvent = new PointsAddedEvent();
/// <summary>
/// Adds the given event handler as a listener
/// </summary>
/// <param name="listener">listener</param>
public void AddListener(PointsAddedEventHandler listener)
{
pointsAddedEvent.AddListener(listener);
}
Notice that we marked the pointsAddedEvent field as protected so that child classes can access that
field to call its Invoke method. Although we could add the field and the method to the
BurningTeddyBear, DisappearingTeddyBear, and ExplodingTeddyBear classes, we know from our
work with inheritance that there’s a better way. Because the TeddyBear class is an ancestor (in this case,
parent or grandparent) of all three of these classes, we add the field and method to the TeddyBear class
instead. That way, all three of these classes will inherit them and we only need to include that code in
one place.
We also remove the teddyBearDestruction field and the reference to that field in the Start method in
the TeddyBear script because we won't need those any more now that we're using an event system.
The Start method in the TeddyBear script is the appropriate place for the script to call the
EventManager AddInvoker method because the TeddyBear Start method executes whenever any of
the 3 types of teddy bear is spawned in the game. All we need to do is add the following code:
Remember, the this keyword gives us a “self reference”, so that's how we pass this particular
TeddyBear script as an argument into the method.
We're getting close, but we still need to change the BurningTeddyBear, DisappearingTeddyBear, and
ExplodingTeddyBear scripts to invoke the event instead of trying to call the TeddyBearDestruction
AddPoints method in their implementations of the ProcessMouseOver method. Replace that method
call with the following code in each of those scripts:
pointsAddedEvent.Invoke(pointValue);
It would also actually be reasonable to remove the line of code that invokes the event from the three
scripts and move it into the TeddyBear OnMouseOver method instead; we could have also done this in
the previous chapter instead of having each of the scripts call the TeddyBearDestruction AddPoints
method. We chose to implement the code this way in case we decide later to add a child class that
doesn't add points when the mouse is over the teddy bear, but either approach is fine.
Delegates and Event Handling 371
We can now remove the entire TeddyBearDestruction script from our game. The only functionality
that script contained was displaying the score, which the HUD now handles instead. Remove the Teddy
Bear Destruction component from the Main Camera and delete the script from the Scripts folder in the
Project window.
Run the game to see that it works as it did before, though this time it uses what we've learned about
events and event handlers.
The UnityEngine.Events namespace contains a set of classes called UnityEvent that we can use
instead of defining our own events. The simplest version of UnityEvent in that namespace is for an
event that assumes any listeners that have been added don't have any parameters; all those listeners are
called when the event is invoked. You should know that the Unity documentation calls the listeners
“callback” or “callback methods”; that's Unity-specific terminology that's interchangeable with the C#
“delegate”.
The other versions of UnityEvent in the UnityEngine.Events namespace are generics that assume the
callback methods have 1, 2, 3, or 4 parameters. We've seen and used generics before; remember, when
we use the List generic we specify the data type the list will hold between a < and a >. We'll see how to
use one of these UnityEvent classes in the next section.
What happens if you need to call methods with more than 4 parameters when your event is invoked?
The UnityEngine.Events namespace also has an abstract UnityEventBase class you can extend for
however many parameters you need. All the versions of the UnityEvent class provided in the
namespace use that approach.
The concepts we've discussed so far in this chapter are important for understanding how callbacks
(delegates) and events work even though we haven't used UnityEvent yet. With that said, when we're
writing Unity games we might as well use UnityEvent, so let's do that now.
Start by replacing all the code in the PointsAddedEvent script with the following:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
372 Chapter 17
/// <summary>
/// An event that indicates that points have been added
/// </summary>
public class PointsAddedEvent : UnityEvent<int>
{
}
This gives us a new class called PointsAddedEvent that's identical to the one parameter version of
UnityEvent where the single parameter for the event handlers listening for the event has to be an int.
We need to create a new class rather than using UnityEngine<int> directly because all the versions of
UnityEngine that have 1 or more parameters are defined as abstract classes. What’s an abstract class?
Recall that it’s a class that serves as a parent class for one or more child classes but we can’t actually
instantiate objects of the abstract class. The PointsAddedEvent class gives us a concrete version of that
abstract class; we can't create instances of abstract classes, so we need a concrete class to call the
constructor for the event in the TeddyBear script.
This change also gives us a number of compilation errors to guide us in our refactoring effort!
Let's start in the TeddyBear script. Start by adding a using directive for the UnityEngine.Events
namespace; we'll need that soon. Our compilation error is in the header for the AddListener method
because we don't have a PointsAddedEventHandler delegate any more.
What do we use for the delegate type for the parameter? The UnityEngine.Events namespace also
provides 0 to 4 parameter versions of a UnityAction class that serves as the delegate for our event
handlers. Changing our AddListener method header to
You might be wondering why we didn't just include the following in our PointsAddedEvent.cs file for
the delegate (similar to what we did for the original event):
/// <summary>
/// Delegate for handling the event
/// </summary>
public class PointsAddedEventHandler : UnityAction<int>
{
}
We can't do that because the UnityAction classes as marked as sealed, which means we can't define
child classes from them.
Our remaining compilation errors are all in the EventManager class. To get rid of them, replace all
occurrences of PointsAddedEventHandler with UnityAction<int>.
Now that everything compiles, go ahead and run the game. As you can see, everything works like it did
before. The difference is that we're using the built-in Unity classes for our delegate and event rather than
the more general C# delegate and event we defined in Section 17.4. Both approaches are valid, but when
developing Unity games we'll use built-in Unity classes as much as possible.
Delegates and Event Handling 373
In this section, we'll look at a single menu button and how it works, then we'll add a very simple menu
system to our fish game in the next section.
Start by creating a new 2D Unity project and renaming SampleScene to Scene0. Create a Sprites folder
and copy an image for a quit button into that folder.
Right click in the Hierarchy pane and select UI > Button - TextMeshPro. Notice that when you do this
you get an Event System and a Canvas with a Button element added to the scene. Rename the Button to
QuitButton and expand it by clicking the arrow to the left of the name. Right click the Text (TMP)
element and select delete (we don’t need to add text on top of our button image). Left click the
QuitButton and drag your sprite from the Sprites folder in the Project window onto the Source Image
value of the Image component in the Inspector. You may need to change the X and Y location of the
image so you can see it in the game window. If your sprite doesn't look like the original in the Game
pane, click the Set Native Size button in the Image component.
The QuitButton element should look like the figure below in the Inspector.
If you run the game and move the mouse over the button, you'll see that it darkens very slightly when
the mouse is over the button. That's because in the Button component the Normal Color value is pure
white (RGBA 255/255/255/255) and the Highlighted Color is not quite white (RGBA 245/245/245/255).
374 Chapter 17
Feel free to adjust the Highlighted Color value to make it more obvious when the button is highlighted if
you'd like.
You'll notice that clicking the button doesn't actually have any effect in our “game” (other than
darkening the button a little more). That's because the Button component has an OnClick UnityEvent to
define what to do when the button is clicked. See, here's why we needed to learn about UnityEvent
first! If you look at the Button component in the Inspector, you'll see an On Click () value at the bottom
that says List is Empty. This is the list of listeners that have been added to listen for the event, but since
we haven't added any listeners yet, nobody “hears” the event when it's invoked.
At this point, we need a method that we can add as a listener for when the quit button is clicked. Create a
new Scripts folder in the Project window, create a new C# script named QuitButtonListener, and change
the script to:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// Listens for the quit button OnClick event
/// </summary>
public class QuitButtonListener : MonoBehaviour
{
/// <summary>
/// Handles the on click event from the quit button
/// </summary>
public void HandleOnClickEvent()
{
Application.Quit();
}
}
We know we can use the HandleOnClickEvent method to listen for the UnityEvent invoked by the
quit button because the method matches the callback signature for a no-parameter UnityEvent. When
the HandleOnClickEvent method is called, the Unity game will close. The documentation for the
Application Quit method tells us that “Quit is ignored in the editor”, so we'll have to actually build
our game to check it out. We'll do that once we're done with our work in the editor.
Attach the new script to the Main Camera in the Hierarchy window.
Now we can add our HandleOnClickEvent method as a listener for the UnityEvent invoked by the quit
button. Select the QuitButton in the Hierarchy pane. In the Inspector, click the + at the bottom right of
the On Click () value in the Button component. Click the small circle to the right of the None (Object)
entry, click the Scene tab, select the Main Camera in the Select Object popup that appears, then close the
popup. Click the dropdown that says No Function and select QuitButtonListener > HandleOnClickEvent
(). There, we've added our HandleOnClickEvent method as a listener for the UnityEvent invoked by
the quit button.
As we said above, we need to actually build our game to make sure our quit button quits the game.
Select File > Build Settings ... from the main menu bar. The Scenes In Build window at the top is empty,
which means only the currently open scene will be included in the built game; although that's definitely
Delegates and Event Handling 375
what we want here, let's explicitly include our current scene in the list. Click the Add Open Scenes
button below the bottom right of the Scenes In Build window to add Scene0 to the build. Click the Build
And Run button at the bottom right of the popup. In the resulting file popup, create a new folder called
Build and double-click the new folder (this is personal preference, you can put your built game
anywhere you want). Click the Select Folder button and wait patiently while your game builds.
Once the Unity Player window opens, notice that the quit button highlights when you mouse over it, and
when you click it the game closes. Success!
In case you're wondering, you can also run your game by using your OS to navigate to your
MenuButtons.exe file and double-clicking that file. If you want to distribute your game to someone else,
you need to give them both the MenuButtons.exe file and the MenuButtons_Data folder. They'll need
both of those to run your game.
We'll also finally have a game with multiple scenes! Copy and paste the Unity project from Section 17.3
to create a new project. Open the project and rename Scene0 in the Scenes folder to Gameplay instead.
Next, we'll add a second scene for our main menu.
Right click the Scenes folder in the Project window and select Create > Scene. Rename the new scene to
MainMenu. Double click the MainMenu scene in the Project window to open that scene. Add sprites for
a play button and a quit button to the Sprites folder in the Project window. Add TextMeshPro Button
elements for a Play Button and a Quit Button to the scene like we did in the previous section. Make sure
you add the second button to the Canvas you get when you add the first button (right click on the canvas
before adding it instead of right clicking in the Hierarchy pane). Place the buttons in the scene in a
reasonable way by changing the X and Y locations of the images.
Now we'll implement the script to handle the On Click event for the buttons on the main menu. Create a
new script called MainMenu and double click the new script to open it in Visual Studio. Change the
script to:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
/// <summary>
/// Listens for the OnClick events for the main menu buttons
/// </summary>
public class MainMenu : MonoBehaviour
{
/// <summary>
/// Handles the on click event from the play button
/// </summary>
public void HandlePlayButtonOnClickEvent()
376 Chapter 17
{
SceneManager.LoadScene("Gameplay");
}
/// <summary>
/// Handles the on click event from the quit button
/// </summary>
public void HandleQuitButtonOnClickEvent()
{
Application.Quit();
}
}
Rename the Canvas to MainMenu and attach the MainMenu script to the MainMenu canvas.
The HandlePlayButtonOnClickEvent method loads the game scene, moving us to our gameplay. The
SceneManager class is in the UnityEngine.SceneManagement namespace, so we need to add a using
directive for the namespace. The HandleQuitButtonOnClickEvent is identical to the
HandleOnClickEvent method from the previous section (with a slightly different name, of course).
Select the Play Button in the Hierarchy window and add the HandlePlayButtonOnClickEvent method
as a listener for its On Click () event. Select the Quit Button in the Hierarchy window and add the
HandleQuitButtonOnClickEvent method as a listener for its On Click () event.
Now we need to set our build to include both scenes. Select File > Build Settings ... from the main menu
bar. Drag the MainMenu and Gameplay scenes from the Scenes folder in the Project window onto the
Scenes In Build window to add those scenes to the build. The order matters, because the scene at the top
of the list will be the scene the game starts in when you run it in the Unity Player. Click the Build And
Run button at the bottom right of the popup. In the resulting file popup, create a new folder called Build
and double-click the new folder. Click the Select Folder button.
Once the game starts, you can click the Quit button to quit the game or the Play button to play the game.
If you start playing the game, though, there’s no way to stop playing it (other than using Ctrl+Alt+Del)!
We have two reasonable choices here: we can build a pause menu where the player can either resume
the game or quit back to the main menu or we can make the game run in windowed mode rather than in
full screen mode. Let’s make the game run in windowed mode; we’ll implement a pause menu later in
the book.
Back in the Unity editor, select Edit > Project Settings … Select Player on the left and select the Settings
for PC, Mac, & Linux Standalone tab on the right. Expand the Resolution and Presentation section (if
necessary) by clicking the arrow to the left of the section title. Change the Fullscreen Mode setting under
Resolution by clicking the dropdown and changing the setting from Fullscreen Window to Windowed.
Set the Default Screen Width and Default Screen Height values and click the x at the top right of the
Project Settings window. Select File > Build And Run to build and run your game. Now you can just
close the window when you want to quit the game. Sweet!
Chapter 18. Exceptions, Equality, and Hash Codes
At this point, we’ve covered almost all the basic C# and Unity ideas needed for an introduction to those
topics. In this chapter, we’ll cover exceptions as well as equality and hash codes for reference types.
18.1. Exceptions
Most of you have probably already discovered that there are times when a program you wrote will
“blow up” – if you try to call a method on an object you haven’t yet created, for example. The program
doesn't really blow up, of course; instead, C# throws an exception, which then terminates the program.
To keep our program from terminating because of the exception, we can handle (or catch) that exception
by using an exception handler. We can also explicitly throw exceptions in our code as necessary. We’ll
cover all those ideas in this section.
Although our Unity games don't typically terminate when exceptions are thrown, we can see those
exceptions getting thrown in the Console window as our game runs. Because this is an indication of a
problem in our game, those exceptions need to be fixed or handled as well.
The key idea behind all this is that our programs throw exceptions when something unusual happens that
has significant negative effects on the program’s ability to continue executing correctly. We’ll see
specific examples below, but remember that exceptions aren’t about “business as usual,” they’re for
taking care of unusual circumstances.
Let’s look at a simple example. Say we have the following code to divide two numbers provided by the
user:
As long as the user enters numbers at the above prompts, the code works fine (although we get strange
results if the denominator is 0 or both the numerator and denominator are 0).
What happens if the user doesn’t enter a number, though? Let’s say we run the code above and enter the
string Bob for the numerator. The code (specifically, the Parse method) will throw an exception –
specifically, a FormatException – and terminate the program. How do we make sure the user can’t
blow up our program by doing this? By including an exception handler. Exception handlers include a try
block and one or more catch blocks, though we’ll also use a finally block when we start doing file IO in
the next chapter.
Because the user can crash our program by causing the FormatException to be thrown, we should
probably do something about that!
378 Chapter 18
try
{
executable statements
}
catch (ExceptionClass exceptionObject)
{
executable statements
}
catch (ExceptionClass exceptionObject)
{
executable statements
}
. . .
finally
{
executable statements
}
We first put the executable code that could throw the exception inside a try block; in other words,
between the { and the } after the try. We then include a catch block for each exception we need to
handle. We include the class of the exception we're catching (FormatException is one such class) and
the exception object that's created when the exception is thrown (you can name that object anything you
want). Then, between the curly braces of the catch block, you include whatever code you want executed
if that exception is thrown. For our example, we end up with
try
{
// get numerator and denominator
Console.Write("Enter numerator: ");
float numerator = float.Parse(Console.ReadLine());
Console.Write("Enter denominator: ");
float denominator = float.Parse(Console.ReadLine());
If either of the calls to the Parse method throws a FormatException, the program immediately goes to
the catch block to execute the code in that block. If all of the code in the try block executes without
throwing an exception, the program simply skips all the catch blocks after the try block code is done and
proceeds to the code following the exception handler.
That solves our immediate problem (catching the thrown exception), but if this exception does get
thrown, we probably want to give the user an opportunity to retry their input. If we realize that the
FormatException is only thrown if there's some error in the user input, our solution becomes clearer –
we should let the user keep trying until they provide valid input. We’ll do that soon.
If you have code within a try block that could raise multiple exceptions, you would still use a single try
block; you simply include multiple catch blocks, one for each exception you want to catch. You should
know, however, that only one catch block will be executed for any given exception; we’ll look at the
implications of that rule more closely a little later.
In some cases, we also include a finally block in our exception handler. The code in the finally block
will get executed whether or not the code in the try block throws an exception. If the code in the try
block doesn’t throw an exception, the program proceeds to the finally block after the try block code is
done. If the code in the try block throws an exception and none of the catch blocks handle that
exception, the program immediately proceeds to the finally block code. If the code in the try block
throws an exception and one of the catch blocks handles that exception, the code in that catch block is
executed, then the program proceeds to the finally block after the catch block code is done.
When is a finally block useful? When we have some code that we know we want to execute whether or
not an exception is thrown in the try block. We’ll see a great example of this in the next chapter when
we’re doing file IO. We’ll discover that we should always close files that we’re reading from or writing
to, whether or not we experience a read or write exception. We’ll therefore make sure we close files in
the finally block of our exception handlers.
Recall that one of the problems we solved with a while loop was validating that a user entered a valid (in
range) GPA value using the following code:
We already know that the user could crash the above code by entering a string rather than a number, and
we also know we can solve that problem using an exception handler. This is a somewhat harder problem
to solve than the previous one, though.
380 Chapter 18
One of the things we need to decide is what to include in the try block. We know we need to include
both calls to the Parse method, and though we could do this with two separate exception handlers, it
makes more sense to include the entire chunk of code above in the try block. Our first cut at our solution
could therefore be something like:
try
{
// prompt for and get GPA
Console.Write("Enter a GPA (0.0-4.0): ");
float gpa = float.Parse(Console.ReadLine());
This solves our immediate problem – the user can’t crash the code by entering a string – but if they enter
a string the catch block executes and they don’t get a chance to try another input. We need the exception
handler to be contained inside a loop to make this happen.
We definitely need a while loop, but what should we use for our loop condition? Conceptually, we need
to loop while the user hasn’t provided a valid input, so let’s use a Boolean flag to tell whether the user
has provided valid input:
valid = true;
}
catch (FormatException fe)
{
Console.WriteLine("Invalid entry! GPA must be a number.");
Console.WriteLine();
}
}
We start by setting the flag to false to force us into the loop body the first time. The only way we get to
the line of code that changes the flag to true (just above the catch block) is when the user enters a
number that’s between 0.0 and 4.0.
There are a couple of things we can clean up in the above code. For example, the code that prompts for
and reads in the GPA is included twice. We originally did that to give us a self-contained while loop to
do the range checking, but now that all the code is contained in another while loop we no longer need to
have that duplication.
It’s also a little confusing understanding when the flag gets set to true. We can certainly figure that out,
but we can make that clearer as well. The following code provides our final solution:
That’s one way we can use exception handlers to provide very robust input validation.
382 Chapter 18
We said above that we can have multiple catch blocks for our exceptions, so let’s look at an example of
that here. It turns out that the float Parse method can actually throw three different exceptions:
ArgumentNullException (if the string we’re parsing is null), FormatException (which we’ve already
discussed), and OverflowException (if the number is too small or too large to fit into a float).
In our example here, it’s impossible for a null string to be parsed, because if the user just presses the
Enter key we read in an empty string (a string with no characters in it) rather than a null string. Let’s say
we want to print a different message for each of the other two exceptions above; here’s our new code:
Catch blocks work much like the if and else if blocks in an if statement, where the block for the first
Boolean expression that evaluates to true is executed then the code leaves the if statement. For catch
blocks, the first block that matches the exception that was thrown is the block that’s executed, then the
code proceeds to the finally block (if there is one) or out of the exception handler.
You probably noticed that the error message in the OverflowException catch block is the same as the
error message if the user enters a valid number but it’s out of range. We know that if the user enters a
number that’s too small or too large to fit in a float that the number is also not between 0.0 and 4.0.
Exceptions, Equality, and Hash Codes 383
That means we can just give the user an error message that makes sense to them without requiring that
they understand the valid ranges for C# data types.
Throwing Exceptions
Up to this point, we’ve talked about how we can go about handling exceptions thrown by our programs.
We now discuss how we can explicitly throw exceptions ourselves (which we can later catch elsewhere
in our code if we choose to).
Let’s say we wanted to have the user enter a date in the format mm/dd/yyyy. There’s actually a lot of
work we’d need to go through to check that format, but for this example, we’ll just make sure the string
the user enters contains two / characters. While there are really clean ways to do this (using something
called regular expressions, which are beyond the scope of this book), we’ll use the approach shown
below:
// count slashes
int slashCount = 0;
string tempString = date;
while (tempString.IndexOf('/') != -1)
{
slashCount++;
tempString = tempString.Substring(tempString.IndexOf('/') + 1);
}
When this code finishes executing, the slashCount variable holds the number of slashes contained in
the input string. If that number is 2, the string contains the correct number of slashes, otherwise the input
format is incorrect.
Here’s where we get to learn how to throw an exception. Check out the following code:
The condition simply checks the value of slashCount; the body of the if statement is the new stuff. To
throw an exception, we start with the throw keyword. We then need to create a new exception object
using a constructor for the exception class we want to use. The ApplicationException class is
designed to be used “when a non-fatal application error occurs”, so that’s the exception we throw here.
The ApplicationException constructor is overloaded, and we’re using the overload that lets us
provide an error message as an argument. If we were to run the code above and force the exception to be
thrown (we won’t provide any slashes in our input), we’d get the output shown in Figure 18.1. Notice
that the error message provided with the exception is the error message we passed as an argument to the
constructor.
384 Chapter 18
Of course, an even better solution would be to enclose the above code in an input validation loop like we
did above for GPA so the user can try again if they get the date format incorrect. We kept our example
here simple to focus on throwing the exception; see if you can figure out how to add the input validation
loop as well.
User-Defined Exceptions
C# provides a class hierarchy with a number of useful pre-defined exceptions in the hierarchy. The
Exception class is the base (root) class for all exceptions in that hierarchy.
If none of the pre-defined exceptions are exactly what you want, you can define a new exception class
yourself. The only constraint is that you should make the Exception class the parent class for your new
class:
using System;
namespace CustomException
{
/// <summary>
/// A custom exception class
/// </summary>
public class MyException : Exception
{
}
}
Because your new exception class inherits the fields and methods from the Exception class, you can
throw and handle your new exception just as we’ve been doing all along.
18.2. Equality
Up to this point in the book, when we wanted to compare two variables we used == for value types or for
strings (which are, as we know, a reference type). We haven’t, however, compared any of the objects
for the classes we’ve written to see if they’re equal. So what is equality anyway? We know what it
Exceptions, Equality, and Hash Codes 385
means for two integers to be equal, but what does it mean for two objects of a particular class to be
equal?
For reference types, the default Equals method (we can also use == for the same behavior) that’s
inherited by any class we define simply checks for reference equality. If two variables refer to the same
object in memory, the Equals method returns true, otherwise it returns false. In many cases what we
really want is value equality, not reference equality. For value equality, what we really care about is the
fact that the important characteristics of two objects are the same even if they're two distinct objects in
memory.
Let's make this more concrete. Let’s assume we’ve developed a Mackerel class that has fields and
properties for length, weight, and the damage the mackerel inflicts when you whack something with it
(we know, we know). What would we mean if we said that two Mackerel objects were equal? We'd
probably mean that their length, weight, and damage are the same. We already learned about overriding
methods when we talked about inheritance, and that’s exactly what we need to do here. Specifically,
here's how we would override the Equals method:
/// <summary>
/// Standard equals comparison
/// </summary>
/// <param name="obj">the object to compare to</param>
/// <returns>true if the objects are equal, false otherwise</returns>
public override bool Equals(object obj)
{
// comparison to a null object returns false
if (obj == null)
{
return false;
}
// compare properties
return (Length == objMackerel.Length) &&
(Weight == objMackerel.Weight) &&
(Damage == objMackerel.Damage);
}
The last statement in the method does the actual comparison we're trying to capture. The two if
statements are also necessary, however. The standard return value for calling Equals with a null
parameter is false, because the object we're using to call the Equals method can't possibly be null; if
it were, we'd get a NullReferenceException for trying to call a method on a null object. The second
if statement makes sure we're actually comparing to a Mackerel; if we aren't, the objects can't be equal
so we return false.
There is another important concept in the above code as well. We’ve included the line of code that says
386 Chapter 18
Mackerel objMackerel = obj as Mackerel;
This line tries to cast obj as a Mackerel object. If obj isn’t actually a Mackerel object, that type cast
returns null; that’s why we check for null in the next line to determine whether we’re comparing to a
Mackerel object. If obj is a Mackerel object, the objMackerel variable gets a reference to obj as a
Mackerel object.
Why do we have to go through all this? Because the standard Equals method requires that we pass in an
object argument, not a Mackerel argument. That means that somewhere in the Equals method we
need to start treating that parameter as a Mackerel so we can access its properties to do the comparison.
The type casting code above is what does that for us.
Microsoft's documentation recommends that if we override the Equals method that we also provide a
type-specific Equals method that (in this case) has a Mackerel parameter rather than an object
parameter; this apparently enhances performance. That method is provided below.
/// <summary>
/// Type-specific equals comparison
/// </summary>
/// <param name="mackerel">the mackerel to compare to</param>
/// <returns>true if the mackerels are equal, false otherwise</returns>
public bool Equals(Mackerel mackerel)
{
// comparison to a null mackerel returns false
if (mackerel == null)
{
return false;
}
// compare properties
return (Length == mackerel.Length) &&
(Weight == mackerel.Weight) &&
(Damage == mackerel.Damage);
}
Notice that in this case we don’t include override in the method signature. That’s because this is an
overload, not an override. Remember, overloaded methods have the same method name but different
parameters.
At this point, we could declare victory and move on, but you should have noticed that our two Equals
methods have a lot of duplicated code. Now that we've written our type-specific Equals method we can
just use that method to make a huge simplification to our first Equals method. Here's the new code:
/// <summary>
/// Standard equals comparison
/// </summary>
/// <param name="obj">the object to compare to</param>
/// <returns>true if the objects are equal, false otherwise</returns>
public override bool Equals(object obj)
{
return Equals(obj as Mackerel);
}
Exceptions, Equality, and Hash Codes 387
If obj is null or isn’t actually a Mackerel object, the obj as Mackerel type cast returns null (which
we then pass as an argument to the type-specific Equals method), so the if statement at the beginning of
the type-specific Equals method handles the two special cases for the equality check. This refactoring
removes our duplicated code so that each chunk of code only appears in one place in our game,
something we keep saying is an important characteristic of good software.
We can now use our Equals methods to compare any object to a Mackerel object.
What’s that all about? It turns out that there’s a very useful thing called a hash code. A hash code
assigns a number to an entity (an object, in our case). In a perfect world, every unique object gets a
unique hash code, but that doesn’t always happen in practice. Why do we care? Because hash codes can
be used to efficiently store objects in a variety of ways. If you continue your studies in programming,
you’ll definitely be learning about a variety of data structures, including hash tables. Not surprisingly,
hash tables use the hash codes we’re examining here.
So why does the compiler give us the above warning? Because if the Equals method returns true, the
GetHashCode method (which is inherited from the Object class) better return the same number for both
objects! The default implementation of GetHashCode can’t provide this guarantee when we override the
Equals method, so we need to override the GetHashCode method as well.
To write our GetHashCode method, we basically need to use the data inside the object to calculate the
hash code for the object. For our Mackerel class, we have length, damage, and weight fields we can
use. One reasonable implementation of the method is as follows:
/// <summary>
/// Calculates a hash code for the mackerel
/// </summary>
/// <returns>the hash code</returns>
public override int GetHashCode()
{
const int HashMultiplier = 31;
int hash = length;
hash = hash * HashMultiplier + weight;
hash = hash * HashMultiplier + damage;
return hash;
}
This is a pretty good hash function that will be fast and has a good chance of generating unique hash
values for Mackerel objects that have different field values. Using a prime number for the
HashMultiplier helps ensure (but doesn’t guarantee) unique hash values.
388 Chapter 18
One final comment. There are many, many possible hash functions, and coming up with good hash
functions that are quick and have a good chance of generating unique hash values for each object has
been an area of research for some time.
Chapter 19. File IO
As part of our motivation for using arrays, we talked about storing 12,000 GPAs for the students at our
university. We showed how we can store those GPAs in an array, and we showed how to read into
elements of an array, but we glossed over a couple of important considerations. First of all, who in their
right mind is going to sit at the keyboard and type in 12,000 GPAs? Even more importantly, what
happens to those GPAs after the program terminates? They disappear! So every time we want to do
something with those GPAs, we have to enter all 12,000 again! There just has to be a way to store these
GPAs for later use, and there is – files.
There are lots of types of files; one common type of file is called a text file. A text file simply consists of
a set of characters. Essentially, any character we can enter from the keyboard can be read from a file
instead, and any character we could print to the screen can also be output to a file. Thus, files give us a
great way to store large amounts of data for input to computer programs, output from computer
programs, or both.
We can also save our objects into binary files. This is a very useful capability if we want those objects to
persist across program executions. Just like variables that are value types, objects disappear when the
program terminates. If we write them to a file, though, we can retrieve them later.
Files are incredibly useful in the game domain, of course. We regularly use files to store configuration
information for the games we write: how many points we want per game, how the game tutorial
progresses from one activity to the next, the stats for each NPC, and so on. Those configuration files are
usually read-only, since they represent core information about how the game works. We want to have
files that we can also write to, though: saved game files, saved profile information for specific players,
etc. You should be able to see why being able to do file IO will help us build better games.
In this chapter, we’ll start by looking at how we do file IO in any C# application. We’ll then move on to
the best ways to interact with files in Unity.
19.1. Streams
To understand how file input and output work in C#, we need to understand streams. In general, streams
are what you actually do input from and output to in C#. You've actually already been using streams in
your console applications because the Console class actually gave us access to an input stream (when
we used the Read or ReadLine methods), an output stream (when we used the Write or WriteLine
methods), and an error stream (which we didn’t use at all). In other words, we’ve already been using
streams, we just didn’t realize it!
Think of a stream as a sequence of data that flows into your program from some input device (the
keyboard or an input file, say) or out of your program to some output device (e.g., the screen or an
output file). In other words, you have a stream of data that you read from or write to, with some input or
output device on the other end of the stream.
Our goal for file IO is to actually use streams to and from files rather than using the standard input
stream (from the keyboard) and the standard output stream (to the console window). To do that, we’ll
use the File and FileStream classes from the System.IO namespace. The FileStream object is the
390 Chapter 19
stream we actually interact with, while the File class gives us useful utility methods for creating,
opening and closing file streams.
Let’s start by creating a file that we can send text output to. One way we can easily create a
StreamWriter object is to use the File class as shown below.
Just as you need to know the name of the book you're going to write, you need to know the name of the
file you want to write to disk before you can create the file object. C# assumes that the file you're trying
to create should be in the same folder as the program you're running, but you can also use the fully-
qualified path name for the output file (e.g., @"C:\chamillard\book\Chapter19\speech.txt" or
"C:\\chamillard\\book\\Chapter19\\speech.txt") if you'd like.
It turns out, however, that if you read the documentation for the File CreateText method, you’ll
discover that the method can throw one of six different exceptions! What should we do if that happens?
One alternative would be to just let the program crash, but that’s almost never a good idea. Instead, we’ll
put the call to the CreateText method inside a try block and include a catch block for any exceptions.
We’ll definitely do that, but we’ll defer discussing the details until we’re all done with our file output.
Now that we have a StreamWriter object, we can write to the file using the WriteLine method. Let’s
write a few lines from a speech:
As you can see, sending output to the file is just as easy as sending output to the console window using
the Console class.
Once we’re done sending output to the file, the last thing we need to do is close the file. Because we do
that in the finally block of our exception handling code, now is a good time to look at the complete
chunk of code:
Notice that we always close the stream writer (which also closes the underlying output file the stream
writer is using) in the finally block. Why do we do it there? Because we know that the code in the finally
block always executes whether or not any exceptions were thrown in the try block and handled (or not)
in a catch block. By doing it this way, we make sure that the file associated with the stream writer is
always closed when we leave this chunk of code.
You should note that we needed to move the declaration of our StreamWriter variable outside the try
block to make it visible in the finally block, and we needed to initialize it to null so the compiler didn’t
think we were trying to use an uninitialized variable.
So how can we tell that the code above actually writes to the file properly? Perhaps the easiest way
would be to just open the file using Notepad++ and making sure it contained the speech lines we wrote.
Another way, though, would be to open the file in our code, read it back in, and confirm that it’s correct
by echoing each line to the console window. Because you need to know how to do file input as well,
we’ll take that approach.
392 Chapter 19
Before we do that, though, you should know that the File CreateText method creates a new file; if a
file with the given name already exists, the contents of that file are overwritten. That may be exactly
what we want, but we might want to do something different instead. For example, we may want to
append text to an existing file. If you need to do something like that, it’s better to open the file using the
File Open method instead. The Open method has two parameters: a path to the file (which can just be
the file name) and a FileMode value. FileMode is an enumeration that has six different values you can
use to specify how the operating system should open the file.
Because the Open method returns a FileStream rather than a StreamWriter, you’d need to use the
following code to replace what we have above:
Using the Open method is obviously a little more complicated than using the CreateText method, but if
you need finer-grained control of how the file is opened it’s definitely the way to go.
Now let’s move on to reading from a text file. The ideas are very similar to those we used when we
wrote to a file; we’re just using a file stream for input rather than output. Our first step is to open the text
file for reading as shown below.
The OpenText method can also throw a number of exceptions, so we’ll put the call to the OpenText
method inside a try block and include a catch block for any exceptions.
Now that we have a StreamReader object, we can read from the file using the ReadLine method.
Reading from a file is a little trickier than writing to a file, though, because we need a way to know
when we reach the end of the file. The good news is that the ReadLine method returns null when it
reaches the end of the file stream. That means we can use a while loop to read each line from the file
until ReadLine returns null. Here’s the complete block of file input code:
We of course want to close the file once we’re done reading from it; we do that in the finally block just
as we did for file output to make sure the file gets closed whether or not an exception is thrown during
our file input processing.
It’s important to note that to properly read from an input file you need to know the format and the
contents of the file. For example, consider a file that consists of a line containing a person’s name
followed by a line containing their birthday (and so on for multiple people). Any program that reads
from that file needs to read in the name followed by the birthday; trying to read the birthday first,
followed by the name, would lead to some potentially interesting but undoubtedly incorrect behavior!
You need to make sure you know how the information is arranged in the input file to read from it
properly.
Now, you may be thinking that by reading an entire line at a time from the file that we've made it
difficult to have multiple values on a line. For example, what if we wanted to store X and Y coordinates
for a set of points in the file? It would certainly be more intuitive to have both coordinates for each point
on a single line; that way, each line in the file would represent information about a single point.
We can actually do this fairly easily using the String Split method. Before we can do that, though, we
need to know the format of each line in the file. Let’s assume that each line is formatted with the x and y
values separated by a comma. Files that are saved as CSV (comma-separated value) files have precisely
this format, so this is a pretty common and intuitive file format. Let’s look at a snippet of code to get us
started, assuming input is a StreamReader that’s already been opened:
The second line of code is the new stuff, of course. The Split method breaks the line string into an
array of strings; each element in the array is one of the strings that’s separated by commas in the line
string. For example, if line has the value 100,200 then tokens[0] is 100 and tokens[1] is 200.
394 Chapter 19
Although we now have the x and y coordinates extracted from the line we read from the file, we
probably want them as integers instead of the strings that are stored in the tokens array. That conversion
is easy to do using the int Parse method we learned about in Chapter 5:
int x = int.Parse(tokens[0]);
int y = int.Parse(tokens[1]);
That’s all you need to know to write to or read from text files. Let’s look at binary files next.
There’s another reason we don’t want to store game information in a straight text file. Imagine that you
use such a file to save player profile information; it would be very easy for the player to open up their
profile and modify their information to get an advantage in the game. Similarly, players could open up a
text file containing game configuration information to reduce the NPC stats to make the game easier.
Although doing either of those things would almost definitely adversely affect the player’s experience
with the game, there are unfortunately many players who will hack a game to gain an advantage. Storing
this information more securely than in straight text is therefore generally a good idea for the games we
build.
When we store and retrieve objects from files, we’re actually interacting with binary files instead of text
files. Many of the ideas are similar, but we’ll use some new classes and features to do the binary file IO.
The first new concept we need concerns our ability to serialize an object. Because binary files will store
a sequence of bytes to represent each object that’s saved in the file, we need a way to convert the objects
we want to save into a stream of bytes. The good news is that we don’t have to worry about the details
of how the conversion to bytes actually works, all we need to do is indicate that we want objects of a
particular class to be serializable. Let’s actually work through a complete example to show how all this
works.
Let’s create a Deck of Card objects like we did in Chapter 12. We’ll actually save the Deck object to a
binary file, then read it back in and print its contents to the console window to show that everything
worked properly.
The first thing we need to do is indicate that Deck objects are serializable. This is actually really easy –
check it out:
[Serializable]
public class Deck
We’re using something called an attribute to mark the Deck class as Serializable (SerializeField,
which we've been using since Chapter 4, is also an attribute). Because we know that a Deck holds Card
objects, we also need to mark the Card class as Serializable:
File IO 395
[Serializable]
public class Card
You might be wondering why we didn’t need to also mark the Rank and Suit enumerations as
Serializable. That’s because C# enumerations have integers as their underlying data type, and
integers are automatically serializable in C#.
Let’s look at the code required to create a Deck object and write it to a file named deck.dat:
This time, we’re using a FileStream object so we can write the serialized Deck object to the file stream.
We’re using the File Create method instead of the CreateText method because we need a
FileStream object, not a StreamWriter object. The biggest difference from when we wrote to a text
file are the next two lines:
The BinaryFormatter class lets us serialize instances of classes that are marked as serializable to a file
stream. So the first line of code above creates a new BinaryFormatter object for us and the second line
of code above serializes our Deck object to the file stream attached to the deck.dat file.
Note that the file name we’re using to store the object information has a .dat extension rather than a .txt
extension. We do that to show that this isn’t a text file; instead, it’s a file of bytes. While you could open
up the file with a text editor, most of what you’d see would be meaningless to you.
The rest of the code is just as we used when outputting to a text file; we catch any exceptions that are
thrown and make sure we always close the output file. Let’s read the deck back in from the binary file
and print it to the console window:
We’re using a FileStream object so we can read the deserialized Deck object from the file stream.
We’re using the File OpenRead method instead of the OpenText method because we need a
FileStream object, not a StreamReader object. The biggest difference from when we read from a text
file are the next two lines:
The BinaryFormatter class lets us deserialize instances of classes from a file stream; deserializing
simply means converting from a stream of bytes into the actual object. So the first line of code above
creates a new BinaryFormatter object for us and the second line of code above deserializes our Deck
object from the file stream attached to the deck.dat file. Because the Deserialize method actually
returns an Object, we need to cast the returned value into a Deck object.
Once we have the Deck object, we tell it to print itself so we can verify that it was saved to the file and
retrieved from the file properly. The rest of the code is just as we used when inputting from a text file;
we catch any exceptions that are thrown and make sure we always close the input file.
That’s all you need to know to write to or read from binary files, so now we can move on to Unity-
specific file IO.
Let's work through an example; we’ll start with the fish game we developed in Section 17.3, though
we've modified that game to use UnityEvent and to display a countdown timer. In our example here,
we’ll enhance the game to use a high score table of the top 10 scores players achieve in 30 seconds. That
File IO 397
means we’ve changed the game a bit to include a countdown timer and to end the game after 30 seconds,
at which point we'll display the high score table. The table will be sorted, of course, and will persist
between executions of the game using the PlayerPrefs class.
Although our focus in this section is on the high score table storage and retrieval, there are a number of
interesting changes related to game play, so we'll discuss those first.
We added a TimerFinishedEvent to the project and added functionality to the Timer class so that it
invokes this event when the timer finishes (although we left the Finished property as well). Using an
event when the timer finishes matches the way people use timers in the real world much more
accurately. Up to this point, a game object with a timer looks at the timer (accessing its Finished
property) on every frame to see if it's done yet; wouldn't that be a horrible way to use a timer in the real
world? In the real world, we set a timer, start it running, then forget about it until it tells us its finished,
usually with a sound, a vibration, or both. That's how our new Timer works, invoking the
TimerFinishedEvent when the timer finishes so whoever is listening can handle the event
appropriately. In our project here, we've refactored the TeddyBearSpawner to handle the
TimerFinishedEvent instead of checking the timer's Finished property every Update, and we've
added a FishGame script that also listens for that event (from a game timer, not a spawn timer) and
handles it as well.
Although we already have an EventManager class to connect event invokers and listeners for those
events, we don't actually need to use that class for the TimerFinishedEvent (in fact, it would be harder
to do so). To understand why we don't use the EventManager class here, remember our motivation for
adding the EventManager class in the first place – we did it so objects in the game don't have to know
about other objects in the game. In both the TeddyBearSpawner and FishGame (which runs the game
timer and retrieves and displays the high score table when the timer finishes by listening for and
handling the TimerFinishedEvent for the game timer) scripts, though, the timers are fields in those
scripts, so the scripts already know about the timers they contain. It's therefore easier and appropriate in
an object-oriented design to just have those scripts add their listeners to their Timer fields directly.
Because we've added a game timer to the game, it makes sense to show that timer to the player so they
know how much time they have left. Displaying the timer is a job most appropriately assigned to the
HUD game object, but how does the HUD script attached to that game object know the current value of
the timer and when it changes (from 30 to 29, for example)? Hopefully by this point you already know
the answer! We added a TimerChangedEvent to the project and have the Timer invoke that event when
the value of the timer changes. The HUD script listens for that event and changes the timer display when
the event is invoked just as it does for the score display and the PointsAddedEvent.
In this case, it makes sense to use the EventManager because the game timer is added as a component in
the FishGame script, which is attached to the Main Camera, and the HUD game object shouldn't have to
know about the Main Camera. This actually leads us to an interesting problem in the EventManager
class.
Recall that in the EventManager class, we already have an AddListener method with the method
header
to let scripts add listeners for the PointsAddedEvent. We would need exactly the same method header
for a script to add a listener for the TimerChangedEvent (which passes the new timer value as an int
when it's invoked), and we're not allowed to have two methods with identical method headers in the
same class. We changed the method names to AddPointsAddedEventListener and
AddTimerChangedEventListener to solve this problem. Here's the revised EventManager class:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
/// <summary>
/// Manages connections between event listeners and event invokers
/// </summary>
public static class EventManager
{
/// <summary>
/// Adds the given event handler as a listener
/// Game objects tagged as Fish invoke PointsAddedEvent
/// </summary>
/// <param name="listener">listener</param>
public static void AddPointsAddedEventListener(
UnityAction<int> listener)
{
// add listener to all fish
GameObject[] fish = GameObject.FindGameObjectsWithTag("Fish");
foreach (GameObject currentFish in fish)
{
Fish script = currentFish.GetComponent<Fish>();
script.AddListener(listener);
}
}
/// <summary>
/// Adds the given event handler as a listener
/// Game objects tagged as MainCamera invoke TimerChangedEvent
/// </summary>
/// <param name="listener">listener</param>
public static void AddTimerChangedEventListener(
UnityAction<int> listener)
{
// add listener to main camera
GameObject mainCamera = GameObject.FindWithTag(
"MainCamera");
FishGame script = mainCamera.GetComponent<FishGame>();
script.AddTimerChangedEventListener(listener);
}
}
And here's our new FishGame class (without the high score table processing code):
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
File IO 399
using UnityEngine.Events;
/// <summary>
/// Game manager
/// </summary>
public class FishGame : MonoBehaviour
{
// game timer support
public const int GameSeconds = 30;
Timer gameTimer;
/// <summary>
/// Awake is called before Start
/// </summary>
void Awake()
{
// create and start game timer
gameTimer = gameObject.AddComponent<Timer>();
gameTimer.AddTimerFinishedEventListener(
HandleGameTimerFinishedEvent);
gameTimer.Duration = GameSeconds;
gameTimer.Run();
}
/// <summary>
/// Adds the given event handler as a listener
/// </summary>
/// <param name="listener">listener</param>
public void AddTimerChangedEventListener(UnityAction<int> listener)
{
gameTimer.AddTimerChangedEventListener(listener);
}
/// <summary>
/// Handles the game timer finished event
/// </summary>
void HandleGameTimerFinishedEvent()
{
// display high score table
}
}
The gameTimer field invokes two events: the TimerFinishedEvent and the TimerChangedEvent. The
FishGame script listens for the TimerFinishedEvent so it knows the game is over and it should display
the high score table. The FishGame script doesn't actually care about the TimerChangedEvent, but the
HUD script does so it can update the countdown timer it displays. The AddTimerChangedEventListener
method above provides a way for the HUD script to actually add (through the EventManager) a listener
for that event, which is invoked by the gameTimer internal to the FishGame script.
To make this work properly, we need to make sure that the gameTimer field isn't null when the
EventManager class calls the AddTimerChangedEventListener method. How do we do that?
Our implementation of the HUD script calls a new AddTimerChangedEventListener method in the
EventManager class from within its Start method; the Start method is where we've done all our
400 Chapter 19
initialization work up to this point in the book. The Start method is called just before any of the Update
method(s) are called for the script, but we don't know in what order the Start methods will be called for
all our game objects when a scene starts. If we added the timer to our FishGame script in the Start
method and the Start method for the HUD script attached to the HUD game object gets called before the
Start method for the FishGame script attached to the Main Camera, the gameTimer field will be null
when the HUD script tries to add a listener for the TimerChangedEvent.
If we need to do initialization before the Start methods start getting called in a scene, we can use the
Awake method. The documentation for the MonoBehaviour Awake method states that "Awake is always
called before any Start functions. This allows you to order initialization of scripts." This is exactly what
we need here, so we initialize and start the gameTimer in the FishGame Awake method so the HUD script
can safely add its listener to that timer in the HUD Start method.
Let's start our work to handle displaying the high scores table. We start by creating another canvas we'll
use to display the high score table when the timer has expired. We're using an additional canvas because
we want it to overlay the gameplay in the scene when it's displayed, and we want to control when the
canvas is visible so that it's only displayed when the game ends.
On the top menu bar, select GameObject > UI > Canvas. Rename the new Canvas in the Hierarchy
window to HighScoreCanvas. Right click HighScoreCanvas and select UI > Image. Rename the image
to Background and change its Width and Height to 400. We want the background to be a partially-
transparent shade of gray, so click Color in the Image (Script) component of the Inspector and change
the color to gray and the A (alpha) value to 127.
Next, we should add a label at the top of the canvas. Right click HighScoreCanvas in the Hierarchy
window and select UI > Text - TextMeshPro. Rename the new Text object Label in the Hierarchy
window. In the Scene view, drag the Label to near the top of the canvas. Next, click the center
Alignment button in the Paragraph section of the TextMeshPro - Text (UI) component to center the label
on the canvas. Change the Text value from New Text to High Scores. Finally, change the Font Style to
Bold and the Font Size to 24.
Let's add one more text element to the canvas. We'll simply have the player press the Escape key to
close the game after they've seen the high score table, so we'll add instructions to do that at the bottom of
the canvas. Go ahead and do that now. At this point, the Game view should look like the figure below.
File IO 401
Okay, let's make it so the game actually exits when the player presses the Escape key. Select Edit >
Project Settings > Input from the main menu bar. Add an axis by adding 1 to the number of Axes listed
in the Size value and click the bottom axis (which is the new one). Change the name of the axis to Exit,
set the positive button to escape, and make sure the Type is set to Key or Mouse Button.
Next, we need to add a script that exits the game when the player presses the Escape key. Right click the
scripts folder in the Project window and select Create > C# Script. Rename the script to ExitGame and
double-click it to open it in Visual Studio. Here's the completed script:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// Exits the game
/// </summary>
public class ExitGame : MonoBehaviour
{
/// <summary>
/// Update is called once per frame
/// </summary>
void Update()
{
// exit game as appropriate
if (Input.GetAxis("Exit") > 0)
{
Application.Quit();
}
}
}
Attach the script to the Main Camera and build the game by selecting File > Build Settings ... from the
main menu bar (remember, we can only close the game down by running it from a built executable).
Click the Add Open Scenes button at the bottom right of the Scenes In Build box, then click the Build
And Run button at the bottom of the popup. Select the folder where you want the build to be placed
(creating a new Build folder if you’d like), then click the Select Folder button. When the game starts
running, press the Escape key to close the game down.
402 Chapter 19
Now let's add the Text elements we'll need to display the top 10 scores. We do this by right-clicking the
HighScoreCanvas game object in the Hierarchy window, selecting UI > Text - TextMeshPro, renaming
the new element appropriately (we named ours Score1Text through Score10Text), placing the elements
in a reasonable way on the canvas, and changing the text for each element so we could tell them apart!
The figure below shows how everything looks after adding those Text elements.
Of course, we don't actually want to start the game with the high score canvas displayed, so we need to
disable it when the game starts. The best place to do this is in the FishGame script that's attached to the
main camera, so we add the following code to a new Start method in that script:
We originally added this code to the end of the existing Awake method in the FishGame script, but it
turned out that we were deactivating the HighScoreCanvas before it could add an event listener it needed
to use (we'll get to that later in this section).
When you run the game now, you'll see that it plays normally without displaying the high score canvas.
Remove the ExitGame script from the Main Camera and add it to the HighScoreCanvas game object
instead. That means that pressing the Escape key doesn't exit the game when the game starts because the
script that processes that input is now attached to the HighScoreCanvas game object, so it's not active
when the game starts either.
Because the game runs in fullscreen mode by default, there’s no way for the player to close the game
(except by using Ctrl-Alt-Del) before the high score canvas appears. Let’s make the game run in a
File IO 403
window so the player can just click the x in the upper right of the window to close the game at any time.
In the Unity editor, select Edit > Project Settings … from the top menu. Select Player on the left. On the
right, change the Fullscreen Mode setting in the Resolution area by clicking the dropdown that says
Fullscreen Window and changing it to Windowed instead. Change the Default Screen Width and Default
Screen Height as you see fit. Click the x in the upper right corner and build and run your game again.
Now the player can stop the game at any time using standard Windows functionality.
Recall that our FishGame script contains a HandleGameTimerFinishedEvent method that gets called
when the game timer finishes. We included a comment in that method saying we'd display the high
score table at that point, but how should we do that? We can assume at this point that we'll be writing a
HighScoreTable script that we attach to the HighScoreCanvas game object to handle retrieving the
saved high score data, adding the current score in the list of high scores as appropriate, saving the
(potentially) revised high score data, setting all the Text elements on the canvas appropriately, and
displaying the canvas.
There are several reasonable approaches we could use when the game timer expires. In one such
approach, the FishGame script could retrieve the HighScoreTable component of the HighScoreCanvas
game object, then call a (yet to be written) Display method in that script. This is a reasonable solution,
since the FishGame script is like the game manager for the game, so it's reasonable for it to know about
the game objects in the game. This approach requires even more detailed knowledge about those objects,
though, because the FishGame script also needs to know that the HighScoreCanvas game object contains
a HighScoreTable component with a Display method. Although that single piece of detailed
information is okay in this particular game, let's use a more general approach that doesn't require that
level of detailed knowledge.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
/// <summary>
/// An event that indicates that the high score table
/// should be displayed
/// </summary>
public class DisplayHighScoreTableEvent : UnityEvent<int>
{
}
Notice that we're using the version of the UnityEvent that has a single int parameter. That's because
we need to include the score for the current game when we invoke the event so the HighScoreTable
script can include the current score in the list of high scores as appropriate.
404 Chapter 19
Next, we add a DisplayHighScoreTableEvent field to the FishGame script and add the following
method so the EventManager can add a listener for that event:
/// <summary>
/// Adds the given event handler as a listener
/// </summary>
/// <param name="listener">listener</param>
public void AddDisplayHighScoreTableEventListener(UnityAction<int> listener)
{
displayHighScoreTableEvent.AddListener(listener);
}
We also add the following to the HandleGameTimerFinishedEvent method in the FishGame script:
To support getting the current game score to update when we invoke the event, we add a hud field to the
FishGame script, mark it with [SerializeField], and populate it in the Inspector. We also add a Score
property to the HUD script so we can get the current game score from the HUD game object. In this case,
we are using detailed information about the HUD game object in the FishGame script. Although we
avoided that approach for the HighScoreCanvas, if we're going to store game-level information (like the
score) in the HUD, we need the FishGame script to be able to access that information directly.
Next, we add the following method to the EventManager class so the HighScoreTable script can add a
listener for the DisplayHighScoreTableEvent:
/// <summary>
/// Adds the given event handler as a listener
/// Game objects tagged as MainCamera invoke DisplayHighScoreTableEvent
/// </summary>
/// <param name="listener">listener</param>
public static void AddDisplayHighScoreTableEventListener(
UnityAction<int> listener)
{
// add listener to main camera
GameObject mainCamera = GameObject.FindWithTag("MainCamera");
FishGame script = mainCamera.GetComponent<FishGame>();
script.AddDisplayHighScoreTableEventListener(listener);
}
Now we can get started on the HighScoreTable script, which we attach to the HighScoreCanvas game
object. We start by adding our listener for the DisplayHighScoreTableEvent (in the Awake method so
it's added before the FishGame script deactivates the HighScoreCanvas) and by enabling the
HighScoreCanvas when that event is invoked:
using System.Collections;
using System.Collections.Generic;
using System.Text;
using UnityEngine;
using TMPro;
File IO 405
/// <summary>
/// A high score table
/// </summary>
public class HighScoreTable : MonoBehaviour
{
/// <summary>
/// Awake is called before Start
/// </summary>
void Awake()
{
EventManager.AddDisplayHighScoreTableEventListener(
HandleDisplayHighScoreTableEvent);
}
/// <summary>
/// Handles the display high score table event
/// </summary>
/// <param name="score">current game score</param>
void HandleDisplayHighScoreTableEvent(int score)
{
// display high score table
gameObject.SetActive(true);
}
}
If you run the game now, you'll see that the HighScoreCanvas appears when the game timer expires. It
would be nicer to pause the game once that happens, so we'll take care of that before we finish off the
game.
For now, though, let's (finally!) actually work on storing, retrieving, and displaying the high score table
using the PlayerPrefs class. Before we start implementing the details, let's talk about how the
PlayerPrefs class works in general.
The PlayerPrefs class stores pairs of keys and values. We can think of the key as the “name” of a
piece of information we're storing and the value as the actual information. For example, if we wanted to
store the player's name (Doofus42, say), we could use "Player Name" as the key and "Doofus42" as
the value. In that case, we'd use
to save that key/value pair. Similarly, if we want to retrieve the player name from storage, we'd use
The key we use is always a string, and the only data types we can save for the value are float, int, or
string. We can use the PlayerPrefs HasKey method to find out if a particular key has a value
associated with it, which we might want to do before setting a key/value pair to make sure we don't
destroy a previous key/value pair with the same key. If we call GetFloat, GetInt, or GetString with a
key that doesn't exist in the preferences (there's no key/value pair for that key), GetFloat returns 0.0f,
GetInt returns 0, and GetString returns an empty string.
406 Chapter 19
Using PlayerPrefs to store a complex object (like a high score table) is somewhat awkward because
we can only retrieve a single value for a specific key. Alternatively, we could use a standard C# binary
file (like we learned about in Section 19.3) to store and access this information, but that approach has a
significant drawback. Specifically, different Operating Systems have different rules about where
applications can write data to device storage. Rather than trying to handle all those different rules in our
code, we can simply use PlayerPrefs instead. It's definitely easier for us to write code that has a single
value per key rather than trying to handle all the different devices we can deploy a Unity game to.
So how do we store the set of 10 high scores in a single float, int, or string? By using a comma-
separated value (CSV) string, of course! Let's look at a couple of helper methods we'll use to work with
those CSV strings, then we'll get back to our HighScoreTable HandleDisplayHighScoreTableEvent
method.
/// <summary>
/// Extracts a list of high scores from the given csv string
/// </summary>
/// <returns>list of high scores</returns>
/// <param name="csvString">csv string of high scores</param>
List<int> GetHighScoresFromCsvString(string csvString)
{
List<int> scores = new List<int>();
return scores;
}
The method starts by creating a new, empty list of scores. We only need to process non-empty strings
(empty strings don't contain any scores), so we use an if statement to check for that. The first line of
code in the if body uses the String Split method to extract the scores (as strings) from the CSV string.
We pass two arguments into the Split method: the string we want to split and the separator (or
delimiter) – in this case, a comma – that separates the values in the string. The method returns an array
of the values as strings. The foreach loop walks the array of values and adds each one to the scores list,
parsing each value string into an int along the way. The last line of code in the method returns the list
of scores.
Our second helper method converts a list of high scores to a csv string (we had to add a using directive
for the System.Text namespace to get this to compile):
/// <summary>
/// Converts a list of high scores to a csv string
File IO 407
/// </summary>
/// <returns>csv string</returns>
/// <param name="scores">list of high scores</param>
string GetCsvStringFromHighScores(List<int> scores)
{
StringBuilder csvString = new StringBuilder();
return csvString.ToString();
}
This method uses a StringBuilder to build up our string; remember, using that approach lets us avoid
creating lots of new string objects as we go along because strings are immutable. The for loop adds each
score, followed by a comma, to the string (except for the last score). We then add the last score to the
string and return the string, using ToString to convert it to a string (instead of a StringBuilder).
Our final helper method doesn't actually deal with CSV strings, but it does add the current game score to
the list of high scores as appropriate:
/// <summary>
/// Adds the given score to the list. If the score table is already
/// full, inserts the score in the appropriate location and drops the
/// lowest score from the list or does nothing if the given score is
/// lower than all the scores in the list
/// </summary>
/// <param name="score">score to add</param>
/// <param name="scores">list of scores</param>
/// <returns>true if the score was added, false otherwise</returns>
public bool AddScore(int score, List<int> scores)
{
// make sure we should add the score
if (scores.Count < MaxNumScores ||
score > scores[MaxNumScores - 1])
{
// make sure we don't grow the list past full size
if (scores.Count == MaxNumScores)
{
scores.RemoveAt(MaxNumScores - 1);
}
return true;
}
else
{
return false;
}
}
We make the AddScore method return true if the given score is actually added to the high scores and
false otherwise. We do that so the consumer of this method only saves the high scores back to player
prefs if the list of high scores has changed. At this point in the book, you should be able to understand
how the method above works. If not, write down a list of 10 (we've added a MaxNumScores constant, set
to 10, to our HighScoreTable script) high scores on a piece of paper and add a new high score that falls
in the middle of the list using the code above as your algorithm for doing that.
/// <summary>
/// Handles the display high score table event
/// </summary>
/// <param name="score">current game score</param>
void HandleDisplayHighScoreTableEvent(int score)
{
// retrieve high scores from storage
List<int> scores = GetHighScoresFromCsvString(
PlayerPrefs.GetString("High Scores"));
The line of code above retrieves the value in player prefs associated with the "High Scores" key (that
value will be an empty string the first time the game runs because we've never saved a value for the
High Scores key) and passes that value into the helper method that converts it to a list of scores.
The block of code above adds the current game score to the list of scores using the AddScore method
discussed above. If the score was actually added to the high scores (remember, that’s why the AddScore
method returns bool instead of void), the block of code also assigns a new value (including the new
score in the appropriate place in the csv string) to the "High Scores" key and saves the new high scores
back to player prefs. Because writing to a file takes a relatively long time compared to other program
operations, we only want to pay that performance price when we actually need to.
File IO 409
There's actually another reason to save back to player prefs at this point. The documentation for the
PlayerPrefs Save method tells us “By default Unity writes preferences to disk during
OnApplicationQuit().” Although we're going to quit the application soon (when the player presses the
Escape key after looking at the high scores table), it's better to make sure we save the information now
just in case the game crashes or something else goes wrong.
The block of code above disables all the Text elements for the scores on the HighScoreCanvas (we had
to add a using directive for the TMPro namespace to get this to compile). We couldn't just disable all the
Text elements on the canvas because the High Scores label and the Press <Esc> to exit game message
are also Text elements on the canvas.
The block of code above re-enables those Text elements that actually contain a high score (there could
be fewer than MaxNumScores high scores saved at this point). First, we build the name of the Text
element we want to process by adding 1 to i because our for loop indexes are 0-based but our Text
element names are 1-based. We then use an additional helper method we wrote (see the code
accompanying the chapter for the details) to find the index of the Text element with the name we're
looking for. Finally, we enable that text element so it will be displayed and set the text appropriately.
The final line of code sets the HighScoreCanvas game object to be active so it's displayed.
When you run the game for the first time, you should get something like the figure below (though of
course your score will hopefully be higher!).
410 Chapter 19
Okay, we're almost done solving this problem! The last thing we want to do is pause the gameplay when
the high scores are displayed.
If you do a web search on Unity “Pause Game”, you'll find a number of reasonable suggestions. The
best suggestion is to set Time.timeScale to 0. This does pause the game, but unfortunately it also
seems to make the game unresponsive to user input, so the game won't close when the player presses the
Escape key.
We said “seems to” above because this doesn't actually make the game unresponsive to user input! If we
replace Input.GetAxis("Exit") > 0 with Input.GetKeyDown(KeyCode.Escape) in our ExitGame
script, the game exits as before when the player presses the Escape key. What's going on?
This took some research to figure out, but it turns out that the Input GetAxis method applies some
input smoothing to the input, so when we set the timeScale to 0 that axis input never changes from 0.
Although we could use the GetKeyDown method approach shown above, we're big fans of using the
named input axes because we think it makes our code more readable. Luckily, there's a way to do that!
We can simply use the Input GetAxisRaw method instead, which "Returns the value of the virtual axis
identified by axisName with no smoothing filtering applied". That's just what we need here, so setting
Time.timeScale to 0 in our HighScoreTable HandleDisplayHighScoreTableEvent method and
changing GetAxis to GetAxisRaw in our ExitGame Update method completes our solution to the
problem in this section.
So where is the high score information in the player prefs actually saved? That depends on the OS you're
building for, so you should read the information about the storage location in the PlayerPrefs
documentation. The storage location for most Operating Systems is partially based on the Company
Name and Product Name for the game, so you should be sure to set those by selecting Edit > Project
Settings > Player from the main menu bar. The Company Name and Product Name values are at the top
of the Inspector.
The previous section showed how we can use PlayerPrefs to store information that potentially changes
as the player plays our game multiple times (though we could of course also save multiple changes in a
single play session, as the player collects upgrades, for example).
File IO 411
We may also want to retrieve read-only information about the configuration of the game itself. Including
game configuration information in a separate file (instead of as constants as we’ve been doing up to this
point) is a good approach for a number of reasons. It lets non-programmers help tune the game during
playtesting, because changes they make to the configuration file immediately propagate into the game
without any programmer intervention. In addition, it also helps support patching the game after it’s
released, since game developers can simply include the new configuration file in the patch instead of
having to release a new executable for the entire game.
For our example, we’ll again start with the fish game we developed in Section 17.3. We include the
revised Timer, TimerFinishedEvent, and EventManager classes from the previous section, but only to
control teddy bear spawning and to add points to the player score; we don't include a game timer or a
high score table in this example. We’ll enhance the game by using a configuration file that contains the
following information: the fish move units per second, the minimum teddy bear impulse force, the
maximum teddy bear impulse force, the minimum teddy bear spawn delay, the maximum teddy bear
spawn delay, and the point value for each bear.
Next, we need to decide what format we'll use for the configuration data file and where we'll store it. We
could save the file as a binary file as discussed in section 19.3, which would make it harder for players
to modify. It's more common during development to save these kinds of files as CSV (comma-separated
value) or XML (eXtensible Markup Language) files so they're easy for designers (rather than
programmers) to change to tune the game. Using a CSV or XML file makes it easy for players to
modify, but that may not be so bad; players can play with the different values in the file to change the
gameplay34. For this example, we'll use a CSV file that we store in the same location as the executable
for our game.
Let's start with a ConfigurationData class that we'll use to store the information listed above. This
class is really just a data container for the configuration data we read from the file. Here's the code for
our ConfigurationData class:
/// <summary>
/// A class providing access to configuration data
/// </summary>
public class ConfigurationData
{
#region Fields
float fishMoveUnitsPerSecond = 5;
float minTeddyBearImpulseForce = 3;
float maxTeddyBearImpulseForce = 5;
int minTeddyBearSpawnDelay = 1;
int maxTeddyBearSpawnDelay = 2;
int teddyBearPoints = 10;
#endregion
In the code above, we initialized each of the fields with a default value in case we don’t successfully
read the values from the configuration data file.
34We hope to release our Battle Paddles game on www.burningteddy.com soon. Because we didn't get a chance to tune the
game before we closed Peak Game Studios, we’ll include a CSV file that players can use to tune the game as they see fit.
412 Chapter 19
#region Constructor
/// <summary>
/// Constructor
/// Reads configuration data from a file. If the file
/// read fails, the object contains default values for
/// the configuration data
/// </summary>
public ConfigurationData()
{
// read and save configuration data from file
StreamReader input = null;
try
{
// create stream reader object
input = File.OpenText(Path.Combine(
Application.streamingAssetsPath, ConfigurationDataFileName));
The code above opens the CSV file so we can read it. The call to the File OpenText method assumes
that we created a StreamingAssets folder in the Assets folder for our project and put the
ConfigurationData.csv file in that StreamingAssets folder. This is a great approach to use because the
file will be accessible both while we’re working on the game and after we’ve built and shipped it. The
Path Combine method builds the path to the CSV file by combining the path to the StreamingAssets
folder (using Application.streamingAssetsPath) and the name of the configuration data file (using
the constant we declared as a field).
// read in names and values
string names = input.ReadLine();
string values = input.ReadLine();
Next, we read in the line in the file that contains the names of the configuration data values and the line
in the file that contains the values for those configuration data values.
}
catch (Exception e)
{
}
finally
{
// always close input file
if (input != null)
{
input.Close();
}
}
}
The rest of the code uses our standard pattern for always closing the input file (as long as we were able
to open it).
File IO 413
/// <summary>
/// Sets the configuration data fields from the provided
/// csv string
/// </summary>
/// <param name="csvValues">csv string of values</param>
void SetConfigurationDataFields(string csvValues)
{
// the code below assumes we know the order in which the
// values appear in the string. We could do something more
// complicated with the names and values, but that's not
// necessary here
string[] values = csvValues.Split(',');
We saw previously how useful the Split method is for extracting comma-separated values into an array
of strings; that’s why we’re using it here.
fishMoveUnitsPerSecond = float.Parse(values[0]);
minTeddyBearImpulseForce = float.Parse(values[1]);
maxTeddyBearImpulseForce = float.Parse(values[2]);
minTeddyBearSpawnDelay = int.Parse(values[3]);
maxTeddyBearSpawnDelay = int.Parse(values[4]);
teddyBearPoints = int.Parse(values[5]);
}
#endregion
The code above uses the appropriate float or int Parse method to parse the string for a particular
configuration data value in the array, then put the parsed value into the corresponding field.
#region Properties
/// <summary>
/// Gets the fish move units per second
/// </summary>
public float FishMoveUnitsPerSecond
{
get { return fishMoveUnitsPerSecond; }
}
/// <summary>
/// Gets the min teddy bear impulse force
/// </summary>
public float MinTeddyBearImpulseForce
{
get { return minTeddyBearImpulseForce; }
}
/// <summary>
/// Gets the max teddy bear impulse force
/// </summary>
public float MaxTeddyBearImpulseForce
{
get { return maxTeddyBearImpulseForce; }
414 Chapter 19
}
/// <summary>
/// Gets the min teddy bear spawn delay
/// </summary>
public int MinTeddyBearSpawnDelay
{
get { return minTeddyBearSpawnDelay; }
}
/// <summary>
/// Gets the max teddy bear spawn delay
/// </summary>
public int MaxTeddyBearSpawnDelay
{
get { return maxTeddyBearSpawnDelay; }
}
/// <summary>
/// Gets the number of points a teddy bear is worth
/// </summary>
public int TeddyBearPoints
{
get { return teddyBearPoints; }
}
#endregion
}
Our class exposes properties for each of the pieces of configuration data so consumers of the class can
get those values.
Next, we need a static class that other scripts in the game can use to access the configuration data. We
called that class ConfigurationUtils; here's the code:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// Provides utility access to configuration data
/// </summary>
public static class ConfigurationUtils
{
#region Fields
#endregion
/// <summary>
File IO 415
/// Gets the fish move units per second
/// </summary>
public static float FishMoveUnitsPerSecond
{
get { return configurationData.FishMoveUnitsPerSecond; }
}
/// <summary>
/// Gets the min teddy bear impulse force
/// </summary>
public static float MinTeddyBearImpulseForce
{
get { return configurationData.MinTeddyBearImpulseForce; }
}
/// <summary>
/// Gets the max teddy bear impulse force
/// </summary>
public static float MaxTeddyBearImpulseForce
{
get { return configurationData.MaxTeddyBearImpulseForce; }
}
/// <summary>
/// Gets the min teddy bear spawn delay
/// </summary>
public static int MinTeddyBearSpawnDelay
{
get { return configurationData.MinTeddyBearSpawnDelay; }
}
/// <summary>
/// Gets the max teddy bear spawn delay
/// </summary>
public static int MaxTeddyBearSpawnDelay
{
get { return configurationData.MaxTeddyBearSpawnDelay; }
}
/// <summary>
/// Gets the number of points a teddy bear is worth
/// </summary>
public static int TeddyBearPoints
{
get { return configurationData.TeddyBearPoints; }
}
#endregion
These are essentially the same properties as the properties exposed by the ConfigurationData class,
but the properties above are static, so they provide static access to the configuration data through the
configurationData field.
/// <summary>
416 Chapter 19
/// Initializes the configuration data by creating the
/// ConfigurationData object
/// </summary>
public static void Initialize()
{
configurationData = new ConfigurationData();
}
#endregion
}
All we have to do to initialize the configuration data is create a new ConfigurationData object,
because as we saw above, the ConfigurationData constructor reads the values from the CSV file and
initializes all the ConfigurationData fields.
Of course, we still need to call the Initialize method above at the start of the game to read in the
configuration data. We do that by adding a new FishGame script and attaching it to the Main Camera.
Here's the code for that script:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// Game manager
/// </summary>
public class FishGame : MonoBehaviour
{
/// <summary>
/// Awake is called before Start
/// </summary>
void Awake()
{
ConfigurationUtils.Initialize();
}
}
Now that we have the configuration data from the file accessible through the properties of our
ConfigurationUtils class, we can replace the constants we have sprinkled throughout our code to use
the file information instead. You should look at the accompanying code to see the details for all those
changes, but here’s a summary:
At this point, we can change any of the values in the CSV file and we’ll see the new values in effect in
the game after we save our CSV changes, close the CSV file, and rerun the game. Go ahead, try it!
Chapter 20. Putting It All Together
Throughout the book we’ve learned about lots of the bits and pieces we can use to build games using C#
and Unity. We’ve even built a couple of small games, but we haven’t really developed a complete game
from scratch. In this chapter, we’ll build an actual game that uses many of the concepts you’ve learned
in previous chapters (and yes, you might even learn a few new things as well).
As a warning, the game we’ll be building here is pretty simple; it’s about the scope you’d expect for a
final project at the end of a first or second game programming course at a college or university. That
means that it’s got a simple menu system, straightforward gameplay, and only very rudimentary
Artificial Intelligence (AI). On the other hand, it gives us a chance to put everything together into a
game, and you can of course use the ideas we implement in this chapter as a foundation for more
complicated games you build on your own.
Implement a game called Feed the Teddies. In the game, the player moves a burger avatar around the
screen, shooting french fries at teddy bears. The teddy bears aren't defenseless, though, they shoot back!
The player's burger takes damage when it collides with a teddy bear or a teddy bear projectile. Selected
game difficulty (Easy, Medium, or Hard) determines the maximum number of teddy bears in the game
and how smart the teddy bears are, how quickly they move, and how quickly they're spawned. When the
game finishes, either because the player ran out of health or because the game timer expired, the player
discovers whether or not they just achieved a new high score.
The main menu will give the player the ability to play, see their current best score, or quit. The difficulty
menu will let the player select between Easy, Medium, and Hard difficulties for the game. The high
score menu will simply show the current high score; the player will click a button to close the menu and
return to the main menu. We'll also include a pause menu that the player can open during gameplay by
pressing the Escape key. The player will be able to either resume the game or quit to the main menu
from the pause menu.
It should be obvious why we're thinking of the main menu, the difficulty menu, and the pause menu as
menus. Perhaps less intuitively, we're also thinking of the high score menu as a menu because it will
behave just like any other menu, with a clickable button to take some action (in this case, close the menu
and return to the main menu).
Just as we did in Chapter 17, we'll make our main menu and difficulty menu separate scenes in our
game. Our pause menu will be a “popup menu” that appears above the gameplay scene, so we'll make
that a prefab we can create and destroy as necessary. This is a different approach from the one we used
in the previous chapter for our high score table, but recall that in our previous approach we included a
Putting It All Together 419
canvas in the scene, then modified whether or not it was displayed as appropriate. Although our game
here only has one gameplay scene, you should be able to easily imagine a game with many gameplay
scenes (e.g., levels). We don't want to have to add the pause menu canvas to each of those scenes in the
editor, so a prefab is the better way to go. Finally, our high score menu will also be a prefab. We'll
display it both from the main menu and at the end of a game, so having a prefab we can use in both
places will be a good approach.
What scripts will we need? It's reasonable to plan to have MainMenu, DifficultyMenu, PauseMenu, and
HighScoreMenu scripts to handle button clicks and any other required processing (like displaying the
high score). The UML for those scripts is provided below.
Now that we have our initial design for the menu system done, we can move on to our menu test cases.
Our test cases include testing the behavior of all four menus.
Test Case 1
Clicking Quit Button on Main Menu
Step 1. Click Quit button on Main Menu. Expected Result: Exit game
Test Case 2
Clicking High Score Button on Main Menu
Step 1. Click High Score button on Main Menu. Expected Result: Move to High Score Menu
Step 2. Click X in corner of player. Expected Result: Exit game
Test Case 3
Displaying High Score on High Score Menu
Step 1. Click High Score button on Main Menu. Expected Result: Move to High Score Menu
Step 2. If no games have been played yet, the High Score Menu displays a No games played yet
message. Otherwise, the High Score Menu displays the highest score achieved so far
Step 3. Click X in corner of player. Expected Result: Exit game
420 Chapter 20
Test Case 4
Clicking Quit Button on High Score Menu
Step 1. Click High Score button on Main Menu. Expected Result: Move to High Score Menu
Step 2. Click Quit button on High Score Menu. Expected Result: Move to Main Menu
Step 3. Click Quit button on Main Menu. Expected Result: Exit game
Test Case 5
Clicking Play Button on Main Menu
Step 1. Click Play button on Main Menu. Expected Result: Move to Difficulty Menu
Step 2. Click X in corner of player. Expected Result: Exit game
Test Case 6
Clicking Easy Button on Difficulty Menu
Step 1. Click Play button on Main Menu. Expected Result: Move to Difficulty Menu
Step 2. Click Easy button on Difficulty Menu. Expected Result: Move to gameplay screen for easy game
Step 3. Click X in corner of player. Expected Result: Exit game
Test Case 7
Clicking Medium Button on Difficulty Menu
Step 1. Click Play button on Main Menu. Expected Result: Move to Difficulty Menu
Step 2. Click Medium button on Difficulty Menu. Expected Result: Move to gameplay screen for
medium game
Step 3. Click X in corner of player. Expected Result: Exit game
Test Case 8
Clicking Hard Button on Difficulty Menu
Step 1. Click Play button on Main Menu. Expected Result: Move to Difficulty Menu
Step 2. Click Hard button on Difficulty Menu. Expected Result: Move to gameplay screen for hard game
Step 3. Click X in corner of player. Expected Result: Exit game
Test Case 9
Clicking Resume Button on Pause Menu
Step 1. Click Play button on Main Menu. Expected Result: Move to Difficulty Menu
Step 2. Click Easy button on Difficulty Menu. Expected Result: Move to gameplay screen for easy game
Step 3. Press Escape key. Expected Result: Game paused and Pause Menu displayed on top of game
Step 4. Press Resume button on Pause menu. Expected Result: Pause Menu removed and game
unpaused
Step 5. Click X in corner of player. Expected Result: Exit game
Test Case 10
Clicking Quit Button on Pause Menu
Step 1. Click Play button on Main Menu. Expected Result: Move to Difficulty Menu
Step 2. Click Easy button on Difficulty Menu. Expected Result: Move to gameplay screen for easy game
Step 3. Press Escape key. Expected Result: Game paused and Pause Menu displayed on top of game
Step 4. Press Quit button on Pause menu. Expected Result: Move to Main Menu
Step 5. Click Quit button on Main Menu. Expected Result: Exit game
Putting It All Together 421
Right click the Sprites folder in the Project window and select Create > Folder; call the new folder
Menus. We're going to keep our menu sprites separate from our gameplay sprites in this game. You'll
find that as you build more complicated games, you'll need to add subfolders to organize your assets for
the game. Games can have hundreds or even thousands of different sprites, and putting all those sprites
into a single folder would be madness!
Next, we add the sprite in the figure below to our Sprites\Menus folder (our sprite is named
quitmenubutton).
This sprite actually has two images. The image on the left is the unhighlighted image and the one on the
right is the highlighted image. Instead of using the default Button color tinting behavior provided in
Unity, we're going to have Unity change the button sprite as the player moves the mouse on to and off of
the button.
To support that functionality, we actually need to treat the image as a sprite strip (like we did for fire and
explosion animations in Chapter 16) because it contains two different sprites. Click the quit menu button
sprite you just added in the Project window. In the Inspector, change the Sprite Mode to Multiple. Click
the Sprite Editor button, select Slice near the upper left corner of the popup, change the Type to Grid by
Cell Count, change C to 2, and click the Slice button. Click the Apply button near the top middle of the
popup and close the sprite editor. If you expand the sprite in the Project window, you'll see that it's been
split into two sprites.
Now we'll add the button to the scene. Create an empty game object in the scene and name it
MainMenu. Right click the MainMenu game object and create a Canvas. Change the UI Scale Mode in
the Canvas Scaler component in the Inspector to Scale With Screen Size and set the Reference
Resolution X and Y values (we used 1024 and 768). Adding a Canvas automatically gives us an
EventSystem, but we want the EventSystem inside the MainMenu game object. Drag the EventSystem
onto the MainMenu game object; it should now appear just below the Canvas at the same indentation.
Right click the Canvas and select UI > Button – TextMeshPro. Drag the quitmenubutton_0 sprite into
the Source Image in the Image component in the Inspector. Place the Quit Button in a reasonable place
in the scene by changing the X and Y locations of the image (remember, we'll have 3 menu buttons on
the main menu). Change the name of the button in the Hierarchy window to QuitMenuButton.
If you run the game now, you'll see the slight change in color when we move the mouse on to and off of
the button. Let's make it so the button changes sprites instead. Select the QuitMenuButton, go to the
Button component in the Inspector, and change Transition to Sprite Swap. Drag the quitmenubutton_1
sprite onto the Highlighted Sprite, Selected Sprite, and Pressed Sprite fields. When you run the game
422 Chapter 20
again, you'll see that the sprite changes between the unhighlighted and highlighted sprites appropriately.
Cool.
Now we need to make the button do something when we click it. In Chapter 17, we wrote a MainMenu
script that processed the clicks on each of the buttons in the menu, and we'll use that same approach here
(we put it in a new Scripts\Menus subfolder):
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// Listens for the OnClick events for the main menu buttons
/// </summary>
public class MainMenu : MonoBehaviour
{
/// <summary>
/// Handles the on click event from the quit button
/// </summary>
public void HandleQuitButtonOnClickEvent()
{
Application.Quit();
}
}
Attach the MainMenu script to the MainMenu game object. Add the MainMenu
HandleQuitButtonOnClickEvent method as a listener for the On Click () event for the
QuitMenuButton in the Inspector.
Remember, we need to use File > Build Settings ... and run our game in the player for the Application
Quit method to work, so do that now; we put our built games into a separate Build folder. The game
should close when you click the Quit button (which lets us pass Test Case 1).
Let's work on passing Test Case 2, which is the move to the High Score Menu from the Main Menu.
Add a sprite for the high score menu button to the Sprites\Menus folder (our sprite is named
highscoremenubutton) and use the Sprite Editor to slice it into two sprites like we did above for the quit
menu sprite. Add a Button – TextMeshPro to the Canvas inside the MainMenu game object and rename
it to HighScoreMenuButton. Set the Source Image to the highscoremenubutton_0 sprite. Set up the
Button component to change to the highlighted sprite as appropriate. Run the game to confirm that
highlighting works properly.
Next, we need to build the prefab for the High Score Menu. Add an empty game object to the scene and
rename it HighScoreMenu. Add a Canvas to the empty game object and move the EventSystem into the
HighScoreMenu game object. Change the Canvas to scale with screen size. Add an Image to the canvas,
center the image in the scene, change the name of the image to Background, set the Width to 400, set the
Height to 300, and make the Color an opaque gray.
Add a Text - TextMeshPro element to the canvas for a high score label. Change the location, text, font
size, and paragraph alignment appropriately to put the label near the top of the canvas. Change the name
of the text element to Label.
Putting It All Together 423
Add another Text - TextMeshPro element to the canvas for the high score message. Change the location,
text, font size, and paragraph alignment appropriately to put the message near the middle of the canvas.
Change the name of the text element to Message.
When we were adding the canvas and the text elements, those were on top of the main menu buttons in
both the Scene and Game views. We ran the game in the editor, though, and the canvas ended up below
the main menu buttons in both those windows. Because we're going to want the High Score Menu to
appear above the Main Menu when the player clicks the High Score button, we need to fix this.
There are of course a variety of ways to solve this problem, although we will point out that using Sorting
Layers isn't one of them for a Canvas object (for those of you who remember Chapter 9). We're going to
actually do some special processing as we move from the Main Menu to the High Score Menu
(deactivating the Main Menu canvas containing the main menu buttons). It turns out that it's fairly
common that we need to do some special processing as we navigate between menus in our games,
especially those with complicated menu systems. Rather than spreading that processing logic out
throughout the menu scripts in our game, we'll add a centralized MenuManager class that handles that for
us.
Here's our initial code for that class (which we put in the Scripts\Menus subfolder):
/// <summary>
/// Manages navigation through the menu system
/// </summary>
public static class MenuManager
{
/// <summary>
/// Goes to the menu with the given name
/// </summary>
/// <param name="name">name of the menu to go to</param>
public static void GoToMenu(MenuName name)
{
switch (name)
{
case MenuName.Difficulty:
break;
case MenuName.HighScore:
We made this a static class so the menu scripts can easily access the methods in the class. Because we
named our MainMenu game object, we can easily find the Main Menu to deactivate it when going to the
424 Chapter 20
High Score Menu. Also note that we added a MenuName enumeration to our project (also in the
Scripts\Menus folder) to make our menu code more readable.
We haven't included code for most of the menus at this point, though we did include code for the
MenuName.HighScore case. The first two lines of code in that case find the main menu canvas and
deactivate it; this works fine for now, though we'll have to modify it slightly before we're done with the
game.
The third line of code uses the Resources class to instantiate an instance of the HighScoreMenu prefab.
The problem we need to solve here is that we need to instantiate a prefab that isn't in the scene and hasn't
been assigned to a field in any of our scripts. We don't want to add an instance of the HighScoreMenu
prefab to each of our scenes (even as an inactive game object) because we'd have the same problem we
discussed with the pause menu; we'd have to include the prefab in every single scene in the game. The
Resources Load method looks for the asset with the given name in any folder named Resources in the
project Assets folder, so we added a Resources folder under the Prefabs folder in the Project window
and created a HighScoreMenu prefab there by dragging the HighScoreMenu game object from the
Hierarchy window and dropping it into that folder. After creating the prefab, we deleted the
HighScoreMenu game object from the current scene. You should read the Resources documentation to
learn more about the tradeoffs associated with using this class, but it's the right choice for us here.
Now we can add a method to the MainMenu class to handle clicks on the high score button:
/// <summary>
/// Handles the on click event from the high score button
/// </summary>
public void HandleHighScoreButtonOnClickEvent()
{
MenuManager.GoToMenu(MenuName.HighScore);
}
Add the new method as a listener for the On Click () event in the HighScoreMenuButton game object
and run the game. Execute Test Case 2, clicking the High Score button on the main menu, then closing
the player at the High Score Menu. The test case should pass fine.
Test Case 3 also partially works at this point because we made the default text for the Message element
the text we want to display if we don't have a high score saved yet. We'd like to finish off this test case,
though, so let's add the required functionality to retrieve a high score from player prefs and display it. To
test the portion of the case where there actually is a saved high score, we'll “seed” a value into player
prefs to test the score display, then clear the key so it doesn't mess up later functionality.
Drag the HighScoreMenu prefab from the Project window into the Hierarchy window so we can modify
the prefab. Create and implement the HighScoreMenu script below and add it to the HighScoreMenu
game object in the Hierarchy window.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using TMPro;
/// <summary>
/// Retrieves and displays high score and listens for
Putting It All Together 425
/// the OnClick events for the high score menu button
/// </summary>
public class HighScoreMenu : MonoBehaviour
{
[SerializeField]
TextMeshProUGUI message;
/// <summary>
/// Start is called before the first frame update
/// </summary>
void Start()
{
// temporary code
PlayerPrefs.SetInt("High Score", 3000);
Drag the Message element from the HighScoreMenu canvas in the Hierarchy window onto the Message
field of the HighScoreMenu script in the Inspector. Go to the Prefab area near the top of the Inspector,
click the Overrides dropdown, and click the Apply All button on the right to apply your changes to the
prefab. Delete the HighScoreMenu from the scene and run the game.
You should get a High Score Menu displaying a high score of 3000. Now change the temporary line of
code in the Start method to
PlayerPrefs.DeleteKey("High Score");
and run the game again. You should get a High Score Menu displaying the No games played yet
message.
At this point, we've tested both possibilities for a high score in Test Case 3 and the High Score key is
currently not saved in player prefs (since there haven't been any games played yet). Delete the temporary
code from the Start method.
It's actually fairly common to add temporary code that lets us test functionality that hasn't been fully
implemented yet (like having a high score saved from an actual game in our current project). This is a
really good approach to use to test the code we're writing as we go along, but it's of course important to
delete the temporary code when you're done using it for testing.
Okay, on to Test Case 4. You've probably noticed that we're adding small bits of functionality as we go
along, using our test cases to guide us to what we should do next. There's actually a development
methodology called Test-Driven Development (TDD) that follows the same approach. One of the core
426 Chapter 20
ideas in TDD is that we start with a set of test cases that all fail (because we haven't implemented
anything yet), then we focus our implementation on getting each of our test cases to pass. That's
essentially what we're doing here.
Test Case 4
Clicking Quit Button on High Score Menu
Step 1. Click High Score button on Main Menu. Expected Result: Move to High Score Menu
Step 2. Click Quit button on High Score Menu. Expected Result: Move to Main Menu
Step 3. Click Quit button on Main Menu. Expected Result: Exit game
Test Case 4 checks that the Quit button on the High Score Menu works properly, but of course it doesn't
because we haven't added it yet. Drag the HighScoreMenu prefab from the Project window into the
Hierarchy window so we can modify the prefab. Add a QuitMenuButton to the HighScoreMenu canvas
and set up the button to highlight/unhighlight properly.
/// <summary>
/// Handles the on click event from the quit button
/// </summary>
public void HandleQuitButtonOnClickEvent()
{
MenuManager.GoToMenu(MenuName.Main);
}
The good news is that going to the Main Menu is always the right thing to do whether we got to the
High Score Menu by clicking the High Score button on the Main Menu or we got to it by finishing a
game.
Add the HandleQuitButtonOnClickEvent method as a listener for the On Click () event in the
QuitMenuButton on the High Score Menu. Go to the Prefab area near the top of the Inspector, click the
Overrides dropdown, and click the Apply All button on the right to apply your changes to the prefab.
Delete the HighScoreMenu from the Hierarchy window.
Putting It All Together 427
Next, we need to modify our MenuManager GoToMenu method to handle the MenuName.Main case:
case MenuName.Main:
// go to MainMenu scene
SceneManager.LoadScene("MainMenu");
break;
You might be wondering why the High Score Menu disappears when we load the MainMenu scene,
especially since we instantiated the HighScoreMenu prefab and added it to the scene when the player
clicked the High Score menu button on the Main Menu. This happens because when we load a scene
using the SceneManager LoadScene method, it loads the scene as it was built in the editor. Because we
don't have an instance of the HighScoreMenu prefab in the MainMenu scene in the editor, it's not
included when that scene is loaded.
Test Case 5
Clicking Play Button on Main Menu
Step 1. Click Play button on Main Menu. Expected Result: Move to Difficulty Menu
Step 2. Click X in corner of player. Expected Result: Exit game
To pass Test Case 5, we need to add a Play button to the Main Menu that moves the game to the
Difficulty Menu when it's clicked. The Difficulty Menu is a “regular” menu, not a popup menu like the
High Score Menu or the Pause Menu, so we'll build a separate scene for the Difficulty Menu.
Right click the Scenes folder in the Project window and select Create > Scene. Rename the new scene
DifficultyMenu. We'll add the difficulty buttons to this menu after we're done implementing the Test
Case 5 functionality.
Add a sprite for the play menu button to the Sprites\Menus folder (our sprite is named playmenubutton)
and use the Sprite Editor to slice it into two sprites like we did for the previous menu button sprites. Add
428 Chapter 20
a Button - TextMeshPro to the MainMenu canvas in the MainMenu scene (make sure you're working in
the MainMenu scene, not the DifficultyMenu scene) and rename the button to PlayMenuButton. Set the
Source Image to the playmenubutton_0 sprite. Set the button up to change to the highlighted sprite as
appropriate. Run the game to confirm that highlighting works properly.
/// <summary>
/// Handles the on click event from the play button
/// </summary>
public void HandlePlayButtonOnClickEvent()
{
MenuManager.GoToMenu(MenuName.Difficulty);
}
and add the new method as a listener for the On Click () event in the PlayMenuButton.
case MenuName.Difficulty:
// go to DifficultyMenu scene
SceneManager.LoadScene("DifficultyMenu");
break;
We now have a second scene in our game, so before running it, select File > Build Settings... from the
top menu bar. Double-click the DifficultyMenu scene in the Scenes folder in the Project window, then
click the Add Open Scenes button in the build settings popup. Click the Build And Run button near the
bottom right of the build settings popup and execute Test Case 5. Although it looks like the Main Menu
buttons are just disappearing when we click the Play button, we're actually moving to the Difficulty
Menu, so Test Case 5 passes.
You can actually execute all the test cases that don't use the Quit button on the Main Menu in the editor
instead of building the project each time. That's really your choice, though we did want to show you
how to include multiple scenes in the build. We always Build And Run each of our test cases at least
once to make sure they work in “the real world”, but feel free to just run the game in the editor as you go
along if that's what you prefer to do.
Test Case 6
Clicking Easy Button on Difficulty Menu
Step 1. Click Play button on Main Menu. Expected Result: Move to Difficulty Menu
Step 2. Click Easy button on Difficulty Menu. Expected Result: Move to gameplay screen for easy game
Step 3. Click X in corner of player. Expected Result: Exit game
Test Case 7
Clicking Medium Button on Difficulty Menu
Step 1. Click Play button on Main Menu. Expected Result: Move to Difficulty Menu
Step 2. Click Medium button on Difficulty Menu. Expected Result: Move to gameplay screen for
medium game
Step 3. Click X in corner of player. Expected Result: Exit game
Putting It All Together 429
Test Case 8
Clicking Hard Button on Difficulty Menu
Step 1. Click Play button on Main Menu. Expected Result: Move to Difficulty Menu
Step 2. Click Hard button on Difficulty Menu. Expected Result: Move to gameplay screen for hard game
Step 3. Click X in corner of player. Expected Result: Exit game
Test Cases 6, 7, and 8 have us clicking the 3 difficulty buttons on the Difficulty Menu to start a game
with the selected difficulty. Let's start by implementing the Easy button.
Open the DifficultyMenu scene and add a DifficultyMenu game object that contains a Canvas and an
EventSystem. Add an Easy menu button (called EasyMenuButton) to the Canvas and set up the button
as we've been doing for our previous menu buttons. Run the game to confirm that highlighting works
properly on the Easy menu button.
Our typical next step would be to add a DifficultyMenu script that listens for and handles a click on
the Easy menu button. Before we do that, though, let's think about what should happen. We know we're
going to want to move to a Gameplay scene, so right click the Scenes folder in the Project window and
select Create > Scene to create that new scene. Moving to that new scene will be easy to implement
using the SceneManager class, but how to do we handle the fact that it should be an easy (rather than
medium or hard) game that starts up there?
This is actually a harder problem to solve than it seems. We'll end up having a FeedTheTeddies script
attached to the Main Camera in the Gameplay scene (as we've regularly done in the past), but we don't
really want our DifficultyMenu script to have to know about the FeedTheTeddies script. That's just
good Object-Oriented design, but even if we were willing to implement the functionality that way, the
FeedTheTeddies script (really, the Main Camera in the Gameplay scene) isn't active when a difficulty
menu button is pressed.
Luckily, we've solved a similar problem before with an EventManager class; we'll need an
EventManager class to support scoring and the game timer during gameplay, but we can use it here as
well. Our EventManager implementation is similar to what we implemented previously because we'll
sometimes need to add listeners for events before the event invokers have been created. In our current
situation, the DifficultyMenu script (the event invoker) won't become active until the player navigates
to the Difficulty Menu, but we'll want to already have a listener added for that event so the listener hears
the event when the DifficultyMenu script invokes it.
We'll start by adding an enum for the event names the EventManager script will handle (you'll see why
this is helpful soon). The list of enum values is incomplete at this point, but we'll simply add more as we
need them:
/// <summary>
/// The names of the events in the game
/// </summary>
public enum EventName
{
GameStartedEvent
}
430 Chapter 20
We put this enum into a new Scripts\Events folder in the Project window; we'll put all our event-related
scripts into this folder as well.
When we implement our EventManager class (coming soon, we promise!), we're going to need to use
an IntEventInvoker class that extends the MonoBehaviour class by adding a UnityEvent<int> field
and an AddListener method. Why do we use a UnityEvent<int> field? By using UnityEvent<int> as
the data type for our field, we can save any of the events we define as child classes of UnityEvent<int>
in that field. Good thing we learned about inheritance, huh?
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
/// <summary>
/// Extends MonoBehaviour to support invoking a
/// one integer argument UnityEvent
/// </summary>
public class IntEventInvoker : MonoBehaviour
{
protected UnityEvent<int> unityEvent;
/// <summary>
/// Adds the given listener for the UnityEvent
/// </summary>
/// <param name="listener">listener</param>
public void AddListener(UnityAction<int> listener)
{
unityEvent.AddListener(listener);
}
}
We make the field protected so child classes can set it to the appropriate class and invoke the event as
appropriate.
We're actually lucky that in this game the only custom events we're going to invoke using the
EventManager are one integer argument events. If we had lots of different custom events with different
numbers and types of arguments, our EventManager and (multiple) EventInvoker classes would be
much more complicated. Not a problem here, though (whew!).
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
We're using a collection from the System.Collections.Generic namespace that we haven't used
before: a Dictionary. The C# documentation tells us that a Dictionary “Represents a collection of
keys and values.” That should sound really familiar to you, because we used PlayerPrefs to store
key/value pairs. The idea works exactly the same, because we store and retrieve values using a particular
key.
The Dictionary class actually gives us much more flexibility than PlayerPrefs does. Recall that with
PlayerPrefs, the key we use is always a string, and the only data types we can save for the value are
float, int, or string. With the Dictionary class, the key and value can be any data type we want
them to be. In the line of code above, our Dictionary uses EventName as the key (so we can use an
EventName like EventName.GameStartedEvent to look up a value in the Dictionary) and
List<IntEventInvoker> as the value (so we can save and retrieve a list of the IntEventInvokers that
invoke the given event name). We'll see how to use the invokers field once we start looking at the
EventManager methods.
As you can see, we needed to define both the EventName enum and the IntEventInvoker class to
implement the line of code above.
Our listeners field lets us look up a list of listeners for a particular event using an EventName as the
key. Each of the listeners is a UnityAction<int> because that's the delegate type we need to use to
listen for a UnityEvent<int>.
#endregion
/// <summary>
/// Initializes the event manager
/// </summary>
public static void Initialize()
{
// create empty lists for all the dictionary entries
foreach (EventName name in Enum.GetValues(typeof(EventName)))
{
if (!invokers.ContainsKey(name))
{
invokers.Add(name, new List<IntEventInvoker>());
listeners.Add(name, new List<UnityAction<int>>());
}
432 Chapter 20
else
{
invokers[name].Clear();
listeners[name].Clear();
}
}
}
The foreach loop above ensures we have empty lists as the dictionary entries in the invokers and
listeners fields for each of the EventName values (we used foreach loops to process all the values of
an enum in Section 10.5 when we used nested loops to fill a deck of cards). The value for each of those
EventName keys is simply an empty list of the appropriate type. We know (looking ahead) that this
method may get called multiple times while the game runs, and trying to add a key that already exists in
a Dictionary throws an exception. We avoid that exception using the if statement above. If the name
key isn't currently in the invokers field, the if body creates a new (empty list) dictionary entry in the
invokers and listeners fields; otherwise, the else body simply clears (e.g., empties) the existing lists
for those entries.
We include this method for efficiency. When it's time to add an invoker or a listener, we won't have to
use an if statement to make sure we have a list for that EventName key in the dictionary already before
adding the new invoker/listener.
/// <summary>
/// Adds the given invoker for the given event name
/// </summary>
/// <param name="eventName">event name</param>
/// <param name="invoker">invoker</param>
public static void AddInvoker(EventName eventName,
IntEventInvoker invoker)
{
// add listeners to new invoker and add new invoker to dictionary
foreach (UnityAction<int> listener in listeners[eventName])
{
invoker.AddListener(listener);
}
invokers[eventName].Add(invoker);
}
When a consumer of the EventManager class calls the AddInvoker method to add an invoker for a
particular EventName, there may already be listeners that were added to the listeners field to listen for
that EventName. The foreach loop in the code above adds each of those listeners as a listener to the new
invoker being added. The last line of code above adds the new invoker to the list of invokers for the
given EventName in the invokers field.
/// <summary>
/// Adds the given listener for the given event name
/// </summary>
/// <param name="eventName">event name</param>
/// <param name="listener">listener</param>
public static void AddListener(EventName eventName,
UnityAction<int> listener)
{
Putting It All Together 433
// add as listener to all invokers and add new listener to dictionary
foreach (IntEventInvoker invoker in invokers[eventName])
{
invoker.AddListener(listener);
}
listeners[eventName].Add(listener);
}
#endregion
}
This method is similar to the AddInvoker method. When a consumer of the EventManager class calls
the AddListener method to add a listener for a particular EventName, there may already be invokers
that were added to the invokers field to invoke that EventName. The foreach loop in the code above
adds the new listener being added as a listener to each of those invokers. The last line of code above
adds the new listener to the list of listeners for the given EventName in the listeners field.
Okay, we said we included the Initialize method above for efficiency, but at this point we haven't
called that method from anywhere. Let's add a new GameInitializer script that does that and attach
the script to the Main Camera in the MainMenu scene (since we know that's the scene we start in when
we run the game):
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// Initializes the game
/// </summary>
public class GameInitializer : MonoBehaviour
{
/// <summary>
/// Start is called before the first frame update
/// </summary>
void Start()
{
EventManager.Initialize();
}
}
Boy, it feels like we've been doing a ton of work without getting to click the Easy button on the
Difficulty Menu! Don't worry, we are getting closer. Even more importantly, we've been building some
important infrastructure into our game to support the event system we need both for some of the menus
and for gameplay.
Next, we add a GameStartedEvent class to the Scripts\Events folder; this is the event that the
DifficultyMenu will invoke when one of the difficulty menu buttons is clicked:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
434 Chapter 20
/// <summary>
/// An event that indicates a game should be started with
/// the given difficulty
/// </summary>
public class GameStartedEvent : UnityEvent<int>
{
}
For readability, we also include an enum for the game difficulty in a new Scripts\Gameplay folder:
/// <summary>
/// The different difficulties in the game
/// </summary>
public enum Difficulty
{
Easy,
Medium,
Hard
}
Let's take a look at the new DifficultyMenu script we add to the scripts\menus folder:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// Listens for the OnClick events for the difficulty menu buttons
/// </summary>
public class DifficultyMenu : IntEventInvoker
{
/// <summary>
/// Start is called before the first frame update
/// </summary>
void Start()
{
// add event component and add invoker to event manager
unityEvent = new GameStartedEvent();
EventManager.AddInvoker(EventName.GameStartedEvent, this);
}
The Start method sets the unityEvent field (inherited from the IntEventInvoker class) to a new
GameStartedEvent object, then adds itself (using this) to the EventManager as an invoker of the
EventName.GameStartedEvent.
Putting It All Together 435
/// <summary>
/// Handles the on click event from the easy button
/// </summary>
public void HandleEasyButtonOnClickEvent()
{
unityEvent.Invoke((int)Difficulty.Easy);
}
}
Notice that when we invoke the event, we cast Difficulty.Easy to an int. We can do this because the
default underlying data type for an enum is int.
Attach the DifficultyMenu script to the DifficultyMenu game object in the DifficultyMenu scene. Add
the HandleEasyButtonOnClickEvent method as a listener for the On Click () event in the
EasyMenuButton on the Difficulty Menu.
Okay, now we have the DifficultyMenu invoking the event when the Easy button is clicked, but at this
point no one is actually listening for that event. Because there are a variety of gameplay characteristics
that depend on difficulty, we'll write a static DifficultyUtils class in the Scripts\Gameplay folder.
For now, this class will listen for the event above to set the gameplay difficulty and start the game; by
the time we're done, this class wil also expose methods to provide the difficulty-dependent gameplay
values to consumers of the class. Here's the code:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
/// <summary>
/// Provides difficulty-specific utilities
/// </summary>
public static class DifficultyUtils
{
#region Fields
#endregion
The difficulty field stores the gameplay difficulty so we can provide difficulty-specific values to the
consumers of the class (through methods we'll write later) during gameplay.
/// <summary>
/// Initializes the difficulty utils
/// </summary>
public static void Initialize()
{
EventManager.AddListener(EventName.GameStartedEvent,
HandleGameStartedEvent);
436 Chapter 20
}
#endregion
The Initialize method adds the HandleGameStartedEvent method (discussed next) to the
EventManager as a listener for the EventName.GameStartedEvent.
/// <summary>
/// Sets the difficulty and starts the game
/// </summary>
/// <param name="intDifficulty">int value for difficulty</param>
static void HandleGameStartedEvent(int intDifficulty)
{
difficulty = (Difficulty)intDifficulty;
SceneManager.LoadScene("Gameplay");
}
#endregion
}
Because the default underlying data type for an enum is int, we can cast the intDifficulty parameter
as a Difficulty to save it into the difficulty field. The second line of code above moves the game to
the Gameplay scene.
We also need to add the following line of code at the end of the GameInitializer Start method:
DifficultyUtils.Initialize();
This line of code has to come after the call to the EventManager Initialize method because the
DifficultyUtils Initialize method uses the EventManager.
We're almost there! Right click the Scenes folder in the Project window and add a new Gameplay scene;
open the new scene. Select File > Build Settings... from the top menu bar and add the new Gameplay
scene to the build. Click the Build And Run button near the bottom right and execute Test Case 6.
Well, we're definitely getting to the Gameplay scene when we click the Easy button on the Difficulty
Menu, but how do we know that the game difficulty is being set to Easy?
This will be easiest to check running the game in the editor. In Visual Studio, set a breakpoint in the
DifficultyUtils HandleGameStartedEvent method on the line of code that loads the Gameplay
scene. Select Attach to Unity near the top middle. In the editor, run the game and execute Test Case 6.
When the game stops at the breakpoint, look at the value of the difficulty field in Visual Studio to
confirm that the difficulty is set to Easy.
That's it for Test Case 6! Add the Medium and Hard buttons to the Canvas inside the DifficultyMenu
game object, add HandleMediumButtonOnClickEvent and HandleHardButtonOnClickEvent methods
to the DifficultyMenu script to handle clicks on those buttons, use the editor to add those methods as
listeners for On Click () on MediumMenuButton and HardMenuButton, and execute Test Cases 7 and 8.
Use the debugger to confirm the difficulty is being set correctly in DifficultyUtils.
Putting It All Together 437
The last menu we need to add to our game is the Pause Menu, another popup menu. Start by opening the
Gameplay scene. We'll build a prefab for the pause menu like we did for the high score menu. Add an
empty game object to the scene and name the game object PauseMenu. Add a new Canvas to the
PauseMenu game object. Change the Canvas to Scale with Screen Size. Move the EventSystem into the
PauseMenu game object. Change the UI Scale Mode for the canvas to Scale With Screen Size and set
the Reference Resolution X and Y values appropriately. Add an Image to the canvas, center the image in
the scene, change the name of the image to Background, set the Width to 400, set the Height to 300, and
make the Color an opaque gray.
Test Case 9
Clicking Resume Button on Pause Menu
Step 1. Click Play button on Main Menu. Expected Result: Move to Difficulty Menu
Step 2. Click Easy button on Difficulty Menu. Expected Result: Move to gameplay screen for easy game
Step 3. Press Escape key. Expected Result: Game paused and Pause Menu displayed on top of game
Step 4. Press Resume button on Pause menu. Expected Result: Pause Menu removed and game
unpaused
Step 5. Click X in corner of player. Expected Result: Exit game
Let's work toward passing Test Case 9, where we click the Resume button on the Pause Menu to resume
a paused game. Add a Resume button to the Pause Menu canvas and set it up like our other menu
buttons.
You should probably realize that we're going to write a PauseMenu script and attach it to the PauseMenu
game object. Before we do that, though, we need to think about what should happen when the player
clicks the Resume button. There are really only two things that should happen here: the game should be
unpaused (game objects start moving again, timers start running again, and so on) and the Pause Menu
should be removed from the scene. Removing the Pause Menu from the scene is appropriately done
from within the PauseMenu script, but who controls when the game is paused and unpaused?
If we think about how Test Case 9 works, in Step 3 we press the Escape key to pause the game. We
mentioned earlier that we knew we'd have a FeedTheTeddies script attached to the Main Camera in the
438 Chapter 20
Gameplay scene, and it's appropriate for us to detect the Escape key being pressed to instantiate the
PauseMenu prefab in the scene within that script. We could pause the game in that script before we
instantiate the prefab, or we can have the PauseMenu script pause the game when it's instantiated. Which
approach is better?
In our opinion, having the PauseMenu script pause the game when it's instantiated and unpause the game
when either menu button is clicked is the better approach, because that contains all the pause/unpause
functionality in a single script. This also makes our implementation a bit easier, because if we had the
FeedTheTeddies script unpause the game when the Resume button is clicked, the best approach would
be to have the PauseMenu script invoke an event (a GameResumedEvent, say) that the FeedTheTeddies
script listens for. We certainly understand how to use our event system properly by this point, but that
event would be added complexity that we don't really need.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// Pauses and unpauses the game. Listens for the OnClick
/// events for the pause menu buttons
/// </summary>
public class PauseMenu : MonoBehaviour
{
/// <summary>
/// Start is called before the first frame update
/// </summary>
void Start()
{
// pause the game when added to the scene
Time.timeScale = 0;
}
/// <summary>
/// Handles the on click event from the Resume button
/// </summary>
public void HandleResumeButtonOnClickEvent()
{
// unpause game and destroy menu
Time.timeScale = 1;
Destroy(gameObject);
}
}
Attach the PauseMenu script to the PauseMenu game object and use the editor to add the
HandleResumeButtonOnClickEvent method as a listener for On Click () on the ResumeMenuButton.
Drag the PauseMenu game object onto the Prefabs\Resources folder in the Project window to create the
prefab and delete the PauseMenu game object from the Gameplay scene.
All we need now is the FeedTheTeddies script to detect the Escape key being pressed to instantiate the
PauseMenu prefab in the scene. Here's the script to add to the Scripts\Gameplay folder:
Putting It All Together 439
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// Game manager
/// </summary>
public class FeedTheTeddies : MonoBehaviour
{
/// <summary>
/// Update is called once per frame
/// </summary>
void Update()
{
// check for pausing game
if (Input.GetKeyDown("escape"))
{
MenuManager.GoToMenu(MenuName.Pause);
}
}
}
We originally tried to use the Input GetAxis method with a new Pause Input axis to detect that the
Escape key had been pressed, but we just ended up infinitely instantiating the PauseMenu prefab as
though the Escape key was interpreted as being pressed on every Update (the GetAxisRaw method didn't
work either). Everything worked fine if we had a breakpoint set on the line that calls the MenuManager
GoToMenu method and continued on from that breakpoint each time, but it didn't work running normally.
That's why we're using the Input GetKeyDown method instead.
Speaking of the MenuManager GoToMenu method, we needed to implement the MenuName.Pause case in
that method:
case MenuName.Pause:
// instantiate prefab
Object.Instantiate(Resources.Load("PauseMenu"));
break;
Attach the FeedTheTeddies script to the Main Camera in the Gameplay scene and execute Test Case 9
starting from the MainMenu scene. Everything should look like it's working fine, but because there's
nothing moving in the Gameplay scene we can't really tell if the game is getting paused and unpaused
correctly for this test case.
In Visual Studio, set breakpoints in the PauseMenu script on both lines of code that change
Time.timeScale. Click Attach to Unity near the top middle of the menu bar. In the editor, run the game
and execute Test Case 9. When the game stops at the first breakpoint, press F10 to Step Over the line of
code in Visual Studio to make sure that line of code doesn't crash. Press F5 in Visual Studio, then click
the Resume button in the Game view of the editor. When the game stops at the second breakpoint, press
F10 to Step Over the line of code in Visual Studio to make sure that line of code doesn't crash. Select
Run > Stop from the top menu bar in Visual Studio, then stop the game in the editor. Everything should
work fine, so Test Case 9 passes.
440 Chapter 20
Test Case 10
Clicking Quit Button on Pause Menu
Step 1. Click Play button on Main Menu. Expected Result: Move to Difficulty Menu
Step 2. Click Easy button on Difficulty Menu. Expected Result: Move to gameplay screen for easy game
Step 3. Press Escape key. Expected Result: Game paused and Pause Menu displayed on top of game
Step 4. Press Quit button on Pause menu. Expected Result: Move to Main Menu
Step 5. Click Quit button on Main Menu. Expected Result: Exit game
Okay, only one more menu test case left. In Test Case 10, we click the Quit button on the Pause Menu to
quit the game and return to the Main Menu.
/// <summary>
/// Handles the on click event from the Quit button
/// </summary>
public void HandleQuitButtonOnClickEvent()
{
// unpause game, destroy menu, and go to main menu
Time.timeScale = 1;
Destroy(gameObject);
MenuManager.GoToMenu(MenuName.Main);
}
You might be wondering why we bother unpausing the game when we're quitting the game anyway. We
do this so that if we return to the Main Menu, then play another game, the new game doesn't start as
paused. That's another good reason to encapsulate all the pause/unpause functionality in the PauseMenu
script.
Drag the PauseMenu prefab from the Project window into the Hierarchy window. Add the Quit button to
the PauseMenu canvas and add the HandleQuitButtonOnClickEvent method as a listener for On Click
() on the new Quit button. Go to the Prefab area near the top of the Inspector, click the Overrides
dropdown, and click the Apply All button on the right to apply your changes to the prefab. Delete the
PauseMenu from the Gameplay scene.
Execute Test Case 10, using the debugger to confirm that the code that sets Time.timeScale in the
HandleQuitButtonOnClickEvent method doesn't crash. Test Case 10 also passes.
The key game objects in our game are the burger avatar for the player, the french fries that the burger
shoots, the teddy bears that are periodically spawned, and the teddy bear projectiles the teddy bears
shoot. We should include the HUD as a game object as well, since it will also interact with other game
objects. The UML for the new HUD script is shown below.
So, what are the object interactions for the burger? There are actually four: the burger shooting
(instantiating) french fries, the burger colliding with french fries, the burger colliding with a teddy bear,
and the burger colliding with a teddy bear projectile.
442 Chapter 20
The burger shooting french fries really doesn't require further detail, though we'll of course have to
implement the shooting functionality in a Burger script. If the burger collides with french fries, we'll
simply destroy the french fries. If the player is bad enough to run into their own french fries, those
french fries don't get to go kill something else in the game, but we're nice enough not to damage the
burger when that happens. We'll implement this functionality in the Burger script as well.
What should happen when the burger collides with a teddy bear or a teddy bear projectile? The burger
will take damage from the collision, with the burger's health indicated by a health bar in the HUD. The
teddy bear or teddy bear projectile will be destroyed as a result of the collision. Some of this processing
will occur in the Burger script, while updating the health bar will happen in a HUD script. Because the
Burger script shouldn't know about the HUD script, we'll use the event system to indicate that the burger's
health has changed. The UML for the Burger script is shown below.
French fries will collide with teddy bears, other french fries, or teddy bear projectiles. If the french fries
collide with other french fries or teddy bear projectiles, both participants in the collision will be
destroyed. This punishes the player if they're foolish enough to shoot their own french fries, but also
gives them access to a strategy where they shoot the teddy bear projectiles with their french fries to
protect their burger. This processing will happen in a FrenchFries script.
When french fries collide with a teddy bear, both participants in the collision will be destroyed. This
collision is worth points for the player, so the score in the HUD will be increased as well. Some of this
processing will happen in the FrenchFries script, but because the FrenchFries script shouldn't know
about the HUD script, we'll use the event system to indicate that points should be added to the score.
Here's the UML for the FrenchFries script:
Putting It All Together 443
Teddy bears collide with the burger, other teddy bears, teddy bear projectiles, or french fries. Collisions
between a teddy bear and a burger and a teddy bear and french fries have already been discussed above.
When a teddy bear collides with another teddy bear, they'll bounce off each other using the Unity
physics system. When a teddy bear collides with a teddy bear projectile, the teddy bear projectile will be
destroyed; this processing will happen in a TeddyBear script. The UML for the TeddyBear script is
shown below.
Teddy bear projectiles collide with the burger, teddy bears, french fries, and other teddy bear projectiles.
The first three collisions have already been discussed above. When a teddy bear projectile collides with
another teddy bear projectile, both participants in the collision will be destroyed. This processing will
happen in a TeddyBearProjectile script. The UML for the TeddyBearProjectile script is shown
below. Although this looks almost identical to the UML for the FrenchFries script, we'll find a number
of important differences between them when we Write the Code.
444 Chapter 20
We'll undoubtedly include other components in our solution when we Write the Code, but this is a good
analysis of how the objects in our game will interact.
Test Case 11
Watch Game Timer Count Down
Step 1. Click Play button on Main Menu. Expected Result: Move to Difficulty Menu
Step 2. Click Easy button on Difficulty Menu. Expected Result: Move to gameplay screen for easy game
Step 3. Wait for game timer to reach 0. Expected Result: High Score Menu displayed above Gameplay
scene
Step 4. Click Quit button on High Score Menu. Expected Result: Move to Main Menu
Step 5. Click X in corner of player. Expected Result: Exit game
Test Case 12
Moving Burger Around Screen
Step 1. Click Play button on Main Menu. Expected Result: Move to Difficulty Menu
Step 2. Click Easy button on Difficulty Menu. Expected Result: Move to gameplay screen for easy game
Step 3. Move burger around screen using arrow keys. Expected Result: Burger moves around screen,
staying in the borders of the screen.
Step 4. Click X in corner of player. Expected Result: Exit game
Test Case 13
Watch Teddy Bears Spawning and Moving Around Screen
Step 1. Click Play button on Main Menu. Expected Result: Move to Difficulty Menu
Step 2. Click Easy button on Difficulty Menu. Expected Result: Move to gameplay screen for easy game
Step 3. Watch teddy bears spawning and moving around screen. Expected Result: Teddy bears move
around screen, staying in the borders of the screen. Teddy bears bounce off each other on collision
Step 4. Click X in corner of player. Expected Result: Exit game
Putting It All Together 445
Test Case 14
Collide Burger with Teddy Bear
Step 1. Click Play button on Main Menu. Expected Result: Move to Difficulty Menu
Step 2. Click Easy button on Difficulty Menu. Expected Result: Move to gameplay screen for easy game
Step 3. Move burger to collide with teddy bear. Expected Result: Teddy bear explodes. Health bar
shows reduced health
Step 4. Click X in corner of player. Expected Result: Exit game
Test Case 15
Shoot French Fries
Step 1. Click Play button on Main Menu. Expected Result: Move to Difficulty Menu
Step 2. Click Easy button on Difficulty Menu. Expected Result: Move to gameplay screen for easy game
Step 3. Shoot french fries using the space bar. Expected Result: French fries move straight up from
burger when shot. Firing rate controlled when space bar held down
Step 4. Click X in corner of player. Expected Result: Exit game
Test Case 16
Collide Burger with French Fries
Step 1. Click Play button on Main Menu. Expected Result: Move to Difficulty Menu
Step 2. Click Easy button on Difficulty Menu. Expected Result: Move to gameplay screen for easy game
Step 3. Move burger to collide with french fries. Expected Result: French fries explode. Health bar
doesn't change
Step 4. Click X in corner of player. Expected Result: Exit game
Test Case 17
Collide French Fries with Teddy Bear
Step 1. Click Play button on Main Menu. Expected Result: Move to Difficulty Menu
Step 2. Click Easy button on Difficulty Menu. Expected Result: Move to gameplay screen for easy game
Step 3. Shoot french fries into collision with teddy bear. Expected Result: French fries and teddy bear
explode. Score increases
Step 4. Click X in corner of player. Expected Result: Exit game
Test Case 18
Watch Teddy Bears Shoot Teddy Bear Projectiles
Step 1. Click Play button on Main Menu. Expected Result: Move to Difficulty Menu
Step 2. Click Easy button on Difficulty Menu. Expected Result: Move to gameplay screen for easy game
Step 3. Watch teddy bears periodically shoot teddy bear projectiles. Expected Result: Teddy bear
projectiles move straight down from teddy bear when shot
Step 4. Click X in corner of player. Expected Result: Exit game
Test Case 19
Collide Burger with Teddy Bear Projectile
Step 1. Click Play button on Main Menu. Expected Result: Move to Difficulty Menu
Step 2. Click Easy button on Difficulty Menu. Expected Result: Move to gameplay screen for easy game
Step 3. Move burger to collide with teddy bear projectile. Expected Result: Teddy bear projectile
explodes. Health bar shows reduced health
Step 4. Click X in corner of player. Expected Result: Exit game
446 Chapter 20
Test Case 20
Damage Burger until Health is 0
Step 1. Click Play button on Main Menu. Expected Result: Move to Difficulty Menu
Step 2. Click Easy button on Difficulty Menu. Expected Result: Move to gameplay screen for easy game
Step 3. Move burger to collide with teddy bears and teddy bear projectiles until health is 0. Expected
Result: High Score Menu displayed above Gameplay scene. All moving objects are paused
Step 4. Click X in corner of player. Expected Result: Exit game
Test Case 21
Collide French Fries with Teddy Bear Projectile
Step 1. Click Play button on Main Menu. Expected Result: Move to Difficulty Menu
Step 2. Click Easy button on Difficulty Menu. Expected Result: Move to gameplay screen for easy game
Step 3. Shoot french fries into collision with teddy bear projectile. Expected Result: French fries and
teddy bear projectile explode. No change in score
Step 4. Click X in corner of player. Expected Result: Exit game
Test Case 22
Watch Teddy Bear Collide with Teddy Bear Projectile
Step 1. Click Play button on Main Menu. Expected Result: Move to Difficulty Menu
Step 2. Click Hard button on Difficulty Menu. Expected Result: Move to gameplay screen for hard game
Step 3. Watch until a teddy bear collides with a teddy bear projectile. Expected Result: Teddy bear
projectile explodes
Step 4. Click X in corner of player. Expected Result: Exit game
Test Case 23
Watch Teddy Bear Projectile Collide with Teddy Bear Projectile
Step 1. Click Play button on Main Menu. Expected Result: Move to Difficulty Menu
Step 2. Click Hard button on Difficulty Menu. Expected Result: Move to gameplay screen for hard game
Step 3. Watch until a teddy bear projectile collides with a teddy bear projectile. Expected Result: Both
teddy bear projectiles explode
Step 4. Click X in corner of player. Expected Result: Exit game
The test cases above include testing all the interactions we identified in the Design a Solution step
except for french fries colliding with french fries. If you think about this, because all french fries move
at the same speed in the same direction (up), there's actually no way to make one french fries collide
with another one. We therefore won't implement or test that interaction. We also included several test
cases (11 and 20) that capture the requirement that the High Score Menu is displayed when the game is
over.
20.8. Write the Code and Test the Code (Basic Gameplay: Stupid Teddies)
Before moving on, remove the effects of gravity from the game world by selecting Edit > Project
Settings >Physics 2D and setting the Y component of Gravity to 0.
To pass Test Case 11, we'll need to implement a HUD containing the timer text, a game timer, and two
timer-related events: a TimerChangedEvent and a TimerFinishedEvent. This work is very similar to
what we did in Section 19.4., so we don't duplicate that discussion here. The most significant changes
we made were to support using our new event system. Look at the code accompanying this chapter for
the details if you'd like.
Putting It All Together 447
As we added the game timer support to our FeedTheTeddies script (which we changed to an
IntEventInvoker, by the way), we encountered our first constant, which determines how long the
game lasts. We know that before we're done with our game we'll be using CSV configuration data, so
we decided to ease our transition to that approach by implementing a ConfigurationUtils class to
return all the configurable data for our game. At this point in our implementation, the properties in that
class will simply return hard-coded values, but by the time we're done it will return values from the CSV
file. Each time we get ready to declare a constant in our code, we'll think about whether it should be a
tunable value (and therefore in ConfigurationUtils) or whether it should be a non-tunable constant
hard-coded into the game.
After doing the work discussed above, our test case works fine until it crashes in the following code in
the MenuManager GoToMenu method:
case MenuName.HighScore:
Recall that we wrote this code when we were going to the High Score Menu from the Main Menu, so we
deactivated the main menu canvas before instantiating the HighScoreMenu prefab. When we go to the
High Score Menu from the Gameplay scene, there is no main menu canvas to deactivate. That means the
GameObject Find method returns null and we get a NullReferenceException when we try to call the
SetActive method on that (null) object. There's an easy fix, of course, to make the code work in both
scenarios:
case MenuName.HighScore:
Although Test Case 11 now passes, we want to point out that the location of the timer text on the screen
doesn't look right when we run at full screen or don't pick exactly the resolution we've been testing at
when using Build Settings ... to run the game in the player. Of course, anyone you distribute your game
to will be using the player (ignoring a web deployment), so we need to fix this problem.
This is an easy fix. Select the timer text element in the Hierarchy window and click the gray box above
Anchors in the Rect Transform component in the Inspector. Set the Anchor to the top right anchor preset
and you're good to go.
Well, maybe not quite yet. Although setting the anchor properly locates the text element correctly, what
about its size? Shouldn't the text be larger at higher resolutions so it takes up the same amount of “screen
real estate?”
448 Chapter 20
The answer is yes, of course. This is also an easy fix. Select the HUD canvas in the Hierarchy window
and set the UI Scale Mode in the Canvas Scaler component to Scale with Screen Size and set the
Reference Resolution X and Y values appropriately. You should also make sure the Match value is set to
0.5. Basically, these changes make sure scaling works properly across different aspect ratios.
If you execute Test Case 11 with a variety of resolutions, you'll see that the timer text is now
consistently placed in a reasonable location and is the appropriate size as well. In fact, before moving on
you should go make these changes to all the menu canvases in the game (including prefabs) to make
sure the menus also scale properly based on the selected resolution.
Test Case 12
Moving Burger Around Screen
Step 1. Click Play button on Main Menu. Expected Result: Move to Difficulty Menu
Step 2. Click Easy button on Difficulty Menu. Expected Result: Move to gameplay screen for easy game
Step 3. Move burger around screen using arrow keys. Expected Result: Burger moves around screen,
staying in the borders of the screen.
Step 4. Click X in corner of player. Expected Result: Exit game
This is a lot like the work we did to set up a fish we could move around the screen in previous games, so
we won't go over the details again here. After you've added the burger sprite, added it to the scene,
added a Box Collider 2D, and implemented and attached a new Burger script to the game object to
handle movement, execute Test Case 12.
Your game should look something like the figure below at this point.
Test Case 13
Watch Teddy Bears Spawning and Moving Around Screen
Step 1. Click Play button on Main Menu. Expected Result: Move to Difficulty Menu
Step 2. Click Easy button on Difficulty Menu. Expected Result: Move to gameplay screen for easy game
Step 3. Watch teddy bears spawning and moving around screen. Expected Result: Teddy bears move
around screen, staying in the borders of the screen. Teddy bears bounce off each other on collision
Step 4. Click X in corner of player. Expected Result: Exit game
Putting It All Together 449
Before we start working on our TeddyBear and TeddyBearSpawner scripts, we want to point out a flaw
in the way we've used edge colliders to keep game objects on the screen up to this point. You may have
noticed that everything seems to work fine in the Game view, but if you run the game in the player the
game objects may not stay on the screen as they should (sometimes bouncing too soon, sometimes
leaving the screen before bouncing) depending on the resolution the player selected. That means that
anyone playing our game can mess up the gameplay just by changing the player resolution! This is
obviously a bad thing, so let's solve that problem here.
We add a new ScreenUtils script to add the edge colliders on the four sides of the screen at runtime
and attach the script to the Main Camera in the Gameplay scene. Here's the code we have at the
beginning of the Start method:
We figure out each end point for the collider in screen coordinates, then convert to world coordinates,
then set the Vector2 components of the collider end point appropriately. At the end of the block of code,
we set the end points of the actual collider and set the Physics 2D material for the collider as well.
Adding the right, top, and bottom colliders is similar.
Our TeddyBear and TeddyBearSpawner scripts and our TeddyBear prefab are almost identical to those
we used in the fish game in the previous chapter; the only difference is that we use some properties in
the ConfigurationUtils class instead of constants in the scripts. The ConfigurationUtils properties
sometimes use new properties in the DifficultyUtils class to return the appropriate difficulty-specific
values. For example, because teddy bears spawn faster at higher difficulties, the minimum spawn delay
is no longer constant. Here's the new MinSpawnDelay property in the DifficultyUtils class:
/// <summary>
/// Gets the min spawn delay for teddy bear spawning
/// </summary>
450 Chapter 20
/// <value>minimum spawn delay</value>
public static float MinSpawnDelay
{
get
{
switch (difficulty)
{
case Difficulty.Easy:
return ConfigurationUtils.EasyMinSpawnDelay;
case Difficulty.Medium:
return ConfigurationUtils.MediumMinSpawnDelay;
case Difficulty.Hard:
return ConfigurationUtils.HardMinSpawnDelay;
default:
return ConfigurationUtils.EasyMinSpawnDelay;
}
}
}
It might seem a little awkward to you that the property above needs to access the EasyMinSpawnDelay,
MediumMinSpawnDelay, or HardMinSpawnDelay property in the ConfigurationUtils class, but we do
it that way so that the ConfigurationUtils class is the only access point for the configuration data
from the CSV file. Here's the new MinSpawnDelay property in the ConfigurationUtils class:
/// <summary>
/// Gets the min spawn delay for teddy bear spawning
/// </summary>
/// <value>minimum spawn delay</value>
public static float MinSpawnDelay
{
get { return DifficultyUtils.MinSpawnDelay; }
}
See the code accompanying the chapter to look at the changes in detail as you see fit.
Test Case 13 now passes, but there is one gameplay tweak we want to make. At this point there's no
limit to how many bears can appear on the screen. Allowing unlimited teddies is of course one
reasonable game design decision, but we prefer having a difficulty-specific limit instead. We added the
appropriate properties to ConfigurationUtils and DifficultyUtils to support that, we tagged the
TeddyBear prefab with a new TeddyBear tag, and we made sure the spawner doesn't spawn a new bear
if the scene already contains the max number of bears for the game difficulty.
Test Case 14
Collide Burger with Teddy Bear
Step 1. Click Play button on Main Menu. Expected Result: Move to Difficulty Menu
Step 2. Click Easy button on Difficulty Menu. Expected Result: Move to gameplay screen for easy game
Step 3. Move burger to collide with teddy bear. Expected Result: Teddy bear explodes. Health bar
shows reduced health
Step 4. Click X in corner of player. Expected Result: Exit game
We can of course get to Step 3 in Test Case 14 and run the burger into a teddy bear, but neither one of
the expected results occurs. Let's blow up the teddy bear first, then work on reducing the burger's health.
Putting It All Together 451
Start by building an Explosion prefab like we did in Section 8.2. We can detect the collision in either the
Burger script or the TeddyBear script. Because the collision affects the burger's health, let's detect it in
the Burger script. We've of course detected collisions between fish and teddy bears before, so the
following method should look familiar to you:
/// <summary>
/// Processes collisions with other game objects
/// </summary>
/// <param name="coll">collision info</param>
void OnCollisionEnter2D(Collision2D coll)
{
// if colliding with teddy bear, destroy teddy and reduce health
if (coll.gameObject.CompareTag("TeddyBear"))
{
Instantiate(prefabExplosion,
coll.gameObject.transform.position, Quaternion.identity);
Destroy(coll.gameObject);
}
}
Run the game now and you should be able to explode teddy bears by running them over with your
burger (come on, how many times do you get to say a sentence like that?).
The other expected result we have is that the health bar should show reduced health. Of course, it's hard
for that to happen here because we don't even have a health bar yet! Let's get to work on that now.
Luckily, Unity provides a Slider UI component that will work great for this. Right click the HUD canvas
in the Gameplay scene and select UI > Slider. Use the Anchor Presets and the Y location in the Rect
Transform component to center the slider at the top center of the game screen, aligned vertically with the
timer text. Change the name of the slider to HealthBar.
Sliders can actually be interactable, like when you have the player use a slider to adjust music volume in
a game, but that's not how we're using the slider here. Expand the HealthBar in the Hierarchy window,
right click Handle Slide Area, and select Delete. In the Slider component in the Inspector, uncheck the
Interactable check box. Change Transition just below the Interactable check box to None, because that
transition setting is for when the player is interacting with the component.
Next, change both the Max Value and Value to 100, since health will go from 0 to 100 and the player
starts with 100 health. You should also check the Whole Numbers check box because our player health
will be an integer.
Clearly, the Burger script (which detects the collision with the teddy bear) shouldn't know anything
about the HUD, so we need to add a new HealthChangedEvent and link the burger as an invoker and
the HUD as a listener for that event through the EventManager. First, we add a new
HealthChangedEvent value to the EventName enumeration. On the Burger side, we change Burger to
be a child class of IntEventInvoker, add a health field
// health support
int health = 100;
452 Chapter 20
set the unityEvent field to a new HealthChangedEvent object and add the script as an event invoker in
the Start method
and add two more lines to the if body for a collision with a teddy bear (using a new
ConfigurationUtils property as well)
The first line of code subtracts the damage the bear inflicts from the current health value, then sets
health to whichever is higher, that value or 0. Basically, the code adjusts the health properly without
letting it go below 0. The second line of code invokes the event with the new health value.
On the HUD side, we add a new field to hold a reference to the health bar slider
// health support
[SerializeField]
Slider healthBar;
write a new method to change the value of the health bar when the health changes
/// <summary>
/// Handles the health changed event by changing
/// the health bar value
/// </summary>
/// <param name="value">health value</param>
void HandleHealthChangedEvent(int value)
{
healthBar.value = value;
}
Select the HUD canvas in the Hierarchy window, then drag the HealthBar onto the Health Bar field of
the HUD script in the Inspector.
If you execute Test Case 14 again, you'll see that we now get both expected results. Be sure to execute
Test Case 14 starting at the Main Menu scene to make sure the Burger Start method doesn't crash
trying to add itself as an invoker to an uninitialized EventManager.
Putting It All Together 453
Test Case 15
Shoot French Fries
Step 1. Click Play button on Main Menu. Expected Result: Move to Difficulty Menu
Step 2. Click Easy button on Difficulty Menu. Expected Result: Move to gameplay screen for easy game
Step 3. Shoot french fries using the space bar. Expected Result: French fries move straight up from
burger when shot. Firing rate controlled when space bar held down
Step 4. Click X in corner of player. Expected Result: Exit game
If we're going to have the burger shoot french fries, we're going to want a french fries prefab we can
instantiate as the burger shoots. Add a french fries sprite to the sprites\gameplay folder in the Project
window, drag the sprite into the Hierarchy window, and rename the game object in the Hierarchy
window FrenchFries. Add a new FrenchFries tag to the tags in the game and add that tag to the
FrenchFries game object. We're going to apply an impulse force to get the french fries moving when we
shoot them, so add a Rigidbody2D component, freeze rotation in Z, and set the Interpolate field to
Interpolate. We're also going to need to detect collisions between the french fries and other game
objects, so add a Box Collider 2D component as well. We don't actually want the french fries to bounce
off the edge colliders surrounding the screen or the other game objects, so check the Is Trigger
checkbox.
As we indicated in the Design a Solution step, we're going to be using a FrenchFries script for some of
our game functionality, so we'll add that script now. At this point, the script will just get the french fries
moving when they're instantiated, but we'll add more functionality as we go along.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// French fries
/// </summary>
public class FrenchFries : MonoBehaviour
{
/// <summary>
/// Start is called before the first frame update
/// </summary>
void Start()
454 Chapter 20
{
// apply impulse force to get projectile moving
GetComponent<Rigidbody2D>().AddForce(
new Vector2(0, ConfigurationUtils.FrenchFriesImpulseForce),
ForceMode2D.Impulse);
}
}
Drag the FrenchFries script onto the FrenchFries game object in the Hierarchy window, drag the
FrenchFries game object from the Hierarchy window onto the Prefabs folder in the Project window, and
delete the FrenchFries game object from the Hierarchy window.
Now that we have the prefab built we can move over to the Burger script to add the french fries
shooting functionality. We need to change the Positive Button in the Fire1 input axis to space (instead of
the default left ctrl), add a prefabFrenchFries field to the Burger script, mark that field with
[SerializeField], and populate that field in the Inspector with the FrenchFries prefab. Then we add
the following code at the end of the Burger Update method:
Go ahead and run the test case. The first part of Step 3 works, with french fries shooting straight up from
the burger, but the firing rate is definitely NOT being controlled!
We'll control the firing rate by using a cooldown timer. When the player presses the space bar, we'll
instantiate a french fries and start the cooldown timer. While the cooldown timer is running, we won't
instantiate any more french fries. Once the cooldown timer finishes, we'll enable shooting again.
That approach gives the player an “automatic burger” with a particular firing rate, but we can also
support a different player tactic where the player repeatedly presses and releases the space bar as quickly
as they can (spam, spam, spam, spam, ...). This is more work for the player, but they'll be able to achieve
a faster firing rate shooting that way.
Almost of our code will be in the Burger script. We'll keep track of whether or not the player can shoot
using a Boolean flag, and we'll also need a countdown timer as a field:
// firing support
bool canShoot = true;
Timer countdownTimer;
/// <summary>
/// Reenables shooting when the cooldown timer finishes
/// </summary>
void HandleCooldownTimerFinishedEvent()
{
canShoot = true;
Putting It All Together 455
}
and create the countdown timer and add the listener at the end of the Start method:
Before we finish off the Burger code, we need to add a Stop method to our Timer class. That way,
when the player releases the space bar we can immediately stop the timer and reenable shooting. Here's
the Timer Stop method:
/// <summary>
/// Stops the timer
/// </summary>
public void Stop()
{
started = false;
running = false;
}
We need to set both the started and the running flags to false to ensure the Timer Finished
property still works properly.
Finally, we change the shooting code at the end of the Burger Update method:
The block of code above stops the cooldown timer and immediately reenables shooting.
We added another condition to the Boolean expression for our if statement so we only shoot french fries
if the canShoot flag is true. At the start of the if body, we start the cooldown timer and set canShoot to
false to disable shooting until the cooldown timer finishes or the player releases the space bar.
The test case now passes fine, but we actually have a huge inefficiency in our game. You may not have
realized it yet, but every french fries we fire stays in the game world forever, even after it leaves the
456 Chapter 20
screen (you can actually see this in the Hierarchy window as you fire french fries, because none of them
are removed from the Hierarchy window as you play). This is really bad because we waste CPU time
updating all those projectiles even though they're really out of the game. We can solve this problem by
adding the following method to the FrenchFries script:
Unity automatically calls the OnBecameInvisible method when the game object the script is attached to
can no longer be seen by the camera, so this is exactly what we need. Execute Test Case 15 again to
verify that the FrenchFries objects are destroyed when they leave the screen.
Test Case 16
Collide Burger with French Fries
Step 1. Click Play button on Main Menu. Expected Result: Move to Difficulty Menu
Step 2. Click Easy button on Difficulty Menu. Expected Result: Move to gameplay screen for easy game
Step 3. Move burger to collide with french fries. Expected Result: French fries explode. Health bar
doesn't change
Step 4. Click X in corner of player. Expected Result: Exit game
In our default tuning configuration, the burger will be slower than the french fries, so we'll never
actually be able to collide the burger with the french fries. Because we're going to include the
configuration data in an CSV file, someone could change the settings so the burger is faster than the
french fries, making this collision possible. We'll implement and test this test case by changing the
ConfigurationUtils FrenchFriesImpulseForce to slow down the french fries to support that
collision, then change that value back to what we think is more reasonable for actual gameplay.
We said we'd do this work in the Burger script ... but we have a problem! We want to handle the
processing in the inherited MonoBehaviour OnTriggerEnter2D method, which is called when “another
object enters a trigger collider attached to this object”. Although the Burger game object has a Box
Collider 2D attached to it, that collider is not marked as a trigger because we use that collider to keep the
burger on the screen. The FrenchFries game object does have a trigger collider attached to it, though, so
we could add the required processing to the FrenchFries script instead, but looking ahead, that's
probably not the correct solution.
Why not? Because when we get to detecting collisions between the Burger and a teddy bear projectile, it
will be much cleaner if the Burger script handle reducing the player's health rather than having the
TeddyBearProjectile script do that. Let's go with our original design decision to have the Burger
handle this processing. Add another Box Collider 2D component to the Burger game object, but check
the Is Trigger checkbox for the new collider. Now add the following method to the Burger script:
/// <summary>
/// Processes trigger collisions with other game objects
/// </summary>
/// <param name="other">information about the other collider</param>
void OnTriggerEnter2D(Collider2D other)
Putting It All Together 457
{
// if colliding with french fries, destroy french fries
if (other.gameObject.CompareTag("FrenchFries"))
{
Instantiate(prefabExplosion,
other.gameObject.transform.position, Quaternion.identity);
Destroy(other.gameObject);
}
}
If you try to execute Test Case 16 now, you'll see an explosion on top of the burger every time you fire.
We have a problem that we didn't notice before because the french fries move so fast. Recall that in our
Burger Update code, we set the new french fries location to
transform.position
which is the center of the burger. Unfortunately, that means that the french fries are immediately
colliding with the burger, so they're immediately destroyed. The best way to solve this problem is to
actually instantiate the french fries slightly above the burger when they're fired so they don't actually
start out in collision with the burger. We changed our french fries instantiation code to
After executing Test Case 16 to confirm that it passes, change the ConfigurationUtils
FrenchFriesImpulseForce to a more reasonable value.
Test Case 17
Collide French Fries with Teddy Bear
Step 1. Click Play button on Main Menu. Expected Result: Move to Difficulty Menu
Step 2. Click Easy button on Difficulty Menu. Expected Result: Move to gameplay screen for easy game
Step 3. Shoot french fries into collision with teddy bear. Expected Result: French fries and teddy bear
explode. Score increases
Step 4. Click X in corner of player. Expected Result: Exit game
As we mentioned above, some of this processing will happen in the FrenchFries script, but we'll use
the event system to indicate that points should be added to the score. We'll start by creating a new
PointsAddedEvent, adding FrenchFries as an invoker for that event, and adding the HUD as a listener
for that event. We also add score text to the HUD to display the current score. This is almost identical to
work we've done in previous chapters, so refer to the code accompanying the chapter for the details.
Next, we add a new field to the FrenchFries script, mark that field with [SerializeField], and
populate that field in the Inspector with the Explosion prefab. Then we add the following method to the
FrenchFries script:
/// <summary>
/// Processes trigger collisions with other game objects
/// </summary>
458 Chapter 20
/// <param name="other">information about the other collider</param>
void OnTriggerEnter2D(Collider2D other)
{
// if colliding with teddy bear, add score and destroy teddy bear and self
if (other.gameObject.CompareTag("TeddyBear"))
{
unityEvent.Invoke(ConfigurationUtils.BearPoints);
Instantiate(prefabExplosion,
other.gameObject.transform.position, Quaternion.identity);
Destroy(other.gameObject);
Instantiate(prefabExplosion,
transform.position, Quaternion.identity);
Destroy(gameObject);
}
}
We instantiate two explosions because we're thinking of both the teddy bear and the french fries as being
stuffed with C4!
We've actually added another potential inefficiency with the changes we just made for this new
functionality, though we won't see that inefficiency in our implementation. Remember, the
EventManager holds a dictionary of the invokers for each event, including the PointsAddedEvent.
When the EventManager AddListener method is called, that method adds the listener to each of the
invokers for the event. Once a particular instance of a french fries game object leaves the scene, we
destroy that game object, but it stays in the EventManager as an invoker for the PointsAddedEvent. We
don't see that inefficiency here because the HUD script is the only listener for this event and it adds itself
before any invokers are added, but it's in general risky to make assumptions about the order in which
invokers and listeners will be added.
/// <summary>
/// Removes the given invoker for the given event name
/// </summary>
/// <param name="eventName">event name</param>
/// <param name="invoker">invoker</param>
public static void RemoveInvoker(EventName eventName,
IntEventInvoker invoker)
{
// remove invoker from dictionary
invokers[eventName].Remove(invoker);
}
and we've removed that inefficiency. Execute Test Case 17 to see that it passes.
Test Case 18
Watch Teddy Bears Shoot Teddy Bear Projectiles
Step 1. Click Play button on Main Menu. Expected Result: Move to Difficulty Menu
Step 2. Click Easy button on Difficulty Menu. Expected Result: Move to gameplay screen for easy game
Step 3. Watch teddy bears periodically shoot teddy bear projectiles. Expected Result: Teddy bear
projectiles move straight down from teddy bear when shot
Step 4. Click X in corner of player. Expected Result: Exit game
It's time for the teddy bears to fight back. We start by adding a TeddyBearProjectile prefab that moves
straight down when it's added to the scene. Here's the TeddyBearProjectile script:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// A teddy bear projectile
/// </summary>
public class TeddyBearProjectile : MonoBehaviour
{
/// <summary>
/// Start is called before the first frame update
/// </summary>
void Start()
{
// apply impulse force to get projectile moving
GetComponent<Rigidbody2D>().AddForce(
new Vector2(0,
-ConfigurationUtils.TeddyBearProjectileImpulseForce),
ForceMode2D.Impulse);
}
/// <summary>
/// Called when the teddy bear projectile becomes invisible
/// </summary>
void OnBecameInvisible()
460 Chapter 20
{
// destroy the game object
Destroy(gameObject);
}
}
Note that the Y component of the Vector2 we use to apply the impulse force is negative so the force
pushes the teddy bear projectile down. We of course also destroy teddy bear projectiles when they leave
the game just like we did for french fries for the same efficiency reason.
Next, we add a Timer field to the TeddyBear script so we can have the teddy bear periodically shoot
teddy bear projectiles, and we also add a field to hold a TeddyBearProjectile prefab we instantiate when
it's time for the teddy bear to shoot one.
// shooting support
[SerializeField]
GameObject prefabTeddyBearProjectile;
Timer shootTimer;
We add a new HandleTimerFinishedEvent method as well to shoot a teddy bear projectile and restart
the timer with a random duration:
/// <summary>
/// Shoots a teddy bear projectile, resets the timer
/// duration, and restarts the timer
/// </summary>
void HandleTimerFinishedEvent()
{
// shoot a teddy bear projectile
Vector3 projectilePos = transform.position;
projectilePos.y -= TeddyBearProjectilePositionOffset;
Instantiate(prefabTeddyBearProjectile, projectilePos,
Quaternion.identity);
We realized that we needed to offset the teddy bear projectile from the teddy bear because (later) we'd
have the same problem we had shooting french fries from the center of the burger.
Finally, we add the timer initialization code at the end of the Start method:
We actually realized at this point that the last two lines of code in the HandleTimerFinishedEvent
method and in the block of code above are identical. Because we hate duplicated code, we pulled those
lines out into a separate StartRandomTimer method and called that method from the two places in the
code above. We also refactored similar code in the TeddyBearSpawner script the same way.
Because the teddy bear firing rate is difficulty-dependent, we added our usual properties to the
ConfigurationUtils and DifficultyUtils classes to support that.
Execute Test Case 18 and you'll see the teddy bears starting to fight back.
Test Case 19
Collide Burger with Teddy Bear Projectile
Step 1. Click Play button on Main Menu. Expected Result: Move to Difficulty Menu
Step 2. Click Easy button on Difficulty Menu. Expected Result: Move to gameplay screen for easy game
Step 3. Move burger to collide with teddy bear projectile. Expected Result: Teddy bear projectile
explodes. Health bar shows reduced health
Step 4. Click X in corner of player. Expected Result: Exit game
Of course, at this point the teddy bears' attempts to fight back are futile since the teddy bear projectiles
don't damage the burger. We can fix that by adding an else if clause to our Burger OnTriggerEnter2D
method:
else if (other.gameObject.CompareTag("TeddyBearProjectile"))
{
// if colliding with teddy bear projectile, destroy projectile and
// reduce health
Instantiate(prefabExplosion,
other.gameObject.transform.position, Quaternion.identity);
Destroy(other.gameObject);
health = Mathf.Max(0, health - ConfigurationUtils.BearProjectileDamage);
unityEvent.Invoke(health);
}
Test Case 20
Damage Burger until Health is 0
Step 1. Click Play button on Main Menu. Expected Result: Move to Difficulty Menu
Step 2. Click Easy button on Difficulty Menu. Expected Result: Move to gameplay screen for easy game
Step 3. Move burger to collide with teddy bears and teddy bear projectiles until health is 0. Expected
Result: High Score Menu displayed above Gameplay scene. All moving objects are paused
Step 4. Click X in corner of player. Expected Result: Exit game
If you try to execute Test Case 20 now, you'll see that the game keeps going even when the burger
health reaches 0. Before we add the code to go to the High Score Menu in that case, let's do a little
refactoring. At this point, we inflict burger damage in two places: in the OnCollisionEnter2D method
on a collision with a teddy bear and in the OnTriggerEnter2D method on a collision with a teddy bear
projectile. Let's pull that code out into a new TakeDamage method that we call from both those places.
We'll also check if the game is over in that new metho.
462 Chapter 20
What should we do if the game is over? The easiest thing (so, obviously, the wrong thing!) to do would
be to have the method call the MenuManager GoToMenu method to move to the High Score Menu. But
the Burger shouldn't really know about the MenuManager class, so this isn't the best choice. As usual,
we'll use the event system for this, with a new GameOverEvent that the Burger script invokes in the
TakeDamage method if health is 0 and the FeedTheTeddies script listens for. The FeedTheTeddies
script calls the MenuManager GoToMenu method when the GameOverEvent is invoked, which is fine
because this script manages the overall game and already calls that method to go to the pause menu as
appropriate.
There are couple of interesting details to point out here. The GameOverEvent has to be a child class of
UnityEvent<int> even though we don't really need to pass an int when we invoke this event. We need
to do it this way because the parameter for the listener in the EventManager AddListener method is a
UnityAction<int>, so we can only add listeners that listen for UnityEvent<int> events. It would
certainly be possible to make a more robust EventManager that allows different versions of
UnityEvent, but because almost all of our events are UnityEvent<int>, we might as well save
ourselves the complexity and just live with a parameter in the listener method that we don't actually use.
The other thing to point out is that we need to declare a new GameOverEvent field in the Burger class
even though it's an IntEventInvoker containing a unityEvent field. That's because we're already
using the unityEvent field for the HealthChangedEvent, so we can't use it for the GameOverEvent as
well.
Although that all made sense to us when we implemented it, it didn't actually work in practice. When we
ran the code after making the changes described above, the High Score Menu popped up the first time
the burger took damage. Here's why.
Even though our dictionaries in the EventManager have different keys for each EventName, we lose that
distinction when we call the IntEventInvoker AddListener method from within the EventManager
AddListener method. When the FeedTheTeddies script calls the EventManager AddListener method
for the GameOverEvent, the listener actually gets added for the Burger unityEvent field, which as we
mentioned above is for the HealthChangedEvent. Everything worked fine for IntEventInvokers that
only invoked a single event, but now our stupid Burger script needs to invoke two events. Ugh!
Although we could probably come up with a really ugly workaround for this, we have to imagine that
we'd run into this situation a lot for more complex games with more events. Let's actually implement a
robust, general solution to this problem. We start by changing our IntEventInvoker class:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
/// <summary>
/// Extends MonoBehaviour to support invoking
/// one integer argument UnityEvents
/// </summary>
public class IntEventInvoker : MonoBehaviour
{
Putting It All Together 463
protected Dictionary<EventName, UnityEvent<int>> unityEvents =
new Dictionary<EventName, UnityEvent<int>>();
/// <summary>
/// Adds the given listener for the given event name
/// </summary>
/// <param name="eventName">event name</param>
/// <param name="listener">listener</param>
public void AddListener(EventName eventName, UnityAction<int> listener)
{
// only add listeners for supported events
if (unityEvents.ContainsKey(eventName))
{
unityEvents[eventName].AddListener(listener);
}
}
}
Instead of having a single unityEvent field, the IntEventInvoker class now has a dictionary of events
keyed by the event name. That lets each instance of the IntEventInvoker class invoke a number of
different events.
Of course, the change above breaks tons of our existing code! The easiest compilation error to fix is in
the EventManager AddInvoker and AddListener methods, where we simply add the event name as an
argument when we call the IntEventInvoker AddListener method. The other two compilation errors
occur in two different places: whenever we invoke an event and whenever we create the event object.
Let's fix those problems in the FrenchFries script as an example.
The FrenchFries script currently invokes the PointsAddedEvent in the OnTriggerEnter2D method
using
unityEvent.Invoke(ConfigurationUtils.BearPoints);
This doesn't work any more, because we've changed our field (now called unityEvents) to a dictionary.
Here's the revised code:
unityEvents[EventName.PointsAddedEvent].Invoke(
ConfigurationUtils.BearPoints);
We're using the event name to key into our unityEvents dictionary to invoke the required event. We're
definitely “glass half full” kinds of authors, so we'll observe that this might actually make our event
invocation code more readable than it was before because now it explicitly says what event is being
invoked.
We also need to fix where we create our event object, which is currently implemented in the
FrenchFries Start method as
This is also a straightforward fix, because all we need to do is add the new event object to the dictionary
with the appropriate key:
464 Chapter 20
unityEvents.Add(EventName.PointsAddedEvent, new PointsAddedEvent());
Okay, go fix all the compilation errors. It always hurts to break a bunch of our code to make our design
more robust, but this was definitely the right thing for us to do here.
When we execute Test Case 20 now, it almost works. Our expected results say that all moving objects
are paused when the High Score Menu is displayed, but that doesn't happen because we haven't paused
the game when we display the High Score Menu. Also, the High Score Menu says that no games have
been played yet, though we just finished a game. We realize at this point that we need to add an
expected result to our test case:
Test Case 20
Damage Burger until Health is 0
Step 1. Click Play button on Main Menu. Expected Result: Move to Difficulty Menu
Step 2. Click Easy button on Difficulty Menu. Expected Result: Move to gameplay screen for easy game
Step 3. Move burger to collide with teddy bears and teddy bear projectiles until health is 0. Expected
Result: High Score Menu displayed above Gameplay scene with a high score displayed (either from this
game or a previous game with a higher score). All moving objects are paused
Step 4. Click X in corner of player. Expected Result: Exit game
We already implemented pause and unpause functionality in our PauseMenu script, so we'll use the same
ideas to pause the game when the High Score Menu is added to the scene and to unpause the game when
the player clicks the Quit button.
Now let's fix the incorrect message on the High Score Menu. To do that, we need to add a new field in
the FeedTheTeddies script for the HUD and populate that field in the Inspector. We also need to write a
new SetHighScore method in that script and call it from both the HandleGameTimerFinishedEvent
and the HandleGameOverEvent methods before going to the High Score Menu:
/// <summary>
/// Sets the saved high score if we have a new or first high score
/// </summary>
void SetHighScore()
{
HUD hudScript = hud.GetComponent<HUD>();
int currentScore = hudScript.Score;
if (PlayerPrefs.HasKey("High Score"))
{
if (currentScore > PlayerPrefs.GetInt("High Score"))
{
PlayerPrefs.SetInt("High Score", currentScore);
PlayerPrefs.Save();
}
}
else
{
PlayerPrefs.SetInt("High Score", currentScore);
PlayerPrefs.Save();
}
}
Putting It All Together 465
Of course, when we did this we realized that the bodies of the HandleGameTimerFinishedEvent and
the HandleGameOverEvent methods are identical. We created a new EndGame method that sets the high
score and goes to the High Score Menu and just call the EndGame method from both those methods. We
actually thought about replacing those methods with the EndGame method, but we couldn't because the
listener for the TimerFinishedEvent from the game timer doesn't have any parameters and the listener
for the GameOverEvent has a single int parameter.
Test Case 20 now passes. Because we made changes to the HighScoreMenu script, it also makes sense at
this point to run Test Cases 3 and 4 again to make sure the High Score Menu still works properly when
we go to it from the Main Menu.
Test Case 21
Collide French Fries with Teddy Bear Projectile
Step 1. Click Play button on Main Menu. Expected Result: Move to Difficulty Menu
Step 2. Click Easy button on Difficulty Menu. Expected Result: Move to gameplay screen for easy game
Step 3. Shoot french fries into collision with teddy bear projectile. Expected Result: French fries and
teddy bear projectile explode. No change in score
Step 4. Click X in corner of player. Expected Result: Exit game
To implement this functionality, we add the following else if clause to the FrenchFries
OnTriggerEnter2D method:
else if (other.gameObject.CompareTag("TeddyBearProjectile"))
{
// if colliding with teddy bear projectile, destroy projectile and self
Instantiate(prefabExplosion, other.gameObject.transform.position,
Quaternion.identity);
Destroy(other.gameObject);
Instantiate(prefabExplosion, transform.position, Quaternion.identity);
Destroy(gameObject);
}
Test Case 21 passes with the above code included, though we admit we had to slow down the teddy bear
projectiles so we could hit them with french fries!
Test Case 22
Watch Teddy Bear Collide with Teddy Bear Projectile
Step 1. Click Play button on Main Menu. Expected Result: Move to Difficulty Menu
Step 2. Click Hard button on Difficulty Menu. Expected Result: Move to gameplay screen for hard game
Step 3. Watch until a teddy bear collides with a teddy bear projectile. Expected Result: Teddy bear
projectile explodes
Step 4. Click X in corner of player. Expected Result: Exit game
We said in the Design a Solution step that “When a teddy bear collides with a teddy bear projectile, the
teddy bear projectile will be destroyed; this processing will happen in a TeddyBear script.” We're not
actually going to do it that way because the TeddyBearProjectile prefab has a trigger collider but the
TeddyBear prefab doesn't. Adding an additional collider to the TeddyBear prefab would be a bad choice
because it would increase the number of collision checks the Unity engine has to do each frame by a
multiple of the number of teddy bears in the scene. Instead, we'll add a prefabExplosion field and an
OnTriggerEnter2D method to the TeddyBearProjectile script to do the required processing:
466 Chapter 20
/// <summary>
/// Processes trigger collisions with other game objects
/// </summary>
/// <param name="other">information about the other collider</param>
void OnTriggerEnter2D(Collider2D other)
{
// if colliding with teddy bear, destroy self
if (other.gameObject.CompareTag("TeddyBear"))
{
Instantiate(prefabExplosion, transform.position,
Quaternion.identity);
Destroy(gameObject);
}
}
It's not at all unusual to make changes to the design as we work through our implementation steps.
Nobody ever gets the design perfect the first time, so don't feel badly that we didn't.
Before executing Test Case 22, we made the Burger a prefab and removed it from the Gameplay scene.
That way, the only exploding collisions we'd see would be teddy bears colliding with teddy bear
projectiles. After making the TeddyBearProjectilePositionOffset value in the TeddyBear script a
little larger (teddy bear projectiles were exploding when they were fired) the test case passes fine.
Test Case 23
Watch Teddy Bear Projectile Collide with Teddy Bear Projectile
Step 1. Click Play button on Main Menu. Expected Result: Move to Difficulty Menu
Step 2. Click Hard button on Difficulty Menu. Expected Result: Move to gameplay screen for hard game
Step 3. Watch until a teddy bear projectile collides with a teddy bear projectile. Expected Result: Both
teddy bear projectiles explode
Step 4. Click X in corner of player. Expected Result: Exit game
We implement this functionality by adding an else if clause to the OnTriggerEnter2D method in the
TeddyBearProjectile script:
else if (other.gameObject.CompareTag("TeddyBearProjectile"))
{
// if colliding with teddy bear projectile, destroy projectile and self
Instantiate(prefabExplosion, other.gameObject.transform.position,
Quaternion.identity);
Destroy(other.gameObject);
Instantiate(prefabExplosion, transform.position, Quaternion.identity);
Destroy(gameObject);
}
To execute this test case, we left the burger out of the Gameplay scene and commented out the body of
the if statement in the OnTriggerEnter2D method. That way, the only exploding collisions we'd see
would be teddy bears projectiles colliding with teddy bear projectiles.
Test Case 23 passes fine, though it took a long time to actually see two teddy bear projectiles collide!
Putting It All Together 467
We've now finished all the basic gameplay functionality for the game. We've been running our test cases
as we went along, confirming that they all work correctly. As for the menu test cases, if you haven't run
each test case in the player yet you should do that now.
We're going to implement very rudimentary artificial intelligence (AI) here, where a teddy bear moves
toward the burger rather than just moving randomly through the Gameplay scene. Smarter teddy bears
will follow the burger more closely, even as the burger moves around, while less smart teddy bears can
be more easily avoided by the burger.
There are several ways to implement that behavior. One way would be to have the teddy bear move
toward the burger with some probability on every update. Smarter bears would have a higher probability
of doing this (a probability of 1 would mean the teddy bear reorients to the burger on every update) and
less smart teddy bears would have a lower probability. This is certainly a workable solution, but given
the way random number generation works, we might have a smart teddy bear that acts stupid for a while,
then suddenly acts very smart for a while (and vice versa for a less smart teddy bear). This behavior
might seem more erratic than is believeable to a player who selected a difficulty level (Hard) that
implied the enemies would be consistently smart.
The approach we'll use instead leads to steady intelligence at a particular level throughout the game.
We'll have the teddy bears use a timer to indicate when they should reorient toward the burger. Smart
bears will use a shorter timer duration, so they'll follow the burger more closely, while less smart bears
will use a longer timer duration. This is actually a reasonably general approach to use to affect the
strength of AI; in our Battle Paddles game, we have the AI plan a set of actions to take, where the
planning takes place more regularly (at shorter timer durations) for the smarter opponents.
If we use the current game difficulty to affect how often teddy bears reorient toward the burger, how
should we use the game difficulty to affect how teddy bear projectiles behave? In exactly the same way!
If we think of the teddy bear projectiles as “smart projectiles”, they should also move toward the burger,
468 Chapter 20
changing course as the burger changes direction. On harder difficulties, the projectiles will reorient
toward the burger more often, perhaps because they have better sensors and control systems.
We'll implement the homing functionality in a HomingComponent script. The great news is that because
Unity uses a component-based approach, we can attach that script to both the TeddyBear and
TeddyBearProjectile prefabs to get the behavior we want from both those prefabs using a single script.
Here's the UML for the HomingComponent script:
Test Case 25
Watch Medium Teddy Bear and Teddy Bear Projectile Homing
Step 1. Click Play button on Main Menu. Expected Result: Move to Difficulty Menu
Step 2. Click Medium button on Difficulty Menu. Expected Result: Move to gameplay screen for
medium game
Step 3. Watch teddy bears and teddy bear projectiles periodically move toward the burger. Expected
Result: Teddy bears and teddy bear projectiles periodically move toward the burger more often than the
easy game
Step 4. Click X in corner of player. Expected Result: Exit game
Putting It All Together 469
Test Case 26
Watch Hard Teddy Bear and Teddy Bear Projectile Homing
Step 1. Click Play button on Main Menu. Expected Result: Move to Difficulty Menu
Step 2. Click Hard button on Difficulty Menu. Expected Result: Move to gameplay screen for hard game
Step 3. Watch teddy bears and teddy bear projectiles periodically move toward the burger. Expected
Result: Teddy bears and teddy bear projectiles periodically move toward the burger more often than the
medium game
Step 4. Click X in corner of player. Expected Result: Exit game
It's unusual for us to have test cases that talk about relative performance compared to previous test cases
(e.g., “more often than the easy game”), but we need to structure the test cases that way because all three
difficulties exhibit the same behavior, just at different speeds.
20.11. Write the Code and Test the Code (Full Gameplay: AI Teddies)
To give the game tuners finer control of gameplay, we want them to be able to independently control the
timing for the teddy bears and the teddy bear projectiles. Assuming we've added 6 new properties to the
ConfigurationUtils class for those values, here's the new method we add to the DifficultyUtils
class:
/// <summary>
/// Gets the homing delay for the given tag for
/// the current game difficulty
/// </summary>
/// <returns>homing delay</returns>
/// <param name="tag">tag</param>
public static float GetHomingDelay(string tag)
{
if (tag == "TeddyBear")
{
switch (difficulty)
{
case Difficulty.Easy:
return ConfigurationUtils.EasyBearHomingDelay;
case Difficulty.Medium:
return ConfigurationUtils.MediumBearHomingDelay;
case Difficulty.Hard:
return ConfigurationUtils.HardBearHomingDelay;
default:
return ConfigurationUtils.EasyBearHomingDelay;
}
}
else
{
switch (difficulty)
{
case Difficulty.Easy:
return ConfigurationUtils.EasyBearProjectileHomingDelay;
case Difficulty.Medium:
return ConfigurationUtils.MediumBearProjectileHomingDelay;
case Difficulty.Hard:
return ConfigurationUtils.HardBearProjectileHomingDelay;
470 Chapter 20
default:
return ConfigurationUtils.EasyBearProjectileHomingDelay;
}
}
}
Although we could have the HomingComponent script call this method directly, up to this point in our
implementation we've used ConfigurationUtils as the single access point for configuration data.
We'll continue that approach here by adding a GetHomingDelay method to the ConfigurationUtils
class as well:
/// <summary>
/// Gets the homing delay for the given tag
/// </summary>
/// <returns>homing delay</returns>
/// <param name="tag">tag</param>
public static float GetHomingDelay(string tag)
{
return DifficultyUtils.GetHomingDelay(tag);
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// A homing component
/// </summary>
public class HomingComponent : MonoBehaviour
{
GameObject burger;
new Rigidbody2D rigidbody2D;
float impulseForce;
float homingDelay;
Timer homingTimer;
The burger, rb2D, and homingDelay fields are to cache (store) values for efficiency; that way we don't
have to retrieve those values each time we need them. We store the impulseForce so the object the
script is attached to keeps moving at the same speed even when it changes direction to move toward the
burger. The homingTimer is a standard Timer component we use to determine when it's time to “home”
again.
/// <summary>
/// Start is called before the first frame update
/// </summary>
void Start()
{
// save values for efficiency
burger = GameObject.FindWithTag("Burger");
homingDelay = ConfigurationUtils.GetHomingDelay(gameObject.tag);
rb2D = GetComponent<Rigidbody2D>();
Putting It All Together 471
We added a Burger tag to the Burger because finding objects by tag is faster (in general) than finding
objects by name in Unity.
// create and start timer
homingTimer = gameObject.AddComponent<Timer>();
homingTimer.Duration = homingDelay;
homingTimer.AddTimerFinishedEventListener(
HandleHomingTimerFinishedEvent);
homingTimer.Run();
}
/// <summary>
/// Sets the impulse force
/// </summary>
/// <param name="impulseForce">impulse force</param>
public void SetImpulseForce(float impulseForce)
{
this.impulseForce = impulseForce;
}
Both the TeddyBear script and the TeddyBearProjectile script call this method from their Start
methods. That way, the homing object maintains its original speed even when changing direction to
move toward the burger.
/// <summary>
/// Handles the homing timer finished event
/// </summary>
void HandleHomingTimerFinishedEvent()
{
// stop moving
rigidbody2D.velocity = Vector2.zero;
// restart timer
homingTimer.Run();
}
}
This is where we actually change our direction and start moving toward the burger. First we stop
moving; otherwise, we'll keep adding force and the game object will move faster and faster over time.
The block of code to start moving toward the burger is the same as the code we used in the Ted the
Collector game in Chapter 9 to move the teddy bear toward the target pickup. Finally, we restart the
homing timer (yes, we forgot to do that when we first wrote the method!) so we “home” again after the
appropriate delay.
472 Chapter 20
We can now execute Test Cases 24, 25, and 26. We temporarily reduced the max number of bears for
each difficulty level to a single bear so we could easily observe the homing behavior of both the
TeddyBear and the TeddyBearProjectiles in the scene. All three test cases pass.
Although we'll use the knowledge we gained about audio sources and audio clips in Chapter 15, it's
actually amazing how quickly implementing sound effects can get complicated. In Chapter 15, we
attached audio sources to the game objects that most logically played the sound effects we were
including in the game, then had scripts attached to those game objects actually play the sound effects.
Unfortunately, that approach won't work here.
We admit that we originally implemented our audio using that approach, but when we got to our
explosions, the explosion sound effect was getting cut off. This was happening because we were
destroying the Explosion game object once its animation finished playing, but that also destroys the
Audio Source component attached to that game object (as it should). Destroying the Audio Source
component stops playing the clip as well, so we didn't get the complete explosion sound effect.
This is, of course, a general game development problem. We were recently writing a small Asteroids
game, and we wanted to play a sound when a bullet destroys an asteroid. We were destroying both of the
game objects involved in that collision, so we couldn't have either one of them play the required sound
effect. The approach we'll use here works in that scenario as well.
We'll implement a static AudioManager class that will hold an audio source and a dictionary of audio
clips for the sound effects in the game. This class will expose an Initialize method to load all the
audio clips, an Initialized property so we can tell whether or not the audio manager has been
initialized, and a Play method the other classes in our game will call to play particular sound effects.
We'll also need an AudioSource that persists across all the scenes in the game. We'll see how we can
implement that when we Write the Code.
Step 5. Press Escape key. Expected Result: Game paused and Pause Menu displayed on top of game.
Pause game sound plays
Step 6. Press Resume button on Pause menu. Expected Result: Pause Menu removed and game
unpaused. Menu button click sound plays
Step 7. Press Escape key. Expected Result: Game paused and Pause Menu displayed on top of game.
Pause game sound plays
Step 8. Press Quit button on Pause menu. Expected Result: Move to Main Menu. Menu button click
sound plays
Step 9. Click Play button on Main Menu. Expected Result: Move to Difficulty Menu. Menu button click
sound plays
Step 10. Click Medium button on Difficulty Menu. Expected Result: Move to gameplay screen for
medium game. Menu button click sound plays
Step 11. Press Escape key. Expected Result: Game paused and Pause Menu displayed on top of game.
Pause game sound plays
Step 12. Press Quit button on Pause menu. Expected Result: Move to Main Menu. Menu button click
sound plays
Step 13. Click Play button on Main Menu. Expected Result: Move to Difficulty Menu. Menu button
click sound plays
Step 14. Click Hard button on Difficulty Menu. Expected Result: Move to gameplay screen for hard
game. Menu button click sound plays
Step 15. Press Escape key. Expected Result: Game paused and Pause Menu displayed on top of game.
Pause game sound plays
Step 16. Press Quit button on Pause menu. Expected Result: Move to Main Menu. Menu button click
sound plays
Step 17. Click Quit button on Main Menu. Expected Result: Exit game (no menu button click sound
plays because the application quits so quickly)
Test Case 28
Gameplay Sound Effects
Step 1. Click Play button on Main Menu. Expected Result: Move to Difficulty Menu
Step 2. Click Easy button on Difficulty Menu. Expected Result: Move to gameplay screen for easy game
Step 3. Shoot french fries using the space bar. Expected Result: French fries move straight up from
burger when shot. Firing rate controlled when space bar held down. French fries shooting sound plays
Step 4. Move burger to collide with teddy bear. Expected Result: Teddy bear explodes. Health bar
shows reduced health. Explosion sound and burger damage sound plays
Step 5. Watch teddy bears periodically shoot teddy bear projectiles. Expected Result: Teddy bear
projectiles periodically move toward the burger when shot. Teddy bear shooting sound plays
Step 6. Move burger to collide with teddy bears and teddy bear projectiles until health is 0. Expected
Result: High Score Menu displayed above Gameplay scene. All moving objects are paused. Player
running out of health sound plays
Step 7. Click Quit button on High Score Menu. Expected Result: Move to Main Menu
Step 8. Click Quit button on Main Menu. Expected Result: Exit game
We'll start with the pause game sound effect, but getting that sound effect to play will require getting the
whole audio system set up.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// The audio manager
/// </summary>
public static class AudioManager
{
static bool initialized = false;
static AudioSource audioSource;
The initialized field is used to keep track of whether or not the audio manager has been initialized
yet. The audioSource field holds the AudioSource we'll use to play all the sound effects.
We use a dictionary of audio clips so we can look up the audio clips to play by audio clip name.
/// <summary>
/// Gets whether or not the audio manager has been initialized
/// </summary>
public static bool Initialized
{
get { return initialized; }
}
At this point in the book, you should be able to easily understand this property!
/// <summary>
/// Initializes the audio manager
/// </summary>
/// <param name="source">audio source</param>
public static void Initialize(AudioSource source)
{
initialized = true;
audioSource = source;
The code above captures the fact that the audio manager has now been initialized and saves the provided
AudioSource to use to play clips later.
audioClips.Add(AudioClipName.BurgerDamage,
Resources.Load<AudioClip>("BurgerDamage"));
audioClips.Add(AudioClipName.BurgerDeath,
Resources.Load<AudioClip>("BurgerDeath"));
audioClips.Add(AudioClipName.BurgerShot,
Resources.Load<AudioClip>("BurgerShot"));
Putting It All Together 475
audioClips.Add(AudioClipName.Explosion,
Resources.Load<AudioClip>("Explosion"));
audioClips.Add(AudioClipName.MenuButtonClick,
Resources.Load<AudioClip>("ButtonClick"));
audioClips.Add(AudioClipName.PauseGame,
Resources.Load<AudioClip>("ButtonClick"));
audioClips.Add(AudioClipName.TeddyShot,
Resources.Load<AudioClip>("TeddyShot"));
}
The rest of the method loads each of the audio clips for the game and puts them in the dictionary using
the audio clip name as the key. Notice that we're using the ButtonClick audio clip for both the menu
button click and pause game sound effects.
One additional benefit of using the dictionary of audio clips is that we can implement all the game
functionality using the in-game audio clip names independent of the names of the actual sound files
you've included in your Unity project. That way, if someone renames an asset (yes, that's going to
happen to you!) you can simply change the name of the audio clip you're loading in this method without
having to change any of your other code. Trust us, that's a win!
/// <summary>
/// Plays the audio clip with the given name
/// </summary>
/// <param name="name">name of the audio clip to play</param>
public static void Play(AudioClipName name)
{
audioSource.PlayOneShot(audioClips[name]);
}
}
The method above plays a particular audio clip from the dictionary. We first retrieve the audio clip from
the dictionary using audioClips[name], then we play it using the AudioSource PlayOneShot method.
Why aren't we using the Play method we used in Chapter 15?
The PlayOneShot method is like a “fire and forget” way to play an audio clip. The downside is that we
can't pause or stop the clip once we start playing it, but that's okay, we don't need that functionality
anyway. The big win here is that our audio source can play multiple clips simultaneously, which we
definitely want in our game. We didn't have to worry about this in Chapter 15 because each game object
had its own audio source playing its own clip, so we could have many of those clips playing at the same
time. We only have one audio source in this game, though, so using the PlayOneShot method to play
multiple clips simultaneously is the way to go.
The next thing we need is an AudioSource that persists across all the scenes in the game. As we
discussed above, when a game object is destroyed, the Audio Source component for that game object is
destroyed as well. Because our audioSource field in the AudioManager class is a reference to an Audio
Source component, we need to make sure that component (and the game object it's attached to) stays
around for the life of the game.
Start by opening the MainMenu scene, right clicking in the Project window, and selecting Create Empty.
Change the name of the new game object to GameAudioSource. In the Inspector, click the Add
476 Chapter 20
Component button and select Audio > Audio Source. There's no need to assign a clip to the Audio
Source because our audio manager will select the appropriate clips to play as the game runs.
Now we need to attach a script to the GameAudioSource game object to initialize the audio manager and
to make the game object persist across all the scenes in the game:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// An audio source for the entire game
/// </summary>
public class GameAudioSource : MonoBehaviour
{
/// <summary>
/// Awake is called before Start
/// </summary>
void Awake()
{
// make sure we only have one of this game object
// in the game
if (!AudioManager.Initialized)
{
We actually need to make sure we only have one instance of the GameAudioSource game object in the
entire game because we don't want our audio manager to end up with multiple audio sources. There are a
variety of different ways we can check this, so we decided to simply check to see if the audio manger
has already been initialized. If it hasn't, this is the first time we've called the Awake method for a
GameAudioSource game object, so we know this is the first instance of that game object being added to
the game.
You might be wondering how we could end up with multiple copies of the GameAudioSource game
object in the game anyway, since we've added it to the MainMenu scene, which is the scene our game
starts in. The issue is that any time we go to the Main Menu we'll try to create an instance of the
GameAudioSource game object, which will happen whenever we return to Main Menu from the High
Score Menu or by quitting a game from the Pause Menu.
The first two lines of code above add an Audio Source component to the GameAudioSource game
object and pass that component in to the audio manager. The last line of code calls the
DontDestroyOnLoad method, which keeps the game object alive as we move between the scenes in the
game.
Putting It All Together 477
}
else
{
// duplicate game object, so destroy
Destroy(gameObject);
}
}
}
The else clause above makes sure we only have one instance of the GameAudioSource game object in
the game. We only get to the else clause if the audio manager has already been initialized, so we know
there's already an instance of the GameAudioSource game object in the game. In that case, we simply
destroy the additional instance of the GameAudioSource game object that was just created.
Okay, we're finally ready to play the pause game sound effect! Given all the up-front work we've done,
we can do that by adding the following line of code to the FeedTheTeddies Update method at the end
of the if clause:
AudioManager.Play(AudioClipName.PauseGame);
Go ahead and run Test Case 27. You won't hear any of the menu button clicks yet, but you should hear
the pause game sound effect in Steps 5, 7, 11, and 15.
To get the rest of Test Case 27 to pass, we need to add the menu button clicks every time we click a
menu button. To do that, add the following line of code at the beginning of each of the
Handle<ButtonName>ButtonOnClickEvent methods in the DifficultyMenu, HighScoreMenu,
MainMenu, and PauseMenu scripts:
AudioManager.Play(AudioClipName.MenuButtonClick);
Run Test Case 27 again. Everything should work fine, so we can move on to Test Case 28.
Although Test Case 28 has us testing all the gameplay sound effects, we'll add them one at a time in the
order they're tested in the test case. That means we'll start with the french fries shooting sound.
Add the following code to the Burger Update method right after we instantiate a new french fries
object:
AudioManager.Play(AudioClipName.BurgerShot);
Run the game now and you'll hear the sound effect whenever the burger shoots french fries.
You'll actually notice if you pause and resume or quit the game, the burger fires french fries when you
click a Pause Menu button. That's because the left mouse button is included as the Alt Positive Button in
the Fire1 axis in the Input manager. Go delete mouse 0 from that value in the Input settings and rerun
the game to see that's solved the problem.
Let's add the explosion sound next. Modify the Explosion script to tell the audio manager to play the
AudioClipName.Explosion clip at the end of the Start method. Run the burger into a teddy bear in the
game; you should hear the explosion sound when you do.
478 Chapter 20
For the burger damage sound, add code to the Burger TakeDamage method right after reducing the
burger health to tell the audio manager to play the AudioClipName.BurgerDamage clip. Run the burger
into a teddy bear in the game; you should hear the burger damage sound when you do. The explosion
sound is also playing at the same time, though it's a little hard to hear both. You can certainly shoot a
teddy bear with french fries to confirm the explosion sound is still working if you'd like.
Modify the TeddyBear script to tell the audio manager to play the AudioClipName.TeddyShot clip
when the teddy bear instantiates a teddy bear projectile. Execute up to Step 5 of Test Case 28 to hear the
teddy bears shooting.
The last sound effect we need to add is the player running out of health sound. Add code to the Burger
TakeDamage method to tell the audio manager to play the AudioClipName.BurgerDeath clip if the
burger runs out of health. Execute Test Case 28 in its entirety to confirm all the sound effects are
working properly.
We're going to use the same structure for this part of our project as we did in Section 19.5. Specifically,
we'll have a ConfigurationData CSV file to store our values, a ConfigurationData script to retrieve
those values from the CSV file, and the ConfigurationUtils class we already have to expose those
values to the rest of the game.
Test Case 29
CSV Configuration Data
Step 0. Before running the game, change a single configuration data value in the
ConfigurationData.CSV file
Step 1. Play the game. Expected Result: Game behaves appropriately based on the configuration data
value
The Expected Result clearly violates our rule of having a specific result, but in this case pragmatism
wins.
Putting It All Together 479
20.17. Write the Code and Test the Code (CSV Configuration Data)
Create a new Scripts\Configuration folder and move the existing ConfigurationUtils script from the
Scripts\Util folder into the Scripts\Configuration folder. We’re going to have multiple files supporting
our configuration data processing, so we might as well group them all in their own folder.
Looking forward, we’re going to use a Dictionary to store all our configuration data. It seems clear
that the values in the dictionary will be the configuration data values, but what should we use for the
key? Although we could use a string (the configuration data value name) as the key, we’ll use the
following enumeration instead:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// Enumeration of the valid configuration data value names
/// </summary>
public enum ConfigurationDataValueName
{
TotalGameSeconds,
BurgerMoveUnitsPerSecond,
BearDamage,
BearProjectileDamage,
FrenchFriesImpulseForce,
BurgerCooldownSeconds,
BearPoints,
TeddyBearProjectileImpulseForce,
EasyMinSpawnDelay,
EasyMaxSpawnDelay,
EasyMinImpulseForce,
EasyMaxImpulseForce,
EasyMaxNumBears,
EasyBearMinShotDelay,
EasyBearMaxShotDelay,
EasyBearHomingDelay,
EasyBearProjectileHomingDelay,
MediumMinSpawnDelay,
MediumMaxSpawnDelay,
MediumMinImpulseForce,
MediumMaxImpulseForce,
MediumMaxNumBears,
MediumBearMinShotDelay,
MediumBearMaxShotDelay,
MediumBearHomingDelay,
MediumBearProjectileHomingDelay,
HardMinSpawnDelay,
HardMaxSpawnDelay,
HardMinImpulseForce,
HardMaxImpulseForce,
HardMaxNumBears,
HardBearMinShotDelay,
HardBearMaxShotDelay,
HardBearHomingDelay,
480 Chapter 20
HardBearProjectileHomingDelay
}
So why use an enumeration instead of a string as our key? Because if you mis-type an enumeration key,
your code won’t compile and you’ll have to fix your typo. If you mis-type a string key, your code will
compile fine, it just won’t work correctly when you run it. It’s always easier to fix compilation errors
than it is to use the debugger to find (and fix) bugs in running code!
Now we need a ConfigurationData script; go ahead and create one in the Scripts\Configuration folder
in the Project window. Let’s walk through the first part of that script together.
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using UnityEngine;
/// <summary>
/// Provides access to configuration data
/// </summary>
public class ConfigurationData
{
#region Fields
#endregion
As you can see, we’ve defined a constant for the configuration data file name. We’ve also declared a
Dictionary called values to store all our configuration data. This is immediately useful here because
that means we don’t have to declare 35 different variables to store each configuration data value. We’ll
also see another great benefit when we read the data from the CSV file (coming soon!).
#region Properties
/// <summary>
/// Gets the number of seconds in a game
/// </summary>
public int TotalGameSeconds
{
get { return (int)values[ConfigurationDataValueName.TotalGameSeconds]; }
}
Go ahead and create properties for the other 34 configuration data values. If the property returns a
float, you don’t have to type cast it before returning it. Let’s look at the ConfigurationData
constructor next.
Putting It All Together 481
#region Constructor
/// <summary>
/// Constructor
/// Reads configuration data from a file. If the file
/// read fails, the object contains default values for
/// the configuration data
/// </summary>
public ConfigurationData()
{
// read and save configuration data from file
StreamReader input = null;
try
{
// create stream reader object
input = File.OpenText(Path.Combine(
Application.streamingAssetsPath, ConfigurationDataFileName));
After reading Chapter 19, you should be able to understand all of the code above.
// populate values
string currentLine = input.ReadLine();
while (currentLine != null)
{
string[] tokens = currentLine.Split(',');
We use a while loop to iterate over the lines in the CSV file; that way, if we change the number of lines
in that file we don’t need to change this code. The currentLine != null Boolean expression stops the
while loop when we’ve reached the end of the file. The last line of code above fills the tokens array with
the strings that are separated by commas (just like GetHighScoresFromCsvString helper method did in
Section 19.4). Our CSV strings will be formatted as name,value.
ConfigurationDataValueName valueName =
(ConfigurationDataValueName)Enum.Parse(
typeof(ConfigurationDataValueName), tokens[0]);
Wow, that line of code looks pretty complicated! It’s not really as bad as it looks, though. First, we take
tokens[0] (the name of the configuration data value on the line we’re currently processing) and parse it
as a ConfigurationDataValueName using the Enum Parse method. Feel free to read the documentation
for that method if you want more details. Because that method returns an object, we need to type cast it
to a ConfigurationDataValueName. Finally, we put that value into our valueName variable.
values.Add(valueName, float.Parse(tokens[1]));
currentLine = input.ReadLine();
}
}
Next, we add the value to the values dictionary using the valueName we just extracted above and the
value that appeared after the comma in the string we’re currently processing (that value is in
tokens[1]). Note that we need to parse the string in tokens[1] as a float because the values in our
dictionary are floats. The other line of code above reads the next line in the CSV file.
482 Chapter 20
catch (Exception e)
{
// set default values if something went wrong
SetDefaultValues();
}
We always want include a catch block when we’re doing file IO in case something goes wrong. If
something goes wrong here, we want to set default values in our dictionary so we can still play the
game. We’ll look at part of the SetDefaultValues method soon.
finally
{
// always close input file
if (input != null)
{
input.Close();
}
}
}
#endregion
The code above simply closes the CSV file when we’re done with it.
Okay, what about the SetDefaultValues method? Here’s the start of that method:
/// <summary>
/// Sets the configuration data fields to default values
/// csv string
/// </summary>
void SetDefaultValues()
{
values.Clear();
values.Add(ConfigurationDataValueName.TotalGameSeconds, 30);
The first line of code clears the dictionary. We need to do that because if the method gets called after
we’ve already added some values to the dictionary, we’ll get an exception that the same key already
exists in the dictionary when we try to add the default values for the values that are already in the
dictionary. The second line of code adds the default value for total game seconds to the dictionary. The
rest of the code in the method adds a default value for the remaining 34 configuration data values.
Great, we’re done with the ConfigurationData class. Add a static configurationData field to the
ConfigurationUtils class and change all the properties that return a hard-coded value to return the
corresponding property from the configurationData field instead. Add an Initialize method to the
ConfigurationUtils class and call this method in the Start method in the GameInitializer class
right before calling the DifficultyUtils Initialize method. Finally, call the ConfigurationData
class constructor in the ConfigurationUtils Initialize method to initialize the configurationData
field.
Next, create a new StreamingAssets folder in the Assets folder and create a CSV file in that folder to
hold all 35 configuration data values. You can create the file using a text editor or any spreadsheet
Putting It All Together 483
software that lets you export a CSV file. Make sure each row in your csv file has the name of the
configuration data item, a comma, and the value for that configuration data item. For example, the first
few rows in our CSV file are:
TotalGameSeconds,30
BurgerMoveUnitsPerSecond,5
BearDamage,10
Of course, there are 32 more lines in the file with the rest of the configuration data names and values.
Run the game to make sure everything works fine. Now you can run Test Case 29 for each of the
configuration data values to make sure they work properly. Of course, because we've been using
ConfigurationUtils as the single access point for the rest of the game throughout our development,
you shouldn't find any problems.
You've probably realized that someone editing the CSV file could include values that don't really make
any sense, like -5 for the number of points a teddy bear is worth. Although you as the programmer
probably wouldn't do that, non-programmers editing the file could do so either by mistake or
maliciously.
Although we didn't include any data validation in our ConfigurationData class, that's certainly
something we could have done. In fact, the Battle Paddles game we plan to give away on the Burning
Teddy web site will come with a configuration data file containing 52 different configuration data values
that can be used to tune the game. Because anyone downloading that game will be able to edit that file
(we want people to play with the game tuning!), we’ll include data validation in the
ConfigurationData class for that game to help protect those editors from themselves.
20.18. Conclusion
Well, that was a LOT of work to build a very simple game! The bad news is that it is a lot of work to
program games. The good news is that it’s a lot of fun doing it. Even better, you now have a solid
foundation for turning your own game ideas into reality using C# and Unity. Rock on.
Appendix: Setting Up Your Development Environment
Learning to program requires that you actually DO lots of programming – not just read about it, hear
about it, or watch someone else do it, but actually do it yourself. That means that you'll need to set up a
programming environment so you can learn all the stuff in this book by actually slinging code. For the
topics covered in this book, you'll need to install Visual Studio Community 2022 and the Unity game
engine. Doing this will let you use Visual Studio to write C# console applications and will also let you
write C# scripts in Unity as you develop Unity games. We'll discuss how to install each below.
Be sure to check the following items in the Workloads area of the installer:
You should also check the following in the Individual components area of the installer:
You can certainly select other Workloads and Individual components if you'd like, but we just installed
the four items above. After selecting those items, click the Install button and wait patiently while
everything is installed.
At this point, you should have a working version of the IDE installed. Let's check to make sure you do.
We put a shortcut on our desktop to make launching the IDE easier; for us, the IDE is installed at
C:\Program Files\Microsoft Visual Studio\2022\Community\Common7\IDE\devenv.exe. Of course, you
can also use Windows to search on Visual Studio and click the Visual Studio 2022 result instead if you
prefer.
During the first startup of Visual Studio, be sure to change Development Settings to Visual C# before
clicking the Start Visual Studio button on the bottom right. When Visual Studio starts up, you should
end up with something like the window below. Yours may not match exactly, but it should be similar.
Setting Up Your Development Environment 485
We're going to compile and run a simple program to make sure the IDE installed correctly. Click the
Create a new project box on the lower right, which will bring you to a screen like the one below. Select
the C# Console App on the upper right, then click the Next button on the lower right. If the C# Console
App doesn’t appear on the right, type C# Console App in the search box at the top and press the Enter
key.
You’ll end up at a screen like the one shown below. Change the Project name from ConsoleApp1 to
InstallTest in the text box at the top left of the screen, change the Location to put the project wherever
you want it to be saved, then click the Next button at the bottom right.
486 Appendix
Next, you’ll come to the screen that lets you set your target framework. We can set a variety of different
target frameworks, but we’ll use .NET 6.0 (Long Term Support) for our target framework. If the
framework isn’t already set to .NET 6.0, click the down arrow at the right side of the Framework
dropdown and select .NET 6.0. You should also be sure to check the checkbox to the left of Do not use
top-level statements. Click the Create button at the bottom right.
After a moment of two, you'll come back to the main screen of the IDE, which should look like this (we
zoomed in on the Code window in the upper left using Ctrl + middle mouse wheel):
Setting Up Your Development Environment 487
If you have the Toolbox pane displayed on the left-hand side, you can simply click the Auto Hide "pin"
just to left of the X in the upper right corner of the Toolbox pane to make it hide itself on the left.
When we told the IDE we wanted a new console app project, it automatically gave us a template for that
kind of application. Compile the application by pressing F6 or by selecting Build > Build Solution from
the top menu bar35. Once you see a "Build succeeded" message in the bar at the bottom of the IDE, you
can actually run the application. Do that by pressing Ctrl+F5 or by selecting Debug > Start Without
Debugging from the top menu bar; when you do, you should get the Command Prompt window below.
Just press any key to return to the IDE.
Exit the IDE. You certainly don't need to save the project if you don't want to – you'll get plenty of
practice creating and saving projects – but feel free to do so if you want to.
35 We've actually seen the keyboard shortcut for building the solution as F7 and Ctrl+Shift+B also. If you select Build >
Build Solution from the top menu bar, the keyboard shortcut for that action is shown to the right. You can also customize the
keyboard shortcuts in Visual Studio if you'd like; see https://docs.microsoft.com/en-us/visualstudio/ide/identifying-and-
customizing-keyboard-shortcuts-in-visual-studio?view=vs-2022
488 Appendix
If everything worked as described here, Visual Studio Community 2022 is installed and you're ready to
move on to setting up the documentation. If your IDE isn't working, you should try working through the
install process again or get online with Microsoft to ask for help.
When we’re developing code, it’s really useful to have the help documentation available directly within
the IDE. The default for help documentation is to simply access help (through the IDE) online. If your
computer always has connectivity to the Internet, this should work fine. If you're sometimes
disconnected while you're programming, though, you'll probably want to set up your environment to use
local help instead.
Start up the IDE, then click Help > Add and Remove Help Content. The IDE starts up the Microsoft
Help Viewer, which lets you add local content. At a minimum, you should add the following topics:
There are actually lots of other help topics that are also already set to be added by default. It doesn’t hurt
anything to just add those as well (though it does consume some of your disk space), but you can also
click the Cancel link next to each topic you don’t want installed if you want to exclude those topics from
your local help. Click the Update button near the lower right of the Help Viewer and wait patiently
while the update packages are downloaded and installed.
Finally, you can read the help documentation through a browser (which will access the online
documentation) or through the Microsoft Help Viewer (which will access the local help content). Both
approaches provide access to the same help content, they just look slightly different – and, of course, if
you're offline you can only access the local help. If you prefer using a browser, you don't need to make
any changes because that's the default. If you prefer using the help viewer (like we do), select Help > Set
Help Preference > Launch in Help Viewer.
Let’s make sure the local documentation installed properly. Click Help > View Help. Enter
System.Collections.Generic in the search box at the upper left and press Enter. You should get a page of
search results like the one shown below.
Setting Up Your Development Environment 489
Your page will look different if you're using online help, but you should see a set of results somewhere
on the page.
Click on the top result in the pane on the left and you should get a window of documentation like the
one shown below.
490 Appendix
You should also know that you can always look at the C# documentation online at
https://docs.microsoft.com/en-us/dotnet/csharp/; just type a search term in the search box near the upper
right of the page and press Enter to look for a specific C# topic.
When you download a Visual Studio project, you can open it in Visual Studio by double clicking the
solution file. The solution file has a .sln file extension, but in case you're not showing file extensions in
Windows Explorer it's the file marked "Microsoft Visual St...", "Visual Studio Solu...", or something
similar as the Type. Let’s see how that works.
Download the Chapter 2 code from the Burning Teddy web site and extract the zip file somewhere on
your computer.
Navigate into the folder called PrintRainMessage (it will be wherever you extracted the Chapter 2 code
zip file) and double click the solution file. When the project opens, you might think something is wrong
because the code pane doesn’t show any code. Simply click Program.cs in the Solution Explorer to see
the code in that file.
Setting Up Your Development Environment 491
That’s all you need to get started with Visual Studio, so let’s move on to Unity.
A.2. Unity
1. Download Unity Hub from http://unity.com/. At the time of this writing, you’ll need to click the
Download Unity button, then click the Download for Windows button (if you’re using a different OS,
click the Download other versions button, then click the download link for your OS in the Download the
Unity Hub section). The sequence of steps may be different by the time you’re doing this, but the bottom
line is you’re downloading Unity Hub (your downloaded file should be named something like
UnityHubSetup.exe)
2. Run the install file you downloaded. Just accepting the default values as you go along should work
fine, but you can of course change them if you want
Unity Hub lets you have multiple versions of Unity installed on your computer and using Unity Hub is
the standard way to install new versions of Unity. By default, Unity installs a shortcut to Unity Hub on
your desktop when it installs. Double click that shortcut; you should end up with a window like the one
below. The first time you run Unity Hub, it may tell you you don’t have a license. If that happens, say
you want to manually handle the license process and log into (or create) your Unity ID. The “license” is
free, but you need a Unity account to activate it.
492 Appendix
Although Unity Hub has been installed, we still need to install a version of the Unity game engine. In
Unity Hub, click Installs in the pane on the left to get a window like the one below.
Setting Up Your Development Environment 493
Click the blue Install Editor button near the upper right, select the version you want to install in the
popup (we usually pick the most recent version in the Official Releases section) and click the Install
button. See the window below.
Check the modules you want to be installed; we usually check Windows Build Support (IL2CPP) and
Documentation (to get local documentation installed). Click the Install button, confirm that Unity Hub
can make changes to your computer (if necessary), and wait patiently while your selected version is
installed.
Click the gear icon near the upper left, select Projects in the pane on the left, and set a default location
where you want Unity to save your Unity projects; see image below. Click the X at the upper right of the
Preferences pane when you’re done.
494 Appendix
Click Projects near the top of the pane on the left to open the list of projects. Click New project near the
upper right corner; you should end up with a window like the one below.
If your window doesn’t look like that, click Core on the left. Click the 2D button to create a 2D project,
change the Project Name to UnityInstallTest, change the Location to wherever you want to save the
Setting Up Your Development Environment 495
project (if you don’t want to save it in the default location), then click the Create project button at the
lower right. After a moment of two, you'll come to the Unity editor, which should look like this:
If the editor has a Services tab on the right, you can just right click on that tab and select Close Tab.
This is the default layout for the Unity editor, but you can change the layout by clicking the layout
dropdown near the upper right and selecting a different layout. You can even configure and save your
own layout; because we prefer a different layout when we develop in Unity, we'll show you how to
configure and save the layout we'll use throughout the book (a layout that's also used in Jeremy Gibson's
excellent Introduction to Game Design, Prototyping, and Development book). You don't have to do this
– you can use any layout you want! – but go ahead and follow along if you want to use the layout we
use. Once we're done our layout will look like this:
496 Appendix
We'll use the 2 by 3 layout as our starting point, so select 2 by 3 from the layout dropdown on the upper
right. Left click on the options selector on the Project window (circled in red in the figure below) and
select One Column Layout.
We can drag windows around in the Unity by holding the left mouse button down on the tab for the
window, then dragging the window to the desired location. This might take a little practice to make sure
the window is "docked" where you want it rather than just floating free, but you should be able to get the
hang of this pretty easily.
Click the dropdown that says Free Aspect at the top of the Game view and select 16:9 Aspect. Move the
windows around until your setup looks like the image below. As is typical in many apps, you can move
the borders on particular windows by dragging their edges.
Setting Up Your Development Environment 497
The last thing we want to add to our layout is a Console window (which acts much like the command
prompt window in our console apps). Select Window > General > Console from the menu bar at the top
of the Unity window and drag the resulting window onto the bottom of the Project window. After doing
that, you'll need to drag the Hierarchy window onto the right side of the Project window so they appear
side-by-side above the Console window.
Finally, click the layout dropdown (which currently shows 2 by 3) and select Save Layout... Call the
layout whatever you want (we called ours Dr. T) and click the Save button. Now you can use this layout
whenever you want by selecting it from the layout dropdown after you create a new project.
Before we finish, let's make sure Unity is set to use Visual Studio Community 2022 as our script editor.
Select Edit > Preferences ... from the top menu bar, then click External Tools in the pane on the left.
Click the dropdown box next to External Script Editor; if Microsoft Visual Studio 2022 is an option,
select it and close the dialog. If Microsoft Visual Studio 2022 isn't an option in the dropdown, select
Browse... Navigate to where your devenv.exe file for Visual Studio Community 2022 is installed and
double-click it. Close the dialog.
Caution: You might think Open by file extension will work fine here, but I’ve had numerous students
have trouble with this in the past. You should definitely select Microsoft Visual Studio 2022 here.
We'll actually add and test our first C# script in Unity in Chapter 2, so you're done setting up your
development environment.
498 Appendix
When you download a Unity project, you need to add it to your projects in Unity Hub and (probably)
update the Unity version as well. Let’s see how that works.
Download the Chapter 6 code from the Burning Teddy web site and extract the zip file somewhere on
your computer.
In Unity Hub, select Projects on the left and click the Open button.
Navigate to the folder called WorstGameEver (it will be wherever you extracted the Chapter 6 code zip
file) and click the Open button. Unless you happen to have the exact same version of Unity installed as I
used to create the project, your WorstGameEver project will look like the image below.
Setting Up Your Development Environment 499
The list of versions that are shown in the Installs section are the Unity versions you currently have
installed on your computer. Scroll down to look at your installed versions, click the radio button to the
left of one of those installed versions, and click the blue button near the lower right that says Open with
the version you selected. Click the blue Change version button in the popup, click the Continue button in
the next popup, then wait for the project to load in Unity.
Unity releases new versions pretty regularly; you don’t have to install each new version, but you’ll
probably want to install the latest version of Unity periodically. To do that, simply follow the
instructions provided above for installing a version of the Unity game engine.