..
#gamedev

Fundamentals to Create a Cross-Platform Game

There are 3 ways to create a cross-platform project.

  1. Using pre-processor #define with if/elses distinguishing what the program should do in the same file (which we don't recommend).
  2. Have a file for each platform-layer and a single common file, and we will handle our game by virtualizing the operating system.
  3. 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:

  1. output: what to draw?
  2. output: what to play?
  3. input: here is the user input
  4. 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.