Games   

   Candy Cruncher   
   Fruit Frolic   
   Letter Linker   
   NingPo MahJong   
News   
   Latest News   
   Archive   
Services   
   Artwork   
   Programming   
Support   
   Lost Your Code?   
   Support Main   
   Message Forums   
About   
   Dev's Diary   
   Contact   
   Bios   
   Jobs   
   About Us   
   Links   
 
 

 

2/26/2002 -- The Whys and Hows of Porting Software

Today's Developer Diary installment is being guest written by Ryan Gordon, the person responsible for porting Candy Cruncher to Linux and other platforms. I've interspersed my own comments, mostly as amplifications of his own statements, but occasionally with clarifications or rebuttals.

Anyone that is interested in having their games ported to Linux or other platforms should definitely contact Ryan, he works quickly, finds bugs quick, and does amazingly high quality work. He can be contacted at icculus@icculus.org or http://www.icculus.org.

So now I hand the mike to Ryan.



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 Windows 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:

Choice: I like my Linux command lines, others like MacOS X's Aqua, others like WinXP. Making a portable game lets your users work and play in the environment they like best. The consumer wins.

Code quality: porting to a new platform exposes bugs you never knew existed in the first place.  It forces you to remove assumptions from the program. It can make it easier to maintain. It can make it easier to license your code to others, or contract out work on that code. Having a game that runs cleanly across several platforms can actually lower the cost of maintenance if you do it right. It also helps maintain the programmers' sanity when problems like buffer overruns become obvious through different memory managers.

Consoles: You want a Playstation 2 port of a game? Your first stop should be Linux. The migration path is easier than going to the console directly, it's cheaper to develop for, and yes, Linux and the PSX2 use the same compiler.

I can't agree with this strongly enough. The porting process involves many different steps, including handling compiler, library and system dependencies. Trying to handle all of these simultaneously can be aggravating and difficult, so it's often easier to do the port in steps. For example, if you know you're going to port from Windows and Microsoft Visual C++ to the Mac and CodeWarrior, it might make sense to first port to CodeWarrior on the PC, THEN port to the Mac. Divide and conquer.

Financial reasons: If a program is done right from the start, the cost to move it to new platforms is small, but the potential for extra sales increases. Mac and Linux users do tell their friends about software they like, and they tend to be very vocal in their praise of the companies that are willing to support their platforms. Games for Linux tend to have an infinite shelf life, whereas their Win32 counterparts tend to land in the bargain bin in a month or so.

I'd like to add that the OS X version of Candy Cruncher is outselling the Windows version by over 2-to-1. If you look at market size alone, you'd think that the OS X version would get its butt kicked, but in fact it's the other way around. Quality of consumer can often make up for a huge difference in quantity of consumer -- this is why niche markets can survive.

However, I do feel obligated to mention a counterargument, and that is support costs. We're in a bit of a quandary right now with our BeOS version. It took almost no time to get Candy Cruncher working on BeOS, but we don't think we'll sell many (if any!) copies. The cost, for us, to launch and support the BeOS version is significantly higher than any income we'd derive from it. So we have three choices: release the BeOS version even though we lose money on it; don't release the BeOS version, which is unfair to BeOS users and goes against our techie souls; or release the BeOS version for free, possibly alienating our paying customers. It's a tough situation and we're not sure what we can do about it.


Warm fuzzies: I like supporting the underdog operating systems. It makes me feel good. Pyrogon's stated intention is to make quality games that don't cost millions of dollars to make. Having players enjoy their games is priority one, so it makes sense to get them into the hands of people that generally appreciate the games more than the twitchy overclockers that are giving Windows gaming a bad name. Note: not that we don't love you overclocking Windows users too!

I am speaking of games, but these statements are generally true for any kind of software.

So 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 non-portability 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, Candy Cruncher has a "H2DDisplayDevice" 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.

A clarification here -- while we're big fans of Lua, Lua isn't actually used in Candy Cruncher. Our upcoming 3D game, ColdStar, however will be leveraging Lua significantly for its scripting and AI components.

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.

I'd like to add examples of the above that bit me while working on Candy Cruncher. The first place -- and this is extremely common -- was in my image file loading code. It's common practice -- and WRONG -- to try to format your structures to match the memory layout you expect. Then you see code like this:

TGAHeader *pHeader;

pHeader = ( char * ) someBufferLoadedFromDisk;

imageWidth = pHeader->width; //BAD!!!

The above code would work fine on a PC, since the TGA data is stored on disk in little endian format, but on a Mac it explodes horribly with wildly incorrect values. The correct way to handle this would be something like:

pHeader = ConvertLittleEndianToNativeEndian( pHeader->width );

The second bug I encountered was because the sizeof(bool) changed between CodeWarrior and ProjectBuilder/GCC on OS X. So when storing values to my preferences file I'd get mismatched sizes, which caused a lot of subtle (and difficult to find) problems.


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.

I'd like to add a corollary here -- minimize system specific dependencies. For example, Candy Cruncher expects each system to implement audio mixing capabilities. This makes porting more arduous than necessary. In fact, I intend to write my own audio mixer just so that I can remove a lot of the gross system specific stuff I have in place right now.

A further corollary to this is to avoid depending on closed source, non-portable libraries for core aspects of your game. It might be great to license XYZ from another vendor, but if they have no plans to port to Linux, then guess what -- neither should you.


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

Thanks to Ryan for a great Developer Diary! Okay, any developer reading this doesn't have a reason not to port to at least one other platform. And Win98 to WinXP doesn't count!

 

Copyright 2002 Pyrogon, Inc. Site by John Krane. All rights reserved.