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!)
--ryan.