A tiny 2D game engine written in C++, compiled to WebAssembly via Emscripten, and usable directly from JavaScript in the browser.
Built as a portfolio/learning project with GitHub Copilot assistance. The goal is clean, readable C++ — not overly complex, but not sloppy either.
The engine runs all game logic in C++ (compiled to WASM) and lets JavaScript handle rendering via the HTML5 Canvas API. This mirrors how real-world engines like Figma's core work.
C++ / WASM side:
Color— RGBA color valueVector2— 2D vector math (add, subtract, scale, length)Renderer— Draws shapes, text, and paths to the HTML5 CanvasKeyboardInput— Tracks held/pressed/released keys each frameMouseInput— Canvas-relative cursor position and mouse button stateEngine— Owns all subsystems, runs the game loop, accepts JS update and draw callbacks
JavaScript side:
- Loads the compiled
.wasmmodule viaModule.onRuntimeInitialized - Provides update and draw callbacks to the engine
- Reads input through
KeyboardInputandMouseInput
| Tool | Purpose |
|---|---|
| C++17 | Core engine logic |
| Emscripten | Compiles C++ → WebAssembly |
| make | Build system |
| HTML5 Canvas | Rendering |
tiny-tiger/
├── src/
│ ├── engine.h — class declarations (Color, Vector2, Renderer, KeyboardInput, MouseInput, Engine)
│ ├── engine.cpp — implementation (EM_JS canvas calls, Emscripten HTML5 input callbacks)
│ └── bindings.cpp — EMSCRIPTEN_BINDINGS block that exposes classes to JavaScript
├── index.html — browser demo
└── Makefile — build rules (requires emcc)
Install Emscripten first: https://emscripten.org/docs/getting_started/downloads.html
Emscripten is installed via emsdk but not added to system/user environment variables permanently, to avoid conflicts with NVM and system Python. Instead, activate it on demand per terminal session.
What is Emscripten? Normally, C++ code is compiled into machine code. That works great for native apps, but browsers can't run machine code; they have their own sandboxed runtime. WebAssembly (WASM) is a binary format that browsers can run, it's fast, low-level, and language-agnostic. It's just machine code for the browser. Emscripten is the compiler toolchain that bridges the gap. It takes our C++ source code and, instead of compiling it to native machine code, compiles it to WebAssembly. It also handles the glue work, generating the JavaScript needed to load, instantiate, and communicate with the WASM module in the browser.
git clone https://github.com/emscripten-core/emsdk.git
# Enter that directory
cd emsdk
Note
You can also get the emsdk without git, by selecting “Clone or download => Download ZIP” on the emsdk GitHub page.
Run the following emsdk commands to get the latest tools from GitHub and set them as active:
# Fetch the latest version of the emsdk (not needed the first time you clone)
git pull
# Download and install the latest SDK tools.
./emsdk install latest
Add the following to your PowerShell profile (notepad $PROFILE):
powershellfunction Invoke-BatchFile {
param([string]$Path)
$tempFile = [IO.Path]::GetTempFileName()
cmd /c "`"$Path`" && set > `"$tempFile`""
Get-Content $tempFile | ForEach-Object {
if ($_ -match "^([^=]+)=(.*)$") {
[System.Environment]::SetEnvironmentVariable($matches[1], $matches[2])
}
}
Remove-Item $tempFile
}
function Enable-Emscripten {
Invoke-BatchFile "E:\Code\emsdk\emsdk_env.bat OR WHEREVER YOU INSTALLED EMSDK"
Write-Host "Emscripten activated." -ForegroundColor Green
}
Then run this in any terminal session where you need Emscripten:
powershellEnable-Emscripten
The environment (emcc, em++, etc.) is active for that session only and resets when the terminal is closed.
After that...
# Build the WASM module
make
# Serve the project (required — browsers block WASM from file:// URLs)
python3 -m http.server 8080Open http://localhost:8080 in your browser.
The included index.html is an interactive demo:
- Arrow Keys or WASD — move the red player square
- Left Click on the canvas — move the green target circle
- Left Mouse Button (hold) — turns the player orange
- Space (hold) — makes the player pulse
Represents an RGBA color. alpha defaults to 255 (fully opaque).
const red = new Module.Color(255, 0, 0);
const glass = new Module.Color(0, 128, 255, 128);A 2D vector with arithmetic helpers.
const position = new Module.Vector2(100, 200);
const velocity = new Module.Vector2(50, 0);
const nextPosition = position.add(velocity.scale(deltaTimeInSeconds));
console.log(nextPosition.xPosition, nextPosition.yPosition);
console.log(nextPosition.getLength());The main engine object. Creates and sizes a <canvas id="gameCanvas"> element.
Module.onRuntimeInitialized = function () {
const engine = new Module.Engine(800, 600);
engine.setUpdateCallback(function (deltaTimeInSeconds) {
// update game logic here
});
engine.setDrawCallback(function () {
// draw the frame here
});
engine.run();
};engine.deltaTimeSinceLastFrame — time in seconds between the last two frames (read inside your callbacks).
engine.stop() — cancels the game loop.
| Method | Description |
|---|---|
clearScreen(color) |
Fill the whole canvas with a color |
drawFilledRectangle(x, y, width, height, color) |
Filled rectangle |
drawOutlinedRectangle(x, y, width, height, color, thickness) |
Rectangle outline |
drawFilledCircle(cx, cy, radius, color) |
Filled circle |
drawOutlinedCircle(cx, cy, radius, color, thickness) |
Circle outline |
drawLine(x1, y1, x2, y2, color, thickness) |
Line segment |
drawTextString(text, x, y, fontSize, color) |
Text using sans-serif |
beginPathDrawing() |
Start a custom path |
movePathTo(x, y) |
Move path cursor without drawing |
drawLineTo(x, y) |
Extend path with a line |
strokeCurrentPath(color, thickness) |
Stroke the current path |
fillCurrentPath(color) |
Fill the current path |
saveDrawingState() |
Push canvas state |
restoreDrawingState() |
Pop canvas state |
applyTranslation(x, y) |
Translate the canvas transform |
applyRotation(angleInRadians) |
Rotate the canvas transform |
applyScaling(xScale, yScale) |
Scale the canvas transform |
setGlobalAlphaValue(alpha) |
Set global opacity (0.0 – 1.0) |
const keyboard = engine.getKeyboardInput();
keyboard.isKeyCurrentlyHeld('ArrowLeft') // true while key is down
keyboard.wasKeyPressedThisFrame('Space') // true on the first frame the key goes down
keyboard.wasKeyReleasedThisFrame('Enter') // true on the frame the key is releasedKey names match the standard KeyboardEvent.key values
('ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', ' ' for Space, 'a'–'z', etc.).
const mouse = engine.getMouseInput();
mouse.getCursorXPosition() // cursor X relative to canvas
mouse.getCursorYPosition() // cursor Y relative to canvas
mouse.isMouseButtonHeld(0) // 0 = left, 1 = middle, 2 = right
mouse.wasMouseButtonPressedThisFrame(0)
mouse.wasMouseButtonReleasedThisFrame(0)- Learn C++ fundamentals through a real, usable project
- Understand how WebAssembly bridges C++ and JavaScript
MIT