Tải bản đầy đủ (.pdf) (44 trang)

Tài liệu Giáo trình C++ P6 ppt

Bạn đang xem bản rút gọn của tài liệu. Xem và tải ngay bản đầy đủ của tài liệu tại đây (423.92 KB, 44 trang )


www.gameinstitute.com Introduction to C and C++ : Week 6: Page 1 of 44










Game Institute



































by Stan Trujillo



Week
6

Introduction to C and C++

www.gameinstitute.com Introduction to C and C++ : Week 6: Page 2 of 44






© 2001, eInstitute, Inc.

You may print one copy of this document for your own personal use.
You agree to destroy any worn copy prior to printing another. You may
not distribute this document in paper, fax, magnetic, electronic or other
telecommunications format to anyone else.















This is the companion text to the www.gameinstitute.com
course of the
same title. With minor modifications made for print formatting, it is
identical to the viewable text, but without the audio.












www.gameinstitute.com Introduction to C and C++ : Week 6: Page 3 of 44



Table of Contents

Lesson 6 – GameDesign 4
Namespaces 4
Templates 8
STL 9
The Snake Sample 12
The Pong Game 18
The Game Classes 20
The GameObject Class 22
The Player Class 25
The HumanPlayer Class 28
The ArtificialPlayer Class 30
The Ball Class 34
The Application Class 37
Final Thoughts 42

www.gameinstitute.com Introduction to C and C++ : Week 6: Page 4 of 44
Lesson 6 – GameDesign

Learning C++ is really a two-part process, although the two are usually done in parallel. The first part is
the C++ language itself, which includes keywords, intrinsic data types, loops, conditionals, arrays,
structures, and classes. The second part is learning to use APIs. This second subject can include both the
standard libraries and third-party libraries.

Using APIs requires knowledge of the language, and to a lesser degree learning C++ requires knowledge
of the standards libraries. Using the cout object, which is not part of the language itself, although it is
supported by every implementation of C++, is a good example.

The remaining subjects that we’ll cover in these lessons fall into both categories. There are some C++
language constructs, and some topics that involve the standard libraries. We’ll start with the C++
language constructs.

It may seem late in the course to be introducing new C++ topics. Indeed, it is, but the idea behind this
course is to concentrate on the core C++ constructs that are used in game programming. The first three
lessons introduced the portions of the language that are almost always used. Here, we’ll cover some topics
that, while important, are not always used.
Namespaces
C++, like most languages in current use today, is still evolving. Some features, such as classes, have been
with the language all along, and others have been added somewhere along the way. Originally, C++
didn’t support the concept of a namespace, but it proved necessary once developers starting using C++ for
large projects.

A namespace is scope in which data types can be defined in order to avoid cluttering the global scope. To
understand what this means, it’s necessary to talk about what it means to define data types globally.

By default, when a new data type is defined it exists globally. This means that it is available to every
function in the application. This is convenient, but it prevents any additional uses of the data type name.
For example, if we declare a structure like this:


struct Player
{
};

This means that, in the global namespace, the name “Player” refers to this structure. If any of the code in
the program tries to declare a different global data type with the same name, an error will result:

class Player // compiler error! Player has already been defined
{
};

These two type definitions don’t mix, because they use the same name. What’s worse, this is true even if
the two data types are exactly the same; a data type cannot be defined twice. This is reasonable enough,
but what if we don’t have any control over the name of this data type? What if Player is defined by an
API that we need to use? This means that the name “Player” has been reserved by an API that we have no
control over. Most of the time, this simply means that we can’t use the name “Player” for our own data
types.


www.gameinstitute.com Introduction to C and C++ : Week 6: Page 5 of 44
But what if we’re using two APIs, and both define a data type with the name “Player”? Now, not only are
we prohibited from using this name, but our code will not compile because there is a name conflict. In
this situation, there is no way to proceed without getting the maker of one of the other APIs to change the
name of their data types. As things stand, the two APIs are incompatible.

Before the introduction of namespace support, API developers would often prefix their data type names
with the name of the company, the name of the API, or an acronym, like this:

class abcPlayer // API “ABC” prefixes all of their data types with “abc”
{

};

struct defPlayer // API “DEF” does the same thing
{
};

This prevented the name conflict problem—most of the time. But there is still a chance that two different
companies will use the same name. And furthermore, the name of each data type is now obscured.

Namespaces allow data types to be declared in a private namespace, or scope. This prevents data type
names from cluttering the global namespace, and therefore prevents name conflicts. (As long as the name
of the namespace itself is unique.) Using namespaces, the APIs abc and def would define their own
version of “Player” without a prefix:

namespace abc
{
struct Player
{
};
};

namespace def
{
class Player
{
};
};

This code defines two different data types, both named Player, but each within its own namespace, so
there is no conflict. What’s more, since the Player name is kept out of the global namespace, we’re free

to use the name “Player” in our code:

namespace abc
{
struct Player
{
};
};

namespace def
{
class Player
{
};

www.gameinstitute.com Introduction to C and C++ : Week 6: Page 6 of 44
};

typedef int Player;

Each usage of the name “Player” refers to a different data type. One is a class, one is a structure, and one
is a typedef to an int. But, because each resides in a separate namespace, there is no conflict (the typedef
version of Player exists in the global namespace).

What’s the catch? Each time you use the name “Player”, you must specific which one you’re referring to.
By default, the global namespace is used, so in this case you’ll get the global “Player”, like this:

Player p; // p is an ‘int’ (via typedef)

If the global Player typedef shown above were not defined, then using the plain name “Player” would

fail, because no such data type exists not in the global namespace.

To indicate a data type that is part of a non-global namespace, the scope resolution operator (::) must be
used, like this:

abc::Player p1;
def::Player p2;

These declarations create two different variables, each with a different type, despite the fact that the data
type name is “Player” in both cases. The first creates a variable based on Player as defined within the abc
namespace, while the second uses the Player data type from the def namespace. We can prove that the
two variables are different by adding member functions to each version of Player:

namespace abc
{
struct Player
{
void Blah();
};
};

namespace def
{
class Player
{
public:
void Yak();
};
};


abc::Player p1;
def::Player p2;

p1.Blah();
p2.Yak();

These function calls are specific to each version of Player, so the fact that this code compiles proves that
the p1 and p2 variables are indeed based on different types.


www.gameinstitute.com Introduction to C and C++ : Week 6: Page 7 of 44
Any number of different data types can be added to a namespace. Here, a namespace called xyz is defined
that includes typedefs, a structure, and a function:

namespace xyz
{
typedef int Type1;
typedef float Type2;
struct Type3
{
};
void Func()
{
}
};

Using data types that are defined as part of a namespace can be performed either with the scope resolution
operator for each usage, like this:

xyz::Type1 a;

xyz::Type2 b;
xyz::Type3 c;
xyz::Func();

Or with the using keyword, which activates a namespace for use by the code that follows:

using namespace xyz;

Type1 a;
Type2 b;
Type3 c;
Func();

The using keyword activates a namespace, but it does not prohibit the usage of names in the global
namespaces, so the code above has access to both the xyz and the global namespace. As a result, the
using keyword doesn’t work with names that are present in both namespaces::

namespace xyz
{
typedef int Type1;
typedef float Type2;
struct Type3
{
};
void Func()
{
}
};

void Func();


using namespace xyz;

Func(); // compiler error! The name “Func” is ambigous


www.gameinstitute.com Introduction to C and C++ : Week 6: Page 8 of 44
This code won’t compile, despite the fact that the xyz namespace has been activated with the using
keyword. In this case it is necessary to use the scope resolution operator to indicate which version of
Func we’re referring to:

xyz::Func(); // calls Func as provided in the xyz namespace

::Func(); // calls the global version of Func

Using the scope resolution operator with no preceding scope indicates the global namespace. This is only
necessary if the current scope includes a name that is the same as one in the global namespace.

During the normal course of game programming, it is not normally necessary to define new namespaces.
Even on a large project, name conflicts are fairly unlikely, but if it does crop up, it is usually just a matter
of talking with the programmer in the next cubicle to decide which type can be renamed to resolve the
conflict.

Using existing namespaces is more common, as many APIs define their data types within non-global
namespaces to avoid conflict. This includes APIs that are part of the standards libraries, as we’ll soon see
when we look at STL.
Templates
Another feature added to C++ after it had become widely used is templates. A template is an incomplete
type. One or more key elements are missing, but can be provided when the data type is used. Templates
are sometimes called parameterized types because they take parameters. But unlike a function, template

parameters are data types—not data values. Consider this code:

template <class T> class Data
{
public:
T GetValue() { return value; }
void SetValue (T v) { value = v; }
private:
T value;
};

This is a template for a class called Data. This template takes one parameter called T (a capital T is the
standard name for a template parameter.) The definition begins with the template keyword, and is
followed by a template parameter list. In this case there is only one parameter, but there can be any
number of parameters. Unlike function parameter lists, template parameter lists are enclosed in angle
brackets. Most of the time parameter names are preceded by the class keyword, but class has a different
meaning when used in this context. A template parameter that uses the class keyword can be any data
type, not just a class.

The Data class declares two member functions and a data member. The functions allow the private data
member to be assigned, and retrieved. Notice the type used as a return value for GetValue, the argument
for SetValue, and the data member. Instead of a type like int, or float, the template parameter name is
used. In each case the data type used is contingent on the parameter passed to the template as T.

In order to use a template, a data type must be supplied, like this:

Data<int> dataObject1;
Data<float> dataObject2;

www.gameinstitute.com Introduction to C and C++ : Week 6: Page 9 of 44


In this case dataObject1 is an instance of the Data class that is parameterized on the int type. The result
is that this object can be used as though each instance of T were actually an int. Likewise, dataObject2
uses the float type. These objects can then be used like this:

dataObject1.SetValue( 33 );
int val1 = dataObject1.GetValue();
cout << val1 << endl; // displays 33

dataObject2.SetValue( 44.5f );
float val2 = dataObject.GetValue();
cout << val2 << endl; // displays 44.5

Once an object has been created based on a template class, there’s no evidence that it is template-based. It
can be used as if the class upon which it is based were written specifically with the provided data type.

Any data type can be used as a template argument, including pointers:

Data<Player*> obj;
Player p;

Obj.SetValue( &p );
Obj.GetValue()->SetName( “slowpoke” );

What are templates for? Templates allow the definition of generic data types. They can be used to write
classes that aren’t specific to any single data type. Templates are often used to write container classes, as
we’ll see in the next section.
STL
As its name implies, the Standard Template Library (STL), is a set of templates. Now part of the standard
C++ libraries, these templates provide functionality that is extremely flexible and powerful.


STL provides support for several data containers. Because they are templates, these containers are
completely generic. They can be used to store any type of data. We won’t cover all of the STL templates,
but STL defines these containers:

• string
• list
• deque
• stack
• vector
• map
• set

Although we won’t cover all of these types, most of the STL containers work in a similar way. The string
data type is a container that stores characters. Unlike other types, string doesn’t require a template
argument, because it is actually a typedef for the STL basic_string type, which is templatized on the
char type, like this:

typedef basic_string<char> string;


www.gameinstitute.com Introduction to C and C++ : Week 6: Page 10 of 44
All of the STL data types are defined in std namespace (short for standard), so this namespace must be
specified with the scope resolution operator, or the using keyword. Declaring a string, therefore looks like
this:

std::string str1;

// or


using namespace std;
string str2;

The string type, through basic_string, provides a host of copy constructors, overloaded operators, and
member functions, all of which serve to make string handling much easier than it is with char arrays.
Here’s an example:

using namespace std;

string a("one"); // string ‘a’ contains “one”
a.append(" "); // append one space to the end
a+="two"; // append another string (‘+=’ is the same as append)
string b(" three"); // string ‘b’ contains “three”
a+=b; // append an object of the string type
int size = a.size(); // retrieve the string length

cout << a.data() << " size=" << size << endl;
// displays ‘one two three size=13’

(If this seems familiar, that is probably because we used the STL string class in Lesson 3 to implement
the HighScores sample.)

The overloaded constructors and operators that the string class provides are nice, but that’s just the
beginning of what this class can do. The string class also provides support for searching, replacing, and
inserting. Likewise, the other STL containers provide member functions that provide convenient features,
such as sorting.

Many of the STL container features rely on the concept of an iterator. An iterator is a class that is
designed solely for manipulating the contents of a container object. STL iterators provide a number of
overloaded operators that allow them to be used just like pointers.


STL defines a different iterator for each container, but the iterator class always has the name iterator.
This is possible because the iterator class is defined inside the container class. As a result, the scope
resolution operator must be used in order to access the iterator data type. For the string class, declaring
an iterator looks like this:

std::string::iterator it;

Despite the odd syntax, this is a simple object declaration. An iterator called it is declared based on the
iterator class, which is defined within the scope of the string class, which in turn is part of the std
namespace; hence the use of two scope resolution operators.

This new iterator is initialized, as the string::iterator class provides a default constructor, but the new
object doesn’t indicate an element until used with an existing instance of string, like this:

www.gameinstitute.com Introduction to C and C++ : Week 6: Page 11 of 44

std::string str(“bolt”);
std::string::iterator it( str.begin() );

This code creates a string called str, and uses the begin member function to return an iterator to the first
string element. In this case the element is the character ‘b’. Now the iterator can be used to retrieve and
assign the contents of the str string. STL overrides the de-reference operator for this purpose, making the
iterator appear as though it is a pointer:

cout << str.data() << endl; // displays ‘bolt’
cout << *it << endl; // displays ‘b’
*it = ‘c’; // assign ‘c’ o the element indicated by ‘it’
cout << *it << endl; // displays ‘c’
cout << str.data() << endl; // displays ‘colt’


To add to the illusion that an iterator is actually a pointer, the iterator class overrides the increment and
decrement operators, which can be used to traverse the elements in the container:

it++; // advances the iterator to the next element
cout << *it << endl; // displays ‘o’
it ; // retreat to previous element
cout << *it << endl; // displays ‘c’

These operators make iterating through a collection of data items stored in an STL container very easy:

std::string s("STL");

for (std::string::iterator it = s.begin(); it != s.end(); it++)
cout << " " << *it ;

In this case a for loop is used to declare an iterator to the first element of a string, and traverse the string
until the end is reached. The stop condition is implemented by comparing the iterator with the return
value from the end member function, which returns an iterator indicating the last element in the string.
This loop displays “ S T L”.

At the very least, STL is a boon for game programmers because they no longer have to write their own
versions of the various container classes that games often require. Before STL, one of the first steps in
programming a game was either writing custom container classes, or locating existing containers. In
either case this was a waste of time, as a properly designed container class is generic enough that it can be
written once and forgotten about.

The string class has obvious benefits, but what about the other STL containers? The list container is an
implementation of a doubly linked list. This type of data structure is similar to an array except that the
elements of the list are not stored contiguously in memory. Instead, each element is a node that is linked

to the next and previous nodes via pointers. There is a performance penalty for this layout, but the
advantages are that the list size is limited only by the amount of available memory. It is also much faster
to insert elements into the list because the new data can be linked into the list without moving the existing
elements.

Unlike the list class, the deque class is more like array. This gives it performance benefits for simple
traversals, but inserting elements into the container is expensive because the existing items must be
moved to make room for the new data. The deque class has an advantage over a basic array because

www.gameinstitute.com Introduction to C and C++ : Week 6: Page 12 of 44
adding items to the front of the collection does not require that the existing items be moved. In the next
section we’ll use this container to write a sample.

The stack container implements a data container in which items can only be added and removed from one
end of the collection. Stacks are sometimes used to implement menu systems in which a collection of
menus can be presented, one over the other. Only the topmost menu has focus, and when it is removed
from the top of the menu stack, the next menu gains the focus. The terms “push” and “pop” are typically
used to describe stack operations. We’ll use this technique for a menu system in the game presented later
in this lesson.

The term vector, used in the context of STL, refers to a container that behaves like a dynamic array not a
data structure that indicates direction and velocity. Unlike the deque container, items cannot be added to
the front of a vector, just as items can’t be added to the beginning of an array without moving all existing
items. The vector class overloads the square bracket so that elements can be accessed as though the
container is a C++ array.

The map container uses a key system to provide access to its contents. Each item added to a map is
accompanied by a key that is used by the map container to store the item. If the item is required later, it
can be located quickly by providing the key. Maps are useful for large collections of items that cannot be
indexed easily. For example, if a collection of objects must be identified by a large range of numbers,

allocating an array large enough to store an element for each possible index is out of the question. The
memory requirements would be excessive, and most of the array elements may be unused. A map can be
used to store data items whose indices are not actually indices into an array. Maps are therefore
sometimes called sparse arrays.

Finally, the set container stores a collection of items that don’t have any intrinsic order. Items can simply
be added to the collection. The concept of a set is prevalent in mathematics.

Notice that, through the use of namespaces, each of the STL container types uses an exceedingly simple
name. This would be very bad form if namespaces weren’t used because it would prohibit the global use
of simple names like list, set, and string.
The Snake Sample
Rather than using the string class to demonstrate STL in a boring console application, let’s use a DirectX
enabled application. We’ll use the deque class to store a list of two-dimensional points. A spherical
model will be drawn at each of these points, and each update cycle will remove a point from the end of
the deque and add one at the beginning. The result is the animation of a shape that looks something like a
snake. The final executable looks like this:


www.gameinstitute.com Introduction to C and C++ : Week 6: Page 13 of 44


To add a bit of interaction, we’ll configure the F2 key to prohibit the removal of deque elements from the
end of the collection, causing the length of the collection to be increased with each update cycle, and the
F3 key to prohibit the addition of new deque elements, causing the “snake” to shrink.

The first step is to add a few data member to the application class, which—naturally—is derived from the
D3DApplication class:

typedef std::deque<POINT> PointDeque;

PointDeque pointDeque;
int xInc, yInc;
bool grow, shrink;
D3DModel sphereModel;

Namespaces and templates are valuable and powerful features, but the syntax required to use them can be
awkward. For this reason, it’s often a good idea to use a typedef to create a simpler alternative name. In
this case we’ve defined the name PointDeque as an alias for the STL deque, parameterized on the
POINT structure. This new name is used immediately after it is defined, to create data members of this
container type.

The xInc and yInc integers will be used to indicate the amount by which each axis is incremented each
time a new point is added to the front of the deque. We’ll use the grow and shrink Booleans to track
whether the F2 and F3 keys are currently being pressed. The last data member is an instance of
D3DModel. As with the Circle sample, although we’ll be rendering multiple instances of the model, only
one model object is required.

www.gameinstitute.com Introduction to C and C++ : Week 6: Page 14 of 44

We’ll also use some literal constants to define values used in the implementation:

const int LimitX = 200;
const int LimitY = 150;
const int IncX = 7;
const int IncY = 4;
const int SnakeLenInitial = 50;
const int SnakeLenMax = 300;
const int SnakeLenMin = 2;
const char* SphereModelFile = "lowball.x";


The LimitX and LimitY values are used to keep the snake in the window. If the snake advances beyond
these values, either in the positive or negative direction, we’ll reverse the sign of the xInc or yInc data
member to reverse the direction in which the snake will advance. The IncX and IncY constants are used
as initial values for the xInc and yInc data members.

The next three values control the number of elements in the container, and therefore the length of the
snake. Initially we’ll add 50 points to the deque. If the user presses F2, we’ll stop the removal of points,
but only until the deque size reaches SnakeLenMax. Likewise, pressing F3 will stop new points from
being added to the deque, but only until the size reaches SnakeLenMin.

The SphereModelFile constant is a string containing the name of the .x file from which the spherical
model will be loaded. Notice that we’re using a different .x file for this sample. The Circle sample used a
model called sphere.x, but this model was fairly high-resolution—meaning that it was composed of
hundreds of polygons. For this sample, because we may be rendering as many as 300 instances of the
model, we’re using a much lower resolution model, with closer to a dozen polygons. In fact, the lowball.x
model is actually a hemisphere whose rounded side faces the camera. This will save us the processing
power of processing polygons that won’t appear because they’ll be hidden by the front of the model.
These types of optimizations are typical of 3D graphics programming. For performance reasons, every
effort must be made to reduce the number of polygons required while still maintaining an effective visual
effect.

Now that we have our data member and constants in place, we’re ready to initialize the application:

bool SnakeApp::AppBegin()
{
// initialize DirectInput
inputMgr = new eiInputManager( GetAppWindow() );
keyboard = new eiKeyboard();
keyboard->Attach( 0, 100 );


// initialize Direct3D
UseDepthBuffer( false );
HRESULT hr = InitializeDirect3D();
if (FAILED(hr))
{
return false;
}

// prepare the "snake"
int x = 0, y = 0;
for (int i=0; i<SnakeLenInitial; i++)

www.gameinstitute.com Introduction to C and C++ : Week 6: Page 15 of 44
{
POINT pt = { x, y };
x -= xInc;
y -= yInc;
pointDeque.push_back( pt );
}

// initialize frames per second object
fps.Reset();

LOGC( 0xff00ffff, "F2 - grow\n");
LOGC( 0xff00ffff, "F3 - shrink\n");

return true;
}

The AppBegin function looks pretty much like that of the other Direct3D applications we’ve written, but

this version uses a loop to add points to the pointDeque data member. This involves the push_back
member function of the deque class, which adds elements to the back of the deque. The result is a list
that begins with a point at 0,0. Each subsequent point is decremented by the xInc and yInc data members
(which have been initialized with the constants discussed previously.) We decrement these values rather
than increment so that the head of the snake is ready for animation by incrementing the position at the
head of the deque.

Now we’re ready to look at how the snake is rendered, a task performed by the DrawScene function:

void SnakeApp::DrawScene(LPDIRECT3DDEVICE8 device, const D3DVIEWPORT8& viewport)
{
PointDeque::iterator it;

for (it = pointDeque.begin(); it != pointDeque.end(); it++ )
{
POINT pt = *it;
float x = static_cast<float>(pt.x);
float y = static_cast<float>(pt.y);
sphereModel.SetLocation( x, y, 0.0f );
sphereModel.Render();
}
}

This function, using an iterator, renders the model represented by sphereModel once for each point in the
pointDeque container. Notice that because of the PointDeque typedef defined earlier, creating an
iterator doesn’t require the use of the std namespace, or a template argument. Had we not used the
typedef, declaring the iterator would look like this:

std::deque<POINT>::iterator it;


A for loop is used to traverse the contents of pointDeque. The body of the loop uses the iterator de-
reference operator to retrieve a copy each element. The x and y data members are then cast to the float
type with the static_cast operator. The resulting values are provided to D3DModel::SetLocation, and
finally, the D3DModel::Render member function is called.


www.gameinstitute.com Introduction to C and C++ : Week 6: Page 16 of 44
The DrawScene function is called by the AppUpdate function, which looks very much like that of the
Circle sample, with one exception: the code that updates the data structures for the purposes of animation
has been separated from the code that performs the rendering. In the Circle sample, both animation and
rendering were performed by the DrawScene function. This is fine for a simple demonstration
application, but for a game, combining the two is usually a mistake.

Updating the state of animated objects in the same function that renders output ties the animation rate to
the rendering rate. If you run the Circle sample on a slow machine, the animation will be slow. A fast
machine will cause the Circle sample to animate the scene quickly. For a game, this means that the game
would be more difficult on a fast machine, and boring on a slow machine. Clearly, that’s not the effect we
want.

The solution is to separate the two tasks, and perform them at separate rates. The animation will be
performed at a regular rate, and the rendering will occur as fast as the machine can manage. Updating the
state of the application at a regular rate is desirable for several reasons, consistent animation just being
one of them. Particularly for networked games, it is vital that the game state is updated regularly and
consistently.

Notice that this means that the application may render the same scene multiple times. If new scenes are
being produced at 60 hertz (60 times a second), and the game state is being updated at 20 hertz, then each
state will be rendered 3 times. Game programmers often address this issue by using interpolation to
“blend” the animation between states.


Another problem is that, although it is easy to update the game state less often than new scenes are
rendered, the same is not true in reverse. If a slow machine produces 15 frames a second, it’s hard to
perform 20 game state updates without resorting to multithreading.

For the Snake sample, and for the game that we’ll look at next, we’ll settle for performing state updates
less often than rendering—so long as the machine is capable of rendering faster than the update rate.
These issues, including multithreading, are addressed in the DirectInput course here at the Institute.

The AppUpdate function looks like this:

bool SnakeApp::AppUpdate ()
{
DWORD timeNow = timeGetTime();
if (timeNow >= lastCycleTime + TimerInterval)
{
UpdateState();
lastCycleTime = timeNow;
}

LPDIRECT3DDEVICE8 device = Get3DDevice();

HRESULT hr;
if( FAILED( hr = device->TestCooperativeLevel() ) )
{
if( D3DERR_DEVICELOST == hr )
return true;

if( D3DERR_DEVICENOTRESET == hr )
{
if( FAILED( hr = Reset3DEnvironment() ) )


www.gameinstitute.com Introduction to C and C++ : Week 6: Page 17 of 44
return true;
}
return true;
}

Clear3DBuffer( 0 );

if (SUCCEEDED(device->BeginScene()))
{
D3DVIEWPORT8 viewport;
device->GetViewport( &viewport );

DrawScene( device, viewport );

ShowFPS();
DrawLog( viewport );

device->EndScene();

Present3DBuffer();

fps.NewFrame();
}

return true;
}

This function uses the high-performance timer function timeGetTime to measure the time that has

elapsed since the last state update. The UpdateState function is called only if the time interval indicated
by TimerInterval has elapsed. This applies to the UpdateState function only. The remainder of the
function, including the DrawScene function, gets called every time that AppUpdate is called.

The value for TimerInterval that the Snake sample uses results in 60 updates per second, but full-scale
games are often forced to use much lower update rates—as low as 10 hertz in some cases.

The UpdateState function—which again, is called at a regular rate, looks like this:

void SnakeApp::UpdateState()
{
if (!appActive)
return;

// check for keyboard input
DIDEVICEOBJECTDATA event;
while (keyboard->GetEvent( event ))
{
if (event.dwOfs == DIK_F2)
{
if (event.dwData & 0x80)
grow = true;
else
grow = false;

}
else if (event.dwOfs == DIK_F3)

www.gameinstitute.com Introduction to C and C++ : Week 6: Page 18 of 44
{

if (event.dwData & 0x80)
shrink = true;
else
shrink = false;
}
}

// update snake
int size = pointDeque.size();

if ( ! grow || size >= SnakeLenMax )
pointDeque.pop_back(); // remove point

if (! shrink || size <= SnakeLenMin)
{
POINT pt = pointDeque.front();
pt.x += xInc;
pt.y += yInc;
if (pt.x > LimitX || pt.x < -LimitX)
{
xInc =- xInc;
pt.x += xInc;
}
if (pt.y > LimitY || pt.y < -LimitY)
{
yInc =- yInc;
pt.y += yInc;
}
pointDeque.push_front( pt ); // add point
}

}

The UpdateState function performs two tasks: user input gathering and the update of the sample’s data
structures—in this case the point deque. The DirectInput keyboard object is used to check the states of
the F2 and F3 keys for the purposes of setting the grow and shrink data members. These data members
are then used to determine whether points will be added and removed from the deque.

The STL deque class, like the list and vector classes, provides the pop_back, pop_front, push_back,
and push_front data members that allow the items to be added or removed from the front or back of the
data collection. In this case we’re using the pop_back and push_front functions only.

Notice the conditionals that are used to perform collision detection before each new point it added to the
list. Each new point is checked against the vertical and horizontal limits, and, if either one has traveled
outside the allowed area, the increment variable used to calculate point positions is reversed so that
subsequent points will travel in the opposite direction. We’ll use this strategy with the following game as
well.
The Pong Game
Although there is some debate, a game called Pong is generally accepted to be the first video game. As
early as 1973 companies like Coleco, Atari, and Magnavox where designing and marketing the first
computer games for the home, and Pong was the only option. Most of these units had a long cord so that
you could play from your couch (this was well before the wireless revolution), and had two dials for

www.gameinstitute.com Introduction to C and C++ : Week 6: Page 19 of 44
exciting multi-player action. (Well, it seemed exciting at the time.) Although Pong is unlikely to make a
comeback, it can’t hurt to remake this classic game, but this time, instead of playing against a human
component, we’ll create an Artificial Intelligence player. The final executable looks like this:



Graphically speaking, the game is exceedingly simple. Two cylinders represent the “paddles”, and the

ball is a sphere. The ball bounces around the screen, and the idea is to maneuver your paddle to deflect it
back toward the opponent’s paddle. Each player gets a point when their opponent fails to intercept the
ball, and it bounces off the “wall” behind. The game gets more difficult in two ways: the ball gets faster,
and the AI opponent gets faster. Whoever gets 30 points first wins the game, at which point a menu is
displayed that asks whether you want to play again:


www.gameinstitute.com Introduction to C and C++ : Week 6: Page 20 of 44


If you select “Play Again”, the state of the game objects is reset, and the game starts over, otherwise the
application terminates. There is another menu that is displayed if you press Escape during game play.
This menu asks if you’re sure you want to quit. If not, the game continues, if so, the application
terminates.

Why write a game that is 30 years old? The point of this sample is not to amaze you with new ideas for
games, but to showcase the C++ features that we’ve covered in this course in the context of a game. By
using a simple game, we can keep the code fairly simple, but still use the features that make C++ such a
potent game programming language.
The Game Classes
Before we look at any code, or design any class interfaces, let’s talk about the entities in the game.
Because we’re writing such a simple game, this is pretty obvious: there are two paddles and a ball. One
paddle is controlled by a human player, and the other is controlled by an artificial opponent. Clearly, we
can write a class to represent each of these entities. In this case, we’ll be using one instance of each of
these classes, but it’s important to note that for many games a single class can be used to represent
multiple entities. A horde of enemies, for example, can be represented as multiple instances of a single
class.

We’ll name our classes HumanPlayer, ArtificialPlayer, and Ball, but before we start coding, let’s
consider whether these classes have enough in common to share any base classes. We know from our

discussion in Lesson 3, that it’s possible that HumanPlayer and ArtificialPlayer, because they are both

www.gameinstitute.com Introduction to C and C++ : Week 6: Page 21 of 44
players, can share a common base class. We’ll call this class Player. This will allow us to write game
code that treats both players the same way, through the interface defined by the base class.

But we don’t have to stop there. The Ball class, although it certainly isn’t a player, is an entity in our
game, and as such has some of the same requirements as the player classes. Specifically, all three of these
objects need to update their state regularly, and they need to be rendered to the back buffer. This is
enough to warrant a base class that is shared by all three classes, which we’ll call GameObject.

The GameObject class will provide an interface common to all of the objects in the game, and the Player
class, which is derived from GameObject, will provide an interface common to both players. This class
hierarchy looks like this:



Notice that we’ve been careful not to break the fundamental rule of inheritance: each derived class is what
its base class is. A human player is a player. An AI player is a player too. In turn, a player is a game
entity, or object, as is the ball.

With our game object class hierarchy in place, we’re almost ready look at the code, but there are two
more class hierarchies that we’ll be using. The first is the application class hierarchy, which by now is
familiar. We’ll be deriving an application specific class from D3DApplication, which is itself derived
from GameApplication. We’ll call our application class PongApp.

The other hierarchy is for the menus that the game uses. There are three such menus, although one of
them is non-interactive. Before game-play begins, a countdown is performed to allow the player (the
human player) to get situated.


These menus are represented by classes derived from a common base class called MenuBase. This class,
given input from the keyboard, calls virtual functions that control the menu. The MenuBase class calls a
function called Up if the up arrow is pressed, Down if the down arrow is pressed, and Select if the Enter
key is pressed. The MenuBase class is derived from a class called eiMenu, which implements the Up,

www.gameinstitute.com Introduction to C and C++ : Week 6: Page 22 of 44
Down, and Select function, along with some other member functions that together implement basic menu
functionality. This menu class hierarchy looks like this:



A menu system in an important part of any game, although not a glamorous one. Without menus, there’s
no way for the user to specify game options or start a new game without exiting and restarting the
application. We won’t detail how the eiMenu class is implemented, but at a conceptual level, this class
uses a stack. The game code can push a menu onto this stack, and it becomes the active menu. When a
selection is made, the menu is popped off of the stack, making the next menu on the stack active. If no
menus are on the stack, then no menus are displayed, and game play is in progress.

The remainder of this lesson covers the game object hierarchy, and how these classes are used in the Pong
game. We’ll see how the menu classes are used in the game, but we won’t cover the implementation of
these classes.
The GameObject Class
Class design is a different process than class implementation. Class design—deciding on class names,
interfaces, and relationships, is probably best done “on paper”, preferably with as many team members
present as possible. This way each member’s perspectives and concerns are reflected in the initial version
of the hierarchy. This process can be heated and rife with conflict, but is worth the trouble if the final
result is superior to what any single team member could have done (which, on a team of qualified C++
programmers, is usually the case).

The best strategy for class design is to start with the concrete classes. These are the classes that will

actually be instantiated and used in the game code. After these classes are designed, at least in rough
form, you can move on to the base classes by analyzing which functionalities and interfaces the concrete
classes might share. This means that class design often begins from the bottom of the hierarchy tree, and
moves to up to the base classes. (Some classes may not share any features or interfaces with other classes,
and will therefore not have base classes.)


www.gameinstitute.com Introduction to C and C++ : Week 6: Page 23 of 44
Class implementation, is usually performed in reverse starting with the base classes, and moving to the
derived classes. This is because derived classes simply cannot be implemented without the base classes in
place.

For our look at the game classes, we’ll follow the implementation path. We’ll start with the common base
class, GameObject, and move to the derived classes. We’ll do this despite the fact that we’ll talk about
design as we go along.

Before we address any data members or member functions, we’ll define the class body with a constructor
and destructor:

class GameObject
{
protected:
GameObject();
public:
virtual ~GameObject() { }
};

The GameObject class is abstract—it is to be used as a base class, but not as a concrete class.
Instantiating an object based on this class doesn’t make any sense, because this class doesn’t provide
enough functionality to stand on its own. One way to prevent this class from being instantiated is to make

its constructors protected. Because the constructor isn’t publicly accessible, it can’t be called from
outside the class, so no objects of this type can be created. But, because the constructor is accessible to
derived classes, it can be used as a base class.

Another way to insure that a class is used only as a base class is by declaring at least one pure virtual
function, but it’s still a good idea to use a protected constructor. This way, if no pure virtual functions are
added, or if they are added but later removed during a revision, the class cannot be misused. We’ll talk
more about pure virtual functions soon.

The GameObject destructor is virtual. This is very important for classes that are designed to be used as
base classes. A non-virtual function would result in derived classes whose destructors would not be called
if the object is deleted using a GameObject pointer.

Now we need to add some member functions, and possibly some data members (highly generic base
classes often have no data members.) So what is the GameObject class? What interface and functionality
can it have if these traits are to be shared by the concrete classes HumanPlayer, ArtificialPlayer, and
Ball? One way to approach this question is to ask what is not appropriate for the base class. Can we add
user input support to GameObject? No, because only the HumanPlayer class will require user input.
Providing support for user input would be inappropriate for the ArtificialPlayer and Ball classes. Can we
add graphics support to GameObject? Yes, because, although we don’t know exactly how each of the
derived classes will represent itself visually, we do know that each of these classes will have a visual
representation.

This means that we can add support for a 3D model, and virtual function that can be used to instruct the
class to render the model. We’ll make this function virtual, in case the derived classes want to override it,
but we can provide a default implementation as well. With these additions, the GameObject class looks
like this:

class GameObject
{


www.gameinstitute.com Introduction to C and C++ : Week 6: Page 24 of 44
protected:
// protected constructor for derivation only
GameObject(D3DModel* m);

public:
virtual ~GameObject() { }
virtual void Render();
private:
D3DModel* model;
};

Notice that we’ve modified the constructor to take a pointer to the D3DModel class. Because this is the
only constructor that GameObject provides, this argument is mandatory in order to create objects of this
type.

The Render function will perform the drawing of the model provided to the constructor, but, in order to
implement this function, we’ll need the position of the model in 3D space. So we need to ask ourselves, is
the model position appropriate for the GameObject class? Yes, because each game object will need a
location in order to appear on the screen. Hence these additions:

class GameObject
{
protected:
GameObject(D3DModel* m);
public:
virtual ~GameObject() { }
virtual void Render();
void SetX(float xx) { x = xx; }

void SetY(float yy) { y = yy; }
void SetZ(float zz) { z = zz; }
void SetXYZ(float xx, float yy, float zz) { x=xx; y=yy; z=zz; }
float GetX() { return x; }
float GetY() { return y; }
float GetZ() { return z; }
float GetXYZ(float& xx, float& yy, float& zz) { xx=x; yy=y; zz=z; }
private:
D3DModel* model;
float x, y, z;
};

Now the position of the model is stored in the GameObject class. These data members are private, but
can be accessed via a public set of accessor functions. Now that we have a position, we can implement the
Render function:

void GameObject::Render()
{
assert(model);
model->SetLocation( x, y, z );
model->Render();
}

The Render function, using the x, y, and z data members, sets the location of the model in 3D space, and
renders it using the D3DModel::Render function.


www.gameinstitute.com Introduction to C and C++ : Week 6: Page 25 of 44
Part of the challenge of designing a base class is being careful not to unnecessarily restrict how the class
can be used. This is why we’ve made the Render function virtual. This way derived classes can opt not to

use this version of the Render function. If, for example, a game object is to be represented as a 2D object,
or sprite, a derived class could implement the Render function to use an alternate method of display.
Derived classes can also override Render in order to augment the base class implementation of this
function, as we’ll see soon.

There’s one more item that we can add to the GameObject class: a member function for updating the
object:

class GameObject
{
protected:
GameObject(D3DModel* m);
public:
virtual ~GameObject() { }
virtual void Update() = 0;
virtual void Render();
void SetX(float xx) { x = xx; }
void SetY(float yy) { y = yy; }
void SetZ(float zz) { z = zz; }
void SetXYZ(float xx, float yy, float zz) { x=xx; y=yy; z=zz; }
float GetX() { return x; }
float GetY() { return y; }
float GetZ() { return z; }
float GetXYZ(float& xx, float& yy, float& zz) { xx=x; yy=y; zz=z; }
private:
D3DModel* model;
float x, y, z;
};

The Update function is an abstract, or pure virtual function, meaning that derived classes must

override
this function. The syntax for a pure virtual function is just like that of a virtual function, except that the
declaration is followed by the equal sign and zero. This doesn’t actually assign the function to zero—it
tells the compiler that no objects can be created using classes derived from this class unless they override
this function. In the case of the Update function, this is a reasonable demand. All objects must update
themselves.
The Player Class
Derived from GameObject, Player represents a game player, be it human or artificial. Deciding what
members should be present in this class requires the same questions as those asked for the GameObject
class, except that the Player class is more specific. Player is a game object, but is also a player, so we can
add member functions and data members that are common to both human players and artificial players.

Like GameObject, the Player class is abstract. It won’t be used to create objects directly—only via
inheritance. This means that we can use a protected constructor, and pure virtual functions. To begin, let’s
add the constructor and the member functions inherited from GameObject:

class Player : public GameObject
{
protected:
Player(D3DModel* model);
public:

×