Fundamentals to Create a Cross-Platform Game
Table of Contents
There are 3 ways to create a cross-platform project.
- Using pre-processor #define with if/elses distinguishing what the program should do in the same file (which we don't recommend).
- Have a file for each platform-layer and a single common file, and we will handle our game by virtualizing the operating system.
- Have a file for each platform-layer and a single common file, with the game being treated as a Service for the operating system.
Virtualizing the operating system for the game
As mentioned in item 2, we can create a cross-platform project by virtualizing the operating system in "our head". Let's look at the example:
// game.h struct Window; void gameMain(); void gameShutdown();
// game.cpp void gameMain() { Window *window = platformOpenWindow(); Sound sound = platformOpenSound(); } void gameShutdown() { platformCloseWindow(window); platformCloseSound(sound); }
// win32_game.cpp struct Window { // win32 stuff prop. }
We define the game's behavior from a "gameMain" where it will virtualize whatever is necessary for the platform-layer to work.
// win32_game.cpp Window *platformOpenWindow() { // code for open window class // return new Window } void platformCloseWindow(Window *window) { } int CALLBACK WinMain(HINSTANCE instance, HINSTANCE prev_instance, LPSTR cmd_line, int cmd_show) { gameMain(); }
Game as a service for the operating system
The game only needs to provide the operating system with the graphics and sound update.
Each operating system has a complexity of settings such as opening files, plug/unplug gamepad, etc. So let's make the operating system only power the game:
- output: what to draw?
- output: what to play?
- input: here is the user input
- timing
With this we created a bidirectional system where the game will have services to provide to the platform-layer (game logic) and the platform-layer will be able to provide services to the game.
Moving logic to the game layer
Let's move the logic to the common file and create a struct with properties that are common across all platforms. That is, to draw pixels into the buffer we only need the width, height, pitch and the memory block void *.
// game.h struct GameBackBuffer { void *memory; int width; int height; int pitch; };
// game.cpp static void render_gradient(GameBackBuffer *buffer, int xo, int yo) { u8 *row = (u8 *) buffer->memory; for(int y = 0; y < buffer->height; y++) { u32 *pixel = (u32 *) row; for (int x = 0; x < buffer->width; x++) { u8 b = (u8) (x + xo); u8 g = (u8) (y + yo); *pixel = ((g << 8) | b); *pixel++; } row += buffer->pitch; } }
Now, in the platform-layers, just pass the pointer to this struct where it will populate what we need in the platform-layer.
// win32_game.cpp GameBackBuffer buffer = {}; buffer.memory = backBuffer.memory; buffer.width = backBuffer.width; buffer.height = backBuffer.height; buffer.pitch = backBuffer.pitch; gameUpdateAndRender(&buffer);
The function gameUpdateAndRender
will receive the common struct with
the values created in the platform-layer.
After modifying it and performing the final update, the pixels will appear.