Shadow Engine — Documentation¶
Shadow is a C++17 2D game engine built on SDL2. It provides a grid-based game framework with physics (Box2D), audio (SDL_mixer), UI, and a first-class interface for LLM-driven gameplay.
Table of Contents¶
- Architecture Overview
- Quick Start
- Game Base Class
- Game2D — Grid-Based Games
- Grid System
- Input System
- Scene & Sprites
- Physics
- Audio
- UI Components
- Geometry Primitives
- LLM Interface
- Build & Platform Support
Architecture Overview¶
Game (SDL window, main loop, subsystems)
└─ Game2D (grid-based games + LLM interface)
├─ Grid (tile grid, coordinate mapping)
│ └─ GridEntity (entities that live on the grid)
├─ InputManager2D (bindable key/mouse input)
├─ Scene (sprite collection)
│ └─ SimpleSprite (renderable object with parts & scripts)
├─ PhysicsManager (Box2D world wrapper)
├─ AudioEngine (SDL_mixer groups & players)
└─ UI (TextDisplay, Button, GameStats)
Subsystem ownership lives in Game. Game2D extends Game and adds the grid layer, entity management, input binding shortcuts, and the LLMPlayable interface.
Quick Start¶
The minimal pattern for a grid-based game:
#include "Engine/Core/Game2D.h"
class MyGame : public Game2D {
public:
MyGame() : Game2D("My Game", 800, 600, /*tileSize=*/20) {}
void initGame() override {
createGrid(40, 30, 20); // 40×30 tiles, 20px each
player = createEntity<GridEntity>(getGrid(), 5, 5);
player->setColor({0, 200, 255, 255});
bindKey(KEY_UP).onPress([this]{ player->tryMove(0, -1); });
bindKey(KEY_DOWN).onPress([this]{ player->tryMove(0, 1); });
bindKey(KEY_LEFT).onPress([this]{ player->tryMove(-1, 0); });
bindKey(KEY_RIGHT).onPress([this]{ player->tryMove( 1, 0); });
}
private:
std::shared_ptr<GridEntity> player;
};
int main() {
MyGame game;
game.run();
}
Game Base Class¶
Header: SDLApp.h
Game owns the SDL window, renderer, physics manager, audio engine, asset manager, and scene list. It runs the main loop and calls your overrides.
Constructor¶
Overridable hooks¶
| Method | When called |
|---|---|
onStart() |
Once, before initializeComponents() — good for audio group setup |
initializeComponents() |
Once, after SDL is ready — build scenes and load assets here |
update() |
Every frame |
renderPostFX() |
After update(), for full-screen post-processing |
Subsystem accessors¶
Renderer* getRenderer();
AssetManager* getAssetManager();
AudioEngine* getAudioEngine();
PhysicsManager* getPhysicsManager();
Scene management¶
FPS¶
void limitFPS(Uint32 fpsLimit); // No-op under Emscripten (browser controls timing)
void drawFPS(); // Render FPS counter to screen
Game2D — Grid-Based Games¶
Header: Game2D.h
Game2D is the recommended base class for 2D tile-based games. It wraps Game and adds a grid, entity list, input shortcuts, UI helpers, and the LLMPlayable interface.
Constructor¶
Required override¶
Optional overrides¶
virtual void updateGame(float dt) {} // Called every frame with delta time in seconds
virtual void renderGame() {} // Called after the grid renders each frame
virtual void onGameOver() {} // Called once when endGame() is triggered
Grid¶
void createGrid(int width, int height, int tileSize = 20);
Grid* getGrid() const;
void renderGrid();
Create the grid once in initGame(). renderGrid() is called automatically; call it manually only for custom render ordering.
Entity management¶
template<typename T, typename... Args>
std::shared_ptr<T> createEntity(Args&&... args);
void removeInactiveEntities(); // Prune entities where isActive() == false
T must derive from GridEntity. Entities are stored and updated automatically.
Game state¶
void startGame();
void endGame(); // Sets gameOver = true, calls onGameOver()
bool isGameRunning() const;
float getDeltaTime() const; // Frame delta in seconds
float getGameTime() const; // Total elapsed game time in seconds
Input shortcuts¶
Shorthand for input.bind(...) — see Input System.
UI helpers¶
std::shared_ptr<TextDisplay> createText(int x, int y, const std::string& text = "");
std::shared_ptr<Button> createButton(int x, int y, int w, int h, const std::string& label = "");
std::shared_ptr<GameStats> createStats(int x, int y);
Grid coloring helper¶
Assigns color0 to all cells with value == value0, and color1 to all cells with value == value1.
Grid System¶
Header: Grid.h
GridCell¶
Each cell in the grid is a GridCell:
struct GridCell {
int value = 0;
bool isSolid = false;
bool isVisible = true;
SDL_Color color = {255, 255, 255, 255};
SDL_Color borderColor = {0, 0, 0, 255};
};
Grid¶
Cell access¶
GridCell& cell(int x, int y);
const GridCell& cell(int x, int y) const;
int getValue(int x, int y) const;
void setValue(int x, int y, int value);
Dimensions¶
Coordinate conversion¶
Rect<float> getCellRect(int x, int y) const;
void screenToGrid(int screenX, int screenY, int& gridX, int& gridY) const;
void gridToScreen(int gridX, int gridY, int& screenX, int& screenY) const;
Bounds¶
Rendering¶
void render(Renderer* renderer);
void setCellColor(int x, int y, SDL_Color color);
void setBorderColor(SDL_Color color); // Set uniform border color for all cells
void fill(SDL_Color color); // Paint every cell the same color
void clear(); // Reset all cells to defaults
TileMap export¶
GridEntity¶
Entities that move on the grid. Extend this to add per-entity logic.
Position¶
int getX() const;
int getY() const;
void setPosition(int x, int y);
void move(int dx, int dy); // Unconditional move (no collision check)
bool tryMove(int dx, int dy); // Returns false if destination is out of bounds or solid
bool canMoveTo(int x, int y) const;
Visual¶
SDL_Color getColor() const;
void setColor(SDL_Color c);
char getSymbol() const; // ASCII char for text/LLM rendering
void setSymbol(char s);
Lifecycle¶
virtual void update(float deltaTime) {}
virtual void render(Renderer* renderer);
bool isActive() const;
void setActive(bool a); // Set false to queue for removal
Input System¶
Header: InputManager.h
Key codes¶
KEY_UP, KEY_DOWN, KEY_LEFT, KEY_RIGHT
KEY_W, KEY_S, KEY_A, KEY_D
KEY_SPACE, KEY_ENTER, KEY_ESCAPE
KEY_R, KEY_P, KEY_TAB
KEY_0 … KEY_9
Mouse buttons¶
InputBinding — fluent API¶
bindKey and bindMouse return an InputBinding& that supports method chaining:
bindKey(KEY_SPACE)
.onPress( [this]{ jump(); } )
.onHold( [this]{ charge(); })
.onRelease([this]{ release(); });
| Method | Trigger |
|---|---|
onPress(cb) |
First frame the key goes down |
onHold(cb) |
Every frame the key is held |
onRelease(cb) |
First frame the key comes up |
Direct state queries (InputManager2D)¶
bool isKeyPressed(KeyCode key) const; // Currently held
bool isKeyHeld(KeyCode key) const;
bool wasKeyJustPressed(KeyCode key) const; // Pressed this frame only
bool isMousePressed(MouseButton button) const;
void getMousePosition(int& x, int& y) const;
int mouseX, mouseY; // Public, updated every frame
Scene & Sprites¶
Header: Scene.h, Sprite.h
Scene¶
A scene is an ordered collection of SimpleSprite objects. Game renders all active scenes each frame.
void addItem(std::shared_ptr<SimpleSprite> sprite);
std::shared_ptr<SimpleSprite> getSpriteById(const std::string& id);
void render(Renderer* renderer);
void setAssetManager(AssetManager* am);
SimpleSprite¶
A renderable object with an optional background texture, child Parts, and attached Scripts.
// Constructors
SimpleSprite(Renderer* renderer, const std::string& imagePath, const std::string& id);
SimpleSprite(Renderer* renderer, const std::string& id);
// Image
void setImage(const std::string& path);
// Scripts
void attachScript(std::shared_ptr<Script> script);
std::vector<std::shared_ptr<Script>>& getScripts();
// Hierarchical parts
Part* addPart(const std::string& imagePath, const std::string& id);
Part* getPart(const std::string& id);
// Physics component
template<typename T>
void addComponent(PhysicsManager* physicsManager); // T = PhysicsComponent
// Lifecycle
void setActive(bool state);
void destroy();
virtual void update(float deltaTime) {}
Scene* getScene() const;
Sprite utility functions¶
Vector2D calculateDirection(const Point2D& origin, const Point2D& target);
float calculateAngle(const Point2D& delta); // Degrees
Point2D getMousePosition();
Point2D getSpriteCenter(SimpleSprite* sprite);
Physics¶
Header: Physics.h
Dependency: Box2D v3
PhysicsManager¶
Top-level owner of the physics world. Retrieved via Game::getPhysicsManager().
World¶
Wraps a b2World and manages simulation stepping and collision events.
b2WorldId getId();
void simulateStep(); // Advances simulation by 1/60 s, fires onCollision callbacks
void addBody(Body* body);
void deleteBodyWithId(const std::string& uid);
Default gravity is (0, 10) (downward).
Body¶
Base class for physics bodies.
b2BodyId bodyId;
b2BodyDef bodyDef;
std::function<void()> onCollision; // Fired on contact begin
void setUid(const std::string& uid);
std::string getUid();
b2BodyId getId();
RigidBody¶
A dynamic box body, 1×1 units, density 1.0, friction 0.3. Created and added to the world automatically.
PhysicsObject¶
Convenience class for placing static or dynamic boxes at an arbitrary position/size.
Attaching physics to a sprite¶
sprite->addComponent<PhysicsComponent>(getPhysicsManager());
Body* body = static_cast<PhysicsComponent*>(sprite->components.back())->getBody();
Audio¶
Header: AudioEngine.h
Dependency: SDL_mixer
Audio is organized in groups containing named players. Initialize in onStart(), load files in initializeComponents().
AudioEngine¶
Retrieved via Game::getAudioEngine().
bool initialize(); // Opens SDL_mixer at 44100 Hz, stereo, 2048-byte buffer
void addAudioMediaGroup(const std::string& id);
AudioMediaGroup& getAudioMediaGroupById(const std::string& id);
AudioMediaGroup& getAudioMediaGroupByIndex(int index);
void playAudioInGroup(const std::string& groupId, const std::string& audioId, bool loop = false);
void stopAudioInGroup(const std::string& groupId, const std::string& audioId);
void stopAudioInAllGroups();
AudioMediaGroup¶
void addAudioPlayer(const std::string& filePath, const std::string& id);
void playAudio(const std::string& id, bool loop = false);
void stopAudio(const std::string& id);
AudioPlayer¶
Supports WAV (chunk) and MP3/OGG (streamed music). Detected automatically by extension.
AudioPlayer(const std::string& filePath, const std::string& id);
void play(bool loop = false);
void stop();
Example¶
// In onStart():
getAudioEngine()->initialize();
getAudioEngine()->addAudioMediaGroup("sfx");
// In initializeComponents():
getAudioEngine()->getAudioMediaGroupById("sfx")
.addAudioPlayer("assets/jump.wav", "jump");
// In-game:
getAudioEngine()->playAudioInGroup("sfx", "jump");
UI Components¶
Header: UI.h
All UI components render using SDL's built-in drawing — no font file required for TextDisplay and GameStats (they use drawSimpleText). Button requires mouse state to be passed each frame.
TextDisplay¶
TextDisplay(int x, int y, const std::string& text = "");
void setText(const std::string& text);
const std::string& getText() const;
void setPosition(int x, int y);
void setColor(SDL_Color color);
void render(Renderer* renderer);
// Stream-style append
display << "Score: " << 100;
Button¶
Button(int x, int y, int w, int h, const std::string& text);
void update(int mouseX, int mouseY, bool pressed); // Call every frame
void render(Renderer* renderer);
bool isClicked() const;
void setLabel(const std::string& label);
void setPosition(int x, int y);
void setSize(int w, int h);
GameStats¶
Key/value integer stats display.
GameStats(int x, int y);
void setStat(const std::string& key, int value);
int getStat(const std::string& key) const;
void addStat(const std::string& key, int delta); // Increment/decrement
void render(Renderer* renderer);
ExplanationOverlay¶
Multi-line text box, useful for debug overlays.
ExplanationOverlay(int x, int y, int width);
void addLine(const std::string& line);
void clear();
void render(Renderer* renderer);
Geometry Primitives¶
Header: Geometry.h
Point2D¶
Point2D(float x = 0, float y = 0);
Point2D operator+(const Point2D& other) const;
Point2D operator-(const Point2D& other) const;
Point2D operator-() const; // Negate
bool operator==(const Point2D& other) const;
void translate(const Point2D& delta);
Vector2D¶
A direction/magnitude represented as a Point2D origin.
Vector2D(float x = 0, float y = 0);
Vector2D normalized() const;
Vector2D operator*(float scalar) const;
Vector2D operator+(const Vector2D&) const;
Vector2D operator-(const Vector2D&) const;
Vector2D& operator+=(const Vector2D&);
Vector2D& operator-=(const Vector2D&);
bool operator==(const Vector2D&) const;
bool operator!=(const Vector2D&) const;
Vector3¶
Vector3(float x = 0, float y = 0, float z = 0);
Vector3 operator+(const Vector3&) const;
Vector3 operator-(const Vector3&) const;
Vector3 operator*(float scalar) const;
Matrix4x4¶
Column-major 4×4 matrix with static factory methods.
static Matrix4x4 identity();
static Matrix4x4 rotationX(float angle); // Radians
static Matrix4x4 rotationY(float angle);
static Matrix4x4 rotationZ(float angle);
static Matrix4x4 translation(float x, float y, float z);
static Matrix4x4 projection(float fov, float aspect, float near, float far);
Matrix4x4 operator*(const Matrix4x4& other) const;
Vector3 multiplyVector(const Vector3& v) const;
Rect\<T>¶
Rect<T>(T x, T y, T width, T height);
T x, y, width, height;
Point2D center, topLeft, topRight, bottomLeft, bottomRight;
void updatePoints(); // Recompute corners after mutating x/y/width/height
LLM Interface¶
Header: Engine/Core/GameState.h, Game2D.h
Any Game2D subclass automatically implements LLMPlayable. Use this to drive your game programmatically — from an LLM, an AI agent, or automated tests.
Registering actions¶
Call registerAction inside initGame():
registerAction("move_up", [this]() -> ActionResult {
if (player->tryMove(0, -1))
return {true, "Moved up"};
return {false, "Blocked"};
});
ActionResult has two fields: bool success and std::string message.
LLMPlayable interface¶
GameState getState() const override;
ActionResult executeAction(const std::string& action) override;
std::vector<std::string> getAvailableActions() const override;
void reset() override;
bool isGameOver() const override;
bool isGameWon() const override;
GameState¶
struct GameState {
bool gameRunning;
bool gameOver;
int score;
int level;
int gridWidth, gridHeight;
std::vector<std::vector<int>> grid; // 2D cell values
std::unordered_map<std::string, Point2D> entities; // name → position
std::vector<std::string> availableActions;
std::string toString() const; // ASCII art representation
std::string toJSON() const; // JSON representation
};
Text output format¶
=== GAME STATE ===
Status: PLAYING
Score: 100
Level: 2
=== GRID ===
....................
..#.................
..$.................
=== ENTITIES ===
player: (5, 10)
food: (3, 6)
=== AVAILABLE ACTIONS ===
- up
- down
- left
- right
==================
JSON output format¶
{
"gameRunning": true,
"gameOver": false,
"score": 100,
"level": 2,
"gridWidth": 20,
"gridHeight": 20,
"availableActions": ["up", "down", "left", "right"]
}
Driving the game from C++¶
MyGame game;
game.run(); // Not needed for headless — call onStart() + initializeComponents() instead
while (!game.isGameOver()) {
GameState state = game.getState();
std::string action = myAI.chooseAction(state.toString(),
game.getAvailableActions());
ActionResult result = game.executeAction(action);
}
Games with built-in LLM support¶
| Game | Executable | Actions |
|---|---|---|
| Snake | snake_example |
up, down, left, right, restart |
| Minesweeper | minesweeper_example |
reveal, flag, restart |
| Tic Tac Toe | tictactoe_example |
place_X, place_O, restart |
| Roguelike | roguelike_example |
up, down, left, right, attack, restart |
Build & Platform Support¶
Prerequisites¶
macOS / Linux
brew install sdl2 sdl2_image sdl2_ttf sdl2_mixer # macOS
# or: apt install libsdl2-dev libsdl2-image-dev libsdl2-ttf-dev libsdl2-mixer-dev
Windows — use the vendored SDL2 binaries in dependencies/ (set -DMYGAME_VENDORED=ON).
Build¶
cd Shadow
mkdir -p build && cd build
cmake .. -DCMAKE_BUILD_TYPE=Release -DMYGAME_VENDORED=OFF
cmake --build . -j$(nproc)
Outputs:
- sdl_app — default entry point (runs main.cpp)
- sdl_app_tests — unit test suite
- snake_example, minesweeper_example, tictactoe_example, roguelike_example
Run¶
WebAssembly (Emscripten)¶
Shadow supports Emscripten. The main loop uses emscripten_set_main_loop automatically when __EMSCRIPTEN__ is defined. limitFPS is a no-op in WASM — the browser controls frame timing.
Platforms¶
| Platform | Status |
|---|---|
| macOS | Supported |
| Linux | Supported |
| Windows | Supported (vendored deps) |
| WebAssembly | Supported (Emscripten) |
CMake options¶
| Option | Default | Description |
|---|---|---|
MYGAME_VENDORED |
OFF |
Use vendored SDL2 from dependencies/ |
CMAKE_BUILD_TYPE |
Debug |
Debug or Release |
Common Patterns¶
Fixed-rate game logic (e.g. snake tick)¶
float tickTimer = 0;
const float TICK_RATE = 0.15f; // seconds per move
void updateGame(float dt) override {
tickTimer += dt;
if (tickTimer >= TICK_RATE) {
tickTimer = 0;
snake->advanceOneStep();
}
}
Marking a cell solid¶
Updating a score display¶
auto scoreText = createText(10, 10, "Score: 0");
// In updateGame():
scoreText->setText("Score: " + std::to_string(score));