Build a Simple World
Create a running 3D scene from scratch using the Aether ECS, physics, and renderer.
In this tutorial you will build a complete Aether world from an empty Rust project. By the end you will have entities with position, velocity, and mesh components, a physics simulation, and a software renderer drawing the scene.
Prerequisites
- Rust stable toolchain installed (rustup.rs)
- Aether source cloned and built (
cargo buildfrom the workspace root)
Create the Project
- Create a new binary crate inside the Aether workspace:
cargo new --bin my-first-world
- Add the required Aether crates to
my-first-world/Cargo.toml:
[package]
name = "my-first-world"
version = "0.1.0"
edition = "2021"
[dependencies]
aether-ecs = { path = "../crates/aether-ecs" }
aether-physics = { path = "../crates/aether-physics" }
aether-renderer = { path = "../crates/aether-renderer" }
- Register the crate in the workspace
Cargo.toml:
[workspace]
members = [
# ... existing members
"my-first-world",
]
Initialize the World and Define Components
- Open
my-first-world/src/main.rs. The ECSWorldis the top-level container for entities, components, and systems. Components are plain data structs that must be'static + Send + Sync:
use aether_ecs::{World, Entity};
#[derive(Clone, Debug)]
struct Position { x: f32, y: f32, z: f32 }
#[derive(Clone, Debug)]
struct Velocity { x: f32, y: f32, z: f32 }
#[derive(Clone, Debug)]
struct Mesh { vertex_count: u32, name: String }
Spawn Entities
- In
main, create the world and spawn a ground plane and a falling cube:
fn main() {
let mut world = World::new();
let ground = world.spawn();
world.add_component(ground, Position { x: 0.0, y: 0.0, z: 0.0 });
world.add_component(ground, Mesh { vertex_count: 4, name: "ground_plane".into() });
let cube = world.spawn();
world.add_component(cube, Position { x: 0.0, y: 10.0, z: 0.0 });
world.add_component(cube, Velocity { x: 0.0, y: 0.0, z: 0.0 });
world.add_component(cube, Mesh { vertex_count: 24, name: "cube".into() });
let sphere = world.spawn();
world.add_component(sphere, Position { x: 5.0, y: 8.0, z: 0.0 });
world.add_component(sphere, Velocity { x: -1.0, y: 0.0, z: 0.0 });
world.add_component(sphere, Mesh { vertex_count: 128, name: "sphere".into() });
}
Each world.spawn() allocates a generational entity ID. Components are stored in archetype-based column storage for cache-efficient iteration.
Add Physics Systems
Systems are functions that query the world for entities matching a component signature.
- Write gravity, movement, and ground-collision systems:
const GRAVITY: f32 = -9.81;
const FIXED_DT: f32 = 1.0 / 60.0;
fn gravity_system(world: &mut World) {
for (_entity, velocity) in world.query_mut::<&mut Velocity>() {
velocity.y += GRAVITY * FIXED_DT;
}
}
fn movement_system(world: &mut World) {
for (_entity, (pos, vel)) in world.query_mut::<(&mut Position, &Velocity)>() {
pos.x += vel.x * FIXED_DT;
pos.y += vel.y * FIXED_DT;
pos.z += vel.z * FIXED_DT;
}
}
fn ground_collision_system(world: &mut World) {
for (_entity, (pos, vel)) in world.query_mut::<(&mut Position, &mut Velocity)>() {
if pos.y < 0.0 { pos.y = 0.0; vel.y = 0.0; }
}
}
Integrate Rapier3D Physics
For production, Aether wraps Rapier3D. Replace the manual systems with the engine integration.
- Configure the physics world and register rigid bodies:
use aether_physics::{WorldPhysicsConfig, PhysicsWorld, RigidBodyType};
fn setup_physics() -> PhysicsWorld {
let config = WorldPhysicsConfig {
gravity: [0.0, -9.81, 0.0],
time_step: 1.0 / 60.0,
max_velocity: 100.0,
enable_ccd: false,
solver_iterations: 4,
};
PhysicsWorld::new(config)
}
- Each tick, step the physics world and write results back to ECS:
fn physics_step(world: &mut World, physics: &mut PhysicsWorld) {
physics.sync_from_ecs(world);
physics.step();
physics.sync_to_ecs(world);
}
Add a Renderer
Aether ships a software rasterizer for prototyping that requires no GPU driver.
- Initialize the renderer and define a camera:
use aether_renderer::{SoftwareRenderer, RenderConfig, Camera};
fn setup_renderer() -> SoftwareRenderer {
SoftwareRenderer::new(RenderConfig { width: 800, height: 600, fov_degrees: 75.0 })
}
fn setup_camera() -> Camera {
Camera {
position: [0.0, 5.0, -15.0], look_at: [0.0, 2.0, 0.0],
up: [0.0, 1.0, 0.0], fov: 75.0, near: 0.1, far: 1000.0,
}
}
- Write a render system that draws every entity with
PositionandMesh:
fn render_system(world: &World, renderer: &mut SoftwareRenderer, camera: &Camera) {
renderer.begin_frame(camera);
for (_entity, (pos, mesh)) in world.query::<(&Position, &Mesh)>() {
renderer.draw_mesh(&mesh.name, [pos.x, pos.y, pos.z]);
}
renderer.end_frame();
}
Run the World Loop
- Tie everything together with a fixed-timestep loop:
use std::time::{Duration, Instant};
const TICK_RATE: u32 = 60;
const TICK_DURATION: Duration = Duration::from_nanos(1_000_000_000 / TICK_RATE as u64);
const MAX_TICKS: u32 = 300;
fn main() {
let mut world = World::new();
// ... spawn entities from step 5 ...
let mut physics = setup_physics();
let mut renderer = setup_renderer();
let camera = setup_camera();
let mut tick: u32 = 0;
loop {
let frame_start = Instant::now();
gravity_system(&mut world);
movement_system(&mut world);
ground_collision_system(&mut world);
render_system(&world, &mut renderer, &camera);
tick += 1;
if tick >= MAX_TICKS { break; }
let elapsed = Instant::now() - frame_start;
if elapsed < TICK_DURATION { std::thread::sleep(TICK_DURATION - elapsed); }
}
println!("Simulation complete after {} ticks", tick);
}
- Run the project:
cargo run -p my-first-world
What Happens at Runtime
Each tick the engine performs these steps in order:
- Gravity -- Every entity with
Velocityaccelerates downward. - Movement -- Position integrates by
velocity * dt. - Ground collision -- Entities below
y = 0are clamped. - Render -- The software renderer draws all
(Position, Mesh)pairs.
The cube starts at y = 10, falls under gravity, and stops at y = 0. The sphere arcs sideways while falling.
Next Steps
- Replace the software renderer with the GPU backend (
aether-rendererwgpu) - Add keyboard input via
aether-input - Add networking (see Add Multiplayer)
- Build game logic visually (see Visual Scripting 101)