Author: Karl-Johan Bailey
This project is a C++ ECS game scaffold with:
- a deterministic main loop
- a data-driven entity/factory system
- chunk streaming from JSON
- async mesh loading
- OpenGL rendering with terrain, grass, rocks, sky, fog, motion blur, and depth of field
Detailed reference docs live in docs/README.md and are split into per-component, per-factory, per-service, and terminal-command pages.
Build and run:
cmake -S . -B build
cmake --build build
./build/duppy gameplayCommon runtime flags:
./build/duppy gameplay --fullscreen --refresh-rate 144 --no-vsync
./build/duppy gameplay --chunks assets/world/chunks_demo.json --chunk-load-proximity 18 --chunk-unload-proximity 30
./build/duppy gameplay --fps 144 --uncapped
./build/duppy gameplay --debug-overlay
./build/duppy gameplay --debug-world
./build/duppy gameplay --debugHot-reload workflows:
./build/duppy gameplay --fps 144 --uncapped --dev
./tools/dev-gameplay.sh--devhot-reloads runtime content such as world JSON, shaders, textures, and models../tools/dev-gameplay.shwatches C++ source andCMakeLists.txt, rebuilds, and relaunches the game automatically.- Use the script when you want source edits to behave more like tooling live refresh.
Single-command build examples:
mkdir -p build &&
g++ -std=c++17 -O2 -pthread -Isrc \
src/main.cpp src/core/*.cpp src/ecs/EntityRegistry.cpp src/ecs/systems/*.cpp src/ecs/factories/*.cpp src/ecs/services/*.cpp src/assets/MeshAssetService.cpp src/data/Json.cpp src/games/*.cpp \
-o build/duppymkdir build
cl /std:c++17 /W4 /EHsc /I src ^
src\main.cpp src\core\*.cpp src\ecs\EntityRegistry.cpp src\ecs\systems\*.cpp src\ecs\factories\*.cpp src\ecs\services\*.cpp src\assets\MeshAssetService.cpp src\data\Json.cpp src\games\*.cpp ^
/Fe:build\duppy.exeType these into the terminal input while the game is running:
q,quit,exit— quit the game- any other line — passed into the game loop as a text input event
Useful launch commands:
./build/duppy attachment./build/duppy target./build/duppy enemy./build/duppy gameplay
If fullscreen is stuck at 60 FPS on your display, make sure you are not forcing vsync and use --refresh-rate 144 or your panel’s native refresh rate.
If you edit .cpp or .h files while only running ./build/duppy ... --dev, nothing will change until the binary is rebuilt. That is expected. Use ./tools/dev-gameplay.sh for source-watch rebuilds.
For frame profiling, prefer --debug-overlay first. It shows the frame debugger without enabling the heavier world-space debug lines, so the numbers are much more trustworthy.
The overlay and title bar now also show gpu_ms, which helps separate CPU-side render setup cost from actual GPU frame time.
src/main.cpp— CLI flags, mode selection, loop configsrc/core/— loop, timing, input, quit handlingsrc/ecs/— components, systems, factories, and servicessrc/graphics/— OpenGL renderer and shader servicesrc/assets/— async mesh loadingsrc/materials/— reusable material presetsassets/world/— chunk config examplesgraphics/shaders/— renderer shaders
Find built-in factory keys:
rg -n 'registerFactory\\("' src/ecs/factories/FactoryKeyService.cppFind chunk examples and proximity settings:
rg -n '"factory"|"coord"|"loadProximityMeters"|"unloadProximityMeters"' assets/world src/ecs/servicesFind mesh loader hooks:
rg -n 'registerLoader|IMeshAssetLoader|makeGltfMeshLoader' src/assets src/graphicsFind material presets:
find src/materials/presets -name '*.h' | sortFor step-by-step extension help, open docs/content-extension.md, docs/components.md, docs/factories.md, docs/services.md, and docs/terminal.md.
Components are data only. Systems read them and decide how to simulate or render them.
IdentityComponent— entity id/name metadataTransformComponent— world position, rotation, scaleHierarchyComponent— parent/child relationshipsAttachmentComponent— attach one entity to anotherTargetComponent— face or track a target entity
MotionComponent— high-level movement intent/stateControllerComponent— gameplay control actionsStatsComponent— movement and combat tuningRigidbodyComponent— mass, gravity, velocity, physics responseColliderComponent— shape and collision settingsPhysicsMaterialComponent— friction/bounce-style tuning
CharacterComponent— character identity/behavior markerAnimationComponent— animation clips and playbackSkeletonComponent— bones and inverse bind dataPoseComponent— runtime bone pose stateIKComponent— inverse kinematics chainsSocketComponent— attach points on bonesBoneKeyComponent— bone lookup helpers
CameraComponent— projection, FOV, DOF, motion blurThirdPersonCameraComponent— follow camera settings
BillboardComponent— world-space textured quads with camera-facing modes and animated frame playbackMeshComponent— mesh asset reference and visibilityMeshRendererComponent— render-oriented mesh stateShaderComponent— shader key, textures, parametersRenderSettingsComponent— global render togglesLightComponent— directional/point/spot lightingSkyComponent— procedural sky and cloud settingsFogVolumeComponent— volumetric fog boxTerrainComponent— terrain mesh + terrain LOD controlsGrassPatchComponent— grass and vegetation instancingRockScatterComponent— rock scatter instancingHudComponent— screen-space and world-space HUD widgetsCombatVolumeComponent— hit/hurt box dataAudioComponent— audio references and playback settingsVfxComponent— fire, sparks, electricity, smoke, steam emitters
SensorComponent— proximity/visibility sensingRaycastComponent— ray query data and debug draw settings
Services own reusable engine behavior.
EntityRegistry— creates entities, stores components, filters viewsEventService— queues and consumes gameplay eventsChunkStreamingService— loads and unloads chunk entities by proximityIChunkSource— pluggable chunk data providerFileChunkSource— JSON file-backed chunk sourceEntityFactoryRegistry— maps string keys to factoriesIEntityFactory— data-driven entity creation interfaceFactoryKeyService— registers the built-in factory keysHudSystem— updates HUD values such as life bars from gameplay stateSpatialHashGridService— broadphase grid for collision and queriesIKService— helper routines for IK chain setupRaycastConeFactoryService— builds ray sensor setups
The current chunk/factory keys live in src/ecs/factories/FactoryKeyService.cpp.
entitycharacterenemyweaponbuildingassetterrainvfx
To add a new factory key:
- Create a new factory header/cpp in
src/ecs/factories/. - Implement an
IEntityFactoryadapter or a direct factory class. - Register it in
registerFactoriesFromEcsFactoriesDir(...). - Add the new source file to
CMakeLists.txt. - Add a chunk entry that uses the new key.
Chunks are defined in JSON and loaded around the player by ChunkStreamingService.
Chunk config shape:
{
"chunks": [
{
"coord": [0, 0],
"entities": [
{ "factory": "entity", "config": { "name": "spawn_marker" } },
{ "factory": "vfx", "config": { "type": "Fire", "positionLocal": [2, 0, 3] } }
]
}
]
}Rules:
coord: [x, y]maps to chunk coordinates on the X/Z planepositionLocalis relative to the chunk originpositionis world-spacefactorymust match a registered factory keyconfigis passed straight into that factory
Streaming settings:
chunkSizeMeters— size of one chunksearchRadiusChunks— how many chunk coordinates to scanloadProximityMeters— how close you need to be before a chunk loadsunloadProximityMeters— how far away before a chunk unloads
Mesh loading is handled by MeshAssetService.
What it does:
- picks a loader by file extension
- loads meshes asynchronously
- stores submeshes, materials, skeletons, and animations
- lets the renderer request assets by source path
To add a new mesh format:
- Implement
assets::IMeshAssetLoader. - Register it in
MeshAssetService::start()or the relevant loader setup. - Return a populated
LoadedMeshAsset. - Point a
MeshComponentat the file path.
Mesh materials can also override shader texture refs through ShaderComponent.textures using slots like albedo, normalgl, roughness, metallic, ao, specular, emissive, displacement, metallicRoughness, and orm.
The glTF loader now auto-imports base color, normal, metallic-roughness, occlusion, and emissive textures plus their common factors, and it also reads KHR_materials_specular when present.
Mesh UVs are loaded from the source asset automatically when they exist; the engine does not generate new UV unwraps during load.
Example:
auto& mesh = registry.emplace<ecs::MeshComponent>(entity);
mesh.meshData.enabled = true;
mesh.meshData.key = "assets/models/business-man/scene.gltf";Reusable material presets live in src/materials/presets/.
Common presets:
DirtHighQualityDirtHighQualityDirtRockLayerHighQualityDirtRockGrassLayerMulchSandStoneStoneGrassSkyRealisticSkyClouds
These presets build render::ShaderComponent descriptions with textures and parameters. Use them when you want a new entity to share a known look without hand-wiring every texture.
For a one-stop include, src/materials/presets/Presets.h now exports the terrain and sky presets listed above.
To create a new preset:
- Copy an existing preset header in
src/materials/presets/. - Change the texture slots and parameter values.
- Return a configured
render::ShaderComponent. - Use that preset from a factory, gameplay setup, or chunk config.
Example:
auto& shader = registry.emplace<ecs::ShaderComponent>(entity, materials::presets::HighQualityDirtRockLayer());
shader.shader.key = "graphics/shaders/terrain";Terrain factories can also take a preset directly:
ecs::services::TerrainConfig cfg;
cfg.material = materials::presets::HighQualityDirtRockGrassLayer();
cfg.shaderKey = "graphics/shaders/terrain";Chunk configs can bind a splat_map (RGBA mask) and select which channel drives the terrain shader's rock-layer mask via splatChannel (0=R,1=G,2=B,3=A).
Instead of wiring rock textures by hand, you can provide splatMaterials and choose a material preset per channel; the active splatChannel decides which preset is used for the rock layer.
HUD widgets are data-driven and can be rendered:
- in screen space, like a classic UI
- in world space, like a floating health label or nameplate
The demo shows a HudComponent with:
- a textured portrait image
- a screen-space life bar driven by
StatsComponent::health - a world-space life label above the player
Example:
auto& hud = registry.emplace<ecs::HudComponent>(hudEntity);
ecs::HudComponent::Widget lifeBar;
lifeBar.kind = ecs::HudComponent::Kind::Bar;
lifeBar.space = ecs::HudComponent::Space::Screen;
lifeBar.sourceEntity = player;
lifeBar.valueSource = ecs::HudComponent::ValueSource::StatsHealth;
lifeBar.label = "Life";
lifeBar.showValueText = true;
lifeBar.positionPx = {96.0f, 28.0f};
lifeBar.sizePx = {320.0f, 24.0f};
hud.widgets.push_back(lifeBar);Texture usage:
- set
textureEnabled = true - assign
texture = {true, "assets/textures/your_image.png", 0} - optionally fill
animatedTexture.frameswith extracted video frames or a flipbook sequence - use
Kind::Imagefor a portrait/icon - use
Kind::Panelfor a textured panel background
Dynamic text:
- set
textand/orlabel - set
showValueText = trueto append the live numeric value - mutate the widget every tick if you want custom runtime text
World-space HUD:
- set
space = HudComponent::Space::World - set
billboard = truefor a camera-facing label - attach the widget to a source entity with
sourceEntity - use
worldOffsetto place it above the actor
Billboards are lightweight world-space textured quads.
- Use
BillboardComponent::FaceMode::CameraPlanefor effects or floating cards that should fully face the camera. - Use
BillboardComponent::FaceMode::YawOnlyfor upright signs and markers. - Use
BillboardComponent::FaceMode::Nonewhen authored transform rotation should stay in control. - Use
animatedTexture.framesplusframesPerSecondfor video-style playback from a frame sequence.
The runtime updater lives in HudSystem; the renderer snapshot lives in GraphicsSystem; and the actual draw calls happen in OpenGlRenderer.
- Create
src/ecs/components/MyComponent.h - Keep it data-only if possible
- Add a system that reads it
- Add a factory or gameplay setup that creates it
- Create a factory class in
src/ecs/factories/ - Parse JSON into a config struct in
FactoryKeyService.cpp - Register the key in
registerFactoriesFromEcsFactoriesDir(...) - Update
CMakeLists.txt - Add an example chunk using that key
- Pick a chunk coordinate
- Add entities with
factoryandconfig - Use
positionLocalfor content relative to the chunk - Let
ChunkStreamingServiceload it by proximity
- Register a loader in
MeshAssetService - Point a
MeshComponentat the new file path - Add a shader/material preset if needed
- Copy a file in
src/materials/presets/ - Update textures and shader parameters
- Reuse it from gameplay code or a factory
docs/entity-registry.mddocs/character-mesh-loading.mddocs/content-extension.md