World Runtime
How the Aether world runtime manages world lifecycle, chunk streaming, tick scheduling, input buffering, and client-side prediction.
The aether-world-runtime crate is the core simulation layer for Aether multiplayer worlds. It manages the complete world lifecycle from manifest loading through authoritative tick-based simulation, chunk streaming with level-of-detail, player input processing, and state synchronization. This guide covers how these systems work together to deliver a consistent multiplayer VR experience.
Overview
The world runtime provides a server-authoritative simulation model. The server is the single source of truth for all game state. Clients send inputs to the server, which validates and applies them, then broadcasts the resulting state. Client-side prediction and interpolation hide network latency from the player.
The runtime is composed of seven modules:
| Module | Responsibility |
|---|---|
tick | Fixed-rate server tick loop with time accumulator |
input_buffer | Per-player input buffering and validation |
prediction | Entity state interpolation and prediction snapshots |
state_sync | Delta-based entity state broadcast |
rpc | Typed request/response dispatch |
session | Player session lifecycle management |
events | Interest-based game event distribution |
All modules are pure data-driven with no async runtime or threading. The caller drives the tick loop externally.
World Lifecycle
A world instance progresses through a defined lifecycle:
- Manifest loading -- The world manifest declares the initial chunk layout, spawn points, entity prefabs, and scripting modules.
- Initialization -- The runtime loads the manifest, allocates chunk storage, and prepares the tick scheduler.
- Active simulation -- The tick loop runs, processing player inputs and advancing game state.
- Shutdown -- The world persists final state and terminates cleanly.
Tick Scheduling
The TickScheduler implements a fixed-timestep simulation using a time accumulator pattern. The caller provides elapsed wall-clock time, and the scheduler determines how many simulation ticks to run:
use aether_world_runtime::tick::TickScheduler;
let mut scheduler = TickScheduler::new(60); // 60 Hz tick rate
// In the main loop, feed elapsed microseconds
let ticks = scheduler.update(elapsed_us);
for tick in ticks {
// Process this tick: read inputs, simulate, broadcast state
process_tick(&tick);
}
The scheduler caps the number of ticks per update to prevent a spiral-of-death. If the server falls behind (e.g., due to a GC pause or load spike), it processes up to max_ticks_per_update and discards excess accumulated time.
Tick Pipeline
Each tick follows a consistent pipeline:
- Collect player inputs for the current tick number from the input buffer.
- Validate and apply inputs to the authoritative world state.
- Run physics simulation.
- Execute scripts.
- Generate delta snapshots for state synchronization.
- Dispatch events and RPC responses.
Input Buffering
The InputBuffer stores incoming player inputs in per-player circular buffers indexed by tick number:
use aether_world_runtime::input_buffer::{InputBuffer, PlayerInput};
let mut buffer = InputBuffer::new(64); // 64-frame buffer per player
// Queue an input from a client
buffer.push(player_id, PlayerInput {
tick: 1042,
movement: Vec3::new(0.0, 0.0, 1.0),
actions: vec![Action::Jump],
})?;
// Retrieve all inputs for a specific tick
let inputs = buffer.inputs_for_tick(1042);
The buffer enforces ordering (tick numbers must be monotonically increasing), rejects duplicates, and handles buffer overflow by dropping the oldest frames.
Chunk-Based Streaming with LOD
Worlds are divided into spatial chunks. The runtime streams chunks to clients based on proximity, using level-of-detail (LOD) tiers to reduce bandwidth:
- Full detail -- Chunks within the player's immediate vicinity. All entities and geometry are sent at full resolution.
- Reduced detail -- Nearby chunks with simplified geometry and fewer entity updates.
- Minimal detail -- Distant chunks with only landmark geometry and no dynamic entity data.
- Unloaded -- Chunks beyond the streaming radius. No data is sent.
The LOD tier for each chunk is recalculated every tick based on the player's current position.
Client-Side Prediction
To hide network latency, clients predict the outcome of their own inputs locally while waiting for server confirmation. The InterpolationBuffer manages timestamped entity state snapshots:
use aether_world_runtime::prediction::InterpolationBuffer;
let mut buffer = InterpolationBuffer::new(32); // Store up to 32 snapshots
// Add a server-confirmed snapshot
buffer.push(EntityState {
tick: 1040,
position: Vec3::new(10.0, 0.0, 5.0),
rotation: Quat::IDENTITY,
velocity: Vec3::new(1.0, 0.0, 0.0),
});
// Interpolate between snapshots at a given time fraction
let interpolated = buffer.interpolate(0.5);
The interpolation uses lerp for position and velocity, and slerp for quaternion rotation.
Prediction Reconciliation
When the server state arrives, the client compares it against its predicted state. If the difference exceeds a threshold, the client applies a correction:
- Store each predicted state alongside the input that produced it.
- When the server confirms a tick, compare the server state to the predicted state for that tick.
- If they match (within tolerance), discard old predictions.
- If they diverge, snap or smoothly correct the entity to the server state and replay subsequent inputs.
State Synchronization
The StateSyncManager tracks per-entity state and generates delta snapshots for each client:
use aether_world_runtime::state_sync::StateSyncManager;
let mut sync = StateSyncManager::new();
// After each tick, generate deltas for a specific client
let deltas = sync.generate_deltas(client_id);
for delta in deltas {
transport.send(client_id, delta, delta.channel())?;
}
// When the client acknowledges a tick, update tracking
sync.acknowledge(client_id, acked_tick);
Each entity maintains a "last acknowledged tick" per client. On each tick, the manager compares the current state against the last-acknowledged state to produce a minimal diff.
Channel Types
State updates are sent over two channel types:
| Channel | Use Case | Delivery |
|---|---|---|
| Reliable | Position, health, inventory changes | Guaranteed, ordered |
| Unreliable | Velocity, rotation updates | Latest-wins, no retransmission |
RPC System
The runtime provides a typed request/response RPC mechanism for client-server communication:
use aether_world_runtime::rpc::{RpcDispatcher, RpcRequest, RpcResponse};
let mut dispatcher = RpcDispatcher::new();
// Register a handler on the server
dispatcher.register("open_door", |req: RpcRequest| {
let door_id: u64 = req.parse_arg(0)?;
// Validate and open the door...
Ok(RpcResponse::success(()))
});
// Dispatch an incoming request
let response = dispatcher.dispatch(request)?;
RPCs support both client-to-server and server-to-client directions.
Player Session Management
The SessionManager tracks player lifecycle through a state machine: Connecting -> Active -> Disconnected -> Reconnecting -> Active (or Removed).
use aether_world_runtime::session::SessionManager;
let mut sessions = SessionManager::new(30_000); // 30s reconnect window
sessions.connect(player_id, connection_metadata)?;
sessions.disconnect(player_id);
sessions.reconnect(player_id, new_connection)?; // restore within window
Event Distribution
Game events are broadcast to interested clients based on scope. Three scopes are supported: Global (all clients), NearEntity (clients near a specific entity), and Player (a single client).
use aether_world_runtime::events::{EventDispatcher, GameEvent, EventScope};
let mut events = EventDispatcher::new();
events.push(GameEvent::new("world_announcement", EventScope::Global));
events.push(GameEvent::new("explosion", EventScope::NearEntity(entity_id)));
events.push(GameEvent::new("quest_complete", EventScope::Player(player_id)));
let deliveries = events.drain();
Key Types
| Type | Description |
|---|---|
TickScheduler | Fixed-timestep tick loop with time accumulator |
InputBuffer | Per-player circular buffer for input frames |
InterpolationBuffer | Timestamped entity state snapshots for interpolation |
StateSyncManager | Delta snapshot generation with per-client ack tracking |
RpcDispatcher | Typed RPC handler registry and dispatch |
SessionManager | Player lifecycle state machine with reconnect support |
EventDispatcher | Scoped game event distribution |
PlayerInput | A single frame of player input tied to a tick number |
EntityState | Position, rotation, and velocity snapshot for an entity |