How are games compiled for multiple operating systems at once?

Started by
6 comments, last by NightCreature83 11 years, 4 months ago
Hey everyone,
I'm new to programming-- I've taken an intro to programming course this semester and I'm taking one focused on C++ next semester.
I want to begin working on small games, and I'm interested in how the games that are shared on Humble Bundle are made for most major operating systems. I figure they're not rewritten for every framework. What's a common &/or easy-to-use toolchain to develop for multiple operating systems at once?

Thanks!
Advertisement
Most use game engines such as Unity 3D or Unreal Engine 3.

The rest are written in cross-platform code and compiled once for each platform by various compilers.


L. Spiro

I restore Nintendo 64 video-game OST’s into HD! https://www.youtube.com/channel/UCCtX_wedtZ5BoyQBXEhnVZw/playlists?view=1&sort=lad&flow=grid

As L. Spiro said, they use code that works across most operating systems already, so all they need to do (in theory, not in practice) is recompile on each system.

The C++ standard library contains file access, algorithms and containers, all cross-platform.
But what about graphics and sound and exploring folders looking for files, and things of that nature?

Using C++'s very simple (and fairly weak) pre-processor language, we can selectively compile code based on #defined words.

Like this:
#define meow //'meow' is now defined as a pre-processor word.

int myInt = 0;

#ifdef meow //IF 'meow' is defined...
myInt = 20;
#else //Otherwise...
myInt = 10;
#endif

std::cout << myInt << std::endl;

This prints '20', since 'meow' is defined. If you comment out the '#define meow' line, then it will print '10'.
Anything code that is skipped over by the pre-processor doesn't even reach the compiler. From the compiler's perspective, the non-included code just doesn't exist.
This all happens 'at compile time' (or rather, before compile time). So you can't change #defines during the program's execution, because the #defines aren't even inside your executable.

Fairly simple right? This is the foundation of all C++'s cross-platform code.

Imagine this: I use a compiler called GCC. GCC is available for Mac, Windows, and Linux ([size=2]I'm glossing over a few things that don't particularly matter to the topic at hand).

Someone wrote a compiler, and manually 'ported' it (rewrote it) for each operating system. On the Windows version of the compiler, they made a slight tweak. They added a #define called "__WIN32__". No other operating system has that word defined with the preprocessor.
On the Mac version of the compiler, they added a #define called "__MACOSX__" and "__APPLE__". For Linux, they have a #define called "__unix__".

Cool beans. So now what happens? Since I'm writing code using this compiler (GCC; other compilers have similar or identical defines), these pre-processor words are already defined for me.

Now I want to create a window. C++ doesn't provide window creation in the standard, but each operating system has their own different window-creating functions. dry.png
So I can write code that looks like this:
bool myCreateWindow(unsigned width, unsigned height)
{
bool result = false;

#ifdef __WIN32__ //If I'm compiling on Windows.

//Code for illustration only - these aren't the real function names.
Window window = Win32_MakeAWindow(Size(width,height));
if(window.IsGood())
{
result = true;
}

#elseif defined(__MACOSX__) //If I'm compiling on Mac OSX.

result = Apple::MakeMeAWindow(width, height, colorDepth);

#elseif defined(__unix__) //If I'm on a unix-based system.

if(UnixCreateWindow(width, height, otherStuff, moreStuff) == WindowCreationSuccess)
{
result = true;
}

#else //If I'm on an operating system that my code isn't familiar with.
#error Warning! Unsurported operating system.
#endif

return result;
}


Now, before compiling, it automatically hides all the code for operating systems we aren't on.

It's rather messy though, and I'm not familiar with the different window-creation, graphic functions, directory traversal functions, and etc... of each operating system.
So, I let someone handle it all for me! People with interest in that kind of stuff create C++ 3rd party libraries that have all the #define mess in it. Then, they compile versions of their libraries for each operating system.

My code only calls the 3rd party library's function, which the 3rd party library makes sure is identically named and with identical arguments for each operating system.
I use multiple 3rd party libraries. Some libraries don't need any #ifdef stuff, because their code only uses the standard library or only uses the core language features.

My code looks like this:
blah blah;
doStuff();

CrossPlatformLibrary::WindowCreation(width, height);

moreStuff();


My code only uses it's own functions, standard library functions, or 3rd party cross-platform library functions.
Theoretically, I can then compile my code on each operating system I want to release on, and package the executable with the 3rd party library versions compiled for that platform.

I say 'theoretically', because there are alot of other things that vary from platform to platform that complicate stuff, which you have to work around: Where to you store your resources on each platform? Where is the correct system folder to save user data? What folder is the program supposed to be installed in? Does the system store memory in a different format than other systems? Am I limited to less RAM? After resolving all these kind of issues, then your game should run on each system.

The end result is your executable for each operating system actually contains different code compiled into, and the executable will be in a different format with different assembly or bytecode, and it'll actually be three different programs that visually function and look the same. But through clever use of cross-platform code, the majority of the code should be identical (except where you add platform-specific features), and only the compiled results should be different.
Thanks L & Servant!
I think I needed both the long and short of it. Thanks for the summary and a really good primer. :)
Just to add to the above posts...while you can certainly author platform-specific code using the pre-processor, in practice that's really messy. It's not hard to imagine how convoluted a real window-creation function would look if you just put the code for multiple platforms all in the same places with #if's and #ifdef's thrown in everywhere. So it's generally better (IMO) to avoid that whenever possible by using other means to selectively compile code. For instance at my current company, we tag platform-specific cpp files with a suffix that tells our build system what platform it should be compiled for. That way each file can contain a whole bunch of implementation-specific code for a single class or a group of related functions.
In practice, you will also have to be careful when using new features of your language of choice (like C++11), because even the same compiler may have different parts implemented. Current example: C++11 threading features are present in GCC on Linux, but not yet on MinGW, same goes for a triviality like std::to_string. So if you want to create multi-platform code, it's a good practice to compile and test often on all your target platforms. This will show you were to implement workarounds for differences in your dependencies.
Just what Servant of the Lord said, code is written so that it will work across multiple platforms - which for the most part isn't too difficult as both the PS3 and 360 supply SDKs written in C++. You do have to be aware of various platform idiosyncrasies though, endian systems for example.


On the last project I worked on we used different solution and project files dependent on the platform. We had a script that automatically generated the project files from the main PC solution.

Just to add to the above posts...while you can certainly author platform-specific code using the pre-processor, in practice that's really messy. It's not hard to imagine how convoluted a real window-creation function would look if you just put the code for multiple platforms all in the same places with #if's and #ifdef's thrown in everywhere. So it's generally better (IMO) to avoid that whenever possible by using other means to selectively compile code. For instance at my current company, we tag platform-specific cpp files with a suffix that tells our build system what platform it should be compiled for. That way each file can contain a whole bunch of implementation-specific code for a single class or a group of related functions.

Another way of doing this is by telling the make/solution/project/build script mechanisme which platform it's target is and then include platform specific code files. No need to specially tag cpps or h files, if they are in the target make/solution/project/build script as long as they are in there it will be compiled in for that platform. However you would still want to use the preprocessor in certain cases where implementation is only slightly different depending on which platform you compile for, and we are getting to the int changing values stuff for particular platforms here. You don't have to rely on the pre processor commands build in to the compiler with this matter either as you can specify per make/solution/project/build script what defines it should set.

You will also want to invest in some interface design as well so that the code that is platform specific presents the same interface to the rest of the code so that the system is easy to interact with.

Worked on titles: CMR:DiRT2, DiRT 3, DiRT: Showdown, GRID 2, theHunter, theHunter: Primal, Mad Max, Watch Dogs: Legion

This topic is closed to new replies.

Advertisement