To celebrate the completion of the Linux port of Candy Cruncher, I thought I'd put down some thoughts about porting games and writing portable software in general. This is a sort of tip-of-the-iceberg, random HOWTO on the subject.
Why port software? Especially in the game industry, is there much to gain by catering to minorities? MacOS is sitting on 5 percent of the world's desktops, and while that's a significant number of users, the other 95 percent is almost all win32 boxes, which means you can get most of the users from writing for one target, and think nothing of the Linux and BeOS users of the world. So what's the point?
Here are my reasons:
I am speaking of games, but these statements are generally true for any kind of software.
So now that we know why, let's explore how.
To start, I have to express the Tao of porting: no code is portable until it gets ported. Sure, we all write wonderful code, and choirs of angels sing while we type, but there will always be unexpected problems that won't be seen until the source is pushed through a strange compiler on a strange operating system running on a strange processor. The trick is to minimize the amount of nonportability right from the start. This takes diligence and a little bit of know-how on the part of the coder. Knowing what you're doing can literally reduce the porting time by weeks. This knowledge is best gathered through experience, but these guidelines can be a push down the road of that first experience. If any of this seems like common sense, then it just means you've been down that road before.
Rule #1: Think before you code.
Sounds simple, doesn't it? Unfortunately, even in the video game industry (ESPECIALLY in the video game industry!), it seems that many developers jump right in and start coding. This is wrong, wrong, wrong. It doesn't take long for a project to become an unwieldy, hardcoded mass of spaghetti, which leads to the usual set of problems; however, if even maintaining (or, heaven forbid, enhancing) the codebase is difficult, it will be twice as hard to make it portable. What you need is a blueprint. Sketch it out, write it down, babble incessantly about your plan to everyone around. Have an attitude that lets people tell you honestly if a plan is stupid, and prepare to revise details or whole subsystems. Better to do this now than find yourself reworking the program during crunch time.
Rule #2: Make abstractions.
If you're writing a Windows game, sooner or later, you are going to have to call a Windows-specific function. Things that can be done portably (like using stdio instead of the win32 API) should be done, but other things, like blitting to the screen, are system-specific. What you should do is take a few minutes and wrap things like DirectDraw in a simple class, and expose their functionality in general ways. Do NOT expose DirectDraw data types, to prevent the urge to bypass the abstraction. If something can't be done through the abstraction layer, then the abstraction layer should expand. Candy Cruncher does this very thing for audio, video, the "registry", input, etc. For example, you've got a "DisplayDevice2D" class. This class is subclassed into a DirectDraw version, a GDI version, an SDL version (for Unix), and a Carbon version (for MacOS). There are some immediate benefits here, even in single-platform development. Note that two of those subclasses are for Windows; this gives Pyrogon the ability to choose the best balance of performance and stability at runtime for any given run of the game. Theoretically, they could add an OpenGL-based subclass of DisplayDevice2D that renders the sprites as textured quads to increase the framerate more, at their users' discretion. This would be added to the framework, without changing the actual game's code, and would benefit the next game they do, too. Good abstraction makes good design, and the benefits are quick ports to other architectures, more flexibility for the power users, and better support for various hardware on the primary platform. It's a huge win.
Rule #3: Be data driven.
Spend as little time in your program as possible. Branch gaming logic out into scripting languages as quickly as possible. This isn't an argument to write your whole game in Perl (unless you want to, I guess); instead, get the stuff that must run fast in C/C++ code (which is almost always the blitters in a 2D game, reference Rule #2) and get the game logic itself out to something you don't need to compile every time you tweak it. I say this not just because it's a good idea, but porting a script interpreter is frequently easier than looking for subtle problems in game logic in C. But what scripting language is best? It depends on what you need. If you need something basic, roll it yourself, but it's better to embed an existing scripting language; there are many that are portable, debugged, and supported to choose from. Perl, Python, and Scheme are just some options. The Pyrogon framework has Lua, which seems to be popular for scripting game logic.
Rule #4: Be sensitive to byte ordering and packing.
If I ever see another game that sends "sizeof (myStructure)" bytes over a network connection, I'm going to scream. I should scream right now, because I will no doubt see this again. Candy Cruncher is not a networked game, but it does run on both Intel (Windows and Linux) and PowerPC (MacOS X) systems, which means that it has to be careful about reading from and writing to files. Between processor types, operating systems, and even compilers, sizes of data types change. I'm talking about something more subtle than the classic C problem of an-int-is-not-always-the-same-size-everywhere, although that's important, too. Structures get packed differently (and not every compiler can understand #pragma pack), data has to be aligned differently, and data gets stored backwards on different processors. If you have to read or write structures to disk, a network socket, or anywhere that a different system may see it, you should send it, one scalar at a time, in an agreed upon format (bigendian or littleendian), and rebuild the structure on the other side of the connection. Do not send floating point numbers ever, if you can help it, since different CPUs have different precisions, and you can only correct for this so far (I can think of at least four ports I've worked on that got bitten by the floating point thing. Be wary.) If you do not do this now, it is nearly impossible to fix it later in a program of any size.
Rule #5: Write what you have to, steal the rest.
I've just told you to write a scripting language and be very careful about how data gets manipulated. Right now you're probably wondering how any of this is supposed to make your job easier. Hey, I said this takes diligence! However, the secret is really in the open source community. Why should you write image decoders, and audio format decoders, and scripting languages, when they are freely available for the taking? Candy Cruncher takes advantage of several cross-platform libraries: Lua, zlib, and Ogg Vorbis, to name a few. The Linux and Mac Classic ports use SDL, SDL_ttf, and SDL_mixer, not to mention Loki Setup for the installer. This is literally years of development time that can just be dropped into place, and more importantly, all of these libraries are cross-platform to start with, so you don't have to wonder how you'll get that .OGG file to play on BeOS; it just will.
Rule #6: Don't use assembly language.
Just don't. If you must, you better write a C version and optimize based from that, so that there is a working fallback. But don't write assembly in the first place. 99.5% of the time you think you need it, you don't. Just say no; Candy Cruncher did.
Rule #7: Listen to your beta testers.
Sooner or later, your port will be ready for external testing (you are going to do a beta test, right?) and you will be unleashing your baby into an unfamiliar world. If this isn't your primary development platform, chances are it isn't a platform you know all the ins and outs of. Even Linux users will find that different distributions do things very differently, and every Linux user has her own routines and traditions. Listen to what they ask you for, and give them what you can. One of the beta testers for Candy Cruncher noted that the game's response was a bit jerky on his box, and wondered if we could make it use the X11 cursor instead of drawing a sprite. We added that. Another wanted to have his Unix login name be the default when entering his high score. It was a good idea that never crossed my mind: added. People with keyboard layouts I've never heard of showed up: fixed. People with exotic display targets poked their heads up: tweaked. Odd sound problems on certain distros: debugged. These requests are to be expected, and are relatively trivial to implement, but they lead to happy customers and, again, a more flexible codebase. You do not want thousands of demo downloads from users that would have bought the game if only the mouse was a little more responsive. You could not have predicted it until someone came along and ran their X-server at an odd color depth. Anticipate possible differences in platforms, but be ready for anything.
Rule #8: Embrace Murphy.
Not every idea is a good one. If something isn't working, chuck it. If a subsystem isn't portable, make it so. If you are modular, and abstract, this makes the code easier to drop into a future product. Like I said, nothing is portable until it's ported, and all the planning in the world doesn't beat Murphy's Law. In such cases, don't be afraid to throw something out and replace it with something that works better. Struggling only makes it worse.
That's your moment of Zen. Now go forth and write portable software! (and be sure to buy a copy of Candy Cruncher for Linux, to help a starving hacker like me!)