Projects

My Game Engine

C++23 engine with SDL3, Vulkan 1.3, and a modular gameplay library.

28 February 2026 · Game Engine, Tools, Runtime

Tech: C++23, SDL3, Vulkan, Meson, Ninja, GLM

Highlights

  • Vulkan 1.3 renderer with SDL3 windowing and input handling
  • Modular gameplay library with hot-reloadable state and scene/controller gameplay
  • Asset packing and cross-platform bundling (Linux/Windows) with Doxygen docs

Overview #

This project is my “engine-first” way of building a top-down action game: a small C++23 runtime that hosts a game-agnostic engine library, plus a hot-reloadable gameplay module. The goal isn’t to compete with existing engines - it’s to have a codebase where I can change gameplay, rendering, UI, and tooling in the same place, and keep iteration tight.

The stack is intentionally boring in the best way:

Architecture: three targets, one loop #

At a high level, everything is organized around a clean boundary between “host”, “engine”, and “game”:

This split is the main reason the project stays pleasant to work on. I can rebuild just the gameplay module, have the runtime detect the updated .so/.dll, and swap it without restarting the whole program.

System architecture diagram
System architecture diagram.

Hot reload that keeps your state #

The hot-reload path is deliberately explicit: the runtime host doesn’t try to be “magical” - it just asks the module to serialize and deserialize its own state.

The loop looks like this:

  1. Load the gameplay module and create a Game instance.
  2. If a previous session left behind a serialized state buffer, attempt to restore it.
  3. Run the game until it exits normally or requests reload.
  4. On reload, ask the module how many bytes it needs to save state, allocate a buffer, then save.
  5. Unload the module, reload the new one from disk, recreate the Game, and restore the saved buffer.

Two details that matter in practice:

That combination gives you the “edit → compile → alt-tab → continue playing” loop, while still being honest about what can and can’t be preserved.

Engine core: services + callbacks #

The engine is intentionally “library-shaped”. Instead of a giant base class you inherit from, you wire it together through a small API:

I like this shape because it forces a contract:

On top of that sits a minimal scene framework (enter/exit/update) that the gameplay module uses for main menu vs gameplay, transitions, and so on.

Vulkan renderer: 2D, but treated like a real renderer #

The renderer is built around a sprite-batching model, but it’s not “just draw sprites”. The batching data includes enough information to support lighting and post-processing without turning everything into bespoke draw calls.

Under the hood, Vulkan setup is kept pragmatic: instance/device/swapchain selection is handled via vk-bootstrap, and GPU memory allocations go through VMA so resource creation stays predictable as the renderer grows.

The key idea is: gameplay pushes a stream of sprite vertices plus a list of draw batches. Each batch describes:

From there, Vulkan becomes a predictable pipeline:

Lighting is represented as a compact GPU-friendly struct (ambient + point lights, capped to a fixed maximum), which keeps “lighting feels good” from exploding into a complex deferred system for a 2D game.

Post-processing is also first-class. The engine keeps a structured “post-process state” (vignette, LUT grading, fog, god rays, CRT, transitions), and the game can treat these like gameplay-controlled knobs rather than hard-coded effects.

Assets: dev-friendly filesystem, release-friendly packs #

One of the easiest ways to kill motivation in a hobby engine is to make assets annoying.

In dev builds, the game reads from the assets/ folder so iteration is instant. In release builds, the pipeline prefers an assets.pak next to the executable so the bundle is relocatable and self-contained.

The runtime host does the boring work:

The AssetManager sits underneath that and exposes a simple “virtual path → bytes” API. It supports disabling filesystem fallbacks entirely in pack mode, which is a small but powerful way to detect missing assets early.

Tooling: builds, bundles, and docs #

The build is Meson/Ninja, and the project is structured so clangd can pick up exact compilation flags from compile_commands.json.

For shipping builds, the tooling does three things:

I also keep a Doxygen config in-repo so the engine/game boundary stays documented as it evolves.

Why this architecture works for me #

The engine is “small” in the sense that it avoids sprawling abstractions, but it’s not a toy. The project has enough structure to support real features (lighting, particles, UI, persistence, packaging) while still being easy to change.

The big wins so far:

If you’re watching the demo videos and wondering “why build all this instead of using X?”, the honest answer is: because the engine is the project. The fun part is making the architecture real.