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();
    }
};

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();
    }
};