|
I had big plans for this month. I was going to put the code out there to do
nifty things like get input from the user, and create a basic game. I started
to write all this when I realized something—I had forgotten some of the
more crucial parts of game programming. In actuality, I had been putting
them off, as they aren't exactly the most exciting of subjects, but we're just
about to the point where they become indispensable. So sorry to disappoint,
but there won't be much graphics-specific code this month. Instead we're going
to cover documentation and code architecture, along with a little bit of
vector math. Now don't get me wrong—the subjects may be a little dry, but they're
crucial to your success as a game programmer, and it'd be a crime for me to
not make mention of them.
Documentation is the long-standing enemy of the novice coder
(and can still be an annoyance to those of us who no longer consider
ourselves novices), but since a lot of game development is done in a team fashion
(indeed, most game teams have a person devoted solely to the practice of design
and documentation) documentation is the glue that holds it all together.
Data
structures, while not as essential, as probably most of us learned them
in a class or three several years ago, also deserve mention as there are several
types of dynamic structures that we'll be using in our games, and some
work better than others in different places. Finally, vector math is going
to be in almost everything that we do AI-wise, as well as an essential part
of collision detection (and response). It's probably been a while since physics/calculus
for most of you, so I'll cover the basics of what you'll need to know.
Let's dig in. First stop:
Documentation
To be honest, there aren't really many standards to game documentation. Don't
get me wrong—the industry makes somewhat excessive use of documents, just
what goes into them often varies from group to group. Either way, there are
several that should be covered regardless of what they're called, which I'm
going to present here.
For consistency's sake, I'm drawing a lot of my information from
"Game
Architecture and Design" by Andrew Rollings and Dave Morris. While
the version I have isn't in print anymore, they released a new version a
couple months ago that addresses some of the major typos and mistakes in
this volume. This is pretty much an essential read for anyone trying to get
into game development—it doesn't cover much of the hardcore game development
stuff (you won't find any tricks for ray tracer engines in there), but it
covers the planning portion of the project (documentation, code architecture),
which is arguably more important.
First is the game treatment. The game treatment is a 1-2 page document that
introduces the vision of the game designer to the reader. It's the first test
of your game idea—writing it all down. At this stage, you don't really need
to worry about things such as technical limitations or what is actually possible;
you only need to worry about your idea and where you want to take it. Cover
everything about your idea that makes it seem cool to you, while getting as
much of your inspiration across as possible.
The next document is where most of the disagreement happens. Many people at
this point will move straight to the monolithic design document, putting everything
about everything into that document. Personally, I like a bit more segmented
approach. While it may not shrink the design doc much, it will provide a sense
of organization. I suggest at this point you present a list of things that
are bad with the idea, and attempt to solve those issues. Get outside input—we all tend to think too highly of our own work. It may end up being a bit
of a reality check, but overall it's better to find out what's wrong now than
to discover the problem months into the development process. Also, don't let
the small things get to you here. If an issue isn't really all that vital,
don't let it be the reason you scrap the entire project. One of the most important
things to develop here is a thick skin—take all criticism at face value,
and then try to work with that to better the overall product.
The most important document (at least for the designer) is the Design Document.
This is a monstrous document, often averaging 100 pages or more, that covers
every aspect of your game (and I mean EVERYTHING). This document covers everything
that you could possibly conceive of in relation to your game—monsters, levels,
enemies, music, story, dialogue, etc. —in one fell swoop. If your main character
tends to lean a bit to the right when he swings his sword, it better be in
this document. If you want your source files organized by name, it better be
in this document. If you want the game to do something when the player sneezes,
you're getting a little ambitious, but it BETTER BE IN THIS DOCUMENT. This
document is not complete until you can hand it to someone else and say "Here's
my idea, make this game" and be confident it will turn out exactly as
you envisioned it. Anything that isn't covered in here is going to consume
many hours of programming and development time later. When in doubt, go into
too much detail (if there is such a thing). Cover your coding standards, your
filenames, your class structure, model resolution, level size, real world scale
comparison, music styles for each level, sound effects, textures, character
names, game mood, and anything else you can think of (most likely a lot more
than just what I've listed here). Once this document is complete, then (and
only then) can you be confident that your idea is solid and ready for production.
You also want to make sure that this document is well organized, for two reasons:
-
Most text editors have an upper limit on document size that this document
will definitely exceed, and;
-
Organization provides logical breaking points for new documents.
Now I don't mean to be frightening or anything, but documentation is the key
to software development, and is often an area in which game developers are
lacking (the days of coding a commercial-quality game in your garage are over).
One small note, though—you most likely won't be able to cover everything
in the documents before you can start coding. All of these documents, especially
the design document, are evolutionary things. The rule presented by Morris
and Rollings is that you can really only document 80% of the project before
you start development, the rest will reveal itself as you go along. So if you
happen to stumble across something that you just can't figure out without busting
out the IDE, put it to the side for now and come back to it later.
While there are other documents you may write, or feel the urge to write,
these three are probably the most frequently occurring. Write as much as you
can, and more than you need—an overabundance of documents never really hurt
anyone.
With documentation out of the way, let's move on.
Code Architecture
There's not much I can cover here—I mean, who can tell YOU how to code,
right? But there are a few things worth mentioning that I picked up in a couple
books and through hard experience.
First, try to make your code structure as clean and simple as possible. Sure,
you can do all sorts of cool stuff with multiple inheritance, abstract classes,
and virtual functions, but just keep in mind that every layer of abstraction
is another set of processor cycles, and thus a slower game. Now I'm not saying
to dumb your program down to a purely procedural level, just be aware of what
goes on behind the scenes. One level of inheritance is perfect, two is acceptable
(and makes things a lot easier), but most architectures that go beyond that
may be a little too processor-intensive for your average game. If you absolutely
HAVE to have that cool class structure, keep it as far from the game loop (or
other time-critical portions of your program) as possible.
That being said, let me bend the rule just a bit. I recommend a base class
for all your drawable objects called GraphicsObject. Have all of your drawable
classes inherit from this class. Why? Because it'll make things easier. All
graphical objects will have a certain set of common functions—translate,
rotate, scale, and draw, among others—which can be either standardized
or virtual. The benefit of this is that the compiler won't have to care what
kind of monster is drawing itself. All it will see is a call to GraphicsObject.Draw()—let
the computer worry about the late binding. I'll cover a bit more on how to
use this idea in a moment—until then, here's a sample prototype for
a graphics object:
// This is the GraphicsObject class
// All drawable objects should inherit from this class
class GraphicsObject
{
public:
//This is the happy constructor
GraphicsObject();
//A virtual destructor, to ease inheritance
virtual ~GraphicsObject();
//Scale, rotate, and translate functions that we'll get to in a later tutorial
void scale();
void rotate();
void xlate();
//Make the draw function purely virtual, so as to ensure that it is implemented
virtual void draw() = 0;
};
This won't be much use yet, but it will serve us greatly as it changes to meet our needs.
Second, a word on data structures. Games are dynamic by nature, so we have
to make some hard decisions. By far there are three data structures you will
use the most as a result of this fact—the STL Vector, the Binary Search
Tree, and the Linked List (in no particular order).
Just a quick note before I hit these—there are hundreds of great tutorials
on data structures on the web. The following is not meant to be complete nor
exhaustive, it is just meant to present the idea of different places to use
the data structures. I figure why should I reproduce all the great work that's
already out there (more often than not by people smarter than me).
First, the STL Vector. The STL Vector, for those of you who don't know, is
basically a dynamic array. It allows you to add any number of objects on the
end, as long as they are of the specified type (this is where our GraphicsObject
scheme comes in handy). Thus, if we were to classify all of our drawables as
a Graphics Object, our draw loop becomes really simple:
vector <GraphicsObject*> world;
void draw(){
for(int i = 0; i<world.size(); i++){
world[i] -> draw();
}
glutSwapBuffers();
}
Next, the Binary Search Tree is very useful for things that we would need to
search for (say, for example, the closest polygons to the player, to ease the
load on the graphics processor). Basically, assign each object you'll need to
sort through a value (say, distance from player), populate the tree, and start
your searches. The benefit of the binary search tree is that it is one of the
most efficient ways of searching out there (and one of the most commonly used,
as well). Quake 3 uses a modified BST format for its level files.
Finally, the Linked List is another exercise in dynamic array fun. Allowing
the user to delete or add at will, the Linked List is a valuable tool for any
programmer.
I can hear the question now—"Why should I use a linked list when I
have a vector already?" Well, it's a simple difference between the way
you're going to use the objects. The linked list is best if you plan on adding
and deleting a lot of data from the list. The vector, when something gets deleted,
has to take the time to move all of the items after the deleted item towards
the front, resulting in roughly n-1 deletion operations. The linked list only
has to first navigate to the deletion target, then perform a couple pointer
swats and a delete call—much much simpler than the vector. So it's just a
matter of judgment. If you're going to have a lot of enemies flying at the
player, I'd suggest a linked list approach, while if you're going to have a
pretty stable world for the player to walk around in, go for the Vector.
And with data structures (briefly) covered, let's hit the last item on the
agenda.
Vector Math
In an earlier tutorial I said that we wouldn’t do many equations until
we hit 3D graphics. Well, that was a half-truth. It's better to get into practice
now, while we're fresh and ready, of using vectors to do just about everything.
By vectors I mean the mathematical construct, not the data structure.
Again, there are hundreds of tutorials and libraries available on the net.
One that I've worked with and liked is GMTL, available from http://ggt.sourceforge.net .
Of course, it's always a good idea to write one for yourself, if for no other
reason than to get the basics down.
I'm just going to run through some of the sample equations here. Vectors will
be noted as points (x,y,z), with the coordinate indicating a vector from the
origin to the point.
First, vector addition. Real easy stuff. If you have v1 = (x1,y1,z1) and v2
= (x2, y2, z2) then v1+v2 = (x1+x2, y1+y2, z1+z2). Subtraction is opposite.
Easy.
Next, basic trigonometry can be used to obtain the magnitude of the vector.
For 2D vectors, the magnitude is the square root of x squared plus y squared
(or sqrt(x*x + y*y)). For 3D, it's the same thing, just throw a z squared into
the equation as well (sqrt(x*x + y*y + z*z)).
Third is the dot product. This is one of two methods for multiplying vectors
(with the other being the cross product), and this one yields a scalar quantity
(scalar means non-vector. 5 is a scalar, (5,0) is a vector). This is useful
because it can help us to determine the general angle between two objects.
If the dot product is greater than 0, then the angle between them is less than
90 degrees. If it is less than 0, then it's greater than 90 degrees. A dot
product of zero means that the angle between the two is exactly 90 degrees.
Given vector v1 = (x1,y1,z1) and vector v2 = (x2,y2,z2), the dot product of
v1 and v2 is (x1* x2 + y1*y2 + z1*z2).
Fourth is the cross product. The cross product yields a vector that is normal
(or perpendicular) to both of the vectors being crossed. Thus, V1 x V2 = V3,
where V3 is normal to both V1 and V2. To get the cross product, using the above
definitions of v1 and v2, the cross product v3 = (y1*z2 - y2*z1, z1*x2 - z2*x1,
x1*y2 - x2*y1). Notice that in this equation, in a 2D situation, the X and
Y resultant values become 0, this yielding a vector that is perpendicular to
the X,Y plane. The cross product becomes extremely useful when producing normals
for lighting, as now we can do the whole thing programmatically.
Finally, just a note about unit vectors. These become useful when doing animation
and using lighting engines, and can be found by dividing each element of a
vector by its magnitude.
And that's it for this month. Tune in next month when we return to the world
of game code and programming (I promise this time).
|