Trending
Opinion: How will Project 2025 impact game developers?
The Heritage Foundation's manifesto for the possible next administration could do great harm to many, including large portions of the game development community.
Micro-optimization and glorious hacks are still very important in some areas of the job, but the biggest challenge is actually getting the damn game shipped. In this article, Peter Warden shares some of the ways he's found to keep code usable in an imperfect environment.
January 19, 2001
Author: by Peter Warden
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?")
Listings 1.1 and 1.2 for a practical demonstration of how I'd approach a simple function.
// 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
(SeperationY
{
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;
}
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.1and 2.2 show one way I might tackle code that was threatening to grow into a crawling horror.
// 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
}
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 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.
// 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!
"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).
Read more about:
FeaturesYou May Also Like