"Code on the cob". Not a very technical name for a column on programming, but since it's for a site named "loonygames", I figured it was only appropriate.
I suppose I should introduce myself. My name is Chris Hargrove (like you hadn't read that in the byline already), I'm currently a programmer for 3DRealms Entertainment, working on Duke Nukem Forever. I've only officially been a programmer in the industry for around two years or so now, but I've been programming "this type of stuff" for a few years longer.
Regardless, I get enough people asking me for information on game programming (or game coding; I'll be using the terms "programming" and "coding" interchangeably throughout this column... some people treat the name difference religiously but I don't) that when loony brought up the idea of me possibly writing a column, it sounded like a cool idea. You'll note that I go by the nickname of "Kiwidog" while online (long story behind that nick...don't ask), so you'll know what I'm talking about when I refer to one of my rants as "kiwidogma" or whatnot.
Anyway, this column is obviously about programming, game programming in particular. I'll be talking about coding for larger projects and the considerations it requires, interactions of various subsystems in a typical game project, and in general just writing good game code that helps get the project done, and with decent code clarity to boot.
I know all too many coders who start on a project, work on it until it's "75% done" or whatever, and then abandon it, giving excuses why the project was dropped. Often the real reason isn't lack of interest or motivation, but that the project hit a snag that made the coder(s) go "oh shit" and realize they were screwed by a design flaw (If this has happened to you, don't be ashamed, it's happened to all of us). This is a problem that professional coders don't run into, yet beginner and intermediate coders run into all too often when working on a game. Where's the difference come in? It's all about solid code design. This is what I'll be stressing in this column, since to me it makes all the difference between a beginner and a professional... more than all the graphics algorithms, networking schemes, AI routines and such combined. Once you know how to write good, solid code, everything else becomes secondary.
To help reinforce this, there will be a lot of code to go along with these columns, and not in the form of tiny little example programs. Instead, we'll work our way toward making an actual game. Something completed and playable. It may be small, and it may be cheesy, but it'll be done... and you'll be able to walk away from it with some insight into how to start (and complete!) your next game project.
Which game, you ask? I don't know yet. That's intentional, though... a lot of code, especially some core functionality, can be used for a great many different projects. All the code we create initially will be general towards almost any game we want to make, keeping our game-specific stuff until later. This is a good thing. All the code design principles we apply to this game will still hold true for a larger endeavor, so don't be fooled by the size.
I'll be spending a lot of time talking about what makes "good" code. Even though you might not agree with some of the style or philosophy used, you should be able to identify that the code is usable in a large, and real, programming project. That's all that matters to me... the code style you work with beyond that basic premise is entirely up to you (and your team).
[size="5"]What You Need
I'm going to assume that you have some programming experience, i.e. that you understand concepts like pointers, basic data structures like linked lists, and so on. At the very least, you should have enough experience to identify what you don't know of these things, and know where to get the information (because I won't be teaching it here). The best scenario is if you've written or tried to write a game before and ran into problems, solvable or not, when it started getting large.
My language of choice is C++, and I'll be using it for the code examples throughout the column, even though the code might often be compliant with pure C. Many people are religious about using purely C or purely the object-oriented constructs of C++, and not mixing them. I'm not a religious pundit on this matter. I mix and match whenever I see fit, using functional programming and object-oriented programming at various points in a project. Languages are tools, and you use what tools work for the job... that might include both design philosophies working together. As far as I'm concerned, code that's well designed shouldn't have a problem with this combination, as long as the mixture makes sense.
The platform I'll be writing the example code for is Microsoft Visual C++ 5.0 (make sure you have service pack 3 installed), under Windows 95. The code would likely be easy to adapt to another Win32 compiler, but I'm not going to bother with that directly... all my project files will be for MSVC++. If you want to use another compiler, by all means do so. As for porting the code to another platform altogether, much of it will be portable, with the exception of some Windows interface stuff (including DirectX code) which will be clearly identified. If you want to port this code to another project, these modules will need to be rewritten, but the others should remain virtually intact. Once again I'll leave such a task in the hands of the public.
We'll be writing this game incrementally with each column, so the project will continue to grow as time passes. Don't be surprised if the output executable ends up looking useless when certain subsystems are still being developed.
Okay, enough overview stuff... time to get this thing on the road. Since the column will be heavily code-design oriented, perhaps a better explanation is in order regarding what I think good code design is. I'll be doing a lot of rambling about this kind of stuff over the first few articles.
Code is evil. Let me rephrase that... code is chaotic (and to many people that may imply that it's evil). Lots of coders have heard of John Carmack's phrase "Fight code entropy", and I agree with that. You need to fight something with as much tendency for disorder as code has (considering all the effort that goes in to stop that tendency). My method of fighting it isn't necessarily starting from a blank slate though. You do often have to rewrite a lot of stuff, that's just the way of things...but many pieces of code don't need the blank slate treatment. And the less you have to rewrite, the better. So how do you keep your code around and still fight code entropy? Simple... don't give the code enough room for entropy to be a factor.
"People say Pandora's Box was evil, but they're wrong. The stuff inside it was evil. The box ain't nothin' but a box." - Anonymous
It doesn't matter how disorderly your code gets on the inside, if it's contained in a nice, tiny little box where nobody gets to see what's in it. A chaotic system can only get disorderly within the environment it's contained in, after all. As long as the box stays intact, and it does what it's supposed to do, then what's inside can be full of demons and evil things unmentionable. All we see from the outside, is the box.
This little premise is the basis for nearly all of software engineering... a solid, well designed system treats all of its key components as isolated (or "encapsulated") boxes connected together and nested within other larger boxes. How these connections are made is the subject of a great many software design books, and there is certainly no one solution. But the common factor is that the boxes that make up the system, whatever they may be, are intact. You work inside the box to write what it does, and work outside the box to connect it to the rest of the system... but you never, ever, open the box. Doing so lets out a whole lot of evil, by breaking encapsulation. Once you've opened a box, it will do as much as possible to infect every piece of code it can, and that can turn your project into a big mess. Even if you're not an object-oriented programmer, the idea of encapsulation is important (just because your code's content isn't object-oriented doesn't mean the code itself isn't).
Let's see how this applies to a game. In a very very simplified form, games fall into subsystems like this:
- User Input
- Game Logic / AI
- etc. These break down into further sub components, i.e. graphics may break down into higher-level rendering components, a lower-level video layer, the video layer's interface into the platform you're writing for, and so on. All this is nothing new, and pretty much every game programmer is aware of these divisions.
What not every game programmer is aware of is just how important keeping these divisions is. Once you know where the separations among modules should be, obey those separations religiously. That includes making a conscious effort to keeping the code isolated, using naming conventions that facilitate the separations, and so forth. Every subsystem is an empire looking to extend its sphere of influence well beyond where it should, and if you don't watch out, they'll succeed. This is not a small matter; it can make or break a large project. When you get to that point on the project where the code is just "too messy" to continue, the code has won. Don't let it.
To me, good code is not a matter of specific naming conventions, the algorithms used or other tricks. That's clever code, not necessarily good code. Good code is all in the connections, how well the code is structured and organized. And in the end, that all boils down to these little Pandora's Boxes.
[size="5"]Interface and Implementation
This idea of code isolation is nothing new, and has been known as a "good" way to code for quite a long time. Many programmers just don't seem to realize it. Pascal has units with "interface" and "implementation" sections. Java has a similar layout, along with package encapsulation. C and C++ have header files and source files, used for the interface and implementation respectively. C++ classes have public and private sections... the language list goes on and on. So what are "interface" and "implementation"?
To use the Pandora's Box analogy, "interface" is the box, and "implementation" is what's in the box. The interface (or "public") part of a subsystem is the legal connection that the outside world has to the subsystem. It's the set of function prototypes, structures, classes, variables (hopefully not many variables), and other definitions that the subsystems outside the box can use to connect with the implementation. The implementation is the meat of the subsystem... it's all the functions, class methods and so forth (public and private) that the subsystem uses to get its job done.
While many subsystems can have massive and complicated implementations, the best interfaces are those that are simple and to the point. And they should be clear what they're interfaces to. In a language like C, the global namespace can get very cluttered with tons upon tons of functions. If you don't name those functions to clearly identify them, you won't know what subsystem they're for nor that they're the primary interface for that subsystem. You also need to take steps to make sure that private implementation functions are not available for use outside the subsystem (or you'll open the box, which is bad). That means keeping them as private class methods, or for C using the "static" keyword in front of local, internal-linkage-only functions, etc. One way or another, the separation has to be made.
We'll be following this very closely as we write our game. All our C/C++ files will belong to a subsystem, (even if that subsystem is the core system "glue" code) all subsystems will have header files, with only those functions that belong in the subsystem interface declared there. Several modules may belong to the same subsystem, and they'll be named accordingly, as will all subsystem functions. We'll be following this more or less to the letter, and to many beginner/intermediate programmers, it may seem a bit anal. But as the project grows more and more complicated, you'll see exactly why we did what we did.
Another "anal" thing we'll be doing is structuring our code around the idea that we're not starting with any absolute target platform. While the code may not be completely portable as-is (due to issues I'll get into as they come up), the code will still be structured to lend portability a hand. That means not locking anything into Windows, DirectX, Glide or whatever. There will be very few assumptions, and the few that are made will be identified as such so they could be easily remedied later. The key here is isolation... don't let any one part of your code "bleed" into any other part of the code. The more isolation and encapsulation you have, the less code entropy you have to worry about.
Next time we'll start our project with some core system material and utility code, getting us set up for our project (regardless of what it ends up being). We'll build up some common subsystems over the next several articles, working our way through user input, graphics (2D or 3D, I haven't decided yet but probably 3D since that's my focus), sound and other stuff. What we create won't be that complicated, so you shouldn't have trouble following along. What matters is the structure, and you should watch it closely. Because when the project is done, that's the stuff you'll want to take with you, not the code itself.
If you have reasonable suggestions of anything specific you'd like to see in this column, (don't say make a Quake clone...that's way beyond the scope of this) send me feedback. If the request is appropriate, I'll see what I can do to work it into the project. Or if you have any other questions or comments, let me know and I'll do what I can to respond.
Until next time,
- Chris"Kiwidog" Hargrove is a programmer at 3D Realms Entertainment working on Duke Nukem Forever.
Code on the Cob is (C) 1998 Chris Hargrove.
Reprinted with permission.