What Went Wrong in Armagetron
The following is taken from three separate posts on the forums by Z-man. Each will be linked appropriately, there is discussion there. It is kept here for a number of reasons. One is that it provides some insight into what the code looks like, and with all the old code we're still working with, it has day-to-day impact on the project even now. Another reason is because it gives us a lot of pointers to what bad design decisions were made, and sheds light on some of the good decisions that were made. In that regard, keeping it where we can get at it will help us to avoid the same pitfalls that are already there and still keep the good stuff that is definitely there. Finally, members of the community who complain that the project may not appear to be going anywhere from time to time can read this and at least get an idea the amount of work that is required to move forward.
We hope that given enough time this entire document will be rendered obsolete and will only be of historical significance, but until then, we're still dealing with it.
The first thing that sometimes proved to be suboptimal was that I never really had a plan of where Armagetron should be going. In part, this was a good thing: Maybe I would have never started to work on it if I had a full plan. The Extreme Programming Gurus tell you tou should not have a big plan of your software. They tell you you should improve it bit by bit, show it to the customer, and repeat. What they don't tell you is that this can only work if your customer knows what he likes. Guess what? My "customer" was a crowd of people from the internet. While crowds can be smart in some cases, most of the time the intelligence of a crowd is the intelligence of the stupidest member divided by the number of people in it. At first, everything was easy.
Hey, network support! You think the game is too slow? No problem, here is a configuration option. You don't want to edit a configuration file? Ok, let's put it into a menu! You want more game modes? Here they are! A dedicated server? Some IFDEFS, and it is done! But it turns out that when you let a product grow that way, it does not grow organically, it grows like a tumor.
Here are some areas where you see it most:
- gGame.cpp. Need I say more? The logic of the various game modes, altough there are only three, is so convoluted I would not dare to change a single line of it today. It is a case for a rewrite.
- Moviepack integration. Instead of thinking how I could integrate custom graphics the right way, I went straight ahead and hacked some files I got in. One global flag selects which variant gets loaded...
- No 3d vector class! There is eCoord, a 2d vector class. Everything that needs to be 3d is calculated manually. The cameras have a xy position ( an eCoord ) and a z position. When you don't have a plan, you don't know what kind of support structure would be useful in the long run.
- Network protocol versioning support to allow client and server to have different progam version without conflict came in much too late. Nobody of the crowd demanded it.
Hmm, that's all I can think of... Maybe having no plan is not so bad after all.
Lesson learned: It may be OK not to know where you want to be in ten years, but you should know where you want to be tomorrow and how to get there. Read: Don't rush to implement features. Sit back and think how they best fit into the architecture. Think about possible future extensions and how to support them here and now. Good ideas take a while to settle and form.
Somehow, I have to think about that old song by Frankie Goes to Hollywood...
Well, many of you will have noticed one of the externally visible design flaws or Armagetron: it does not handle data flexibly. I could actually stop here because those of you who have either created or installed and used alternative content could take over the rant:
It starts with the obscure .mod format the original lightcycles are delivered in. It is a relatively simple text based mesh description format: lines starting with "v" define a vertex, the first number that follows is the vertex index, the next three numbers are the vertex's coordinates. Lines beginning with "f" define faces; the three numbers that follow are the indices of the vertices that form the triangle. ( This is the first time this abomination is actually documented. ) There's no space for texture coordinates or surface normals. AA just maps the textures in a predefined way by projecting them from the sides. The reason for using this format instead of the more common .ase, if things have to be text oriented, was only to obscure that I was using 3ds max for the models. And I lost my .ase to .mod converter, so there's not going to be more of them.
Then there are the configuration options. Nothing wrong with them from the user side; and from the developper side, it's easy to add new options to the code. So what am I complaining about here? Simple: every configuration option is linked to a variable that needs to be there when the config files are loaded. That leaves almost only one choice: the variable needs to be global. And every programmer gets taught in kindergarten that global variables are evil. They make it very difficult, for example, to have two Lighcycles run with different settings.
There is, however, one thing that is even worse than global variables for settings. It's global constants for things that should be variable. I'm talking about the names of the content files. Yep. All the names of the .mod, .png and .wav files are hardcoded into the application.
And even when it is clear that an abstraction would be needed instead of hardcoding decisions, a global variable is used. The decision wether the moviepack or the classic data files are used hangs on a single variable. All the rendering paths that depend on it look the value of that variable up and change their behavior based on it: the floor gets rendered slightly different, the rim walls completely different, and don't get me started on the lightcycles themselves. Since everything is bound to one single variable that can have only two values, it is not possible to combine moviepack walls with classic cycles.
Then, there are the language files and fonts. Fonts and their implementation are a topic for another WWWIA, but their interaction with the language files fits here. While the language files offer relatively painless translation ( if you can figure out what I meant with the original English ), they have one crucial flaw: they have to be in an encoding that fits to the font that's currently in use. That leaves two choices: use an encoding that contains most of the characters actually in use ( works for German, English, French by choosing latin-1, for example ), or allow each language file to select its own encoding and font. Both approaches have the drawback that everyting that goes beyond ASCII is easily lost on the internet ( my Eclipse already destroyed deutsch.txt once. ) The second approach has the additional problem that, when receiving chat messages form a server, you'll have to know their encoding. The first approach has the drawback that you'll never fit all important characters ( at least latin, greek and korean ) into one font texture.
So, what can be improved here? The configuration variables can stay the way they are for now; they may be evil global variables, but at least they are convenient. They make certain things impossible, like running two networking subsystems at once, but there's lots of other global variables that prevent that.
The obvious thing to tackle next, I think, would be the content selection. The next best thing to the current system would be to store the resource names and settings ( wall height, texture transformation matrices ) in configuration variables. To select a certain "mod", AA would just load a .cfg file with all these settings from the mod directory. That way, we could select the overall appearance of the game in a simple way. However, you would not be able to select custom cycles for every player, because the cycle model is still stored in a global variable.
So, the first sensible step would be to abstract everything about appearance away from the main game; let there be one factory class that the cycle comes to when it needs to get rendered. Implement three such factory classes at least: one for the classic Armagetron look, one for Moviepacks, and one that's really new and does things right. ( Exercise: add one that uses glTron's rendering code :) ) The two legacy classes should offer minimal configurability, say by letting you select a custom search path for the files. For the real one, devise file formats for cycles, walls, floor, skies and rim walls. They should be simple text files in a chunky format to make them easily extensible. Let users mix&match them if they want. Let users select their own cycle by choosing one of the cycle definition files. Let this work reliably over the net, too: If you are riding with Pacman and I have that file, too, I should see you as Pacman. ( Of course only if I want to allow it. ) Make it easy for users to download and install new visuals, make the arena style configurable by the servers. Only one global variable should be involved in all this: The path and name of the visuals configuration file.
For the language files, the answer should be simple: use UTF-8. ( That's the encoding variant of unicode that does not need more room than ASCII when only ASCII is used. ) Divide all characters that are actually used over several font textures. Drop tString in favor for std::wstring internally.
Closing words: I'm tired now, so I'll stop rambling before I write something really silly.