Skip to content

Make your own actor ​

Goal ​

Copy templates/blank. Define a reusable crate with a dynamic root and a mesh component, then place it as data. Keep its contact count in saved actor state.

Files you will edit ​

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

Steps ​

  1. Create src/level.json:
json
{
  "format": 1,
  "actors": [
    {
      "class": "MeshActor",
      "position": [0, -0.5, 0],
      "props": {
        "asset": "cube-muted",
        "scale": [20, 1, 20],
        "collision": "block",
        "halfExtents": [10, 0.5, 10]
      }
    },
    {
      "class": "Crate",
      "name": "crate",
      "position": [0, 4, 0],
      "props": {
        "mass": 2
      }
    }
  ]
}
  1. Replace src/game.ts. Class declarations are safe at module scope; spawning belongs in the level or init. configureComponents sets per-instance body data without changing the component graph.
ts
import { box, defineActor, defineGame, mesh, MeshActor, validateLevel } from 'gameable';
import settings from '../settings.json';
import data from './level.json';

void MeshActor;
const Crate = defineActor('Crate', {
  components: {
    root: box([0.5, 0.5, 0.5], { body: 'dynamic', reportHits: true }),
    look: mesh('cube-mint', { attach: 'root' }),
  },
  props: { mass: 2 },
  tags: ['breakable'],
  configureComponents(props) {
    return {
      root: {
        body: {
          shape: 'box',
          dims: [0.5, 0.5, 0.5],
          kind: 'dynamic',
          mass: props.mass,
          flags: { reportContacts: true },
        },
      },
    };
  },
  construct(self) {
    self.state.contacts = 0;
  },
  onHit(self) {
    self.state.contacts = (self.state.contacts as number) + 1;
  },
});
void Crate;
const eye = { x: 0, y: 5, z: 9 };
const target = { x: 0, y: 1, z: 0 };
const identity = { x: 0, y: 0, z: 0, w: 1 };
export default defineGame({
  features: settings.features,
  world: settings.world,
  assets: ['cube-mint', 'cube-muted'],
  level: validateLevel(data),
  init(ctx) {
    ctx.camera.set(eye, identity);
    ctx.camera.lookAt(target);
  },
});
  1. Add hooks only when needed: beginPlay for first-step work, tick for fixed updates, and endPlay for cleanup. Preallocate scratch outside tick; put JSON-safe persistent instance data in self.state. To add interaction, include interactable() as a component, declare the Interactable props, and supply onInteract(self, who, ctx). To reuse an existing actor, defineActor('MyPickup', { ...PickupActor.spec, onPickedUp(...) {} }) preserves its built-in behaviour and adds your callback (write a real callback body when using this pattern).

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 crate falls onto the floor and its mesh follows its body. In a host with contacts enabled, inspect actors.named('crate')?.state.contacts: it increases on reported contacts. A snapshot/restore keeps that value.

See also ​