Examples
Complete, runnable code samples for common game patterns with simple-3d.
Each example is self-contained — copy it into main.cpp and build.
Replace asset paths with your own files.
Hello World — Spinning Cube
The minimal simple-3d program. Creates a box, two lights, a camera, and a HUD label. The box spins 60°/s around Y and 24°/s around X. Press ESC to quit.
#include <Simple3D/Simple3D.h> using namespace Simple3D; class HelloGame : public Game { Entity* box = nullptr; Label* hud = nullptr; float angle = 0.0f; float elapsed = 0.0f; public: void Start() override { SetWindowTitle("Hello simple-3d"); SetClearColor(Color(0.08f, 0.08f, 0.12f)); // Box mesh 5 m in front of the origin box = CreateEntity("Box"); box->AddModel("Models/Box.mdl"); box->SetPosition(0.0f, 0.0f, 5.0f); // Three-point lighting auto key = CreateEntity("KeyLight"); key->AddDirectionalLight(Color(1.0f, 0.95f, 0.85f), 1.0f); key->SetRotation(Quaternion(50, -45, 0)); auto fill = CreateEntity("FillLight"); fill->AddDirectionalLight(Color(0.3f, 0.4f, 0.6f), 0.4f); fill->SetRotation(Quaternion(-20, 160, 0)); auto back = CreateEntity("BackLight"); back->AddDirectionalLight(Color(0.6f, 0.5f, 0.4f), 0.3f); back->SetRotation(Quaternion(30, 120, 0)); CreateCamera(); hud = CreateLabel("Time: 0 s ESC to quit"); hud->SetPosition(20, 20); hud->SetFontSize(15); hud->SetColor(Color(0.75f, 0.75f, 0.75f)); } void Update(float dt) override { angle += dt * 60.0f; elapsed += dt; box->SetRotation(Quaternion(angle, angle * 0.4f, 0)); hud->SetText("Time: " + std::to_string((int)elapsed) + " s ESC to quit"); if (IsKeyDown(Key::Escape)) Quit(); } }; int main() { return CreateGame<HelloGame>()->Run(); }
3D Platformer
Third-person platformer with physics player, follow camera with collision avoidance, animated character, 10 collectible coins, a score HUD, and background music. Player movement is delegated to a Lua script (see Lua API → platformer example).
class PlatformerGame : public Game { Entity* player; Camera* camera; Label* scoreLabel; Label* msgLabel; int score = 0; public: void Start() override { SetWindowTitle("Coin Collector"); SetClearColor(Color(0.4f, 0.6f, 0.9f)); // ── Level geometry ─────────────────────────────────── auto ground = CreateEntity("Ground"); ground->AddModel("Models/Ground.mdl"); ground->AddRigidBody(0.0f); ground->AddBoxCollider(Vector3(100, 1, 100)); // Platforms float platPositions[][3] = {{5,2,0},{-5,4,3},{0,6,-5}}; for (auto& p : platPositions) { auto plat = CreateEntity("Platform"); plat->AddModel("Models/Platform.mdl"); plat->SetPosition(p[0], p[1], p[2]); plat->AddRigidBody(0.0f); plat->AddBoxCollider(Vector3(3, 0.5f, 3)); } // ── Player ─────────────────────────────────────────── player = CreateEntity("Player"); player->AddModel("Models/Character.mdl"); player->AddAnimationController(); player->AddRigidBody(70.0f); player->AddCapsuleCollider(0.4f, 1.75f); player->SetPosition(0, 2, 0); player->SetCollisionLayer(1); player->SetCollisionMask(0xFF); player->AddScript("Scripts/player.lua"); // ── Collectible coins ──────────────────────────────── for (int i = 0; i < 10; ++i) { float x = (float)(rand() % 30 - 15); float z = (float)(rand() % 30 - 15); auto coin = CreateEntity("Coin" + std::to_string(i)); coin->AddModel("Models/Coin.mdl"); coin->SetPosition(x, 1.5f, z); coin->AddScript("Scripts/collectible.lua"); // spins coin->AddTriggerSphere(1.2f); coin->SetCollisionLayer(2); coin->SetCollisionMask(1); // only player coin->SetOnTriggerEnter([this, coin](Entity* other) { if (other != player) return; ++score; scoreLabel->SetText("Coins: " + std::to_string(score) + "/10"); PlaySound("Sounds/coin.wav"); DestroyEntity(coin); if (score == 10) { msgLabel->SetText("You collected all coins!"); msgLabel->SetVisible(true); } }); } // ── Lighting ───────────────────────────────────────── auto sun = CreateEntity("Sun"); sun->AddDirectionalLight(Color(1.0f, 0.95f, 0.8f), 1.0f); sun->SetRotation(Quaternion(50, -30, 0)); // ── Camera ─────────────────────────────────────────── camera = CreateCamera(); camera->Follow(player, 6.0f, 2.5f); camera->SetSmoothFollow(true, 7.0f); camera->SetCollisionEnabled(true); camera->SetFOV(60.0f); // ── HUD ────────────────────────────────────────────── scoreLabel = CreateLabel("Coins: 0/10"); scoreLabel->SetPosition(20, 20); scoreLabel->SetFontSize(22); scoreLabel->SetColor(Color::WHITE); msgLabel = CreateLabel(""); msgLabel->SetPosition(300, 250); msgLabel->SetFontSize(32); msgLabel->SetColor(Color(1, 1, 0)); msgLabel->SetVisible(false); PlayMusic("Music/outdoor.ogg"); } void Update(float dt) override { if (IsKeyDown(Key::Escape)) Quit(); // Respawn if player falls off the map if (player->GetPosition().y < -20) { player->SetPosition(0, 3, 0); player->SetLinearVelocity(Vector3::ZERO); } } };
First-Person Shooter Setup
FPS camera with mouse capture, weapon model as a child entity attached to the camera, and a simple shoot mechanic using a trigger sphere.
class FPSGame : public Game { Entity* player; Entity* weapon; Camera* camera; Label* crosshair; Label* ammoLabel; int ammo = 30; public: void Start() override { SetWindowTitle("FPS Demo"); SetClearColor(Color(0.3f, 0.5f, 0.3f)); // Level auto floor = CreateEntity("Floor"); floor->AddModel("Models/LevelFloor.mdl"); floor->AddRigidBody(0.0f); floor->AddMeshCollider("Models/LevelFloor.mdl"); // Player capsule (invisible) player = CreateEntity("Player"); player->AddRigidBody(80.0f); player->AddCapsuleCollider(0.4f, 1.8f); player->SetPosition(0, 1, 0); player->AddScript("Scripts/fps_player.lua"); // Camera in FPS mode camera = CreateCamera(); camera->SetFPSMode(player); camera->SetFPSSensitivity(0.15f); camera->SetFPSHeadOffset(1.65f); camera->SetFOV(75.0f); // Crosshair label (centered) crosshair = CreateLabel("+"); crosshair->SetPosition(630, 358); // ~center of 1280x720 crosshair->SetFontSize(24); crosshair->SetColor(Color(0, 1, 0)); ammoLabel = CreateLabel("Ammo: 30"); ammoLabel->SetPosition(20, 680); ammoLabel->SetFontSize(20); ammoLabel->SetColor(Color::WHITE); PlayMusic("Music/fps_ambient.ogg"); } void Update(float dt) override { if (IsKeyDown(Key::Escape)) { camera->Detach(); // release mouse cursor Quit(); } // Simple fire on left click if (IsMouseButtonDown(MouseButton::Left) && ammo > 0) { ammo--; ammoLabel->SetText("Ammo: " + std::to_string(ammo)); PlaySound("Sounds/gunshot.wav"); } } };
2D Sidescroller
Classic 2D sidescroller with a Tiled tile map, animated player sprite, Box2D physics, and an orthographic follow camera.
class SideScrollerGame : public Game { Entity* player; public: void Start() override { SetClearColor(Color(0.6f, 0.8f, 1.0f)); // Tile map — also creates Box2D collision from Tiled collision layers LoadTileMap("Maps/level1.tmx"); // Player sprite (Spriter animated) player = CreateEntity("Player"); player->AddAnimatedSprite2D("Sprites/hero.scml"); player->PlayAnimation2D("idle"); player->AddRigidBody2D(60.0f); player->AddBoxCollider2D(0.7f, 1.6f); // world units player->SetPosition(2.0f, 4.0f, 0.0f); player->AddScript("Scripts/sidescroller_player.lua"); // Coin collectibles for (int i = 0; i < 8; ++i) { auto coin = CreateEntity("Coin" + std::to_string(i)); coin->AddSprite2D("Sprites/coin.png"); coin->SetPosition(3.0f + i * 2.0f, 3.0f, 0.0f); coin->AddRigidBody2D(0.0f); coin->AddCircleCollider2D(0.3f); coin->AddTriggerSphere(0.6f); coin->SetOnTriggerEnter([this, coin](Entity* o) { if (o == player) { PlaySound("Sounds/coin.wav"); DestroyEntity(coin); } }); } // Orthographic camera following player auto cam = CreateCamera(); cam->Setup2D(100.0f); // 100 pixels per world unit cam->Follow(player, 0, 0); cam->SetSmoothFollow(true, 8.0f); PlayMusic("Music/adventure.ogg"); } void Update(float dt) override { if (IsKeyDown(Key::Escape)) Quit(); } };
AI Navigation with NavMesh
Level with a nav-mesh baked from the dungeon floor, a player character, and an enemy
that pathfinds to the player using MoveToward(). The enemy stops attacking
when the player moves away.
class AIDemo : public Game { Entity* player; Entity* enemy; Label* statusLabel; public: void Start() override { // ── Level (static — must be placed before BuildNavMesh) ── auto floor = CreateEntity("Floor"); floor->AddModel("Models/Dungeon.mdl"); floor->AddRigidBody(0.0f); floor->AddMeshCollider("Models/Dungeon.mdl"); floor->SetCollisionLayer(2); // camera avoidance uses layer 2 // Bake nav-mesh from static geometry BuildNavMesh(0.35f, 1.8f, 0.25f); // ── Player ─────────────────────────────────────────── player = CreateEntity("Player"); player->AddModel("Models/Player.mdl"); player->AddAnimationController(); player->AddRigidBody(70.0f); player->AddCapsuleCollider(0.4f, 1.8f); player->SetPosition(0, 1, 0); player->AddScript("Scripts/player.lua"); // ── Enemy ───────────────────────────────────────────── enemy = CreateEntity("Enemy"); enemy->AddModel("Models/Skeleton.mdl"); enemy->AddAnimationController(); enemy->AddRigidBody(80.0f); enemy->AddCapsuleCollider(0.45f, 1.8f); enemy->SetPosition(12, 1, 8); // ── Lights and camera ──────────────────────────────── auto pt1 = CreateEntity("Torch1"); pt1->AddPointLight(Color(1.0f, 0.6f, 0.2f), 8.0f); pt1->SetPosition(0, 3, 0); auto cam = CreateCamera(); cam->Follow(player, 7.0f, 3.0f); cam->SetSmoothFollow(true, 5.0f); cam->SetCollisionEnabled(true); statusLabel = CreateLabel("Enemy: idle"); statusLabel->SetPosition(20, 20); statusLabel->SetFontSize(16); statusLabel->SetColor(Color::WHITE); } void Update(float dt) override { if (IsKeyDown(Key::Escape)) Quit(); float dist = (enemy->GetPosition() - player->GetPosition()).Length(); if (dist < 1.8f) { // Attack range enemy->StopMoving(); auto* anim = enemy->GetAnimationController(); if (anim) anim->Play("attack", 0, true, 0.1f); statusLabel->SetText("Enemy: ATTACKING"); } else if (dist < 20.0f) { // Chase range — MoveToward auto-navigates via nav-mesh bool moving = enemy->MoveToward(player, 3.5f, dt); auto* anim = enemy->GetAnimationController(); if (anim) anim->Play(moving ? "walk" : "idle", 0, true, 0.2f); statusLabel->SetText("Enemy: chasing dist=" + std::to_string((int)dist) + "m"); } else { enemy->StopMoving(); auto* anim = enemy->GetAnimationController(); if (anim) anim->Play("idle", 0, true, 0.3f); statusLabel->SetText("Enemy: idle"); } } };
Split-Screen Co-op
Two players in the same scene with independent cameras, each occupying half the screen. Each camera follows its own player independently.
class CoopGame : public Game { Entity* p1; Entity* p2; Camera* cam1; Camera* cam2; public: void Start() override { // Shared level auto ground = CreateEntity("Ground"); ground->AddModel("Models/Ground.mdl"); ground->AddRigidBody(0.0f); ground->AddBoxCollider(Vector3(80, 1, 80)); // Player 1 — WASD controls in script p1 = CreateEntity("Player1"); p1->AddModel("Models/P1.mdl"); p1->AddRigidBody(70.0f); p1->AddCapsuleCollider(0.4f, 1.8f); p1->SetPosition(-3, 2, 0); p1->AddScript("Scripts/player1_wasd.lua"); // Player 2 — Arrow key controls in script p2 = CreateEntity("Player2"); p2->AddModel("Models/P2.mdl"); p2->AddRigidBody(70.0f); p2->AddCapsuleCollider(0.4f, 1.8f); p2->SetPosition(3, 2, 0); p2->AddScript("Scripts/player2_arrows.lua"); // Sun auto sun = CreateEntity("Sun"); sun->AddDirectionalLight(Color::WHITE, 1.0f); sun->SetRotation(Quaternion(50, -30, 0)); // Left viewport — follows P1 cam1 = CreateCamera(); cam1->Follow(p1, 6.0f, 3.0f); cam1->SetSmoothFollow(true, 7.0f); cam1->SetViewport(0, 0.0f, 0.0f, 0.5f, 1.0f); // Right viewport — follows P2 cam2 = CreateCamera(); cam2->Follow(p2, 6.0f, 3.0f); cam2->SetSmoothFollow(true, 7.0f); cam2->SetViewport(1, 0.5f, 0.0f, 0.5f, 1.0f); PlayMusic("Music/coop.ogg"); } void Update(float dt) override { if (IsKeyDown(Key::Escape)) Quit(); } };
Networking — Client/Server Game State
Minimal multiplayer: server broadcasts all player positions, clients send their own position. Scene entities (created on the server) replicate automatically; custom messages handle player-specific state.
class MultiGame : public Game { NetworkManager* net = nullptr; Entity* myPlayer = nullptr; bool isServer = false; static const int MSG_PLAYER_POS = 100; public: void Start() override { net = CreateNetworkManager(); // Register message handler (both client and server) net->SetOnMessage([](int sender, int msgId, const std::vector<uint8_t>& data) { if (msgId == 100 && data.size() == sizeof(float) * 3) { float x, y, z; std::memcpy(&x, data.data() + 0, 4); std::memcpy(&y, data.data() + 4, 4); std::memcpy(&z, data.data() + 8, 4); // update remote player entity at (x,y,z)… } }); // Launch as server (in real code, read from config/args) isServer = true; if (isServer) { net->StartServer(7777, 16); net->SetOnClientConnected([](int id) { // spawn remote player entity… }); net->SetOnClientDisconnected([](int id) { // destroy remote player entity… }); } else { net->Connect("127.0.0.1", 7777); net->SetOnServerConnected([]{ /* ready */ }); } // Create local player myPlayer = CreateEntity("MyPlayer"); myPlayer->AddModel("Models/Character.mdl"); myPlayer->AddRigidBody(70.0f); myPlayer->AddCapsuleCollider(0.4f, 1.8f); myPlayer->SetPosition(0, 2, 0); myPlayer->AddScript("Scripts/player.lua"); auto cam = CreateCamera(); cam->Follow(myPlayer, 6.0f); cam->SetSmoothFollow(true); } float broadcastTimer = 0; void Update(float dt) override { if (IsKeyDown(Key::Escape)) Quit(); // Broadcast position 20 times/sec broadcastTimer += dt; if (broadcastTimer >= 0.05f) { broadcastTimer = 0; Vector3 pos = myPlayer->GetPosition(); std::vector<uint8_t> data(12); std::memcpy(data.data() + 0, &pos.x, 4); std::memcpy(data.data() + 4, &pos.y, 4); std::memcpy(data.data() + 8, &pos.z, 4); if (net->IsServer()) net->Broadcast(MSG_PLAYER_POS, data); else if (net->IsConnected()) net->SendToServer(MSG_PLAYER_POS, data); } } };
Trigger Zones — Checkpoint System
class CheckpointGame : public Game { Entity* player; Vector3 checkpoint = Vector3(0, 2, 0); Label* msgLabel; public: void Start() override { auto ground = CreateEntity("Ground"); ground->AddModel("Models/Ground.mdl"); ground->AddRigidBody(0.0f); ground->AddBoxCollider(Vector3(100, 1, 100)); player = CreateEntity("Player"); player->AddModel("Models/Character.mdl"); player->AddRigidBody(70.0f); player->AddCapsuleCollider(0.4f, 1.8f); player->SetPosition(0, 2, 0); player->AddScript("Scripts/player.lua"); // Checkpoint A MakeCheckpoint(Vector3(-10, 0.5f, 5), Vector3(-10, 2, 5), "Checkpoint A"); MakeCheckpoint(Vector3(10, 0.5f, 0), Vector3(10, 2, 0), "Checkpoint B"); // Death zone below the map auto deathZone = CreateEntity("DeathZone"); deathZone->SetPosition(0, -15, 0); deathZone->AddTriggerBox(Vector3(200, 5, 200)); deathZone->SetOnTriggerEnter([this](Entity* other) { if (other == player) { player->SetPosition(checkpoint); player->SetLinearVelocity(Vector3::ZERO); msgLabel->SetText("Respawned!"); msgLabel->SetVisible(true); } }); auto cam = CreateCamera(); cam->Follow(player, 6.0f); cam->SetSmoothFollow(true); msgLabel = CreateLabel(""); msgLabel->SetPosition(300, 200); msgLabel->SetFontSize(28); msgLabel->SetColor(Color(1, 0.5f, 0)); msgLabel->SetVisible(false); } float msgTimer = 0; void Update(float dt) override { if (IsKeyDown(Key::Escape)) Quit(); if (msgLabel->SetVisible(true), msgTimer += dt, msgTimer > 2.0f) msgLabel->SetVisible(false), msgTimer = 0; } private: void MakeCheckpoint(Vector3 pos, Vector3 respawnPos, const std::string& name) { auto cp = CreateEntity(name); cp->AddModel("Models/CheckpointFlag.mdl"); cp->SetPosition(pos); cp->AddTriggerCapsule(1.5f, 4.0f); cp->SetOnTriggerEnter([this, respawnPos, name](Entity* other) { if (other == player) { checkpoint = respawnPos; msgLabel->SetText(name + " reached!"); msgLabel->SetVisible(true); PlaySound("Sounds/checkpoint.wav"); } }); } };
HUD — Timer, Health Bar, Score
class HUDDemo : public Game { Label* healthLabel; Label* scoreLabel; Label* timerLabel; Label* fpsLabel; float elapsed = 0; int health = 100; int score = 0; float fpTimer = 0; int frames = 0; public: void Start() override { SetWindowTitle("HUD Demo"); healthLabel = CreateLabel("HP: |||||||||||||||||||| 100"); healthLabel->SetPosition(20, 20); healthLabel->SetFontSize(18); healthLabel->SetColor(Color(0.2f, 1.0f, 0.2f)); scoreLabel = CreateLabel("Score: 0"); scoreLabel->SetPosition(20, 50); scoreLabel->SetFontSize(20); scoreLabel->SetColor(Color::WHITE); timerLabel = CreateLabel("00:00"); timerLabel->SetPosition(600, 20); timerLabel->SetFontSize(22); timerLabel->SetColor(Color(0.9f, 0.9f, 0.9f)); fpsLabel = CreateLabel("FPS: 60"); fpsLabel->SetPosition(20, 680); fpsLabel->SetFontSize(12); fpsLabel->SetColor(Color(0.5f, 0.5f, 0.5f)); CreateCamera(); } void Update(float dt) override { elapsed += dt; fpTimer += dt; frames++; // Update timer MM:SS int m = (int)elapsed / 60, s = (int)elapsed % 60; auto pad = [](int v) { return (v < 10 ? "0" : "") + std::to_string(v); }; timerLabel->SetText(pad(m) + ":" + pad(s)); // FPS counter (update once/sec) if (fpTimer >= 1.0f) { fpsLabel->SetText("FPS: " + std::to_string(frames)); fpTimer = 0; frames = 0; } // Simulate health drain with H key if (IsKeyDown(Key::H) && health > 0) { health = std::max(0, health - (int)(dt * 20)); float ratio = health / 100.0f; healthLabel->SetColor(Color(1.0f - ratio, ratio, 0)); healthLabel->SetText("HP: " + std::to_string(health)); } if (IsKeyDown(Key::Escape)) Quit(); } };
Orbit Camera (RPG-style)
class RPGCamera : public Game { Entity* player; public: void Start() override { player = CreateEntity("Player"); player->AddModel("Models/Character.mdl"); player->SetPosition(0, 1, 0); player->AddScript("Scripts/player.lua"); auto ground = CreateEntity("Ground"); ground->AddModel("Models/Ground.mdl"); ground->AddRigidBody(0.0f); ground->AddBoxCollider(Vector3(100, 1, 100)); auto sun = CreateEntity("Sun"); sun->AddDirectionalLight(Color::WHITE, 1.0f); sun->SetRotation(Quaternion(50, -30, 0)); // Orbit camera — hold RMB and drag to rotate auto cam = CreateCamera(); cam->SetOrbitMode(player, 7.0f); cam->SetOrbitAngles(0.0f, 25.0f); // initial yaw=0°, pitch=25° cam->SetOrbitSensitivity(0.25f); cam->SetOrbitPitchLimits(-10.0f, 70.0f); cam->SetSmoothFollow(true, 8.0f); cam->SetCollisionEnabled(true); cam->SetFOV(55.0f); } void Update(float dt) override { if (IsKeyDown(Key::Escape)) Quit(); } };
Procedural Scene — Grid of Entities
class ProceduralGame : public Game { float time = 0; std::vector<Entity*> boxes; public: void Start() override { SetClearColor(Color(0.05f, 0.05f, 0.08f)); // 8×8 grid of boxes with staggered heights for (int x = -4; x < 4; ++x) { for (int z = -4; z < 4; ++z) { auto b = CreateEntity("Box" + std::to_string(x) + "_" + std::to_string(z)); b->AddModel("Models/Box.mdl"); b->SetPosition(x * 2.5f, 0, z * 2.5f); b->SetScale(Vector3(0.8f, 0.8f, 0.8f)); boxes.push_back(b); } } // Ambient fill + accent point light auto amb = CreateEntity("Amb"); amb->AddDirectionalLight(Color(0.2f, 0.2f, 0.3f), 0.5f); auto pt = CreateEntity("Accent"); pt->AddPointLight(Color(0.2f, 0.6f, 1.0f), 20.0f); pt->SetPosition(0, 5, 0); auto cam = CreateCamera(); cam->SetOrbitMode(pt, 18.0f); cam->SetOrbitAngles(0, 35); } void Update(float dt) override { time += dt; // Animate each box with a sine wave based on its grid position int i = 0; for (auto* b : boxes) { float phase = (i % 8) * 0.4f + (i / 8) * 0.3f; float h = std::sin(time * 2.0f + phase) * 1.5f; Vector3 pos = b->GetPosition(); pos.y = h; b->SetPosition(pos); b->SetRotation(Quaternion(time * 30.0f + phase * 60, 0, 0)); ++i; } if (IsKeyDown(Key::Escape)) Quit(); } };