Custom C++ Game Engine and Tools
A custom cross-platform 2D game engine supporting the following platforms and graphics APIs:
Windows (D3D11, OpenGL)
Linux (OpenGL)
MacOS (Metal, OpenGL)
iOS (Metal)

Design goals / philosophy
- Simplicity, game developer experience, joy of programming
- Inspiration from sokol and stb (header-only approach, minimal/no dependencies, avoid higher complexity graphics APIs like Vulkan and D3D12)
- Prefer C99 style; use STD library and modern C++ language features where sensible
- Minimize 3rd party dependencies (currently only dependencies are for Linux windowing and multi-platform audio). Using dependencies is a time saver, but making a custom engine isn't for people on a time crunch :) There are some use cases I will consider using 3rd party code for long term, like supporting the myriad of different gamepad vendors, but for the most part I prefer using code that I fully own.
- Prioritize good mobile experience (sensible default touch behaviour with option to go lower level, sensible fallbacks ex. on-screen controller) Provide good high level APIs while allowing for lower level access. Information hiding is not a particularly high concern unless needed for safety.
- Performance is a secondary concern, usability and feature completion are higher priority during earlier stages of development. Avoid decisions that will reduce flexibility for optimizing in the future (ex. heavy library usage, too much abstraction, overly-opinionated workflow decisions).
Project structure and architecture
The project uses a very simple CMake hierarchy to build the project with the game being the executable and the engine a linked static library (I may consider dynamic linking approach in the future for hot loading support). The project is structured as a GitHub template so you can simply make a repo from the template to create a new game project. Updating the engine for a particular game project is as simple as replacing the engine directly.
engine/ // Engine project, builds as a static lib
- dependencies/ // 3rd party dependencies, kept to a minimum
- engine/ // Core engine (Vector math, game objects, etc.)
- platform/ // Platform/device layer (GFX API and OS code)
- samples/ // Sample games
- CMakeLists.txt
- entry.cpp
game/ // Game project, links to the engine lib
- assets/
- CMakeLists.txt
- main.cpp // `RealMain` definition here, which gets called
// from the OS-dependent entrypoint from engine lib.
CMakeLists.txt // Top-level CMake spec
entry.cpp - Entrypoint
// Just the declaration; defined within the game code
int RealMain(Platform* platform);
// Run loop boilerplate, called by game
void GameUpdateFn(void* context) {
((Game*)(context))->Update();
}
void RunLoop(Platform* platform, Game* game) {
platform->Run(GameUpdateFn, game);
}
/*
* Platform-specific entry-points using preprocessor macro
*/
#if defined(PLATFORM_WINDOWS) && defined(BACKEND_DIRECTX)
#include "platform/directx/directx.cpp"
...
directx.cpp
int WINAPI WinMain(
HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow
) {
auto platform = new PlatformDirectX(
hInstance, hPrevInstance, lpCmdLine, nCmdShow
);
return RealMain(platform);
}
game boilerplate - main.cpp
class SampleGame : public Game {
public:
using Game::Game;
void Start() override {}
void Update() override {}
};
// Game's `RealMain`; minimal boilerplate to initialize engine + game.
int RealMain(Platform* platform) {
platform->Init();
Game* game = new SampleGame(platform);
game->Start();
RunLoop(platform, game);
platform->Shutdown();
delete game;
delete platform;
return 0;
}
platform.h - platform interface
enum class KeyCode { ... };
enum class MouseButton { ... };
enum class GamepadButton { ... };
...
class Platform {
public:
Platform() = default;
virtual ~Platform() = default;
virtual void Init() = 0;
virtual void Run(void (*func)(void*), void* context) = 0;
virtual Shader* LoadShader(ShaderDef shaderDef) = 0;
virtual GameObject* CreateGameObject(){ return new GameObject(); };
virtual Texture* CreateTexture(const char* path, TextureSettings settings)
virtual Sprite* CreateSprite(Texture* texture) = 0;
virtual Sound* CreateSound(const char* path) = 0;
virtual bool IsKeyPressed(KeyCode key) = 0;
virtual bool IsMousePressed(MouseButton button) = 0;
virtual bool IsGamepadButtonPressed(GamepadButton button) = 0;
virtual void SetGamepadVibration(int amountLeft, int amountRight){};
virtual void SetKeyReleasedCallback(void (*func)(KeyCode, void*), void* context) = 0;
virtual void SetMouseReleasedCallback(void (*func)(MouseButton, void*, vec3), void* context) = 0;
virtual void SetGamepadReleasedCallback(void (*func)(GamepadButton, void*), void* context) = 0;
virtual vec3 GetMousePos() = 0;
...
virtual void Shutdown() = 0;
}
directx platform
class PlatformDirectX : public Platform {
public:
PlatformDirectX(HINSTANCE hInstance, ...
void Init() override {
...
hwnd = CreateWindow(...)
}
void Run(void (*func)(void*), void* ctx) override {
MSG msg = { nullptr };
while (msg.message != WM_QUIT)
{
if (PeekMessage(&msg, nullptr, 0U, 0U, PM_REMOVE))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
else
{
...
func(ctx); // Game loop function
swapChain->Present(1, 0);
}
}
}
bool IsKeyPressed(KeyCode key) override {
auto keyCode = GetWindowsKey(key);
return (GetAsyncKeyState(keyCode) & 0x8000) != 0;
}
...
}
Text rendering
Given that 2D and mobile games are a fairly important use case for this engine, good text rendering is a paramount initial feature. To this end, I went with a fairly standard approach of using multi-channel signed distance fields, with an optional bitmap fallback. This supports very clean text at large sizes without losing edge fidelity like standard SDFs. For smaller text in particular, bitmaps can be more effective for clarity, particularly if text will not be changing.

Shaders / Materials
It was an important early feature for me to support custom shaders without changes to the engine. While the support is limited currently to a single uniform buffer block, it can dynamically support any number of uniform fields within that block, using shader reflection at runtime to resolve bind offsets.
Currently, shader code must be provided for each platform the game wants to support (ex. HLSL for DirectX, GLSL for OpenGL, and MSL for Apple platforms). I have not yet made a decision around the long-term solution for shader compilation, and whether a IR cross compilation solution (via tooling like slang) will be used with a single authoring language (similar to Unity with GLSL), or whether a custom shader language will be used that compiles to each platform (similar to Godot's shader language approach).
enum UniformFieldType { FLOAT, FLOAT4, BOOL };
struct UniformField {
UniformFieldType type;
const char* name;
union {
float f;
float f4[4]{0,0,0,0};
bool b;
};
};
struct UniformFieldOffset {
const char* name;
uint32_t offset;
};
class Shader {
...
std::vector<UniformFieldOffset> uniformFieldOffsets;
};
// DirectX shader impl
class ShaderD3D : public Shader { ... };
Shader* LoadShader(const char* path, InputLayoutType inputLayoutType) {
auto* shader = new ShaderD3D();
d3dDevice->CreateVertexShader(..)
d3dDevice->CreatePixelShader(..)
...
// Use shader reflection to populate shader->uniformFieldOffsets
D3DReflect(...);
...
}
class Material {
public:
explicit Material(Shader* shader): shader(shader){}
virtual ~Material() = default;
Shader* shader;
void BindConstantBuffer(uint8_t* dst) {
for (auto f : uniformFields) {
// Get offset from corresponding uniformFieldOffsets by name
uint32_t offset = ...;
// Bind using offset and size based on field type
switch(f.type) {
case Shader::FLOAT:
memcpy(dst + offset, &f.f, sizeof(float));
break;
case Shader::FLOAT4:
memcpy(dst + offset, f.f4, sizeof(float) * 4);
break;
case Shader::BOOL:
uint32_t v = f.b ? 1 : 0;
memcpy(dst + offset, &v, sizeof(uint32_t));
break;
}
}
}
std::vector<Shader::UniformField> uniformFields;
std::vector<TextureBuffer> textures;
};
Coroutines
A basic abstraction around coroutines. As a game developer, I enjoy the ability to easily queue behaviours on a parallel or sequential basis (or comibation of the two).
class Coroutine {
public:
enum class State { Idle, Running, Completed };
virtual ~Coroutine() {}
void Start() {
if (state == State::Idle) {
state = State::Running;
OnStart();
}
}
bool Update(float deltaTime) {
if (state != State::Running) return false;
if (OnUpdate(deltaTime)) state = State::Completed;
return isComplete;
}
virtual void OnStart() {}
virtual bool OnUpdate(float deltaTime) = 0;
private:
State state = State::Idle;
};
class WaitXSeconds : public Coroutine {
private:
float duration;
float elapsed = 0;
public:
WaitXSeconds(float seconds) : duration(seconds) {}
bool OnUpdate(float deltaTime) override {
elapsed += deltaTime;
return elapsed >= duration;
}
};
class ParallelCoroutines : public Coroutine {
private:
std::vector<Coroutine*> routines;
bool ownRoutines;
public:
ParallelCoroutines(bool ownsRoutines = true) : ownRoutines(ownsRoutines) {}
~ParallelCoroutines() {
if (ownRoutines) {
for (auto r : routines) delete r;
}
}
void Add(Coroutine* routine) { ... }
void OnStart() override {
for (auto r : routines) { r->Start(); }
}
bool OnUpdate(float deltaTime) override {
bool allComplete = true;
for (auto r : routines) {
if (!r->IsCompleted()) {
r->Update(deltaTime);
if (!r->IsCompleted()) {
allComplete = false;
}
}
}
return allComplete;
}
};
class SampleGame : public Game {
public:
using Game::Game;
void Start() override {
parallel = new ParallelCoroutines(true);
parallel->Add(new WaitXSeconds(2.0f));
parallel->Add(new WaitXSeconds(3.0f));
parallel->Start();
}
void Update() override {
float deltaTime = platform->GetDeltaTime();
bool complete = parallel->Update(deltaTime);
}
private:
ParallelCoroutines* parallel;
};
IDE plugin tooling
I wanted to experiment with CLion plugins similar to Unity and Unreal's Rider plugins. This very simple plugin injects placeholder methods in a Game subclass.

public class EditorActions extends AnAction {
@Override
public void actionPerformed(@NotNull AnActionEvent event) {
OCStructImpl struct = getSubClassStruct(event, "Game");
if (struct == null) return;
String methodText = getMethodText(event.getPresentation().getText());
WriteCommandAction.runWriteCommandAction(event.getProject(), () -> {
var newMethod = OCElementFactory.expressionCodeFragment(methodText, event.getProject(), struct, false, false);
struct.addBefore(newMethod, struct.getLastChild());
});
}
private String getMethodText(String label) {
if (label.contains("KeyboardReleased")) {
return KEYBOARD_RELEASED;
} else if (label.contains("MouseReleased")) {
return KEYBOARD_RELEASED;
}
...
}
String MOUSE_RELEASED =
"""
void MouseReleasedCallback(MouseButton button) override {
if (button == MouseButton::Left) {
printf("LEFT pressed\\n");
} else if (button == MouseButton::Right) {
printf("RIGHT pressed\\n");
}
}
""";
...
}