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.tssrc/level.json
Steps
- 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]
}
}
]
}- Replace
src/game.tswith this complete definition. Imports and thevoidreferences keep premade registrations in the compiled guest. A JSON import widensformatto number, so pass it throughvalidateLevel.
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],
});Add more entries using the premade reference. Give entries unique
namevalues foractors.named(name). Usetagsfor groups.yawis degrees about +Y, zero facing +Z; optional quaternionrotation: [x,y,z,w]overrides yaw.attachTonames a parent and makes the child's position/rotation local to it. Forward references are allowed. The editor schema isdocs/schemas/level.schema.json; it describes the format, whilevalidateLevelalso 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 asvalidateLevel(data, { classes: [...] }), or{ classes: 'any' }; those pass with their props unchecked.An entry without a
nameis 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. Optionalscale: [x, y, z]scales the root; parts attached to it follow, a body's dims do not (a box stays its half extents). Optionalplaceat the top of the file names the world the level plays in, a manifest splat id such asenv.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 containsactorsin file order andnamedhandles. It adds actors rather than replacing the previous level. Destroy those returned handles with reasonlevelUnloadwhen unloading, or declare several levels and letctx.levelsswitch 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.