Strayfire: Writing A "Simple" Game
by (14 January 2002)



Return to The Archives
Introduction


Please take a few moments to look over exhibit A delta C double niner on the right. What do you see?

The gamer in you might've said, "I see a spaceship, a bunch of bullets, some blue gems, a couple enemies, and some explosions." If he said anything along the lines of, "I see a bloody mess", please ignore him and his insolence :].

The developer in you probably said something like, "I see a few 3D models, some particle effects, and a bunch of sprites." That's pretty much what I said about my mental picture of Strayfire before I actually started coding it. It sounded so "simple." Add to it some "simple" mechanics like weapon upgrades and enemy paths; it sounded like a project I could bang out from scratch in a month or so. If only that were so. The problem is that a game is a lot more than what you see in a screenshot. Its a lot more than the obvious gameplay mechanics you notice in the demo. A game, in my humble opinion, is anything but "simple."

Granted Strayfire is nothing on par with most big-time commercial games like Quake, but its still a somewhat complex program. In this article, I'll take a look back at the development of the game, explaining a bit about what went into its creation.
exhibit A delta C99


What is Strayfire anyway?


Before we get started, I suppose it'd be wise to introduce Strayfire to those of you who've never heard of it. The official description goes a little like this: "Strayfire is an arcade-style shoot'em up game, with beautiful 3D accelerated graphics, loads of firepower, hordes of alien enemies to blast, and special effects galore! If you're a fan of pulse-pounding shoot'em up gaming, you definitely won't want to miss this one!"

Put another way: Strayfire is a small game I wrote, blessed with the awesome 3D artwork of Max Shelekhov, and musical madness of Pete Burke and Jeff Jordan. I won't be talking much about the art or music in this article since they're not my area of focus, but I would like to take this moment to again thank Max, Pete, and Jeff for their excellent work on the project! This article will mainly focus on programming-related issues.

If you'd like to check out the shareware demo of Strayfire, visit here.

And with that out of the way, let's start from the beginning...


The First Few Weeks


Armed with my stack of soda-stained design notes and concept sketches, I started coding the game. Strayfire was written in C++ using MSVC. I'd have to say that the first few weeks of development were definitely the most fun! This is the time when I had total free reign to mold the game architecture, develop tools, and generally fool around with fun ideas. There wasn't much in the way of "release" pressure. After a few weeks of coding I had a neat little "playable" demo using my own crusty "programmer art."


Exhibit Gabba Gabba
On the left (labeled exhibit gabba gabba), you'll see a screenshot of the game after only three or so weeks of work. I kept snapshots of the game during its development, so you can check out the demo that produced that screenshot if you want. Its available here, but I warn you its pretty odd: strayfire-early.zip (460k)

With a nice tool set, collision detection, some nice particles, and a working game architecture -- ignoring the temporary art -- it would've been tempting to say "I'm almost done. Just a few more weeks..." But as you probably know by now if you've ever written a game, a "playable" tech demo is not a game.

So what's missing? The stuff that really eats time are often the least obvious, including developing the actual game levels, world interaction, the in-game menus, the high score system, the gui, tweaking external sound and art resources to fit properly in the game, balancing game play, adding details, bug fixing, optimizing, polishing, and of course the most dreaded for PC developers worldwide: testing on other peoples' computers!! "It works for me!" isn't good enough if you plan to ship.
The reason I wrote this section is mainly to tell "new" developers that when you start thinking you're almost done, don't get too excited; there's probably a whole lot left to do. As Michael Abrash once reflected from one of his former managers: "After you finish the first 90% of a project, you have to finish the other 90%."


The Good Stuff


In this section I'll point out a few things about Strayfire's development which I think turned out well and would recommend to others. They're formatted as "tips", based on my experiences with Strayfire.

Developing A Strong Tool Set

Write good tools! I really can't emphasize this enough. As it turned out, the tools I wrote in the early days of Strayfire held their own throughout the entire project with very few modifications. That includes the object mesh converter (view image), the bounding box placement utility, the map editor (view image), and various smaller utilities. Without an easy-to-use map editor, developing the level content for Strayfire would've been a lot more difficult. The majority of the levels I designed in my notepad. Technically, I could've keyed in path coordinates and settings for each node using a text editor, but that would've taken ridiculous amounts of time. I realize it might seem boring to write tools early on when the project is most exciting and flexible, but I strongly recommend that you take the time to develop a focused and useable tool set for developing game content if you're really interested in shipping someday.

Developing On A Low-End Machine

Despite constant temptation to upgrade, I kept my TNT2 in my primary machine throughout the entire development of Strayfire. I decided early on that the minimum requirements for Strayfire would need to be reasonable, meaning folks with TNT2s and Voodoo3s should be able to play the game. At some point I upgraded from my p2-350 to a p3-500, but that wasn't until I was already sure the game was running well on the old machine. If I had written the game with a Geforce 2 or 3, its entirely possible I could've been lazy on the optimizations given that those things can churn out some serious mojo. While I consider it to have been rather beneficial, I can't in good conscience recommend that you develop on a crappy machine. I guess the point here is just to always keep in mind your target machine. Test your project anywhere possible.

Ease Of Distribution

Have you ever had a friend ask if they could see the latest build of your project? If so, and if your response was "Wow, that would take forever to track down all the required files.", its probably time to change that! Organizing resources well and avoiding hard-coded filenames will definitely pay off over the "simplicity" such dirty techniques may offer in the beginning. In Strayfire I had a 'data' directory that held only the most recent files that the game used. They were accessed in the game as "\\data\\file.dat" rather than "c:\\stuff\\temp\\version1\\file.dat". Old or obsolete versions of files were moved to a trash directory. Before distribution, my file packing tool would grab the data directory and smash all of the resources into a single file. The file utility functions in the game could seamlessly switch between using the packed data files or searching for files on disk. Adding this functionality early on, again, was without a doubt a huge time saver. Additionally, I saved a copy of working builds (complete with data files) every few weeks to see how the game's progressed since the last build.

Granted that's not the most sophisticated way to do version control, but my point is not that you should neccesarily do it like that, but just that you should keep your project as organized as possible. It would've been hell tracking down every resource at the last minute and making sure they're the most recent. How you actually organize it is of course up to you. If you can afford something like alienbrain, that'd probably make life easier.

Implementing Playback Demos

Adding the ability to record and play back games was incredibly useful for debugging the game logic, not to mention a cool feature for the final game. There are many things that can go wrong in a game, and unfortunately a lot of them are extremely hard to reproduce without recreating the exact same conditions that caused it in the first place. Knowing that, its often useful to record your input so that when you notice a quirk in the game, you can play it back to find out exactly where and how it happened.

To do this in Strayfire, I implemented a fixed time step scheme (for an example on flipCode, see here), and represented input key states using bits. Any given "input state" in Strayfire can be represented by a single integer since there are only a few binded keys. When the "move forward" key is pressed for example, a bit assigned to that key is set; otherwise its cleared. Same with every other input event in the game, they each have their own bit for on or off. This approach offers a compact way to store the complete game input state at any given time, which proved useful for recording games. Here's some psuedo-code illustrating the basic approach I used to record and replay Strayfire games:


// called once per game logic update to obtain the replayed input events;

void CGamePlayback::playUpdate(int gameTicks, CGameInput &gameInput)
{
    while(m_pcurrent < m_pevents)
    {
        // check if the current input is still active, else move to the next;

        if(gameTicks != m_playEvents[m_pcurrent].gameTickNext())
        {
            gameInput.clearEvents();
            gameInput.event() = m_playEvents[m_pcurrent].event();

return; } else { m_pcurrent++; } } }

// called once per game logic update to log the current input events; void CGamePlayback::recordUpdate(int gameTicks) { // if the input state has changed, save the old one and log the new one; // otherwise, just increment the current input event length; if(m_recordEvent.event() != m_readEvent.event()) { m_recordEvent.gameTickNext() = gameTicks;

saveEvent();

// new event; m_readEvent.gameTick() = gameTicks; m_readEvent.gameTickLength() = 1; } else { m_readEvent.gameTickLength()++; }

m_recordEvent = m_readEvent; }


If you're careful to reset your data before launching a level (including things like random number generator seeds), pumping the same player inputs through the game over and over again should produce the exact same results. Using code similar to what's listed above, seven minutes of average Strayfire play usually results in a playback file thats around 30kb. The size will be larger or smaller depending on how much key-bashing the player is actually doing, but I'd say the file size is quite acceptable. I never actually tried to trim it down any further, but I'm sure there are plenty of good ways to do it.

Rewriting Can Do Wonders
Approximately 0.66666666666666666666666666666667f of the way through the project, I noticed there were a number of things that I wasn't happy with. I took a look at how much memory my program was taking up, and was shocked to see it was around 70MB with a level loaded. Obviously something was wrong. So I dedicated a weekend to "rewriting" the game, which was more like just reorganizing, compacting, and cleaning up anything that was funky. After a couple days of cleaning, the program was trimmed down by a few thousand lines and required only around 28MB of memory with the same level loaded. The shocking bit was that there was no smoking gun making the game eat too much memory. It was just a whole bunch of little things like unused data that entities were storing, abundant stores of (unused) allocated particles, oversized string buffers for simple lookup tables, old resources that weren't being used anymore, etc.

As an added note, I learned from this experience that optimizing is usually best done shortly after you come up with a correct working implementation. Its too easy to say "I'll fix that up later", but how often do you really go back and optimize if the code doesn't become a problem? And how well will you remember the exact parameters of the original problem? Did you comment it well? From this point on, I worked on getting code working first, then cleaned it up and optimized right away before moving on.


The Bad Stuff


In this section I'll comment on some of the things that were done poorly in the Strayfire project, as well as some general things to watch out for. Again I'll format them as "tips" for other developers.

Look Into Scripting Languages

In the early days of Strayfire I was working with Lua and Small, trying to decide between the two. For whatever reason, I chose neither; this was a mistake. The game logic in Strayfire is all hard-coded in the game. Granted the enemies and special sequences aren't very complicated, but in retrospect I definitely should've gone with a scripting language. It would've resulted in a smaller program (less code, less bloat), and would've likely been much easier to add new enemies/weapons/sequences through a simple script interface. In addition, had I used a scripting language, the tools could've been released so that players could create their own levels and weapons for the game (something they've been asking for.) If I do a sequel or add-on pack for Strayfire in the future, its quite likely that I'll convert all of the actual "game code" over to a scripting language such as Lua.

Beware The Ancient Bugs!

I don't know about you, but every few months I usually notice some pretty big improvements in my programming practices. Looking at code I wrote even 6 months ago sort of freaks me out since I've learned so much since then. Funnily enough, the most annoying bugs that crept up in Strayfire were bits that were written in the first few days or adapted from older projects I'd written. As an example, there was this one crash bug that I could only get to occur on my second machine. It was extremely difficult to reproduce, happening quite rarely and "randomly." I assumed it had to be a pointer bug, being the sly devil that it was, but I couldn't figure out where it was. I check all my pointers! I kept the game open on my second machine until the crash finally happened, and with a little luck I found a case where I could reproduce it. I immediately recompiled the code on that machine, and stepped in with the debugger. There it was, this code caused the crash:


void CRenderEngine::flushDeadLights()
{
    ASSERT(m_lpDevice != NULL);
    
    for(int i=0; i<MAX_LIGHTS; i++)
    {
        if((m_lights[i] != NULL) && !m_lights[i]->isAlive())
        {
            m_lpDevice->LightEnable(i, FALSE);
            m_lights[i] = NULL;
        }
    }
}
 


This method was called by the render engine's "startScene" method, so it was very hidden away and quite easy to forget about. At first glance (and without the rest of the lighting system code), the above function might seem harmless, but the way the lighting system was designed was pretty poor. When a new light was added to the system, the pointer was passed along to the render engine and stored in the active light array. But what happens if the actual light object is deleted yet the pointer stored in the array isn't cleared? Yep, who knows, its pointing to something funky. Maybe it'll crash when accessed, maybe it won't; either way its dangerous business. In this example, either the array pointer should've been cleared when the light itself was deleted, or the lighting system should've been implemented differently from the start. When I noticed how shoddily this was written, I wanted to swivel-kick my monitor. Talk about embarrasing. I'd hopefully never write it like that these days, but that's exactly my point -- beware forgotten code and old coding practices! After this experience I went back through all of the project's "older" code in search of similar garbage.

Real Beta Testers Are Hard To Come By!
To a lot of people, being a "beta tester" sounds like fun! You get to play unreleased games all day, right? Sort of. Its really easy to find volunteers willing to play your game, but its not always easy to get real testers who are willing to test the hell out of it. How many people really want to sit around all day trying new builds as you tweak settings and fix little bugs? Without a budget to pay testers, this can be rather difficult for smaller companies because most of the actual gamers expect the same level of polish that any game from a big company would have. To be honest, I can't really offer any tips on finding free beta testers. If you find anyone willing to test, don't let them go! ;] One friend in particular (Alex Tchernychov) was extremely helpful when it came to testing game content and bouncing around ideas. I imagine all of those 2:00am ICQ messages and file transfers from me got pretty annoying while he was trying to get his own work done, but his extensive testing and constant feedback definitely helped make the game more robust. Thanks a lot, Alex.

Given the wide range of PC hardware out there, without a massive testing force at your disposal you're no doubt going to run into compatibility problems. Cutting it down as much as possible is of course the goal.

Keeping Motivation & Avoiding Sidetracks

Finishing and releasing a game is extremely difficult. When I see people make, polish, and release even Tetris clones, I have a certain level of respect for them. It doesn't sound hard, but it is. There are so many ways to not finish your game. There are so many excuses. There are so many "real life" distractions. As an example, one of the many "excuses to quit" that haunted me while working on Strayfire was that the game engine wasn't very technically interesting after the first few weeks. I've always been a bit more interested in researching and writing 3D engine junk and demo effects, making it rather tempting to move on to something more challenging after the first several weeks of Strayfire development. Most of the work after the game engine was developed could be classified as "grunt work." That's the stuff that eats time and doesn't always result in pretty graphics on the screen, but its very important.

Add to that some other factors like seeing my friends at game companies working on interesting engines, seeing the neat stuff people were doing on flipCode, seeing new graphics hardware come out, seeing new consoles launch, all combined with my personal longing to do something more "impressive" as my startup company's first game; these thoughts plagued me from time to time. But I knew I'd never finish anything if I gave in to start something even more ambitious. The problem of course is that its a never-ending cycle. It can always be more technically interesting, and it can always be more impressive. If your goal is to release your game, just establish clear goals, stay focused, and actually finish what you're working on piece by piece. Be realistic given the resources you have. The final product might not be the monster game you've always wanted to create, but its a stepping stone and you'll learn a lot that'll help you do something more interesting next time around. Finish it!


The Final Product


Overall, Strayfire took longer to complete than I initially planned, but after several months it finally came together. That's the good stuff. While I know its not a ground-breaking title, I think its a lot of fun, and I'm extremely happy to see that it was actually finished and released. It was an incredible learning experience, with many lessons directly applicable to future projects. I hope some of what I've shared in this article about the game's programming was worth reading. If you're up for checking out the demo (or supporting an independent development company ;), please visit here.

Again and again to Max Shelekhov, Pete Burke, and Jeff Jordan, thanks for your incredible work on the art and music. Strayfire would've been a mess without you :) And of course a big thanks to Alex and everyone else who helped out with testing.

That's all! Thanks for reading,
- Kurt Miller (01/14/2002)

 

Copyright 1999-2008 (C) FLIPCODE.COM and/or the original content author(s). All rights reserved.
Please read our Terms, Conditions, and Privacy information.