Getting started
What Ragnavoxel is, how a game is laid out on it, the Application host and its hooks, AppConfig, the fixed-step loop, and building the engine and its samples.
Ragnavoxel is a C++20 game engine on Vulkan 1.4 (with volk and VMA), SDL3, Jolt Physics, TileDB, miniaudio and an AoSoA archetype ECS. It began as LostVale’s microvoxel engine and is now general: any voxel size, any camera (perspective, orthographic, off-centre), 3D voxel scenes, skinned and rigid actors, and a 2D sprite and tile-map renderer, all behind a small application host.
Everything is in namespace rv, and the public headers live under include/ragnavoxel/:
| Folder | What |
|---|---|
app/ |
Application, the host that owns everything and runs the loop |
core/ |
types, units and the voxel size, the job system, noise and hashing, AoSoA, logging, resource paths |
ecs/ |
entities, components, queries, resources, the staged scheduler |
voxel/ |
chunks, palettes, materials, primitives, the mesher, the asset library |
world/ |
VoxelScene (bounded worlds), ChunkStreamer (unbounded worlds), chunk components, occupancy |
render/ |
the GPU device, pipelines, the 3D renderer, sprites, view maths |
scene/ |
cameras and camera rigs |
model/ |
prop models and microvoxel model grids |
physics/ |
the Jolt wrapper |
audio/ |
the audio engine |
platform/ |
window, keyboard, mouse and gamepads |
storage/ |
TileDB world storage |
Building
The engine builds with CMake presets and takes its dependencies from vcpkg:
cmake --preset release
cmake --build build/release
That builds the library, its tests, the samples in build/release/ragnavoxel/samples/ and the editor in build/release/ragnavoxel/editor/. Vulkan pipelines are cached per GPU and driver in ~/.cache/ragnavoxel/, so only the first launch after a driver or shader change compiles them.
The Application
app/Application.hpp owns the window, the GPU device, the renderer, input, the ECS world and (if asked for) physics and audio, and runs a fixed-step loop. Subclass it and override its hooks:
#include "ragnavoxel/app/Application.hpp"
#include "ragnavoxel/scene/Camera.hpp"
#include "ragnavoxel/scene/CameraControllers.hpp"
struct MyGame : rv::app::Application {
using Application::Application;
rv::scene::Camera camera;
rv::scene::OrbitController orbit;
void onStart() override { /* load content, build the world */ }
void onFixedUpdate(double dt) override { /* simulation at a fixed rate */ }
void onUpdate(float dt) override {
orbit.update(camera, rv::scene::cameraInputFromDevices(input(), dt), glm::vec2(framebufferSize()), dt);
}
void onRender(rv::app::FrameContext& f) override { f.camera = camera.view(); }
};
int main(int argc, char** argv) {
rv::app::AppConfig cfg;
cfg.title = "My game";
cfg.parseArgs(argc, argv);
return MyGame(cfg).run();
}
The hooks
| Hook | When |
|---|---|
onStart() |
once, before the first frame |
onFixedUpdate(dt) |
every fixed step (AppConfig::fixedStep, 1/60 s by default), at most maxStepsPerFrame times a frame; physics steps after it |
onUpdate(dt) |
once a frame after the fixed steps; clock().alpha says how far the frame lies between steps, for interpolation |
onRender(frame) |
fill the camera, environment and render settings; submit sprites, actors and UI to renderer() |
onResize(size) |
the drawable changed size (pixels) |
onShutdown() |
as the loop ends |
ECS systems registered on fixedSystems() run every fixed step and those on frameSystems() once a frame, both in stage order (see The ECS). The world holds the app::Clock and a pointer to the platform::InputState as resources.
The host also gives you window(), device(), renderer(), input(), world(), physics() and audio() (null when switched off), clock(), framebufferSize(), aspect(), quit(code), and loadTexture(path) for PNG, JPG, TGA and BMP images.
AppConfig
| Field | Default | Meaning |
|---|---|---|
title, width, height, fullscreen, vsync |
“Ragnavoxel”, 1600 × 900, windowed, on | the window |
validation |
off | Vulkan validation layers (slow; for engine work) |
voxelSize |
1/64 m | the world’s voxel edge, set before anything is created (see Voxels and units) |
scene3D |
on | the 3D scene (voxels, actors, sky, post), or a 2D game of sprites and UI; switchable later with renderer().setSceneEnabled |
renderer |
sized for a streamed world | the renderer’s budgets; app::compactRenderer() suits small scenes and 2D, gfx::RendererConfig::interfaceOnly() launchers and tools |
physics, audio |
on | whether they start |
shaderDir |
the engine’s compiled shaders | RV_SHADER_DIR overrides it |
fixedStep, maxStepsPerFrame |
1/60 s, 8 | the fixed step, and how many a frame may take before a long hitch slows the game instead of spiralling |
screenshotPath, screenshotFrames |
test aid: render that many frames at a fixed 1/60 s step, save a screenshot and quit |
parseArgs reads --screenshot PATH, --frames N, --size WxH, --fullscreen, --validation, --no-vsync and --mute. A --screenshot run steps exactly one fixed step a frame, so its pictures are reproducible.
The clock
app::Clock holds seconds (simulation time: fixed steps taken), frameDelta (real seconds since the last frame, clamped to 0.25), fixedStep, alpha, frame and steps.
Resources beside the executable
core/Resources.hpp: a shipped build keeps its assets and shaders beside its executable, and a development build reads them from the source tree. Ask for each by name with the development path as the fallback:
const char* assets = rv::resourceDir("assets", "/home/me/game/assets");
That gives, in order: the environment variable RV_ASSETS_DIR when set, the folder assets beside the running executable when it exists, else the fallback.
Where to next
- Voxels and units for the storage hierarchy and choosing a voxel size.
- Worlds to put voxels on screen with
VoxelSceneorChunkStreamer. - Samples and tests for two small games to read.
Written from the engine's headers and source as of commit 02e81b2 (2026-11-01).