An entity component system (ECS) Vulkan game engine written in Rust
Find a file
2026-07-02 22:06:58 +01:00
.claude stuff 2026-06-22 10:31:13 +01:00
core Removed: manual serilsation and deserialsaition of components 2026-07-02 22:06:58 +01:00
documents Push 2026-06-26 19:31:08 +01:00
editor Fixed: minor issues with the editor raycast and terrain system 2026-07-01 23:57:13 +01:00
game Added: hail support 2026-07-02 21:33:10 +01:00
macros Removed: manual serilsation and deserialsaition of components 2026-07-02 22:06:58 +01:00
project Fixed: minor issues with the editor raycast and terrain system 2026-07-01 23:57:13 +01:00
voxel_game Removed: manual serilsation and deserialsaition of components 2026-07-02 22:06:58 +01:00
.gitignore Push 2026-06-26 19:31:08 +01:00
.luarc.json Added: basic lua docs 2026-06-24 22:32:50 +01:00
Cargo.lock Added: hail support 2026-07-02 21:33:10 +01:00
Cargo.toml Added: UI to lua, fixed issues with buffers not clering 2026-06-30 21:45:10 +01:00
CLAUDE.md Removed: manual serilsation and deserialsaition of components 2026-07-02 22:06:58 +01:00
frag.spv Added: more textures 2026-06-18 22:44:37 +01:00
README.md Hastily updated readme 2026-06-26 19:38:36 +01:00
rewrite.md Changed: Checked all code and it's comments, made several corrections to the comments 2026-06-11 16:00:33 +01:00
shell.nix Changed: nix.shell 2026-05-14 12:28:59 +01:00

Apostasy Engine

Apostasy is a Rust game engine built for a Morrowind and Vintage Story inspired voxel RPG. It targets Vulkan and is designed around open-world RPG systems, voxel worlds, and scene-driven gameplay while staying data-driven.

What the engine does

  • Windowing and render loops via winit
  • Shared World containing entities, components, tags, and resources
  • System registration at compile time (#[start], #[update], #[fixed_update], #[late_update], #[prerender])
  • Asset loading including glTF models, YAML materials, and custom asset loaders
  • Vulkan rendering backend with offscreen viewport, shadow maps, and frustum culling
  • Chunk-based heightmap terrain and 32³ voxel chunks with threaded meshing
  • Physics: AABB collision, raycasting, velocity integration
  • Lua scripting with hot-reload, custom components/resources, and full world API
  • egui-based editor UI with dockable viewport

Workspace layout

Crate Purpose
core Engine runtime: ECS, rendering, terrain, voxels, physics, UI
macros Proc-macros: #[derive(Component)], #[derive(Resource)], #[derive(Tag)], system attributes
editor Scene editor binary
game Game binary
voxel_game Standalone voxel demo binary
script_api Rust library for scripts compiled to WASM (separate from Lua scripting)

Getting started

fn main() {
    init_core(
        RenderingBackend::Vulkan,
        vec![Packages::Voxel, Packages::Terrain],
    )
    .unwrap();
}

Packages are optional feature bundles (Packages::Voxel, Packages::Terrain, Packages::ItemSystem) that register their own startup systems and resources.

Entities and Components

The engine uses an ECS-style world. Entities are spawned via world.spawn() and identified by a stable EntityId.

let id = world.spawn()
    .with_component(Transform::default())
    .with_component(Velocity::default())
    .with_component(Collider::player())
    .with_tag(Player)
    .build();
  • Component — data attached to an entity; derive #[derive(Component)]
  • Tag — zero-size marker for querying; derive #[derive(Tag)]
  • Resource — singleton data on the world; derive #[derive(Resource)]
// Query entities
let ids = world.get_entities_with_component::<Transform>();
let cam = world.get_entity_with_tag::<ActiveCamera>()?;

// Access components
let t = world.get_component::<Transform>(id);
let t = world.get_component_mut::<Transform>(id);

// Resources
world.insert_resource(ShouldExit);
let dt = world.get_resource::<DeltaTime>()?.0;

Systems

Register systems globally at compile time:

#[start]
fn setup(world: &mut World) -> Result<()> { ... }

#[update]
fn tick(world: &mut World) -> Result<()> { ... }

#[fixed_update]
fn physics(world: &mut World, delta: f32) -> Result<()> { ... }

Optional attributes: mode = "game" | "editor" | "all", priority = <u32>.

Lua Scripting

Apostasy supports runtime Lua scripting via mlua. Scripts are plain .lua files placed in res/scripts/ and loaded automatically at startup. The engine hot-reloads any script whose file changes on disk each frame.

Scripts declare top-level functions that the engine calls at the appropriate lifecycle stage:

-- res/scripts/my_script.lua

-- Declare a custom Lua-side component with default fields.
register_component("Orbit", { radius = 5.0, speed = 30.0, angle = 0.0 })

-- Declare a Lua-side global resource.
register_resource("Config", { paused = false })

function start(world)
    local e = world:spawn()
    world:set_name(e, "Planet")
    world:add_component(e, "Transform", { local_position = { 6, 0, 0 } })
    world:add_component(e, "Orbit", { radius = 6.0, speed = 40.0 })
end

function update(world)
    local dt = world:delta()
    world:query("Orbit"):for_each(function(id, orbit)
        orbit.angle = (orbit.angle + orbit.speed * dt) % 360.0
        local rot = quat.from_axis_angle(vec3.up, orbit.angle)
        world:set_component(id, "Transform", { local_position = rot:rotate(vec3(orbit.radius, 0, 0)) })
        world:set_component(id, "Orbit", orbit)
    end)
end

function fixed_update(world, delta) end
function late_update(world) end
function prerender(world) end

World API (available on the world argument)

Method Description
world:spawn() Spawn an entity, returns an Entity handle
world:spawn_at_position(vec3) Spawn into the cell containing a position
world:despawn(entity) Remove an entity
world:set_name(entity, name) Set entity display name
world:get_name(entity) Get entity display name
world:add_component(entity, "Name", {fields}) Add a component with field values
world:set_component(entity, "Name", {fields}) Update component fields
world:get_component(entity, "Name") Read component fields as a table
world:add_tag(entity, "TagName") Add a tag
world:has_tag(entity, "TagName") Check if entity has a tag
world:set_parent(entity, parent) Set parent entity
world:get_children(entity) List of child entities
world:get_ancestors(entity) List of ancestor entities
world:get_all_entities() All entity handles in the world
world:query("ComponentName") Returns a query; call :for_each(fn(id, data))
world:raycast(origin, dir, max_dist, exclude) Raycast; returns {entity, distance, normal} or nil
world:get_resource("Name") Get a Lua resource table
world:set_resource("Name", table) Update a Lua resource
world:set_material_color("MatName", {r,g,b}) Override a material's color at runtime
world:log(msg) Print to the engine log
world:delta() Seconds since last frame
world:time() Total elapsed engine time in seconds

Math globals

The prelude exposes vec2, vec3, vec4, and quat as global constructors with operator overloading (+, -, *, /, unary -).

local p = vec3(1, 0, 0)
local q = quat.from_axis_angle(vec3.up, 45.0)
local rotated = q:rotate(p)
local d = vec3.forward:dot(vec3.up)  -- 0
Global Constants / helpers
vec3 .zero, .one, .up, .down, .forward, .back, .left, .right; :length(), :normalized(), :dot(), :cross(), :lerp()
quat .identity(), .from_axis_angle(axis, degrees), .from_euler(x, y, z); :rotate(vec3), :normalized(), :inverse()
math Extended with math.clamp, math.lerp, math.sign

Build & run

# Editor
cargo run --manifest-path editor/Cargo.toml

# Voxel demo
cargo run --manifest-path voxel_game/Cargo.toml

# Game
cargo run --manifest-path game/Cargo.toml

Requirements

  • Rust stable, edition 2024
  • Vulkan-capable GPU and drivers
  • cargo on PATH

Current limitations

Apostasy is not a finished engine:

  • Limited shader variety
  • No audio system
  • Editor is still early-stage
  • Performance is not yet optimized