Publishing game titles for multiple platforms is no new idea, but it is usually the case that the title is developed for a single operating system and the source code is subsequently ported to additional platforms. Ports can be costly, time-consuming, and downright ugly, however, if the original code is heavily dependent on interfaces or data types which are not present on the platform targeted by the port. The only alternative is to develop for multiple platforms simultaneously from the very beginning of a project. Simultaneous development requires that a system architect have some expertise in each of the platforms for which a title is targeted for release. It is the focus of this article to demonstrate how major subsystems of a game engine can be written to function on both Windows and Macintosh operating systems.
General Design Philosophy
When programming for multiple platforms, it is usually desirable to hide code that is dependent on a particular operating system by using a layered design. Platform-specific code should be safely tucked away at the lowest level possible, and code inhabiting the higher levels should require no knowledge of how the lower levels are implemented. This black box approach should be used to encapsulate major subsystems of a game engine as well as any miscellaneous functions and data types which may be depend on the underlying operating system. Once these lowest layers have been written, an applications programmer should be able to write code that interacts only with the interface that the game engine itself exposes to higher layers. The game engine is then responsible for communicating the right information to and from the operating system.
There appear to be two popular ways of organizing platform-dependent code. The first is to have separate Windows and Macintosh versions of a file that implements a particular subsystem. Ports invariably lead to this arrangement. This approach, however, usually leads to duplication of a lot of code which is actually common to both platforms. It also carries the drawback of having to touch multiple files to make a small change which requires that each implementation be altered. The examples in this article shall use a second approach: conditional compilation. We can define two flags, named WINDOWS and MACOS, which when used with #if … #elif … #endif blocks tell the preprocessor to either keep a block of code, if the corresponding flag is defined to be 1, or remove a block of code, if the flag is defined to be 0.
Consider the following simple data type abstraction example which uses the above scheme.
typedef HWND WindowReference;
typedef WindowPtr WindowReference;
This creates a small layer between the operating system and higher levels of the game engine which hides the platform-dependence of the WindowReference type. Any functions or derivative data types that need to refer to a window should use the WindowReference name instead of the name defined by the operating system’s API. This way, anything that refers to a window does not require separate definitions for each platform since it does not directly depend on a platform-specific data type.
This practice of isolating platform dependencies at the lowest possible level will be apparent throughout the code accompanying this article. The remainder of this article will be spent building abstraction layers for 3D graphics, sound, and networking subsystems upon which higher levels can rest without knowledge of what operating system they are running on. The differences between the two platforms will be pointed out, and methods for avoiding common pitfalls will be presented.