simple-3d
A clean, minimalistic 3D/2D game framework for C++23 and Lua.
Write game logic against a stable, engine-agnostic API. No engine internals leak into your code.
Subclass Game, override Start() and Update(), ship.
What is simple-3d?
simple-3d is a high-level C++ game framework built as a clean wrapper around an Urho3D engine fork. It exposes a minimal, game-first API without requiring you to understand any of the underlying engine's internal types, subsystems, or component model.
Urho3D types (Urho3D::Node, Urho3D::Scene, Urho3D::Application, etc.)
are never visible in the public headers. They are hidden behind PIMPL classes.
The only exception is the math library — Vector2, Vector3,
Color, Quaternion and friends are re-exported as Simple3D:: type
aliases because they are ABI-identical and there is no benefit to wrapping them further.
The result: you write entity->AddRigidBody(1.0f) and simple-3d handles the
Urho3D::RigidBody component, GetOrCreateComponent, collision mask setup, and
physics world registration behind the scenes.
Why simple-3d?
Zero engine coupling
Your game code never includes a single Urho3D header. Swap the backend from U3D to NOVA3D with one CMake flag — zero source changes.
Designed for game code
API methods match what you think about while writing a game: "give this entity a capsule collider", "make the camera follow the player". Not engine primitives.
Lua for gameplay logic
Per-entity Lua scripts with Start(), Update(dt), and Stop(). Reloadable at runtime without recompiling the engine.
Full 3D + 2D in one
3D physics (Bullet), 2D physics (Box2D), sprites, tile maps, orthographic camera, and nav-mesh — all in the same library with one unified API surface.
No boilerplate
10 lines for a complete game loop, physics entity, animated character, and follow camera. Everything a game needs, nothing an engine demo needs.
Battle-tested backend
Built on top of an Urho3D fork — a mature engine with Bullet, Recast/Detour, ENet, LuaJIT, and bgfx rendering support.
Key Features
Entity system
Create scene objects with CreateEntity(name). Attach models, colliders, lights, animation controllers, and Lua scripts in any order.
Physics — 3D (Bullet)
Dynamic and static rigid bodies, box/capsule/sphere/mesh colliders, trigger volumes with enter/exit callbacks, impulse/force/velocity control, collision layers.
Physics — 2D (Box2D)
2D rigid bodies, box and circle colliders, linear velocity, impulse, ground detection. Runs alongside the 3D physics world in the same scene.
Camera modes
Third-person follow, orbit (RMB drag), first-person FPS (mouse captured), orthographic 2D, split-screen, smooth lerp, collision avoidance.
Navigation
BuildNavMesh() bakes from static geometry. entity->MoveToward(target, speed, dt) auto-navigates. FindPath() returns waypoints.
Lua scripting
Per-entity .lua scripts via Urho3D's LuaScriptInstance. Simple3D adds custom bindings: MoveRelative, IsOnGround, ApplyImpulse, input helpers.
Animation
Skeletal animation via AnimationController. Named clips, per-layer blending, fade in/out, weight control, speed multiplier.
Networking
ENet-based client/server. Scene entities auto-replicate. Custom messages (IDs 100–32767) with raw byte payloads. Typed connection callbacks.
Audio
Stream OGG/WAV music with loop control. One-shot SFX with per-call volume. Master volume. Spatial 3D audio planned.
2D rendering
Static sprites, Spriter animated sprites (.scml), flip control, Tiled tile map loading (.tmx).
Input
Keyboard (held / first-frame), mouse position / delta / buttons, multi-touch with pressure. Gamepad planned.
UI labels
On-screen text (HUD): screen position, font size, RGBA color, visibility toggle. Full UI panel/button system planned.
Architecture at a glance
Every public class in simple-3d uses the PIMPL idiom: the .h file
declares struct Impl; and stores it in a unique_ptr<Impl>.
The .cpp file defines Impl and can include any Urho3D header it needs.
This guarantees that rebuilding the engine never forces recompilation of game code.
Full architecture documentation →
Quick Start (C++)
One header, one subclass, four methods — that is everything required for a complete interactive game.
#include <Simple3D/Simple3D.h> using namespace Simple3D; class MyGame : public Game { public: Entity* player; Camera* camera; void Start() override { // ── Scene ──────────────────────────────────────────── auto ground = CreateEntity("Ground"); ground->AddModel("Models/Ground.mdl"); ground->AddRigidBody(0.0f); // 0 = static ground->AddBoxCollider(Vector3(100, 1, 100)); // ── 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->AddScript("Scripts/player.lua"); // ── Lighting ───────────────────────────────────────── auto sun = CreateEntity("Sun"); sun->AddDirectionalLight(Color(1.0f, 0.95f, 0.85f), 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); // ── Audio ──────────────────────────────────────────── PlayMusic("Music/theme.ogg"); } void Update(float dt) override { if (IsKeyDown(Key::Escape)) Quit(); } }; int main() { return CreateGame<MyGame>()->Run(); }
Build instructions → · Full C++ API reference →
Lua Script Example
Attach scripts to individual entities. Each script has its own isolated Lua state with access to the owning entity, input, audio, and scene. Scripts are hot-reloadable at runtime.
-- Scripts/player.lua -- Loaded via entity->AddScript("Scripts/player.lua") in C++ speed = 6 -- m/s horizontal movement jumpForce = 8 -- impulse on Space local anim -- AnimationController reference function Start() entity = self.node -- Simple3D convention: alias self.node anim = entity:GetAnimationController() end function Update(dt) local move = Vector3(0, 0, 0) if Input:IsKeyDown("W") then move.z = move.z + 1 end if Input:IsKeyDown("S") then move.z = move.z - 1 end if Input:IsKeyDown("A") then move.x = move.x - 1 end if Input:IsKeyDown("D") then move.x = move.x + 1 end if move:Length() > 0 then entity:MoveRelative(move:Normalized() * speed * dt) anim:Play("run", 0, true, 0.2) else anim:Play("idle", 0, true, 0.2) end if Input:IsKeyPress("Space") and entity:IsOnGround() then entity:ApplyImpulse(Vector3(0, jumpForce, 0)) PlaySound("Sounds/jump.wav") end end
Full Lua API reference → · More examples →
Module Status
| Module | Status | C++ API | Lua binding | Notes |
|---|---|---|---|---|
| Core | ✅ Done | Game class | — | Game loop, window, types, time |
| Entity | ✅ Done | Entity | Via self.node |
PIMPL over Urho3D::Node, registry |
| Camera | ✅ Done | Camera | — | Follow, FPS, orbit, ortho, split-screen |
| Audio | ✅ Done | Audio methods | 🔄 | PlayMusic, PlaySound, master volume |
| Input | ✅ Done | Input methods | Via Input: |
Keyboard, mouse, touch |
| Physics 3D | ✅ Done | Entity physics | Via entity extensions | Bullet — rigid bodies, triggers, layers |
| Physics 2D | ✅ Done | Entity physics 2D | — | Box2D — rigid body, box/circle |
| Animation | ✅ Done | Entity animation | Via GetAnimationController() |
Skeletal, blend layers, weights |
| Navigation | ✅ Done | Navigation | 📋 | Recast/Detour, MoveToward, FindPath |
| Networking | ✅ Done | NetworkManager | — | ENet, client/server, auto-replication |
| 2D sprites | ✅ Done | Entity 2D | — | Static + animated Spriter |
| Tile maps | ✅ Done | LoadTileMap | — | Tiled .tmx format |
| UI — Label | ✅ Done | Label | — | HUD text, position, color, visibility |
| Lua scripting | 🔄 In progress | — | Lua API | Core done; custom bindings Task #3 |
| Full UI system | 📋 Planned | — | — | Panels, buttons, images — Task #17 |
| Scene management | 📋 Planned | — | — | Level transitions — Task #18 |
| Spatial audio | 📋 Planned | — | — | 3D positional sound — Task #23 |
| Android | 📋 Planned | — | — | NOVA3D backend — Task #8 |
| Web / Emscripten | 📋 Planned | — | — | Task #9 |
Backends
The same simple-3d source works with two different engine backends. Switch with a single CMake variable; no changes to game code are needed.
U3D (default)
Links against the upstream u3d-community/U3D Urho3D community fork.
Stable Bullet physics, LuaJIT scripting, Recast/Detour navigation, ENet networking.
Supports Linux, Windows, and macOS desktop builds.
cmake -S . -B build \ -DSIMPLE3D_ENGINE=U3D \ -DU3D_HOME=/path/to/u3d/build
NOVA3D
Links against Robert Vokac's Urho3D fork which adds an XNA C++ runtime underneath. Same public API surface as U3D. Primary target for Android and experimental hardware. Identical game source builds against either backend.
cmake -S . -B build \ -DSIMPLE3D_ENGINE=NOVA3D \ -DNOVA3D_HOME=/path/to/nova3d/build
Comparison with Direct Urho3D Use
| Aspect | Direct Urho3D | simple-3d |
|---|---|---|
| Game entry point | Subclass Urho3D::Application, wire subsystems, create context |
CreateGame<MyGame>()->Run() |
| Create a physics object | Create Node, GetOrCreateComponent<RigidBody>, GetOrCreateComponent<CollisionShape>, set shape type, set mass, add to world | entity->AddRigidBody(1.0f); entity->AddCapsuleCollider(0.4f, 1.8f); |
| Engine types in headers | Yes — Urho3D::Node*, Urho3D::Scene*, etc. in game .h files |
No — only Simple3D:: aliases; no Urho3D includes needed |
| Camera follow player | Write follow logic in Update, handle offset and smoothing manually | camera->Follow(player, 6.0f); camera->SetSmoothFollow(true); |
| Nav-mesh pathfinding | Set up NavigationMesh component, NavigationAgent, handle path callbacks, implement steering | BuildNavMesh(); enemy->MoveToward(player, 3.0f, dt); |
| Backend portability | Tightly coupled to Urho3D | Swap U3D ↔ NOVA3D with one CMake flag |
License
simple-3d is open source. See the LICENSE file in the repository for the full text. The underlying Urho3D fork (U3D) is distributed under the MIT license.