For open worlds with more objects than fit in memory at once: villages, forts, ruins,
loot, NPCs spread over kilometres. You write the placements to a file; the engine loads
the 128 m cells around the camera, draws the scenery, and tells the game when its
gameplay objects come near and go away. Pair it with terrain_world for the ground
(GANDER terrain, "Streamed worlds").
The file
assets/world/<name>.txt, one line each (write it from a script for big worlds):
cell 128 # cell size in meters (default 128)
load 300 # objects within this of the camera load (default 300; 800 in Pro builds)
far 2000 # scenery beyond `load` and up to here shows as impostor cards (default 2000)
model house assets/stream/models/house.glb solid
model barrel assets/stream/models/barrel.glb
model guard assets/stream/models/guard.glb
obj house 120.5 ground 33.2 yaw=90 scale=1.2
obj barrel 118 ground 30 tag=loot gold=25
obj guard 125 ground 36 tag=guard route=north hp=80
obj - 100 12 40 tag=spawn team=red
model NAME PATH [solid]: models underassets/stream/download only when a cell needs them;solidgives every object of that model a static mesh collider while its cell is loaded.obj MODEL X Y Z [yaw=deg] [scale=s] [tag=word] [key=value ...]:Ycan beground(stands on the terrain passed toworld_open);-as the model is an object with no model (spawn points, triggers, zone markers).- An object without a tag is scenery: the engine loads, draws, collides and unloads it. An object with a tag is gameplay: the game spawns and draws its own state for it.
The game side
var world: gander::World;
var live: i32[>..] = []; // tagged objects in loaded cells
fn main() {
land = gander::terrain_world("highlands", gen);
world = gander::world_open("assets/world/highlands.txt", land);
}
export "gander_frame" fn frame(dt: f32) {
// ... camera ...
gander::draw_terrain(land);
gander::world_draw(world); // loads cells, draws scenery and far cards
var e = gander::world_event(world);
while e.kind != gander::WORLD_NONE {
if e.kind == gander::WORLD_ENTER {
if gander::world_obj_is(world, e.obj, "guard") { spawn_guard(e.obj); }
live.push(e.obj);
} else {
save_and_drop(e.obj); // its cell unloaded: keep what matters
}
e = gander::world_event(world);
}
for i: i64 in 0..live.len { // tagged objects are the game's to draw
let m = gander::world_obj_model(world, live[i]);
if m.id != 0 { gander::draw_model(m, gander::world_obj_xform(world, live[i])); }
}
}
- Object ids (
e.obj) are stable (the line order in the file): key the game's own state by them, so a looted chest stays looted when its cell comes back. Store that in the save (GANDER saves) as a list of ids. world_obj_get(world, obj, "gold", out)reads a value from the line;world_obj_pos,world_obj_yaw,world_obj_scale,world_obj_xform,world_obj_model,world_obj_tag.world_source(world, i, pos)(i 0..7) loads cells around more places than the camera: other players in multiplayer, a convoy far ahead.world_source_off(world, i)stops it.world_radius(world, load, far)overrides the file's radii.- Probes:
world_cells_loaded,world_drawn(scenery models),world_cards(far cards).
Rules
- Never keep gameplay state only on a tagged object between its LEAVE and the next ENTER: the engine forgets it. Keep it in the game, keyed by the object id.
- Do not draw scenery yourself: the engine does, and turns it into cards past
load. - Big worlds need
camera_clip(0.1, 6000.0)or the far scenery and terrain are clipped. - Spawn the player once
terrain_ground_ready(land, x, z)is true.