Skip to content

Add a door that needs a key ​

For a kit door in one level entry, use HingedDoor with lockedBy: 'key': pick the right door.

Goal ​

Copy templates/blank. Place a key pickup and a locked sliding door. Both actors use one inventory, indexed by player seat; solo uses seat zero.

Files you will edit ​

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

Steps ​

  1. Create src/level.json. The item id key is inventory data; the mesh id sphere-mint comes from the template's asset manifest.
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]
      }
    },
    {
      "class": "Pickup",
      "position": [2, 0.9, -2],
      "props": {
        "asset": "sphere-mint",
        "give": {
          "item": "key",
          "count": 1
        },
        "bob": 0
      }
    },
    {
      "class": "Door",
      "name": "gate",
      "position": [0, 1, -4],
      "props": {
        "asset": "cube-mint",
        "lockedBy": "key",
        "consumeKey": true,
        "openOffset": [0, 3, 0],
        "seconds": 1,
        "autoCloseSeconds": 3,
        "facingDegrees": 180
      }
    }
  ]
}
  1. Replace src/game.ts. Keep configuration in init, before the first actor step. Loading the level happens before init, but collection starts later. Preserve the interaction component's HUD writer by avoiding another HUD model in this example.
ts
import {
  defineGame,
  prefab,
  PlayerStart,
  MeshActor,
  validateLevel,
  PickupActor,
  Door,
  configureGameplayActors,
  createInventory,
  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;
void PickupActor;
void Door;
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);
}
const bag = createInventory({ owners: 1, slots: 4, items: [{ id: 'key' }] });

export default defineGame({
  features: settings.features,
  world: settings.world,
  assets: ['cube-mint', 'cube-muted', 'sphere-mint'],
  level: validateLevel(data),
  player: { prefab: Hero, spawn: 'playerStart', camera: 'thirdPerson' },
  init(ctx) {
    configureGameplayActors(ctx, { inventory: bag });
  },
  systems: [walk],
});
  1. lockedBy clears after a successful unlock; consuming a key does not relock the door after it closes. openYaw selects rotation instead of openOffset translation when nonzero. The stock door collider is fixed at [0.5,1,0.15] half extents; its optional mesh uses its own asset dimensions. For rooms, provision inventory owners for all seat indices.

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: approach within two metres before collecting the key: the prompt is Locked and E does nothing. Collect the sphere, return, and press E: the door slides up over one second. It closes three seconds after opening fully, and E can reopen it without another key. bag.count(0, 'key') is now zero.

See also ​