Building a Macro-Abstracted Game Engine

Modern C++ game development often involves significant boilerplate: managing window handles, rendering contexts, and event polling loops.

In this tutorial, we will explore a specific architectural pattern: Hiding the engine lifecycle behind preprocessor macros. This allows the end-user to write games that look like simple scripts, while maintaining the performance of C++.

Reference Implementation

The concepts taught here are based on the architecture of Wizard Engine.

View Source Repo →

The Philosophy

The goal of this architecture is inversion of control. Instead of the user calling `Init()` and `PollEvents()` manually, we provide a syntactic wrapper that handles this automatically.

We want the user code to look like this:

// Desired API
APP_MAIN("My Game", 800, 600) {
    
    APP_LOOP(true) {
        // Game Logic Here
    }

}

To achieve this, we need to construct a singleton backend and wrap standard C++ entry points.

Prerequisites

We will use SDL2 as our backend rendering and windowing provider. Ensure you have:

Step 1: Global State Management

Since our macros need to access the window and renderer from anywhere, we will use a Singleton pattern. This class acts as the bridge between the high-level macros and the low-level SDL calls.

#pragma once
#include <SDL2/SDL.h>
#include <iostream>
#include <string>

class EngineCore {
public:
    // Singleton Access
    static EngineCore& Get() {
        static EngineCore instance;
        return instance;
    }

    // State
    SDL_Window* window = nullptr;
    SDL_Renderer* renderer = nullptr;
    bool isRunning = false;

    // Initialization
    void Init(const std::string& title, int w, int h) {
        if (SDL_Init(SDL_INIT_VIDEO) < 0) exit(1);

        window = SDL_CreateWindow(title.c_str(), 
            SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, 
            w, h, SDL_WINDOW_SHOWN);

        renderer = SDL_CreateRenderer(window, -1, 
            SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC);
            
        isRunning = true;
    }

    // Cleanup
    void Shutdown() {
        SDL_DestroyRenderer(renderer);
        SDL_DestroyWindow(window);
        SDL_Quit();
    }
    
    // Stop Request
    void Quit() { isRunning = false; }
};

Step 2: Macro Abstraction

This is where the magic happens. We need to replace `int main` and the `while` loop. This technique is heavily utilized in the reference implementation (Wizard Engine).

The Entry Point

We define a macro that creates the standard `main` function, initializes our singleton, and then leaves an opening brace for the user.

#define APP_MAIN(title, width, height) \
    int main(int argc, char* argv[]) { \
        EngineCore::Get().Init(title, width, height); \
        /* User code block will naturally follow this macro */

The Game Loop

The loop is trickier. In a game engine, a single frame usually consists of: Present (prev frame) -> Clear -> Poll Events -> Run Logic.

We can pack the boilerplate into the `while` condition using the C++ comma operator.

// Helper for event polling
inline void internal_poll() {
    SDL_Event e;
    while(SDL_PollEvent(&e)) {
        if(e.type == SDL_QUIT) EngineCore::Get().Quit();
    }
}

// The Loop Macro
#define APP_LOOP(cond) \
    while (cond && EngineCore::Get().isRunning && \
          (SDL_RenderPresent(EngineCore::Get().renderer), \
           SDL_RenderClear(EngineCore::Get().renderer), \
           internal_poll(), true))

Step 3: Asset Wrapper

To complete the engine, we need a way to draw images without exposing `SDL_Texture` pointers to the user.

class Sprite {
    SDL_Texture* tex;
    SDL_Rect rect;
public:
    Sprite(const char* path, int x, int y) {
        SDL_Surface* surf = SDL_LoadBMP(path);
        tex = SDL_CreateTextureFromSurface(EngineCore::Get().renderer, surf);
        rect = {x, y, surf->w, surf->h};
        SDL_FreeSurface(surf);
    }

    void Draw() {
        SDL_RenderCopy(EngineCore::Get().renderer, tex, NULL, &rect);
    }
};

Final Result

By assembling these components, we have created a development environment that abstracts away the C++ complexity. Here is what the end-user experience looks like:

#include "engine.hpp"

// Setup window
APP_MAIN("My Retro Game", 800, 600) 
{
    // Load assets
    Sprite hero("assets/hero.bmp", 100, 100);

    // Start Loop
    APP_LOOP(true) 
    {
        // One line rendering
        hero.Draw();
    }
    
    return 0;
}

Want to see this in production?

The Wizard Engine repository includes this architecture plus advanced features like Audio, Physics, and ECS.

Check out Wizard Engine