Home/GANDER docs/GANDER streamed worlds

GANDER streamed worlds

Open worlds of placed objects (villages, forts, loot, NPCs) that stream in 128 m cells around the camera.

world.mdllms-full.txt

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 under assets/stream/ download only when a cell needs them; solid gives 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 ...]: Y can be ground (stands on the terrain passed to world_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.