
What
Your Mother Never Told You
about Game Programming
By
Peter
Warden
Gamasutra
January
19, 2001
URL: http://www.gamasutra.com/features/20010119/warden_01.htm
Game
programming, because of its history, has an image that no longer fits with the
reality of the job. The popular view of game programmers, at least in nerdy
circles, is of a bunch of code cowboys coding six impossible things before breakfast,
spending most of their time worrying about how to squeeze those last few cycles
out of that inner loop. Micro-optimization and glorious hacks are still very
important in some areas of the job, but, as can be witnessed by the ill fate
of some recent high-profile projects, the biggest challenge is actually getting
the damn game shipped. The successful companies are those that have adapted,
and have strategies for getting the projects done.
There's a lot of software engineering literature already out there, and I've
mentioned some of my favorites in my list of resources at the end of this article.
In the course of my work, I've run across some practices and patterns that I've
found useful. Some are quite specific to the unique challenges of creating games,
such as the fact that you almost never get what software engineers would consider
an adequate list of requirements. Others are not talked about in polite circles
because they're treating the symptoms of a bad coding process rather than the
causes. I think that these are still worth knowing, because sometimes trying
to minimize the damage is the best thing to do.
My main aim with this article is to share some of the ways I've found to keep
my code usable in an imperfect environment. The real solution is of course an
overhaul of the whole development process, but for the times that isn't possible,
I hope these guidelines will be of some help.
The Importance of Naming
"Are the
identifiers in the code clear and fitting?" is a useful rule of thumb for
determining whether the code I'm writing is going to be maintainable. Since
the compiler ignores the names when it generates the final machine code, it's
common for naming to be seen as a cosmetic detail. However, I believe the quality
of the names is a good indicator of how well thought out the code's design was.
Fundamentally, you should be able to describe to someone else precisely what
your code does, or you might as well have written it in binary machine code.
If you can't describe what it does, then neither you nor anyone else will be
able to work with it in the future, which leaves it useless. If you can describe
it, then the best way to keep that description for future need is to bake it
into the code, by using clear, meaningful identifiers.
This is not about whether you want Hungarian notation, or prefer verbose or
snappy variable names, it's about whether they convey any information or just
muddy the waters. Seeing a long list of "x"s, "foo"s, or
swear words -- or more commonly, very vague and ambiguous nouns -- implies that
the writer of the code is confused about what he or she is writing. If I'm having
trouble with naming, I revisit my code's design, because I obviously don't have
the concepts involved clear enough in my head.
An especially important case is function and method names. A function with a
name that's unclear or misleading will waste a lot of debugging and maintenance
time. ("Of course Player.Render() polls the keyboard, why didn't I think
of that earlier?")
See Listings 1.1 and 1.2
for a practical demonstration of how I'd approach a simple function.
Fat Classes, Fat Functions
Classes packed
with members and functions packed with variables both worry me, for pretty much
the same reason; nobody ever designs one of these crawling horrors, they just
happen. Usually things were added to them a little bit at a time as short-term
hacks to get something working, often under pressure to get a feature or bug
fixed for a deadline. Once in, they never get removed, other changes to the
code start to rely on them being there, and ultimately people take a more relaxed
view of hacking the code around some more, since it's a mess already.
The example that sticks most in my mind was a camera class that ended up with
responsibilities for player input, the player's on-screen character, informative
displays, and plenty of other stuff. The code limped along, though debugging
was made a nightmare by the convoluted and nonintuitive paths that the code
took through this beast of a class. The real crunch hit when camera changes
were needed. The camera class was at that point so closely tied to everywhere
else in the game that changing it cascaded bugs through the whole system.
The textbook answer to this sort of problem is to revisit the whole design and
figure out how to incorporate the changes cleanly. If, as usual, there's deadline
pressure, I now prefer to put in placeholder non-member functions that are as
loosely coupled to the rest of the code as possible if a 'home' for the functionality
isn't immediately obvious. This is a far from perfect solution, but at least
contains the hackiness rather than infecting a whole class, and makes it obvious
that the code is a hack rather than hiding it away.
Listings 2.1 and 2.2
show one way I might tackle code that was threatening to grow into a crawling
horror.
When You've Got a Shiny
New Hammer, Every Problem
Looks Like a Nail
When I learn about
a new feature, I naturally find lots of places in the code I'm writing where
it could come in handy. The problem is, having little experience with the new
feature, I'll inevitably end up using it where it will cause problems further
down the line. Trying to debug an object whose inheritance tree looks like a
cat's cradle is not fun, exceptions can be equally obscure unless used with
care, and working out how templates work on different type parameters can be
nearly impossible. Another consideration is whether the next person to work
with the code will understand the feature you're using. When you definitely
need them, inheritance et al. are lifesavers, but they come with costs too,
and can be used to obscure the code far more easily than to make it clearer.
A good coding standard will help, but thinking, "Can I use a simpler way?"
rather than, "Can I use <feature> here?" is most of the battle.
I find this also applies to using new technology. There's a lot of kudos to
be gained by learning to use a new technique before your peers, but I have to
control my enthusiasm before I decide how to approach a problem that could be
helped with a new technique. It's vital to be critical and look at the costs
and benefits of using the technology as opposed to going with a less sexy but
better-understood technique. A good example is Tom
Forsyth's presentation looking at subdivision surfaces and asking what they'll
bring to a game. With most graphical techniques the main cost is not implementing
them in the engine, it's giving creative control to the artists by giving them
the tools to create wonderful effects with them.
Over-general Designs
In game programming,
you almost never get handed a complete set of requirements. One way of dealing
with this is to attempt to write a very general system that will cope with a
very wide range of needs. Though lecturers are very fond of such top-down, abstract
systems as examples, I've found that the design of the system has to be aware
of the specifics of how it will be used, or the code that relies on it ends
up working around the system rather than being helped by it. The heart of the
problem is that there's always a choice of which abstractions best model the
problem, and until you know the problem well, picking the right abstractions
is impossible.
With input, picking an abstraction that encompasses joypads, mice, joysticks,
and keyboards -- and isn't awkward to use -- is not something I've ever succeeded
at. Even though they belong to a family of devices with similar purposes, finding
a useful subset of properties that they have in common is very hard.
The way I've found around such problems is to tackle the problem from the other
end and work through some examples of how I think the system is likely to be
used. This focuses my mind on what the common cases will be; since they're the
most important, I can make sure the design concentrates on those. It's a lot
easier to spot potential problems at an early stage, rather than realizing them
after it's all been implemented, causing you to have to corrupt the abstract
design by hacking in workarounds.
Take the case of input. I might see two main needs, one for pretty undemanding
tasks such as menu control, the other for things such as control of the main
character where all the information you can get from the control device is essential.
In the first case, a simple abstract system that posted events to interested
parties when a major input change happened would probably do everything that
was needed. For the latter, I'd expose the full interface to the device with
minimal wrapping, and let the calling code pull out the information it wanted.
Trying to cope with the radically different needs of the client code within
the same system would mean both would be served poorly.
Listings 3.1 and 3.2
take a look at how to design the interface to a graphics module, and how tricky
it can be to try to make it general.
"Keep It Simple, Stupid!"
The real competition in games these days is no longer in the graphical technology, but in the content. To give the artists and designers as much time as possible to get all that great content in, we need to get a version of the game they can use very early on in the project. This kind of rapid development isn't common in games; in the past the priority has been on writing extremely efficient code, with the time taken to write it not as important. Now the emphasis is increasingly on getting something working at all, and then worrying about efficiency where it's important. This doesn't mean you can be careless writing code, of course, just that the varying times that different ways of coding something would take are now much more important than in the past. It's hard to adjust to writing very naïve code where needed when every instinct is to always have efficiency as your highest priority.
Premature Optimization
Is the Root of All Evil
Compared to writing code that works, optimizing working code is straightforward.
It's also often a lot more fun! The problem is that debugging, maintaining,
or re-optimizing code that's already been optimized is far harder than doing
the same with clearer code. This may sound academic, but in practice the code
that's optimized early on is seldom the code that takes up most of the execution
time, since programmers are notoriously bad at guessing the bottlenecks in the
code. Time that could be spent optimizing the really important code is instead
wasted on fixing code that would take almost none of the execution time, even
if it was written to be easily understood rather than to be fast. Profiling
is absolutely vital. You'll often find that 90 percent of the time is spent
in 5 percent of the code, and so the gains you make from optimizing that section
are far bigger than any other code. Knowing how to optimize is a vital skill,
but knowing when not to do it can be just as important.
Lying Comments
When I first started writing commercial game code, my code was liberally littered
with comments, and I couldn't imagine any drawbacks to this. As time passed,
I noticed something odd: the code and the comments grew increasingly out of
sync, and I found that wrong comments cost me more time than correct comments
saved me. The cutting and pasting, and late night alterations that happen when
the pressure's on all meant that the code changed while the comments didn't.
I find code in which code and comments seem to clash very difficult to sort
out, largely because the comments have to be chased up just in case they're
true, even if they seem obviously wrong. The problem is that there's no natural
reason for the comments and code to stay connected, and in the heat of development
the time needed to maintain the comments is seldom available. To minimize the
problem, my style is now to have a bare minimum of comments but to have my code
very verbose, with all steps of the algorithms laid out in simple steps and
with clear names for everything throughout. I reserve comments for areas where
I'm doing something that's unusual or obscure, in the hope that their scarcity
will make them stand out, and thus be less likely to be forgotten when the code
changes. Obviously there are exceptions to this, such as in assembler where
the language doesn't give you the chance to be verbose, and so comments are
essential; you just have to bite the bullet and spend time maintaining them.
Conclusion
I've tried to apply the optimization mantra to my coding process and look at what problems consume most of my coding time. They are pretty mundane, even obvious, but if I can save even a little time by focusing on them, it'll mean we can get the game out faster. I know this is a bit of a pipe dream, but have you ever thought how nice it would be to actually implement something from the "if we have time" wish list?
As long as I'm bugged by how much cooler the games I've worked on could have been if we'd only had a little more time, I'll keep trying to figure out how to improve my coding.
Resources
Web Sites
http://www.refactoring.com has lots more ideas on how to rescue code that's in trouble.
http://www.extremeprogramming.org details an interesting approach to software design that's refreshingly grounded in practice. Will give you lots of ideas you can try adding to your current process.
http://www.muckyfoot.com/downloads/tom.shtml has an excellent presentation from the Windows Game Developer Conference that takes a level-headed look at the emerging technology of subdivision surfaces and how they can actually be used to improve games.
http://mindprod.com/unmain.html is both a very funny and a very scary guide to writing unmaintainable code
Books
Gamma, Erich, and others. Design Patterns. Reading, Mass.: Addison-Wesley, 1995 (ISBN 0201633612).
Lakos, John S. Large-scale C++ Software Design. Reading, Mass.: Addison-Wesley, 1996 (ISBN 0201633620).
McConnell, Steve C. Code Complete: A Practical Handbook of Software Construction. Redmond, Wash.: Microsoft Press, 1993 (ISBN 1556154844).
McConnell, Steve C. Rapid Development: Taming Wild Software Schedules. Redmond, Wash.: Microsoft Press, 1996 (ISBN 1556159005).
McConnell, Steve C. Software Project Survival Guide. Redmond, Wash.: Microsoft Press, 1997 (ISBN 1572316217).
McConnell, Steve C. After the Gold Rush: Creating a True Profession of Software Engineering. Redmond, Wash.: Microsoft Press, 1997 (ISBN 0735608776).
Meyers, Scott. More Effective C++: 35 New Ways to Improve Your Programs and Designs. Reading, Mass.: Addison-Wesley, 1995 (ISBN: 020163371X).
Sutter, Herb. Exceptional C++: 47 Engineering Puzzles, Programming Problems, and Solutions. Reading, Mass.: Addison-Wesley, 1999 (ISBN: 0201615622).
//
Example 1.1
// Original
int IsFree(int Num, int Mode)
{
int RangeX, RangeY;
for(int i = 1; i <= g_MaxUnit; i++)
{
if(i != Num)
{
switch(Mode)
{
case
NX_NEAR_ALL:
RangeX
= g_Units[Num].Width + g_Units[i].Width;
RangeY
= g_Units[Num].Height + g_Units[i].Height;
break;
case
NX_NEAR_ENEMY:
case
NX_NEAR_ENEMYDANGER:
RangeX
= g_Units[i].Range + g_Units[i].Width + g_Units[Num].Width;
RangeY
= g_Units[i].Range + g_Units[i].Height + g_Units[Num].Height;
break;
default:
assert(false);
break;
}
if((abs(g_Units[i].X
- g_Units[Num].X) < RangeX) &&
(abs(g_Units[i].Y
- g_Units[Num].Y) < RangeY))
{
if(Mode
== NX_NEAR_ALL) return i;
if((Mode
== NX_NEAR_ENEMY) &&
(g_Units[i].Civilization]
!= g_Units[num].Civilization)) return i;
if((Mode
== NX_NEAR_ENEMYDANGER) &&
(g_Units[i].Civilization
!= g_Units[num].Civilization) &&
(g_Units[i].Force
> 0)) return i;
}
}
}
return 0;
}
//
Example 1.2
// Version with meaningful names added
int GetNearUnit(int MyUnitIndex, int AcceptableType)
{
int RangeX, RangeY;
const SUnit& MyUnit=g_Units[MyUnitIndex];
for(int OtherUnitIndex = 1; OtherUnitIndex <=
g_CurrentUnitCount; OtherUnitIndex++)
{
const SUnit&
OtherUnit=g_Units[OtherUnitIndex];
if(MyUnitIndex !=
OtherUnitIndex)
{
switch(AcceptableType)
{
case
ANY_UNIT:
RangeX
= MyUnit.Width + OtherUnit.Width;
RangeY
= MyUnit.Height + OtherUnit.Height;
break;
case
ENEMY_UNIT:
case
DANGEROUS_ENEMY_UNIT:
RangeX
= OtherUnit.Range + MyUnit.Width + MyUnit.Width;
RangeY
= OtherUnit.Range + MyUnit.Height + MyUnit.Height;
break;
default:
assert(false);
break;
}
const
int SeperationX=abs(OtherUnit.XPos - MyUnit.XPos);
const
int SeperationY=abs(OtherUnit.YPos - MyUnit.YPos);
if
( (SeperationX<RangeX) &&
(SeperationY<RangeY))
{
if(AcceptableType
== ANY_UNIT) return OtherUnitIndex;
const
bool OtherIsEnemy=(OtherUnit.Civilization] != MyUnit.Civilization);
if((AcceptableType
== ENEMY_UNIT) && OtherIsEnemy) return OtherUnitIndex;
const
bool OtherIsDangerous=(g_Units[OtherUnit].Force > 0);
if((AcceptableType
== DANGEROUS_ENEMY_UNIT) && OtherIsEnemy && OtherIsDangerous)
return OtherUnitIndex;
}
}
}
return 0;
}
//
Example 2.1
// Bad
void
ProcessEverything(void) {
g_Camera.Process();
}
void
CCamera::Process(void) {
// lots and lots of code
g_Player.rotY+=SomeMemberVariableThatGetsTheEffectWeWant;
// lots more code
}
//
Example 2.2
// Still bad, but a lot more visible!
void
ProcessEverything(void) {
g_Camera.Process();
//
PW-HACK for Alpha!
g_Camera.HACK_AlterPlayersYRotation();
}
void
CCamera::HACK_AlterPlayersYRotation(void) {
g_Player.rotY+=SomeMemberVariableThatGetsTheEffectWeWant
;
}
void
CCamera::Process(void) {
//
lots and lots of code
// no hack hidden away here, it's at a higher,
more visible level, much more likely to get fixed
// lots more code
}
//
Example 3.1
// Classic example of problems
class C3DTriangle {
// some data
void Draw(void);
};
// This interface instantly
cripples performance, no matter what the implementation is like
// Modern graphics architectures achieve their performance by dealing with large
groups of
// triangles at once, it'd be almost impossible to do that with this interface
//
Example 3.2
// Better, but still probably unacceptable for games
class C3DTriangle {
// some data
};
class C3DTriangleRenderer
{
void DrawTriangleList(C3DTriangle* const pList,int
nTriangleCount);
};
// This interface is a lot better, but is still a bad fit for a lot of rendering
architectures
// The idea of a design like this is to hide platform specifics, but once you
start altering
// the interface to fit the platforms characteristics as you'd have to here,
you've lost any
// platform independence. All you end up with is an interface that apes a particular
API, and
// so doesn't work well on any other platforms, and just adds an extra layer
of complexity to
// the program to no good effect. Wrapping up an API doesn't give you platform
independence!
Copyright © 2003 CMP Media Inc. All rights reserved.