Load a made world
Goal
A world package (a folder with world.json) replaces the template arena. The hero stands at the world's start; loose things can be pushed and make sounds; game code can find things by name.
Files you will edit
src/main.tssrc/systems/world.ts
Steps
Put the package in
public/worlds/forgotten-attic/and name it insrc/main.ts. The third-person template wires the world module to the game; add its address:tsconst WORLD_URL = 'worlds/forgotten-attic/';Write the address without a leading slash; the engine also accepts
/worlds/<name>/and reads it from the game's folder. For custom URLs, usegameUrl()fromgameable/assets.The template reads
VITE_WORLDfrom.env.local, so the address can stay out of code. With it set, the host loads the world instead of the arena:tsimport { createWorldBridge, world, type WorldBridge } from 'gameable/world'; // modules: [physics(), input(), audio(), splat(), world(), ...] await engine.get('world').load(WORLD_URL); let worldBridge: WorldBridge | null = null; const adapter = createEngineAdapter(engine, { modules, characters, thing: (command) => worldBridge?.handle(command), }); worldBridge = createWorldBridge({ world: engine.get('world'), events: adapter.events });In the visit template the same package is one input:
?world=worlds/forgotten-attic/world.json(orworldin the lookup's JSON) in place of the place's own fields.Build your level from the world's things in
src/systems/world.ts. The system there already stands the hero at the world's start when theloadedevent arrives and takes the arena's own props out. Ask thethingsfacade for the rest; it answers inside the guest, with no call to the host:tsimport { things, type GameContext } from 'gameable'; const heroAt = { x: 0, y: 0, z: 0 }; export function nearTheTrunk(ctx: GameContext): boolean { const trunk = things.find('the trunk'); if (trunk === null) return false; // Where to stand to use it, and what the hero is beside now. const spot = things.standFor(trunk); const beside = things.near(heroAt, 1.2)[0]; return beside === trunk || Math.hypot(spot.x - heroAt.x, spot.z - heroAt.z) < 0.5; }Knocks and moves arrive as events, and a loose thing takes commands:
tsexport function hearTheWorld(ctx: GameContext): void { for (const event of ctx.events) { if (event.tag !== 'world-event' || event.val.kind !== 'hit') continue; const knocked = things.all[event.val.thing]; if (knocked.kind === 'bottle' && event.val.strength > 0.6) things.reset(knocked); } }
Verify
sh
npm run devThe made world is on screen with the hero at its start; walking into a loose thing pushes it and plays its sound; the walls stop you. Then assert it headlessly, as tests/world.test.ts does: hand the game a world-event of kind loaded and check where the hero went.
sh
npm testts
expect(move.val.position.x).toBeCloseTo(list.spawn.x);To see every thing's name over it, open examples/world-walk on the same folder (GAMEABLE_WORLDS_DIR=<folder> npm run dev -w examples/world-walk) and switch Names on. A world whose loose things come as points too shows a second switch there, Things as points; in a game it is one call, engine.get('world').drawThingsAs('splat') (or 'mesh', or 'record': as the package says).
A package of one thing and no place (a thing by itself) loads by the same call, and things.world.kind is 'object'. Pass lite: true on a phone for the thing's lighter copy:
ts
const made = await engine.get('world').load('worlds/pink-menace/', { lite: onAPhone });
console.log(made.plain, made.copies); // true, 'full' on a desk or 'lite' on a phoneSee also
- Worlds — the package's format
packages/world/README.md— gameable/world- Load a splat environment — a bare place with no things
- Add a physics body