Skip to content

Jakubeich/voidengine

Repository files navigation

VoidEngine

VoidEngine is a desktop-oriented C++20 game engine with an integrated editor, OpenGL renderer, ECS-based scene layer, physics, audio, Lua scripting, runtime game packages, and asset pipeline tooling.

The goal of this project is to explore how a practical game engine is structured end to end: reusable engine code, editor workflows, runtime loading, tools, tests, and a runnable sample project in one CMake-based repository.

VoidEditor clean editor screenshot

Project Highlights

  • Integrated VoidEditor with scene hierarchy, inspector, viewport, asset browser, console, profiler, undo history, gizmos, and play mode.
  • ECS scene system with UUID-backed entities, transform hierarchy, serialization, prefabs, scripting hooks, and runtime package loading.
  • OpenGL rendering layer with shaders, meshes, textures, framebuffers, forward/deferred rendering paths, shadows, skybox, IBL, SSAO, bloom, and post-processing.
  • Gameplay-oriented systems for physics, raycasting, triggers, input mapping, gamepad support, audio playback, music streaming, Lua scripting, particles, and terrain.
  • Asset pipeline tools for baking meshes/textures and packaging runtime assets into .vepack archives.
  • Standalone subsystem tests covering rendering, scenes, physics, audio, scripting, editor panels, resources, and asset tooling.

Tech Stack

Area Technologies
Language C++20
Build and packaging CMake, CPack
Rendering OpenGL, GLSL, GLFW, GLAD, GLM
Editor UI Dear ImGui, ImGuizmo, tinyfiledialogs
Assets and data Assimp, stb, nlohmann/json, custom .vemesh, .vetex, .vepack, .vegame.json formats
Physics Jolt Physics
Audio OpenAL Soft
Scripting Lua, sol2
Tests Standalone CMake test executables

Documentation

Repository Contents

  • VoidEngine: the reusable static engine library in engine/
  • VoidEditor: an editor application for inspecting scenes and game packages
  • VoidDemo: a runtime executable that boots the included sample package
  • vebake: offline asset baking tool for meshes and textures
  • vepack: archive tool for .vepack runtime asset bundles
  • game/signal_breach.vegame.json: a ready-to-run sample package used to test the runtime/editor workflow
  • tests/: standalone test executables that cover engine subsystems

Engine Scope

VoidEngine is designed for desktop OpenGL projects where you want one repository to contain:

  • the core application/runtime layer
  • the renderer
  • scene and ECS management
  • editor-facing panels and play mode
  • asset import/bake/package tooling
  • runtime package loading
  • automated tests for core subsystems

It is a practical engine codebase, not only a toy sample. The current implementation is strongest in the following areas.

Engine Systems

System What is implemented What it is for
Application core ve::Application, Window, Time, EventBus, Input, GamepadManager, JobSystem, SplashScreen Bootstrapping a desktop app, owning subsystems, running the main loop, and wiring per-frame update/render flow
ECS / scene graph Scene, Entity, generic component stores, UUID-backed entity pool, parent-child transform hierarchy, snapshots Building gameplay objects, editor objects, and serializable runtime scenes
Rendering RenderDevice, ShaderProgram, Mesh, Texture2D, CubemapTexture, Framebuffer, Camera OpenGL resource management, shader compilation/hot reload, drawing, camera control, off-screen rendering
Materials Material, MaterialInstance, texture bindings, custom uniform properties Authoring Blinn-Phong or PBR-style material setups and per-instance overrides
Scene rendering forward and deferred paths, shadow resources, skybox, IBL, SSAO, bloom, tone mapping, color grading, FXAA, vignette Building lit 3D scenes with post-processing and environment lighting
Visibility and batching instancing, static batching, frustum culling, occlusion culling, Hi-Z infrastructure, culling stats Reducing draw overhead and exposing render visibility diagnostics
Physics PhysicsWorld, fixed physics tick, rigid bodies, colliders, triggers, raycasts, screen picking Deterministic gameplay stepping, movement, collision, overlap detection, and editor/runtime picking
Audio AudioDevice, AudioClip, AudioSource, AudioMixer, MusicStream, MusicPlaylist SFX and music playback with groups, priorities, virtualization, and playlist/crossfade support
Scripting ScriptComponent, Lua runtime integration, hot reload, ECS/scene bindings Driving gameplay logic from Lua with update/physics hooks and scene mutation
Terrain and particles terrain heightfield component, splat layers, foliage layers, particle emitters Outdoor scenes, terrain LOD, billboard/mesh particles, lightweight VFX
Asset pipeline AssetBaker, AssetPack, TextureManager, ResourceManager Converting source files into runtime-friendly formats and managing loaded resources
Serialization SceneSerializer, GamePackage Saving/loading scenes to JSON and grouping startup scene + controls + assets into a runtime package

Editor and Tooling

Tool What is implemented What it is for
VoidEditor hierarchy, inspector, viewport, asset browser, console, profiler, undo history, scene/game tabs, gizmos Working with scenes and packages visually, then entering play mode from the same application
ViewportPanel Scene tab, Game tab, editor camera, game camera, picking, transform gizmos, snap settings Editing transforms and verifying runtime camera/gameplay output inside the editor
Undo/redo UndoHistory, value commands, transaction commands, scene commands Keeping editor modifications reversible and grouping drag/gizmo edits into clean transactions
vebake mesh and texture baking into .vemesh / .vetex Converting source assets into formats that are faster for runtime loading
vepack pack/list/extract .vepack archives Shipping runtime asset bundles and inspecting archive contents

What You Can Build With It

VoidEngine is currently well-suited for:

  • single-player desktop 3D games
  • gameplay sandboxes built around ECS entities and components
  • editor-backed prototypes where play mode and scene editing live in one app
  • package-driven demos where a .vegame.json decides startup scene, controls, and asset roots
  • projects that want engine code, editor code, and runtime assets in one CMake repository

Build The Project

Requirements:

  • CMake 3.25+
  • C++20 compiler
  • Git
  • Desktop OS with OpenGL support
  • Internet access on the first configure step, because dependencies are fetched through FetchContent

Configure and build from the repository root:

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

If you use a multi-config generator such as Visual Studio or Xcode, the executable path may include a configuration directory such as Debug or Release.

Run the editor

./build/bin/VoidEditor

Run the included sample package

./build/bin/VoidEditor game/signal_breach.vegame.json
./build/bin/VoidDemo game/signal_breach.vegame.json

The sample package is included to verify that the runtime package, controls config, assets, scripting, and editor/game workflow all work in a real project. It is not the main subject of this README; the engine is.

Public API Layout

The main convenience include is:

#include <void_engine/engine.h>

That header aggregates the public engine API from:

  • void_engine/core/*
  • void_engine/platform/*
  • void_engine/renderer/*
  • void_engine/ui/*
  • void_engine/scene/*
  • void_engine/editor/*

If you prefer narrow includes, you can include only the headers you need, such as:

#include <void_engine/core/application.h>
#include <void_engine/scene/scene.h>
#include <void_engine/renderer/render_device.h>
#include <void_engine/renderer/camera.h>

Minimal Application Example

The main integration point is ve::Application. You derive from it, override lifecycle methods, and use the subsystems owned by the base class.

#include <void_engine/engine.h>

class MinimalApp final : public ve::Application {
public:
    MinimalApp()
        : ve::Application(makeConfig()) {}

protected:
    void onInit() override {
        m_device.init();

        m_scene.initPhysics();
        m_camera.setPerspective(60.0f, getWindow().getAspectRatio(), 0.1f, 500.0f);
        m_camera.setPosition({0.0f, 2.0f, 6.0f});

        auto sun = m_scene.createEntity();
        m_scene.addTransform(sun);
        m_scene.addLight(sun, ve::LightComponent::makeDirectional({-0.4f, -1.0f, -0.2f}));

        auto actor = m_scene.createEntity();
        m_scene.addName(actor, "Actor");

        auto& transform = m_scene.addTransform(actor);
        transform.position = {0.0f, 0.5f, 0.0f};

        m_scene.addRigidbody(actor, {.motionType = ve::PhysicsMotionType::Dynamic});
        m_scene.addCollider(actor, ve::ColliderComponent::makeBox({0.5f, 0.5f, 0.5f}));
    }

    void onWindowResize(const ve::WindowResizeEvent&) override {
        m_camera.setAspectRatio(getWindow().getAspectRatio());
    }

    void onPhysicsUpdate(ve::f64 fixedDt) override {
        m_scene.stepPhysics(static_cast<ve::f32>(fixedDt));
        m_scene.stepScriptPhysics(static_cast<ve::f32>(fixedDt), &getInput());
    }

    void onUpdate(ve::f64 dt) override {
        m_camera.update(dt, getInput());
        m_scene.stepAnimations(static_cast<ve::f32>(dt));
        m_scene.stepParticles(static_cast<ve::f32>(dt));
        m_scene.stepScripts(static_cast<ve::f32>(dt), &getInput());
    }

    void onRender() override {
        m_device.setViewport(0, 0, getWindow().getWidth(), getWindow().getHeight());
        m_device.setClearColor(0.08f, 0.09f, 0.11f, 1.0f);
        m_device.clear();
        m_scene.render(m_camera, m_device);
    }

private:
    static ve::AppConfig makeConfig() {
        ve::AppConfig cfg;
        cfg.window.title = "VoidEngine Minimal App";
        cfg.window.width = 1600;
        cfg.window.height = 900;
        cfg.enableAudio = true;
        return cfg;
    }

    ve::Scene m_scene;
    ve::RenderDevice m_device;
    ve::FreeCamera m_camera;
};

int main() {
    MinimalApp app;
    app.run();
}

What this gives you:

  • window creation and OpenGL context setup
  • a fixed physics tick owned by the application
  • input, audio, jobs, time, and event systems
  • a place to update gameplay (onUpdate)
  • a place to run deterministic simulation (onPhysicsUpdate)
  • a place to render (onRender)

Working With Scenes and ECS

ve::Scene is the central runtime/editor container. It owns entities, generic component stores, physics integration, render integration, and serialization entry points.

Implemented convenience components include:

  • TransformComponent
  • MeshRendererComponent
  • RigidbodyComponent
  • ColliderComponent
  • LightComponent
  • TriggerComponent
  • ScriptComponent
  • NameComponent
  • AnimatorComponent
  • TerrainComponent
  • ParticleEmitterComponent

Entity and component example

ve::Scene scene;

ve::Entity player = scene.createEntity();
scene.addName(player, "Player");

auto& transform = scene.addTransform(player);
transform.position = {0.0f, 1.0f, 0.0f};
transform.setEulerDegrees({0.0f, 45.0f, 0.0f});

scene.addRigidbody(player, {
    .motionType = ve::PhysicsMotionType::Dynamic,
    .mass = 80.0f,
    .linearDamping = 0.15f
});

scene.addCollider(player, ve::ColliderComponent::makeCapsule(0.9f, 0.35f));

ve::Entity cameraPivot = scene.createEntity();
scene.addTransform(cameraPivot);
scene.setParent(cameraPivot, player);

Generic multi-component query

scene.each<ve::TransformComponent, ve::RigidbodyComponent>(
    [](ve::Entity entity, ve::TransformComponent& transform, ve::RigidbodyComponent& body) {
        if (body.isDynamic()) {
            transform.position.y += 0.01f;
        }
    }
);

Rendering Resources, Meshes, and Materials

You can render scene-driven entities through Scene::render, while resource types such as Mesh, ShaderProgram, Texture2D, and Material remain usable directly.

Baked mesh + texture example

ve::ShaderProgram shader;
shader.loadFromFile("content/shaders/pbr_lit.glsl");

ve::Mesh crateMesh;
crateMesh.loadBaked("content/meshes/crate.vemesh");

ve::Texture2D crateAlbedo;
crateAlbedo.loadBakedFromFile("content/textures/crate_albedo.vetex");

ve::Material crateMaterial = ve::Material::createTextured(&shader, &crateAlbedo);
crateMaterial.setProperty("u_tint", glm::vec3(1.0f, 1.0f, 1.0f));

ve::Entity crate = scene.createEntity();
scene.addName(crate, "Crate");
scene.addTransform(crate).position = {0.0f, 0.5f, 0.0f};
scene.addMeshRenderer(crate, {
    .mesh = &crateMesh,
    .material = &crateMaterial,
    .enableInstancing = true
});

What the renderer supports today

  • GLSL shaders from separate files or combined #type vertex / #type fragment files
  • shader hot reload
  • forward and deferred rendering paths
  • skybox and image-based lighting resources
  • directional, point, and spot lights
  • shadow-map resources and cascade data plumbing
  • terrain rendering with splat layers and foliage layers
  • billboard or mesh-based particle emitters
  • instancing and static batching flags on mesh renderer components
  • culling and renderer statistics exposed through scene stats

Resource Management

The engine also exposes a generic handle-based ResourceManager and a higher-level TextureManager.

Use this when you want:

  • reference-counted handles instead of raw ownership everywhere
  • loading state tracking (Loading, Ready, Failed)
  • keyed lookup for reusable resources
  • placeholder/fallback textures for missing assets

Texture manager example

ve::TextureManager textures;

auto albedoHandle = textures.load(
    "crate_albedo",
    "content/textures/crate_albedo.png",
    {.generateMipmaps = true},
    ve::TexturePlaceholder::White
);

const ve::Texture2D* albedo = textures.resolve(
    albedoHandle,
    ve::TexturePlaceholder::White
);

This is useful in editor/runtime code where you need safe fallback behavior instead of failing hard on every missing texture.

Input Mapping

ve::Input is the low-level polling layer. ve::InputMap lets you define higher-level actions such as Jump, Fire, or MoveForward, then bind them to keyboard, mouse, or gamepad input and serialize that mapping to JSON.

#include <GLFW/glfw3.h>

ve::InputMap inputMap;
inputMap.addAction("move_forward");
inputMap.addAction("fire");
inputMap.addAction("dash");

inputMap.addBinding("move_forward", {ve::InputSource::Keyboard, GLFW_KEY_W});
inputMap.addBinding("move_forward", {ve::InputSource::GamepadAxis, GLFW_GAMEPAD_AXIS_LEFT_Y, -1.0f, 0.20f});
inputMap.addBinding("fire", {ve::InputSource::MouseButton, GLFW_MOUSE_BUTTON_LEFT});
inputMap.addBinding("dash", {ve::InputSource::Keyboard, GLFW_KEY_SPACE});

// each frame
inputMap.update(getInput());

if (inputMap.isActionPressed("dash")) {
    // trigger dash once
}

float moveForward = inputMap.getActionValue("move_forward");
bool firing = inputMap.isActionHeld("fire");

inputMap.saveToFile("config/controls.json");

Use this layer if you want rebinding, gamepad support, and data-driven action names instead of hard-coded key checks scattered throughout gameplay code.

Audio

The application base class already owns an AudioDevice. On top of that you can create clips, sources, a mixer, music streams, and playlists.

ve::AudioMixer mixer;

ve::AudioClip laserClip;
laserClip.loadFromFile(getAudioDevice(), "content/audio/laser.wav");

ve::AudioSource laser;
laser.init(getAudioDevice());
laser.setClip(&laserClip);
laser.setMixer(&mixer);
laser.setMixerGroup(ve::AudioMixerGroup::Sfx);
laser.setVolume(0.8f);
laser.setPriority(5);

// when the player fires
laser.play();

Current audio stack includes:

  • clip loading from file or PCM memory
  • source playback, pause, stop, looping, pitch, 3D position, velocity
  • mixer groups and master routing
  • source priority and preemption
  • music streaming and playlists
  • virtual backend support used by tests for deterministic machines without a real device

Physics, Raycasts, and Picking

Scene integrates physics with ECS components and exposes both low-level PhysicsWorld access and scene-aware helpers.

ve::SceneRaycastHit hit;

ve::PhysicsRay ray;
ray.origin = {0.0f, 2.0f, 6.0f};
ray.direction = glm::normalize(glm::vec3(0.0f, -0.2f, -1.0f));
ray.maxDistance = 100.0f;

if (scene.raycastClosest(ray, hit)) {
    if (scene.hasName(hit.entity)) {
        std::cout << "Hit entity: " << scene.getName(hit.entity).name << "\n";
    }
}

Editor-style screen picking is also available through:

  • Scene::screenPointToRay
  • Scene::pickClosest
  • Scene::pickAll

Trigger overlap queries are available through:

  • Scene::getTriggerOverlaps
  • Scene::isTriggerOverlapping

Lua Scripting

Attach a script path to an entity through ScriptComponent. The runtime will load the Lua file, bind ECS/scene functionality, and call lifecycle hooks.

Implemented hook names used by the current runtime/tests:

  • on_init()
  • on_update(dt)
  • on_physics_update(dt)
  • on_reload()
  • on_shutdown()

Runtime characteristics:

  • scripts can mutate transforms, names, and other entity state
  • scripts can create and destroy ECS entities through scene bindings
  • hot reload is supported through ScriptComponent::hotReload
  • physics and input bindings are available from Lua runtime code

Attach a script like this:

ve::Entity scripted = scene.createEntity();
scene.addTransform(scripted);
scene.addScript(scripted, {
    .path = "content/scripts/player_controller.lua",
    .enabled = true,
    .hotReload = true
});

Scene Serialization and Game Packages

VoidEngine has two different but related persistence layers:

  • SceneSerializer: serializes a scene to JSON
  • GamePackage: groups startup scene, controls config, and asset paths into a .vegame.json

Important implementation detail:

  • MeshRendererComponent stores non-owning Mesh* and IMaterial*
  • because of that, SceneSerializer uses AssetResolver callbacks to convert pointers to string IDs on save and string IDs back to pointers on load
  • if you do not provide a resolver, scenes still load, but mesh/material references are not reconstructed automatically

Save/load a scene

ve::SceneSerializer serializer(scene);
serializer.saveToFile("content/scenes/my_scene.scene.json");
serializer.loadFromFile("content/scenes/my_scene.scene.json");

Create a game package

ve::GamePackage pkg;
pkg.title = "My VoidEngine Project";
pkg.description = "Runtime package for a desktop prototype";
pkg.scenarioId = "my_project";
pkg.startupScene = "content/scenes/my_scene.scene.json";
pkg.controlsConfig = "config/controls.json";
pkg.assetRoot = "content";

pkg.saveToFile("my_project.vegame.json");

This package format is what the editor and runtime use to open a complete playable setup.

Asset Pipeline

The asset pipeline is split into two concerns:

  • baking source files into runtime-friendly formats
  • packing directories into a runtime archive

vebake

vebake converts source assets into engine-specific binary formats:

  • .vemesh: baked mesh, deduplicated vertices, precomputed bounds
  • .vetex: baked texture, offline mip chain, optional lossless compression

Usage:

./build/bin/vebake mesh input.obj output.vemesh
./build/bin/vebake texture input.png output.vetex --srgb

vepack

vepack produces .vepack archives or inspects/extracts them:

./build/bin/vepack assets/ build/install/assets.vepack
./build/bin/vepack --list build/install/assets.vepack
./build/bin/vepack --extract build/install/assets.vepack extracted-assets

These tools are useful when you want runtime loading to avoid repeated expensive parsing of source formats.

Editor Workflow

VoidEditor is the visual front-end for the engine. The current editor stack includes:

  • scene hierarchy panel
  • inspector panel
  • asset browser panel
  • console panel
  • profiler panel
  • undo history panel
  • viewport with Scene and Game tabs
  • transform gizmos with translate/rotate/scale modes
  • snapping for translate/rotate/scale
  • play mode integration

Practical editor flow

  1. Start the editor with no arguments for a clean scene workflow.
  2. Start the editor with a .vegame.json if you want to boot a packaged project immediately.
  3. Use the Scene tab to inspect and edit through the editor camera.
  4. Use the Game tab to inspect the runtime/game camera.
  5. Press Ctrl+P to toggle play mode.

The editor code is not a separate toy UI; it is directly wired into engine-side scene, viewport, undo, and runtime package systems.

Testing

The project uses standalone executables rather than a single ctest suite. That keeps subsystem tests directly runnable.

Example:

cmake --build build --target TestGamePackage
./build/bin/TestGamePackage

The repository currently includes tests for:

  • scenes and generic component APIs
  • rendering and framebuffers
  • shaders and textures
  • physics, triggers, and raycasting
  • audio device, mixer, playback, and music streaming
  • Lua scripting
  • resource and asset pipeline systems
  • editor panels and play mode

Packaging

Install into a packaged layout:

cmake --build build --target install

Create distributable packages with CPack:

cd build
cpack

Current packaging setup:

  • macOS: .app bundle and DMG
  • Windows: NSIS and ZIP
  • Linux: TGZ and DEB

Repository Layout

  • engine/: reusable engine library
  • editor/: editor application
  • demo/: sample runtime application and package bootstrap logic
  • game/: included sample package and config
  • assets/: runtime assets
  • samples/: sample scenes, scripts, and configs
  • tools/: vebake and vepack
  • tests/: standalone subsystem tests
  • cmake/: dependency and packaging setup

Current Status

VoidEngine already covers a serious amount of ground for a public engine repository:

  • engine runtime
  • ECS scene layer
  • renderer
  • editor
  • package format
  • tools
  • tests

At the same time, it is still an actively evolving codebase, not a finished commercial engine. Public documentation should therefore be read as documentation of the current implementation, not a promise that every future API detail will remain unchanged.

Publishing Note

This repository currently does not contain a top-level LICENSE file. If you want to publish it publicly for others to use, fork, or evaluate, adding a license is the next concrete step after this README.

About

Desktop-oriented C++20 game engine with an integrated editor, OpenGL renderer, ECS scenes, physics, audio, Lua scripting, and asset pipeline.

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors