Writing cross platform Code.
Warning. We are about to get a bit application specific here. Most of what I’m about to write about applies more to gaming systems than anything else.
Writing code for more than platform at once isn’t as cut and dried as it sounds. While straight C itself is portable across platforms (or should be) often the hardware you are trying to manipulate isn’t. The PS2 just isn’t the same as the XBox or the GameCube (and rightly so, otherwise there would be no difference).
We aren’t just talking about graphics hardware here, but also sound hardware, input systems (joysticks, game pads, mice, keyboards and so on), sound hardware, the amount and layout of memory, the ability to use .dll’s, access of harddrives and CDRoms and so on. You can see how an app built for one platform might be dramatically different in implementation for another. In some cases it may not even be possible on a particular platform due to differences in CPU capability, or graphics functionality. You aren’t about to see Quake III on the GameBoy Advance anytime soon, that’s for sure.
However, that doesn’t stop some code being the same. Take an average FPS game. While the rendering specifics will be different across the XBox, the GameCube, the Playstation2 and the PC, it’s eminently possible to share code across each platform. Stuff like your entity control system, AI path following system, AI decision making, the controlling mechanisms of the game itself can (and should) be shared across systems. That way the game will ‘play’ the same on each system, with the same kinds of responses on each. Mortal Kombat 1 on the SNES got a lot of bad reviews because the developers threw away the original code and just tried to re-implement it all themselves. Mortal Kombat 2 on the home consoles was a much better conversion because they did a routine for routine conversion, thus preserving the way the original game played.
So, here you have a bunch of source code some of which is platform specific, and some of which is not. The first thing to do is not to mix the two. Put your platform specific stuff (like reading files from harddrive / CD-ROM, working out time, actually gathering input from the input devices) in separate files. For stuff like reading files and time you should have a pretty standard interface that returns time in milliseconds, and file reading, well, use the C fopen, fread and fclose as your example to copy there. For stuff like input systems, you need a standard interface the game uses to control and move characters. The game itself shouldn’t be using data that comes directly from the joypads, or you’ll find it difficult to convert the code across for a different control mechanism. The interface for the input system should be flexible enough that it can handle both digital and analog inputs. For example if you use an array of ints for your button presses then the game can assume that a button can be pressed anything from 0 (up) to 255 (down) with different amounts of pressure from 1 to 254. Make sure the game can handle this. If your input device on a specific platform is only capable of pressed fully down or up, then your input system will set the integer for that button to either 0 or 255 directly. This way the game will not need to change between platforms. The game won’t have degree’s of motion, but that’s got more to do with the input system available than your code.
This approach is necessary for some games on the Playstation 2, since it supports both analog joypads (the black ones) and straight older digital ones, that aren’t pressure sensitive. Your game has to support both types.
Incidentally, this approach to creating an interface between the ‘real’ input data and the data your game uses is called ‘Abstraction’. Data is presented to your application in a standardize format regardless of it’s original. You are abstracting yourself away from hardware implementation and into pure data manipulation.
Anyway, back to the code. So you’ve put your implementation specific stuff inside of separate routines for ease of conversion between platforms. What about all your graphics stuff? That’s one of the area’s that you will have the greatest differences. This is where you need to be the most clever. You need a standard interface like you created for the input system between the game and the graphics implementation. The degree and point at which this separation occurs in the code between platform independent code and the actual setting up and addressing of the graphics hardware depends on how much extra work you want to make for yourself between each platforms code. The higher you do it, the more you will get out of the graphics system on any given hardware, but the more work it will be because all the graphics systems will start to be different higher up in the code.
To explain that a little better, imagine the main game wants to load a 3d model. Do you create a specific file format for each graphics system you want to use? If you do, you can save yourself a lot of code later inside the code that renders the model because the data for the model is ‘ready to render’ so to speak. It’s formatted for the specific system it’s going to be used on. However that means having several different tools to preprocess this data into graphic system format. Your model would have to be processed differently per platform, which takes time.
On the other hand, if you go for a unified file format, at some point you will end up massaging that data into an acceptable graphics system format anyway. It’s inevitable since all the graphics systems are different across platform. If you don’t preprocess it, then you end doing it at run time, which takes time and takes away from other things your CPU could be doing. The same issues apply to actual texture data. Do you keep it all 32 bit across all the platforms, or do you just go 16 bit because that’s what the PS2 is best at rendering?
You begin to see where intelligent decisions regarding where you decide to ‘go specific’ can become important, and no where more so than graphic systems.
Something else you should be doing with your graphics systems is making them scalable. That means that depending on what’s going on on screen you can reduce out the complexity of what you are rendering to keep frame rates up. This becomes critical in things like particle systems. If you are reducing your frame rate to a crawl you need to be able drop certain effects, or the scale of those effects to keep the frame rate up. The game becomes auto balancing in this way. If you have model Level Of Detail (and if you don’t know what that means, see some of my other articles for an explanation) you can even use this to reduce scene complexity.
One of the other things that scalability gives you is the ability to transfer a graphics system across platforms more easily. For example, the same graphics system on different video cards on the PC. Not all offer you the same blend modes. You need to create a system that if blend mode XYZ is not available on any given graphics card, the graphics system will find a way around this, by creating a different kind of effect or shader to compensate. It may not like quite as pretty as the original visualization, but it will still run, and run fast. You can always tweak the system later. This approach works just as well for cross platform systems as it does for different video cards on the PC.
Inserting platform specific code in the main codebase.
Sometimes you end up having to mix some code in the codebase rather than sticking it in a separate file. In some cases, if it’s a function that’s called repeatedly per frame, and it’s only two or three lines the overhead of actually having to do a procedural call just doesn’t make sense. There are two solutions to this. One is using compiler options to handle it. Remember those?
Do windows specific code
Do playstation2 specific code
This will make the compiler only compile code for a platform you designate via the _WIN32 flag (which by the way, Visual C++ sets up for you automatically). It’s a bit inelegant, but it does make you very aware in your source code of what you are doing.
The second way is inlining. What does that mean?
Well, it means that you create the platform specific implementations inside of separate files, but ask the compiler to ‘substitute’ the actual code inside of the routine for the call inside of the main code. OK, that’s too complicated.
Imagine you have a routine you call this way.
void WorkOutVectorFromAngles(angles_t angles, vect_t vector)
do calculations here…
void MakeMissileLaunch(missile_t missile, angles_t angles)
Yes, I know this is rubbish code, but it shows the point being made. As it stands right now, the code above will generate two routines, one of which calls the other passing variables between then. If we ask the compiler to inline the routine WorkOutVectorFromAngles instead of it generating a routine called by the function MakeMissileLaunch, it will actually insert the code directly into the function. If WorkOutVectorFromAngles is used alot in the code base, everywhere it is called the call code will be replaced by the actual code itself. Warning, this can bloat your code base a lot if this is called alot and the function is pretty big. But it does make the code a little faster since all the processor overhead of actually calling the function and stuffing data on the stack is avoided. Plus it gets us around the issue of inserting platform specific code in the main codebase.
One last thing. Sometimes system architecture can have a huge impact on your implementation. Take the Playstation2 for instance. It has fragmented memory all over the place (unlike the Xbox) and with it’s dual vector units, it can be much harder to get the best out of it. You literally have to code specifically for that platform in order to get the best out of it. Cross platform code in this instance tends to slow you down, and while it’s definitely a good idea for ease of conversion, you are not going to be able to manipulate and put as many polygons on the screen as code that is tailor made for the PS2 and nothing else. For example, every time you do a matrix calculation, you should be using vu0 to do it for you, since it’s much quicker than the main CPU at this kind of thing. But other machines don’t have that, so you end up writing more specific code for the PS2. It’s a pain, but this is what professional programmers get paid for.