Getting Started

This guide walks you through every step: installing dependencies, building the library, integrating it into your CMake project, and writing your first game.

Requirements

DependencyMin versionNotes
CMake3.21 Modern target-based build. Earlier versions lack target_sources(FILE_SET).
C++ compilerC++23 GCC 13+, Clang 16+, MSVC 2022 17.5+ (v143). C++23 is required for std::expected and deducing this.
U3D (Urho3D fork)any recent Must be pre-built. Default search path: /rv/data/library/github.com/u3d-community/U3D/cmake-build-debug. Override with -DU3D_HOME=….
Gitany For cloning the repository.

System Packages

Debian / Ubuntu

# C++ toolchain
sudo apt install gcc-13 g++-13 cmake ninja-build git

# U3D runtime dependencies
sudo apt install \
  libsdl2-dev libsdl2-image-dev \
  libbullet-dev \
  liblua5.1-dev libluajit-5.1-dev \
  libenet-dev \
  libfreetype6-dev \
  libassimp-dev

# Use gcc-13 as default
sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-13 130
sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-13 130

Fedora / RHEL

sudo dnf install gcc13 g++13 cmake ninja-build git \
  SDL2-devel bullet-devel lua-devel luajit-devel \
  enet-devel freetype-devel assimp-devel

macOS (Homebrew)

brew install cmake ninja gcc@13 sdl2 bullet lua luajit enet freetype assimp

Note — Ninja

All build commands below use the Makefile generator. Replace -j$(nproc) with -GNinja and drop -j for faster incremental builds: cmake -S . -B build -GNinja && ninja -C build

Build — U3D Backend (default)

The U3D backend links against the u3d-community/U3D fork and is the recommended target for Linux, Windows, and macOS desktop development.

1

Clone simple-3d

git clone https://github.com/openeggbert/simple-3d
cd simple-3d
2

Configure (U3D in default location)

cmake -S . -B build-u3d -DCMAKE_BUILD_TYPE=Debug

If U3D is not in the default path, specify it explicitly:

cmake -S . -B build-u3d \
  -DCMAKE_BUILD_TYPE=Debug \
  -DU3D_HOME=/path/to/U3D/cmake-build-debug
3

Build

cmake --build build-u3d -j$(nproc)

The static library libsimple3d.a (Linux) or simple3d.lib (Windows) is produced in build-u3d/.

4

Run sample (optional)

./build-u3d/samples/blupi_proto/blupi_proto

Build — NOVA3D Backend

NOVA3D is Robert Vokac's Urho3D fork with an XNA C++ runtime underneath. The public API of simple-3d is identical between backends.

cmake -S . -B build-nova3d \
  -DSIMPLE3D_ENGINE=NOVA3D \
  -DNOVA3D_HOME=/path/to/nova3d/build
cmake --build build-nova3d -j$(nproc)

Windows — MinGW Cross-Compile

Cross-compile from Linux to Windows using the MinGW-w64 toolchain bundled with simple-3d. Install mingw-w64 first:

# Debian/Ubuntu
sudo apt install mingw-w64

# Configure and build
cmake -S . -B build-windows \
  -DCMAKE_TOOLCHAIN_FILE=cmake/toolchains/mingw-w64.cmake \
  -DSIMPLE3D_ENGINE=U3D
cmake --build build-windows -j$(nproc)

The toolchain file sets up the x86_64-w64-mingw32 cross-compiler and configures DLL search paths automatically. The resulting .exe and .dll files can be copied to a Windows machine and run without further setup.

Windows native build

Native Windows builds (MSVC or MinGW on Windows itself) are supported but not regularly tested in CI. The CMake configuration is identical; use the Visual Studio 2022 generator or a Windows MinGW installation.

Web / Emscripten (Planned)

Web support via Emscripten is defined in CMakeLists.txt but not yet tested end-to-end (Task #9). The build command is:

# Requires Emscripten SDK activated in current shell:
# source /path/to/emsdk/emsdk_env.sh
emcmake cmake -S . -B build-web -DSIMPLE3D_ENGINE=U3D
cmake --build build-web -j$(nproc)

Status

This build path is experimental. Track progress in Task #9 in the project's issue tracker.

CMake Options Reference

OptionDefaultDescription
SIMPLE3D_ENGINE U3D Backend selection. Values: U3D, NOVA3D.
U3D_HOME auto-detect Path to the U3D build directory (where U3DConfig.cmake lives).
URHO3D_HOME fallback Alias for U3D_HOME. Checked if U3D_HOME is not set.
NOVA3D_HOME auto-detect Path to the NOVA3D build directory. Used only when SIMPLE3D_ENGINE=NOVA3D.
SIMPLE3D_BUILD_TESTS OFF Build unit tests. Requires Google Test. Tests do not need a display or GPU.
SIMPLE3D_BUILD_SAMPLES ON Build sample applications (currently blupi_proto).
CMAKE_BUILD_TYPE Release Use Debug during development for full symbols and assertions.
CMAKE_TOOLCHAIN_FILE Pass cmake/toolchains/mingw-w64.cmake for Windows cross-compile.

Integrating into Your Project

The recommended way to consume simple-3d is as a CMake subdirectory:

# Recommended: Git submodule
git submodule add https://github.com/openeggbert/simple-3d ext/simple-3d
# CMakeLists.txt for your game
cmake_minimum_required(VERSION 3.21)
project(my-awesome-game CXX)

# The engine backend is set by the parent project or on the cmake command line
# option(SIMPLE3D_ENGINE "U3D or NOVA3D" "U3D")

add_subdirectory(ext/simple-3d simple3d)

add_executable(my-game
    src/main.cpp
    src/GameplayScene.cpp
    src/EnemyController.cpp
)

target_link_libraries(my-game PRIVATE simple3d)
target_compile_features(my-game PRIVATE cxx_std_23)

# Copy game data next to the executable
add_custom_command(TARGET my-game POST_BUILD
    COMMAND ${CMAKE_COMMAND} -E copy_directory
        ${CMAKE_SOURCE_DIR}/Data $<TARGET_FILE_DIR:my-game>/Data)

Automatic include propagation

The simple3d CMake target propagates include/ as a public include directory. After target_link_libraries(my-game PRIVATE simple3d) you can use #include <Simple3D/Simple3D.h> without any additional include path flags.

Recommended Project Structure

my-game/
├── CMakeLists.txt
├── ext/
│   └── simple-3d/           ← git submodule
├── src/
│   ├── main.cpp             ← Game subclass + main()
│   ├── GameState.h/.cpp     ← scene management
│   └── EnemyAI.h/.cpp      ← gameplay logic
└── Data/                    ← all runtime assets
    ├── Models/
    │   ├── Character.mdl
    │   ├── Ground.mdl
    │   └── Enemy.mdl
    ├── Textures/
    │   └── Character_D.png
    ├── Animations/
    │   ├── idle.ani
    │   ├── run.ani
    │   └── jump.ani
    ├── Scripts/
    │   ├── player.lua
    │   └── enemy.lua
    ├── Sounds/
    │   ├── jump.wav
    │   └── coin.wav
    ├── Music/
    │   └── theme.ogg
    └── Maps/
        └── level1.tmx

IDE Setup

VS Code with CMake Tools

  1. Install extensions: C/C++ (Microsoft), CMake Tools (Microsoft).
  2. Open the project folder. CMake Tools will detect CMakeLists.txt automatically.
  3. Press Ctrl+Shift+PCMake: Configure. Select your kit (GCC 13 or Clang 16).
  4. Set CMake arguments in .vscode/settings.json:
{
    "cmake.configureArgs": [
        "-DU3D_HOME=/path/to/U3D/cmake-build-debug",
        "-DCMAKE_BUILD_TYPE=Debug"
    ]
}
  1. Press F7 to build, F5 to debug (requires launch.json).

CLion

  1. File → Open → select the project folder.
  2. In Settings → Build → CMake, add CMake options: -DU3D_HOME=/path/to/U3D/cmake-build-debug
  3. CLion automatically detects the CMakeLists.txt and creates run configurations.
  4. Use the built-in debugger with full variable inspection.

Your First Game

Create src/main.cpp. The complete program below creates a spinning box, a directional light, a camera, and a HUD label — all in under 40 lines.

#include <Simple3D/Simple3D.h>
using namespace Simple3D;

class HelloGame : public Game {
public:
    Entity* box     = nullptr;
    Label*  hud     = nullptr;
    float   angle   = 0.0f;
    float   elapsed = 0.0f;

    // Start() is called once after the engine is fully initialised.
    // Create all scene objects here; never in the constructor.
    void Start() override {
        SetWindowTitle("Hello simple-3d");
        SetClearColor(Color(0.08f, 0.08f, 0.12f));

        // A rotating box at world origin, 5 m in front of the camera.
        box = CreateEntity("Box");
        box->AddModel("Models/Box.mdl");
        box->SetPosition(0.0f, 0.0f, 5.0f);

        // Directional sun light.
        auto sun = CreateEntity("Sun");
        sun->AddDirectionalLight(Color(1.0f, 0.95f, 0.85f), 1.0f);
        sun->SetRotation(Quaternion(50, -30, 0));

        // Fill light from below.
        auto fill = CreateEntity("Fill");
        fill->AddDirectionalLight(Color(0.3f, 0.35f, 0.5f), 0.4f);
        fill->SetRotation(Quaternion(-50, 30, 0));

        // Default camera looks at world origin from Z=-
        CreateCamera();

        // On-screen timer.
        hud = CreateLabel("Time: 0.0 s  |  Press ESC to quit");
        hud->SetPosition(20, 20);
        hud->SetFontSize(16);
        hud->SetColor(Color(0.8f, 0.8f, 0.8f));
    }

    // Update() is called every frame.  dt = seconds since last frame.
    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();
}

Your First Lua Script

Any entity can have a Lua script attached. The script receives the same lifecycle callbacks as C++ code. Scripts live in the Data/Scripts/ folder.

Step 1 — Attach the script in C++:

// In Start():
box->AddScript("Scripts/spin.lua");

Step 2 — Create Data/Scripts/spin.lua:

-- spin.lua: Rotate the entity around Y axis.
local angle = 0

function Start()
    entity = self.node   -- simple-3d convention: alias self.node
end

function Update(dt)
    angle = angle + dt * 90            -- 90 degrees per second
    entity:SetRotation(Quaternion(angle, 0, 0))
end

The entity convention

In Urho3D Lua, the owning Node is always accessible as self.node. Simple3D convention is to copy it to a module-level entity variable in Start() for cleaner syntax in Update() and event callbacks.

Lua script with animation and physics

-- Scripts/player.lua
speed     = 6
jumpForce = 8
local anim

function Start()
    entity = self.node
    anim   = entity:GetAnimationController()
    if anim == nil then
        print("[player.lua] WARNING: no AnimationController found")
    end
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

    local moving = move:Length() > 0
    if moving then
        entity:MoveRelative(move:Normalized() * speed * dt)
    end

    if anim then
        anim:Play(moving and "run" or "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

Asset Paths

All asset paths are relative to Urho3D's resource search directories. By default, simple-3d adds the following directories to the search path at startup:

  1. The directory where the game executable resides.
  2. The current working directory at the time of launch.
  3. Each directory also has its Data/ sub-folder added automatically.

This means if your executable is at build/my-game and assets are in build/Data/, the path "Models/hero.fbx" resolves to build/Data/Models/hero.fbx.

Canonical directory layout

my-game/               ← project root
├── CMakeLists.txt
└── Data/              ← all game assets
    ├── Models/        ← 3D meshes (OBJ, FBX, MDL, GLTF)
    ├── Textures/      ← diffuse, normal, specular maps (PNG, JPG, DDS)
    ├── Animations/    ← skeletal animations (.ani)
    ├── Materials/     ← Urho3D material definitions (.xml)
    ├── Techniques/    ← custom render techniques (.xml)
    ├── Scripts/       ← Lua scripts (.lua)
    ├── Sounds/        ← sound effects (WAV, OGG — short files)
    ├── Music/         ← background music (OGG, WAV — streamed)
    ├── Maps/          ← Tiled tile maps (.tmx)
    ├── Sprites/       ← 2D sprites (PNG) and Spriter files (.scml)
    └── Fonts/         ← fonts for UI labels (.ttf, .otf)

Reference assets with paths relative to Data/:

// Correct:
entity->AddModel("Models/Character.mdl");       // → Data/Models/Character.mdl
entity->AddScript("Scripts/player.lua");      // → Data/Scripts/player.lua
PlaySound("Sounds/jump.wav");                  // → Data/Sounds/jump.wav
LoadTileMap("Maps/level1.tmx");               // → Data/Maps/level1.tmx

Supported Asset Formats

CategoryFormatsNotes
3D models OBJ, FBX, GLTF, MDL MDL is Urho3D's native binary format (faster loading). FBX and GLTF are imported via Assimp at build time.
Textures PNG, JPG, DDS, KTX DDS with mipmaps is preferred for GPU efficiency. Urho3D auto-generates mipmaps from PNG/JPG.
Skeletal animation ANI (Urho3D native) Export from FBX/GLTF using Urho3D's AssetImporter tool.
Sound effects WAV, OGG Short sounds. Loaded fully into memory. Keep under ~2 MB.
Music OGG, WAV Streamed from disk. OGG recommended (smaller files, same quality).
2D sprites PNG, JPG Transparent backgrounds require PNG.
Animated sprites SCML (Spriter) Spriter 2D animation definition format.
Tile maps TMX (Tiled) Tiled Map Editor format. Orthographic and isometric supported.
Materials XML (Urho3D) Optional — simple-3d applies default materials when none is specified.
Scripts LUA Plain UTF-8 Lua 5.1 / LuaJIT compatible scripts.

Debugging

Debug vs Release build

Always develop with -DCMAKE_BUILD_TYPE=Debug. This enables:

Console log output

simple-3d forwards all Urho3D log messages to stdout. Look for lines starting with [Warning] or [Error] when assets fail to load.

# Run with verbose logging:
./my-game 2>&1 | tee game.log

Common runtime errors

MessageCauseFix
Failed to load resource Models/hero.mdl Asset not found in any resource path. Check that Data/ is next to the executable and the file exists.
NULL pointer in Entity::AddRigidBody Calling physics methods before Start() completes. Never call engine API from constructors — use Start().
Blank window, no 3D scene No camera created, or camera not looking at scene. Call CreateCamera() in Start(). Default position is Z=0 looking toward +Z.
Entity falls through floor Static rigid body has no AddBoxCollider / AddMeshCollider. Always pair AddRigidBody(0.0f) with a collider call.
Lua script not running File path wrong, or script has a syntax error on load. Check log for Failed to load Script; run luac -p yourscript.lua to validate syntax.

Build Troubleshooting

ErrorCauseFix
Could not find U3DConfig.cmake U3D_HOME not set or wrong path. Pass -DU3D_HOME=/path/to/U3D/build to CMake.
error: 'std::expected' was not declared Compiler does not support C++23. Upgrade to GCC 13+ or Clang 16+.
undefined reference to Urho3D::… U3D library not found at link time. Check U3D_HOME points to a fully built U3D (run ls $U3D_HOME/lib).
LuaJIT not found U3D was built without LuaJIT, or headers missing. Install libluajit-5.1-dev and rebuild U3D with -DURHO3D_LUA=ON -DURHO3D_LUAJIT=ON.
linker: multiple definition of Simple3D::… simple-3d added twice as a CMake subdirectory. Use if(NOT TARGET simple3d) guard before add_subdirectory.