by Drew "Gaiiden" Sikora Introduction
Everyone wants to make games, but few people actually end up making them. Why is that? This can be attributed to several factors, such as skill, knowledge, dedication, and goals. You can take those four variables and assign them any values you want and mix them up every which way, but doing so will still not reveal whether a person will succeed or fail. See, all those attributes describe the person's willingness and ability to achieve what he or she has set out to do – but it says nothing about how he or she goes about actually doing it. And that's
where a lot of people trip up.
This article will introduce you to a method of development that will supplement the four previous properties in order to enable you to achieve your goals. This isn't some new-age method I thought of – it's a tried and true formula. Perhaps you've already thought of it yourself, since it's nothing complex. If you haven't, or just have no idea what the title of this article means, then read on. Why Planning is Good
I know you're just itching to click on that link that says "Incremental Development" so you can find out just what the heck I'm blabbering about. Well be patient with me for a while longer as I take a little time to explain why planning your projects is important. And I don't just mean big projects. Don't stop reading this because you're doing a dinky tic-tac-toe game.
If you think of constructing games as constructing a building, you'll get a good idea of what will be expected planning wise. The first thing that goes into making a building is laying down requirements. These requirements are then used as guidelines in creating the blueprint. The blueprint is then used to determine, before construction begins, if there is anything that looks wrong. What do you think is easier, changing the placement of a wall on a blueprint or moving the actual wall itself? Finally, the blueprint is used during construction to create the actual building.
To complete the analogy lets look at constructing a game. Instead of requirements we have rules. These rules are then used as guidelines to create the design document, which in turn is used to help create the technical specs. These two documents are our blueprints. You can guess where it goes from there, onto construction and debugging and testing and so forth.
Yeah sure, you may think extensive planning for a game of Snake is unnecessary, that you may spend more time planning that it would take to actually code the game. This may be so for some people. However, that shouldn't matter. The fact is that you will be forced to plan out larger projects, so why not practice on the smaller ones? It's important to get into the habit so you can't rationalize your way out of planning later on.
I would also wager that those who choose to dive straight into coding would actually end up spending more time sifting through buggy and disjointed code than it would have taken them to plan it first and code it second. It's hard for people to believe that planning is a timesaving method, because they think of the hours spent with pencil and paper or in Word, drafting or typing up the rules and specs as a total waste. The common belief is to fix bugs when they pop up, instead of trying to squash them before they ever even manifest themselves.
Even with simple projects, you'll start out with a clear idea of how everything will work. This "big picture", however, will soon become muddled in your head once you get knee deep into code. Having hard copies to look at while coding is well worth the time and effort. So now that we know planning is good, let's learn how to take our planning to the next level: construction. Incremental Development
The concept of incremental development centers on the construction phase of the project. By now you have already completed the design document, the technical specification, and the architecture (which is sometimes blended with the tech spec). It's important that you have these things because they will aid you during development, as that is their purpose.
Loosely translated, incremental means "bit by bit". Therefore you can assume that we're talking about steps in development. "Oh great!" you think. "Milestones!" Not quite. Milestones are a bit too big for our purposes, both in terms of length and importance. Checkpoints would be a better description, although I prefer the terms "builds".
Let's take a scenario where you are attempting to program a simple game of Breakout in DirectX. You briefly sketch out the interface and list the modules you'll need, such as CBall, CBrick, CPaddle, and so forth. You then start constructing these modules one by one, laying out their headers and filling in the routines with executable code. Then you create the main source file and integrate everything. Finally, when everything is in place, and with a flourish of the hand, you start the compile. Sure, you're bound to get errors – let's be realistic here. But, you figure, you can just go down the list and wipe them out one by one. The compile finishes. No, it aborts. You have so many errors that the compiler refuses to continue. No problem, you say, it'll just take a while. This is the whole point of debugging right? You find a missing semi-colon and recompile. Only 115 errors now. Ha! You fix another error and recompile. Now you have 124 errors. What??
I'm sure you all know the story. A lot of people think they can get away with build first, debug later, not realizing that fixing one error doesn't mean more won't pop up. Even if you were to compile the first time with only 3 errors, you would most likely fix those three (say, missing header files) and all of the sudden end up with 98 real
errors. This can create quite a devastating feeling for the programmer.
The point of incremental development is to prevent this from happening. Breaking it Down
The whole idea behind incremental development is to keep the time between compiles as short as possible. You want to take small baby steps for two reasons:
- Theoretically, the less code you add to the existing structure the less chance of bugs and errors popping up with the next compile. This is, of course, not always true – there will still be times when the errors stack up no matter what. Even so, this can become a rare event if the amount of new code added between compiles is reduced.
- When the errors do pop up, they're a lot easier to get rid of when you know exactly what was added since the last stable build of the project. The more you add, the easier it is to lose track of things as they tend to become spread out. This is why when adding new code it should be an encapsulated module or something where the code is localized for easy debugging.
Reading the above two points, we can begin to define the term "baby steps" into something workable. Obviously incremental development emphasizes constant compiles, but the problem is deciding when to compile. Obviously you don't want to spend half of your total development time compiling the project, even if it can drastically reduce errors. There is
a point where incremental development can actually become time inefficient, and that's when you spend more time compiling than coding or debugging.
Another thing to consider when deciding build points is that when you compile the project, you want the new build to run
and actually do something
on top of the old build. Don't do a compile just to debug a routine that isn't even called yet in the execution. Wait until you've added a whole game function. For example, if you had a space game, you might compile to simply display the player's ship. Then you would code in all the weapons programming and then compile to test and debug the shooting of the lasers. You wouldn't
compile the project until you were able to actually shoot the lasers. For example, compiling to display the guns themselves would be a waste – they should have been shown with the ship the first time or you should have waited until the lasers could fire. Just showing weapons adds nothing of testable value to the previous build.
With this is mind, it's a lot easier to decide when to compile. Keep in mind these guidelines:
- If you are adding an entirely new game object, you may want to break the object down into smaller pieces. For example, a space ship would be broken down into its display, movement, weapons, shields, etc. if possible. Compile, test, and debug each of these pieces not separately, but on top of each other.
- The amount of new code to add depends on its localization. If the new code is spread out over many files, you should not add too much. If the code is localized to a single file, more is acceptable. The whole point is to make it easier to determine where you screwed up when the errors come marching in.
- It's important to plan out your entire build order, rather than take it as it comes. This is because it makes no sense to compile and test weapons when you can't even see the game object that's shooting them. Make sure each additional compile builds onto the previous compiles.
- You should always have a playable version of the project on hand as a previous build. This is good because it's a stable version you can fall back onto when things get hairy, you always have something to show to impatient clients, and as a visual aid to other team members, it's priceless. Don't do a compile that adds nothing to the playability.
If you need an example, I made a small Asteroids demo in SDL. It didn't do much except display a ship that moved and rotated and fired a laser that did nothing, and asteroids were careening around and there was a parallax star field effect. This was my first project in SDL and it only took me three days, a few hours each day, to get this far. Here was my build order:
- Initialize SDL
- Create full screen app
- set transparency key (black)
- exit on ESC keypress
- enable file stream
- create image data file
- read in test image
- modify image data file for interface elements
- load in interface elements (radar screen, status indicators, etc)
- properly display interface elements
- create Vector class (see vector.cpp/vector.h)
- Starfield class, Builds 1-3 (see starfield.h/starfield.cpp)
- Asteroid class, Builds 1-3 (see rock.cpp/rock.h)
- Player class, Builds 1-4 (see player.cpp/player.h)
This was taken from the main source file, and only progresses up to the game's current state – there are actually 20 main builds but, as you can see, each game object has its own builds assigned to it. Here's an excerpt from the Starfield class source:
- create (draw) starfield layer
- allow more than one starfield layer
- animate starfields (using arrow keys to simulate player movement)
I would like you to notice Build 3. Even though at this time there was no player ship to be seen on the screen, the build still called for implementation of the arrow keys to simulate player movement. It's important to realize that you shouldn't have to mix up the building of objects because of dependencies. This is discussed more in the next section.
On average, each of these builds created about 5-10 errors. Debugging was as tedious as ever, but was made easier by the fact that I knew what code to look at for mistakes. The hardest part of debugging is knowing where to start, where to look. When you know where the new code is, it can greatly simplify the problem. Putting it Together
Now that we've learned how to break a project down into builds spaced realistically apart, let's look at actually putting them all together.
Several problems can arise from incrementally developing your project. As I briefly touched upon in the last section, dependencies can be a hindrance. In the Starfield class example, I had to make sure the star field would animate properly when the player moved, however I had not yet implemented the player object. I could have skipped the build, built the player object up to the point where it would function in this case, and then have come back to do Build 3 of the Starfield class. This is an acceptable option, but in some cases, like this one, it's just easier to emulate the object you're dependent upon. Plus, this emulation allows you greater control over the input given to the object being tested.
Another thing involving dependency is the calling of non-existent routines by an object. The object may need the data provided by these routines to function properly. Again, you could jump ahead and develop the object containing these routines, but again, a better way exists. Just like the emulation method, supplying dummy routines for the object is a great way to control the data the object receives from the routines. Think stubs - instead of coding the actual routines, you can just have them return values. Since you can then manually tweak the values, you can stress test the heck out of the object. You would not have had this level of control if you had gone ahead and built the actual object.
As your project gets larger and larger, the possibility of getting a lot of errors even with small additions to the existing code increases. If you've planned your builds carefully, you should be able to determine exactly where the new code interacts directly with the old code. These contact points should be listed as hotspots for debugging. Keeping a record of these contact points for each build is invaluable to the debugging process. This is because you can then use these contact points as a map to trace your way back through the build order to locate problems at various junctures when necessary. It also creates a structure for the project - you can chart the points visually and determine, for example, if changing this routine will affect the function of another.
You can easily work around dependency problems like those described above by planning in advance. If you set up a "scaffold" around the code being built, you can create a controlled environment for testing. This scaffold would support the existing code by providing input that the code uses to perform its functions. The above two problems are solved using this scaffold example. Scaffolds can remain in place as long as necessary, usually until they are replaced with the actual code that performs the functions they are emulating, and should be simple constructs to ease in the debugging since they are really non essential components. Having a scaffold in place is important in team environments because each person will be working on a different portion of the code. A team member may be constructing an object that requires input from another object being worked on by another team member. Instead of hassling that team member to finish or provide the latest build of the object, he can just use the scaffold to emulate the input. Conclusion
Incremental development helps to reduce errors and speed up construction. It allows you to identify architectural problems sooner rather than later thanks to the constant building and checking. It can provide a map of checkpoints allowing you to trace the relationship of objects. It can improve testing by providing to objects highly tunable input.
Sure, it's common sense to build the project every now and then to check for errors, but not everyone may actually take the time to determine when
the best time would be to build and check. Furthermore, not everyone stops to determine how these incremental builds can help them in other ways. Hopefully this article has given you a broader sense for what incremental development can really do to make construction of a project, big or small, much easier.
Drew Sikora is a part-time writer residing in New Jersey. He's written a total of 19 articles and interviews, 3 of which are appearing in Game Design Perspectives
, published May 2002 by Charles River Media. Click Here
to check it out! As always, questions and comments can be sent to email@example.com