on the Cob:
this be madness, yet there is method in it"
ell, it looks like the day you've been waiting for has finally arrived! Okay maybe not, but let's pretend it is, okay? :) Today's the day we start building our game, starting with the usual project setup stuff and some core system code we want to use to get things off the ground. This article will be the first to have the project code with it, and every successive article will have the most recent version of the project. We'll be starting off small, but it's going to get bigger and bigger. Some of the articles are going to be a bit on the large side, others will be pretty tiny, depending on my schedule, as there's this other game I'm working on that kinda takes precedence, ya know. :) But I'll try to keep the content rolling regardless.
I've already got tons of positive feedback in response to the first article, thanks for all the great words guys...keep 'em coming. I've also got a bunch of suggestions on what type of game to make, as diverse a set as there are games out there. So making a decision that pleases everyone isn't easy, perhaps it's impossible. But a choice had to be made somewhere.
After a bit of hard thinking, I finally came up with the general idea of what this game is going to be. My goals have been simple; keep it fairly small (but big enough to still be indicative of a real project) and keep it simple, but make it fun enough to warrant actually playing. Originality was high on my list but not a requirement; I would settle for a clone if it got the point across. Fortunately I don't think we'll have to settle. The overall goal is mostly to create something that demonstrates the interconnections of many game-related programming components without a lot of junk beyond that.
So here's the plan...
This is something myself and a couple friends thought up a while back but never had time to embellish on, so it's about time to do so. The basic idea is this. Picture billiards, AKA pool, your typical bar variety. Now take the balls off the flat table, and put them inside a cube. Then take that cube and put it in outer space, in zero gravity. Cut pockets in the inside corners of the cube. Now grab a cue, and hop inside. Zero-gravity Pool, basically, of the extremely disturbed variety. The kind of virtual pool that the little green men might like to play. Maybe we'll even throw some planet textures on the pool balls for a cosmic twist.
Sound different? Sound fun? I think so, and I hope you do too. This is what we'll be writing over the next many articles in this column.
En route to this end goal, we'll also be implementing a couple "sub-games" during development... one (taken from the sheer number of people who suggested this) will likely be a small 3D Pong implementation. After that, possibly a simplistic 3D form of Breakout. The reasons for making these smaller sub-games in addition to the final one are understandable: widen the programming audience appeal a bit, and demonstrate how a common framework can make developing several similar games easier. For example very little of the Pong implementation will be unusable by the final game, and Breakout shares most of the common material as well. All three games involve the same solid interior world space, collision detection against basic primitives, and so forth. The game logic for the first two games is surprisingly minimal. So using them as midstream "test" apps doesn't seem too far-fetched a thing to do.
For example, in the model skinning tool called "Cannibal" which I wrote for the Duke team's artists, I wrote a quickie implementation of "Nibbles" in an environment subwindow just to see how long it would take. How long? Around 15 minutes. You can do a lot of stuff with a good framework in place, and we're going to make sure we create a solid, working framework for this project. A couple mini-games here and there will put that to the test.
So for you all rooting for 3D Pong, you have your wish. You'll just get a bit more in addition. :)
Anyway, back to the end project.
To avoid having to think up some cheesy name like "Galactic Pool" or
whatever, I'm going to suspend creating an actual title for the game
at this point, and stick with a project name. In a small tribute
to the page I'm writing this column for, as well as the Shakespearean
quote at the top, we'll call the project "Madness". Later we'll
come up with a real title, but for now, it's Madness through and through.
The Big PictureNow that this game has some direction, I should probably address a few platform issues first:
None of those topics are going to be delved into in great detail; I could spend fifty articles discussing 3D alone if I wanted, but I don't (I tried before, and that got real tedious). The same goes for sound, networking, and so on; the details of any of these topics would take much more time to cover in full depth than a small column like this has to offer. But there are a ton of resources (both in books and online) that deal with all these things. If you need specifics for any particular topic we go over, there's more than enough places to find it (on occasion I'll recommend some specific books as well). Once again, these things are not my focus. My focus is putting it all together; getting all these smaller sub-topics connected in the big picture. If I find there's a topic covered that gets too many questions about the internals then I might go into greater detail if possible, but I'd like to avoid it. Hopefully the code itself will help explain things.
I realize that I can't assume everyone already knows about half the stuff I'm going to talk about... that'd be a misguided assumption. But I have to assume at least some stuff, or this series will never get done. At the very least, I'll try to point you all in the direction of some good related books and other material when I can.
With that in mind, it's time
to get down to the meat:
Setting UpNOTE: You don't actually have to follow the steps below to set up the project we're making; you can download the entire project in its current form with each new article. The steps I'm explaining are just what I've used, and what you might use if you redid the process yourself. It'll be VERY helpful if you download the project for an article before reading the rest of the article, so you can follow along. I'll put notes about what to look at during the explanations, written in brackets like [look at thisfile.cpp]
Getting the project started is a quick thing; we just create a new project in Microsoft Visual C++ 5 (I'll use MSVC for short from now on) configured as a "Win32 Application" project type. This will start us off with a new blank project to work with. Since the project name is simply "Madness", we'll use that.
Whatever directory we want the project "root" to be in (where all the MSVC project files go, and Debug/Release subdirectories are formed), we'll also want to make two more subdirectories, "Src" and "Dist". Src is where we'll put all the source code, to keep it separate from the root of the project. The Dist directory will be our "Distribution" directory, where the executable will be run from. If the game were to be released as executable & data files only, this is the directory we'd give to the public. All data files we'll put underneath this Dist directory.
Once we've done that, the project's all set. Theoretically you'd be sitting in MSVC in front of a blank project right now.
Before we dive into the first of the source files, one thing that's very handy to have is a set of source file template macros. If you use an IDE of any sort, it likely has support for some form of keystroke macros or another macro language. For example you can create many types of macros in MSVC using VBScript. When you start getting into lots of source files and you want to keep your code organized, having a somewhat standardized layout is a quick and easy way to help. Even if you don't use an editor that supports these kinds of macros, you can still create some template .C/.CPP/.H/etc files to do the same thing (albeit with a bit more work each time you use them). It's still very much worth doing though. You might not follow these layout templates to the letter all the time, but they give you a great place to start whenever you need to add new code.
[Look at m_layout.dsm]
I've created a small pair of layout macros in the file m_layout.dsm included with the project, which are what I'll use when adding a new header or source body file. You may not end up needing them as is, but they'll give you a place to start should you want to create your own layout templates in VC5. If you want to use these macros as-is, it's helpful to customize your menu so you have quick access to them.
Now that our environment
is the way we want it, time to add the first of many source files to
come. We'll start off with some system glue. This will be
part of our first "subsystem" or "package" (choose any name you like),
which we'll just call "System".
Down With The SystemSince Win32 applications begin at WinMain, we'll want to bring WinMain into the picture pretty quickly. But at the same time, we want to lock as little of our code into Windows as possible, right? So we'll want some separation very early on as to what part of our system glue is Windows, and what part isn't. To keep things simple at this point, we'll split the subsystem into two pieces, the "Main" part and the "Windows" part. Regardless of the platform we're running on, our "main" block can be broken into three key functions... initialization (one-time startup), the primary frame-by-frame loop, and shutdown. This is what the "Main" part of the system package in sys_main deals with, so it can safely assume little to nothing about the underlying Windows platform work going on. Back in sys_win, WinMain will take care of making sure these functions get called appropriately.
I name my source files (and their corresponding headers) to reflect the subsystem they're in and some suffix indicative of their role in the subsystem. Some subsystems only have one file, so the suffix might just be "main", or "man" (for manager), but a one-file subsystem is still a subsystem. We'll probably have at least 20 or so subsystems/packages in the project by the time we're through with it.
[Look at the sys_win and sys_main .h and .cpp files]
So we'll start out with two source body files, sys_main.cpp and sys_win.cpp, along with two corresponding headers, sys_main.h and sys_win.h. Right now the source files will only contain a few functions (enough for us to be able to build the thing) as well as a few odds and ends.
You certainly don't have to follow this naming scheme in your own work. There are a ton of different naming conventions out there. What's important is that you pick one and stick with it, and make sure it gets the job done. Personally I believe subsystem isolation through file and variable/function/etc. prefixing (often called "package prefixing") is one of the simplest yet most effective naming schemes out there, but everybody has their preference. The primary criteria are that your convention should clearly identify the locale and general intent of your code and data, and that subsystem encapsulation is protected as much as possible. The use of the "static" prefix for implementation (not interface) global variables and functions is an example of this, since static vars/funcs cannot be linked to from outside a source file and hence cannot cause external linkage conflicts. This is the kind of stuff you have to consider when working on projects of a larger scale.
[Look at sys_defs.h, in the Interface Definitions block]
Next, I'm tossing in a common definitions/includes file named "sys_defs.h". This header has some macros for a few common types and so forth. The type macros could have just as easily been typedefs, but since the names are fairly common it's nice to have them as macros so they can be undefined when a few conflicts come up (some Windows DirectX headers end up using a couple of these same types).
A few of you may be asking, why a whole bunch of type macros? What's wrong with "int" etc? The answer is a platform issue. The "int" type for example may default to either signed or unsigned depending on the compiler, and while most compilers have options to force the default to one or the other, it's not always wise to assume anything. We're writing for MSVC which defaults to signed, but who knows if we may theoretically want to port the game in the future, right? In addition, the default size of "int" has caused lots of problems for many people over the past several years. In the 16-bit days, "int" defaulted to a 16-bit value, i.e. a short. But in the 32-bit days, "int" now defaults to a 32-bit value, a long. Worrying about compiling for a 16-bit compiler is not really an issue to us, but this is just another case of when assumptions can hurt you.
So I follow a general rule of thumb; "int" and other generic integral types are fine to use provided you don't assume a size or default sign. When either of those are important to a specific piece of data, use a type that forces the data in the direction you want. Hence the macros. The only data type assumption I'm making in this program deals with "float" and "double", which I'm assuming are IEEE 32- and 64-bit floating point values respectively. I only make this assumption because I don't think I'll be using doubles anywhere in this app (or very sparingly), and I doubt there will be any code written that assumes a strict 32-bit float size (any exceptions will be clearly noted).
I'll warn you now, there will probably be a few cases where I slip and violate this rule of thumb. I don't expect this program to be perfectly portable, but what I can do to make it an easier process, I'll do as much as I can.
[sys_defs.h: Look at the Interface Required Headers and Interface Trailing Headers blocks]
Anyway, before and after the type macros and a few others, are a bunch of initial C library includes and the few common ones from the project thus far. I have no problem including C library headers in virtually every source file for a couple reasons, A) I use that stuff all the time, B) They don't slow down compile time significantly (pre-compiled headers assist this), and C) Duplicating such functionality needlessly only hurts inlining potential. As for the headers from our code, we'll be putting many of the early/common headers for the project in this block. As code gets more diversified, however, we'll be leaving the more specialized headers out on their own to be included only as necessary (to assist compile time). To clearly identify Windows-dependent code, a macro is required in order to include Windows-specific headers. This must be defined by any source file before it includes sys_defs if it wants the Windows code. Initially several files will have this macro, but fewer and fewer will need it as the project goes on.
[Look at the m_misc .h and .cpp files]
Finally we'll start one more
"mini-subsystem", for some miscellaneous utility stuff. These
will go in m_misc.cpp/m_misc.h. Right now this will just hold
a few functions for some string operations I'll probably refer to every
now and then, but we'll likely add to it later. This subsystem
also has a general "current time" function, but since it ends up dealing
directly with Windows, the function will be in sys_win instead and be
cascaded from m_misc (you'll see me doing this every now and then).
Even if Windows can't be completely isolated in all aspects of the application,
the naming references to it should be whenever possible.
A Glimpse AheadThe rest of the code should be pretty self-explanatory thus far, and very little of the functional content itself will be new to many of you. When the material gets more complicated, I'll be explaining things at the function-by-function level a bit more, but that's not necessary yet. What's important here is the coding trend we're working toward. Take the time to look over the layout of the code so far and see the direction we're heading. Note the use of the SYS_, SYSW_, and M_ package prefixes on every interface function, for example. The system package is pretty small right now (and it will likely stay pretty small), but we'll have several "offshoot" subsystems that will augment the core considerably. We'll start getting some memory management and file management code out of the way next time, to get our system stuff in a good working state. After that, we'll move on to user input, graphics...
...and all the Madness that follows. :)
Until next time,
- Chris"Kiwidog" Hargrove is a programmer at 3D Realms Entertainment working on Duke Nukem Forever
|Credits: Code on the Cob logo illustrated by and is © 1998 Dan Zalkus. Code on the Cob is © 1998 Chris Hargrove. All other content is © 1998 loonyboi productions. Unauthorized reproduction is strictly prohibited, so don't do it. We have ways of making you talk.|