Skip to content

Place actors from a level file ​

Goal ​

Copy templates/blank into your game. Place a floor and a PlayerStart in src/level.json; keep behaviour in src/game.ts. WASD moves a mint cube in world axes, and the built-in third-person camera follows it.

Once the world collider is loaded, placed actors with characterMovement (including a placed Character) are lifted when their capsule feet are inside the floor, with a [level] warning; missing floor is an error. A character falling more than 5 m below both the world spawn and its lowest observed floor logs a [physics] error once per actor per world load.

In level.json a placed character's class is Character, the level class name in Premade actors. CharacterPawn is its name in code (spawnActor(ctx, CharacterPawn, ...)); a level file that names CharacterPawn is refused by validateLevel and the game's build fails.

Files you will edit ​

  • src/game.ts
  • src/level.json

Steps ​

  1. Create src/level.json. Positions are metres, Y is up. A box uses half extents; its visible scale and collider size are authored independently.
json
{
  "format": 1,
  "actors": [
    {
      "class": "PlayerStart",
      "position": [0, 1.2, 0]
    },
    {
      "class": "MeshActor",
      "position": [0, -0.5, 0],
      "props": {
        "asset": "cube-muted",
        "scale": [20, 1, 20],
        "collision": "block",
        "halfExtents": [10, 0.5, 10]
      }
    }
  ]
}
  1. Replace src/game.ts with this complete definition. Imports and the void references keep premade registrations in the compiled guest. A JSON import widens format to number, so pass it through validateLevel.
ts
import {
  defineGame,
  prefab,
  PlayerStart,
  MeshActor,
  validateLevel,
  type GameContext,
} from 'gameable';
import settings from '../settings.json';
import data from './level.json';

// Register the level's classes in the guest bundle.
void PlayerStart;
void MeshActor;
const Hero = prefab({
  asset: 'cube-mint',
  scale: [0.6, 1.8, 0.6],
  body: { shape: 'capsule', dims: [0.4, 0.5], kind: 'character' },
});
function walk(ctx: GameContext): void {
  const move = ctx.input.axis2('KeyA', 'KeyD', 'KeyS', 'KeyW');
  ctx.physics.moveCharacter(ctx.player, move.x * 4, 0, -move.y * 4);
}

export default defineGame({
  features: settings.features,
  world: settings.world,
  assets: ['cube-mint', 'cube-muted'],
  level: validateLevel(data),
  player: { prefab: Hero, spawn: 'playerStart', camera: 'thirdPerson' },
  systems: [walk],
});
  1. Add more entries using the premade reference. Give entries unique name values for actors.named(name). Use tags for groups. yaw is degrees about +Y, zero facing +Z; optional quaternion rotation: [x,y,z,w] overrides yaw. attachTo names a parent and makes the child's position/rotation local to it. Forward references are allowed. The editor schema is docs/schemas/level.schema.json; it describes the format, while validateLevel also checks registered class prop names. A tool that reads a level without loading the game (the sandbox on Node) passes the game's own classes as validateLevel(data, { classes: [...] }), or { classes: 'any' }; those pass with their props unchecked.

    An entry without a name is still named, from the file alone: Door1, Door2, ... by class in file order, skipping names the file uses. The sandbox's edit mode edits actors by that name and writes it into the file the first time it touches one, so give the actors people will move names you would say out loud. Optional scale: [x, y, z] scales the root; parts attached to it follow, a body's dims do not (a box stays its half extents). Optional place at the top of the file names the world the level plays in, a manifest splat id such as env.cave; left out, the page's default place.

    Every actor a level placed carries source: { level, entry } on its handle, which is what makes it editable in the sandbox; an actor spawned from code has none and is shown locked. So place what a person might want to move, turn, scale or remove in the level file, and spawn from code only what is born during play (projectiles, drops, waves).

    For loading another document during play, call loadLevel(ctx, level); its result contains actors in file order and named handles. It adds actors rather than replacing the previous level. Destroy those returned handles with reason levelUnload when unloading, or declare several levels and let ctx.levels switch them: switch levels at runtime.

Verify ​

From the copied game root run npx tsc --noEmit -p tsconfig.json, then npm run build (in this checkout use npm run build -w templates/blank). Blank is a bundle template, with no dev-server script: open the resulting bundle in the common Gameable player used by your app (examples/play/README.md describes local hosting).

Check it: the cube starts above the floor, settles on it and moves with WASD. Change PlayerStart X to 3 and rebuild: the new run starts there. Temporarily change a class to MissingActor: validation reports its JSON path and known classes before any level actor is spawned.

See also ​