Game Programming in Practice Part III
Game Programming in Practice Part III
Contents
� 1. Introduction
� 2. The name of the game
� 3. Let's get serious
� 4. Designing the interface
� 5. Music
� 6. Sound effects
� 7. Speech
� 8. Sound design
� 9. Inventory
� 10. Items
� 11. Areas
� 12. Player position
� 13. Looking around
� 14. Moving around
� 15. Object manipulation
� 16. Game loop
� 17. Use your imagination
1. Introduction
Welcome to the third and final installment of Practical BGT, the series of
tutorials in which you develop actual playable games while learning the basic
concepts
and techniques of BGT programming. If you haven't completed the first two
installments in this series, we strongly recommend that you do so before starting
out
on part III.
Let us take just a few minutes to go over what you learned in the first two parts.
In part I, Memory Train, you took a straight dive into the syntax and basic
functionality of BGT. You learned how the execution of your game program can be
described
in terms of functions calling other functions and making decisions based on what
they return. More specifically, you saw how to display a game window and ask for
keypresses, how to load and play sounds and background music, how to keep track of
time, and, last but not least, how to exit your game.
In Part II, Windows Attack, you took a more systematic approach to game design. You
took the description of a game and extracted from it a list of required sounds,
a description of the game state, and finally, the source code of the game itself,
complete with a self-made script class. In addition, you learned to harness the
power of a sound pool to position an arbitrary number of sounds in real-time.
This final installment will provide you with some powerful programming techniques
and design approaches to make your life easier as you begin to work on larger-scale
projects. You will probably not learn any new syntactic constructions here, nor
will the mini game you will write be anywhere as spectacular as those you
programmed
in the first two parts. Instead, this part is designed to put the tools into your
hands which, combined with imagination and creativity, will allow you to create
games of high software quality and consistent user experience.
If anything in this tutorial should appear confusing or unclear, chances are you
will find the missing puzzle pieces in the language tutorial, the BGT reference,
or, indeed, in the first two parts of this series. Sometimes it is also a good idea
to just continue reading with dogged determination, and you might find the answer
in the next paragraph. In any case, you should take frequent breaks, maintain a
calm, rational, detective-like approach to complexity, and remember that bugs
happen
to the best of us, and are valuable lessons only real life can teach.
the Kingdom of Quang stood proudly. Proudly indeed it stood, impervious to the
ravages of slime, until its two supreme rulers, during one of their regular and
excessive
drinking binges, got into a fateful and bloody argument about who stole the cookies
from the cookie jar. The question was never answered, but such was the monstrosity
of the ensuing war that the mighty Kingdom of Quang fell and sank beneath the sea.
The wizards of Quang, however, having gazed deeply into their crystal balls,
knew that tragedy was afoot, and managed to transfer all the knowledge of their
high culture into a powerful magical artefact---the Amulet of Quang.
You, a secret agent of the even more secret world government, have been transported
to the vast underground empire of Quang. Your mission is to recover the Amulet
of Quang and transport it back to the surface of the earth.
3. Let's get serious
Here is a more serious description of the game.
The Amulet of Quang is an audio adventure in which the player can explore his
surroundings, walk from place to place, and interact with the various objects which
litter the game world. Every area of the game map may provide its own background
music, and objects may or may not emit sound. The player will be informed by means
of a spoken message when the main character spots a new object. The player can
obtain a detailed description of an object by examining it. Some objects can be
picked
up. The objects the player is carrying are organized into an inventory list which
can be accessed at any time during the game. It is possible to use objects in
the inventory list. The game can be paused at any time.
4. Designing the interface
Exercise
Analyzing the description in the previous section, create a detailed specification
of the game's interface. The interface consists of everything the player needs
to know in order to successfully play your game. Mostly, this would be the kind of
information you might put into a user manual. Here are a few questions to put
you on the right track:
1. How does the player walk around the map?
2. When and in which way is the player informed of available objects?
3. Will there be footstep sounds?
4. How does the player examine, pick up, or use an object?
5. How does the player access the inventory list?
6. How does the player examine or use an object in the inventory list?
7. How does the player pause and unpause the game?
8. Finally, how would the player exit the game?
Here is my solution. In case our answers differ, this does not mean that you have
got it wrong, but merely that we have chosen to create slightly, or maybe even
dramatically, different kinds of games. The main reason why I am detailing my
solution here is to show you the level of detail required for a user interface
specification.
In general, the more complete your specification at design time, the less trial and
error at coding time. It is perfectly reasonable at design time to leave certain
details open to discussion or change, but this should happen as the result of
deliberate choice rather than unconscious omission. So let us agree for the moment
to stick to my specification, and if afterwards you feel that your solution is
worthwhile trying, I shall leave it as an exercise to you to go back and redo from
that point onward. In fact, I highly recommend doing so. Also, do not be put off if
it seems to take days, or even weeks, to get exactly right. This is to be expected.
The player walks around the game map by using the four arrow keys. Pressing and
then immediately releasing an arrow key will move the player exactly one step in
the corresponding direction. Holding an arrow key will move the player one step in
the corresponding direction every half second. In this way, a steady motion is
established just by comfortably holding down a key, but an impatient player can
still accelerate the process by tapping the key more quickly.
To give the player feedback that motion has indeed occurred, the game will play a
footstep sound for every step the player takes. If, after taking that step, the
available directions for the next step have changed, we will play a slightly
modified footstep sound to alert the player to that change. For example, assume the
player is walking down a long east-west corridor, and suddenly comes upon an exit
to the north. This would be the time when our modified footstep sound would be
played. So we define two footstep sounds, one of them soft and unobtrusive, the
other slightly more demanding, which might be accomplished by making it slightly
louder or higher in pitch.
Let's move on to what we might call the model of visibility. This is the set of
rules which determine what objects the player sees at any given time. Such a model
could get arbitrarily complex, taking into account such details as distance and
size of objects, lighting conditions, fog, or other objects which obstruct the
view.
For this design, however, we will keep it simple by defining just two basic
visibility rules. An object is visible if
1. it has the same x or y coordinate as the player, i.e. it is straight to the
north, east, south, or west,
and
2. there is no wall anywhere between the player and the object.
Whenever the player moves such that a new item is spotted, we will provide a spoken
message stating what kind of object it is, in what direction it lies, and how
many steps it would take to get to it. Note that we will only speak the objects as
they move into view, not those which were already visible before. For example,
if the player took one step north, we wouldn't need to speak any visible objects to
the north or south because they would have been visible before taking the step.
If this sounds confusing, I recommend you reread the two visibility rules above and
then form some examples in your mind to illustrate how they work. Specifically,
it is important to understand that taking a step to the north or south can only
change what is visible to the east or west, and vice versa.
A general description of the area at large is spoken when the player enters it, and
repeated as requested via the l command. Note: l as in look.
To examine, pick up, or use an object, the player must first move to that object's
location. A special sound is then played to indicate that an object has been
reached, and the object is announced. To examine it, the player uses the x command.
The t command will attempt to pick up the object. Finally, hitting the space
bar will attempt to interact with the object in some way. For instance, hitting the
space bar on a door might try to walk through that door, and hitting the space
bar on a gong might strike the gong.
The tab command will cycle through the objects in the player's inventory. To use an
object in the inventory, the player first cycles to this object by pressing
tab until the object is announced, and then presses u, as in use. To examine an
object in your inventory, you would first cycle to it by pressing tab repeatedly,
and then press i, as in inventory information.
To pause the game at any time, the player can press the p command. The same command
also resumes a paused game.
Finally, the escape key will exit the game. The game may also exit when the player
wins or dies, but escape is always available to exit prematurely.
Pressing page up or page down will allow the player to adjust the volume of the
music. This is especially useful in conjunction with spoken messages, because the
music might drown out the voice if too loud.
To make the experience of learning the interface as smooth as it could possibly be,
we will implement a help feature which will make our game self-documenting.
Help can be accessed at any time by pressing f1, whereupon the game will provide
spoken information about all the possible keyboard commands at that time.
5. Music
Exercise
One important aspect of game music is its ability to gracefully fade in and out.
Write a fade function which can be called as follows:
fade(sound@ noise, double start_volume, double end_volume, double time, bool
interruptable);
When called, this function should fade the given sound object from the start volume
to the end volume in the given time in milliseconds. If interruptable is true,
the player can interrupt the process by pressing the space bar. The fade function
returns true if the player interrupted the process using the space bar, and false
otherwise.
bool fade(sound@ noise, double start_volume, double end_volume, double time, bool
interruptable)
{
noise.volume = start_volume;
timer t;
double elapsed = t.elapsed;
while(elapsed < time)
{ noise.volume = start_volume + (end_volume-start_volume)*elapsed/time;
wait(10);
elapsed = t.elapsed;
if(key_pressed(KEY_SPACE) && interruptable)
{
return true;
}
}
noise.volume = end_volume;
return false;
}
We will use a sound object to play our in-game music, so let us declare this object
now:
sound music; // The one sound object used for all music
Next, we need a variable to keep track of the current volume of the music as the
player configured it using the page up and page down commands:
int music_volume; // Volume for all game music
Now let us define a function to change the currently playing music. This function
will accept as parameter the filename of the music to change to. The elegant thing
about this function is that it will fade out the currently playing song, then fade
in the new one, resulting in a graceful transition from one track to the next.
And while we are at it, let's define some more music-related functions:
Note that in the remainder of our source code, we will never manipulate the music
object directly, but only through the functions we just defined. Software
engineering
calls this approach modularization. It makes sure that whenever something is wrong
with the music, we need only check the correctness of our music functions, because
since all music-related activity takes place through these functions, this is where
any music-related bug would logically be found.
sound_pool pool; // The sound pool used for all sound effects
Here is my solution:
� step.wav: A standard footstep
� step_alert.wav: A footstep after which the available directions have changed
� item.wav: The player has reached an object
9. Inventory
Now we are slowly making our way from the very general-purpose modules to the more
game-specific functionality. In this section we deal with the player's inventory
list, a dynamically expanding or shrinking list of all the items the player is
currently carrying. We are going to use an array to keep track of the handles to
the items in the player's inventory, and we will provide functions through which
this array is manipulated.
// Removes an item from the inventory; returns true on success, false on failure
bool inventory_remove(item@ entry)
{
uint index; // For searching the inventory for the entry
for(index=0; index<inventory.length(); index++)
{
if(inventory[index] is entry) // Found it
{
break;
}
}
if(index >= inventory.length()) // Entry not found
{
return false;
}
// Move all above entries one slot down
for(uint i = index+1; i<inventory.length(); i++)
{
@inventory[i-1] = @inventory[i];
}
// Finally, make the inventory one slot smaller
inventory.resize(inventory.length() - 1);
inventory_position = -1; // Make it invalid
return true;
}
void inventory_cycle()
{
if(inventory_position+1 >= int(inventory.length()))
{
inventory_position = 0; // Go back to first if at the end
}
inventory_position++;
}
10. Items
The inventory list is all about managing and cycling through the items in the
player's possession. Let us now turn to the question of what an item is in the
first
place.
In an adventure game, an item can be defined as any object with which the player
might want to interact. For example, if one of the game's challenges consisted
of unlocking a door with a key, both the door and key would be considered items. We
use the word item here as a generic term for any object in the game world,
including
treasures, monsters, doors, and weapons.
For our purposes, an item must provide at least three pieces of information:
1. A name; this is how the game refers to the item when describing what the player
sees, or when cycling through the inventory.
2. A description; this is what the player hears when examining the item.
3. A takeable flag; this determines whether or not the item can be picked up. Note
that flag is just programmer lingo for bool. Setting a flag to true is sometimes
referred to as just setting it, and setting it to false can also be called clearing
it.
Let us translate this into code:
class item
{
string name;
string description;
bool takeable;
item()
{
alert("Programming Error", "Cannot make item without parameters.");
}
item(string name, string description, bool takeable)
{
this.name = name;
this.description = description;
this.takeable = takeable;
}
}
11. Areas
Areas comprise the geography of an adventure game. They have been known under
various names, including rooms, levels, or maps. But no matter how we refer to
them,
they are the world through which the player navigates.
Some mainstream games go out of their way to simulate real world geography,
complete with heights, textures, and weather conditions. While all of this is
certainly
possible in BGT, for simplicity's sake we shall stick with a basic setup here,
keeping track of just the locations of walls and items on our maps. We represent
this information in two arrays, both of them two-dimensional. In addition, every
area will provide a general description of where the player is located. This will
be announced when the player chooses to look around by pressing the l command.
class area
{
int size_x; // East-west size of area
int size_y; // North-south size of area
bool[][] walls; // true for wall tiles, false for floor space
item@[][] items; // Items on the map
string description; // Announced when looking around
area()
{
alert("Programming Error", "Cannot construct area without parameters.");
}
area(int size_x, int size_y, string description)
{
this.size_x = size_x;
this.size_y = size_y;
this.description = description;
walls.resize(size_x);
items.resize(size_x);
for(int i=0; i<size_x; i++)
{
walls[i].resize(size_y);
items[i].resize(size_y);
for(int j=0; j<size_y; j++)
{
walls[i][j] = false;
@items[i][j] = null;
}
}
}
void add_wall(int x, int y)
{
walls[x][y] = true;
}
void add_item(item@ entry, int x, int y)
{
@items[x][y] = @entry;
}
bool get_wall(int x, int y)
{
if(x<0 or x>=size_x or y<0 or y>=size_y)
{
return true; // The wall of the universe
}
bool ret = walls[x][y];
return ret;
}
item@ get_item(int x, int y)
{
item@ ret = @items[x][y];
return ret;
}
}
12. Player position
To represent the player's current position, we need to store the current area as
well as the player's current x and y coordinates.
area@ player_area;
int player_x;
int player_y;
13. Looking around
Let us now define a function which looks out from the player's location in a given
direction, and reports on what the player sees. This function takes an x and
a y increment. For instance, to look eastward, you would supply an x increment of 1
and a y increment of 0. To look south, you would supply an x increment of 0
and a y increment of -1.