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.tssrc/level.json
Steps
- 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
}
}
]
}- Replace
src/game.ts. Class declarations are safe at module scope; spawning belongs in the level orinit.configureComponentssets 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);
},
});- Add hooks only when needed:
beginPlayfor first-step work,tickfor fixed updates, andendPlayfor cleanup. Preallocate scratch outsidetick; put JSON-safe persistent instance data inself.state. To add interaction, includeinteractable()as a component, declare the Interactable props, and supplyonInteract(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.