Spawn enemies in waves
Goal
Copy templates/blank. Place two TargetPoints and a Spawner. Two waves produce pink enemy actors; K destroys one for this isolated exercise.
Files you will edit
src/game.tssrc/level.json
Steps
- Create
src/level.json.pointsis a TargetPoint tag; it is not an array of coordinates.delayis a pre-wave delay in seconds.
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": "TargetPoint",
"position": [-2, 1, 0],
"tags": ["enemy-spawns"]
},
{
"class": "TargetPoint",
"position": [2, 1, 0],
"tags": ["enemy-spawns"]
},
{
"class": "Spawner",
"name": "waves",
"position": [0, 1, 0],
"props": {
"actorClass": "WaveEnemy",
"points": "enemy-spawns",
"waves": [
{
"count": 2,
"every": 0.5
},
{
"count": 3,
"every": 0.5,
"delay": 1
}
],
"maxAlive": 2
}
}
]
}- Replace
src/game.ts. Register the custom class before validation. The Spawner tracks each child and notices destruction on the next actor step; no manualwaves.killedcall is needed. The example enemies stand still; add movement or combat hooks to WaveEnemy for your game.
ts
import {
actors,
defineActor,
defineGame,
destroyActor,
mesh,
Spawner,
spawnerWaves,
MeshActor,
TargetPoint,
validateLevel,
type GameContext,
} from 'gameable';
import settings from '../settings.json';
import data from './level.json';
void Spawner;
void TargetPoint;
void MeshActor;
const WaveEnemy = defineActor('WaveEnemy', {
components: { root: mesh('cube-pink') },
tags: ['enemy'],
});
const eye = { x: 0, y: 6, z: 10 };
const target = { x: 0, y: 0, z: 0 };
const identity = { x: 0, y: 0, z: 0, w: 1 };
const model = { text: { alive: 0, finished: false, controls: 'K: remove one enemy' } };
function battle(ctx: GameContext): void {
if (ctx.input.pressed('KeyK')) {
const enemies = actors.ofClass(WaveEnemy);
if (enemies.length > 0) destroyActor(ctx, enemies[0]);
}
const spawner = actors.named('waves');
if (!spawner) return;
const waves = spawnerWaves(spawner);
model.text.alive = waves.alive();
model.text.finished = waves.finished();
ctx.hud.set(model);
}
export default defineGame({
features: settings.features,
world: settings.world,
assets: ['cube-pink', 'cube-muted'],
level: validateLevel(data),
init(ctx) {
ctx.camera.set(eye, identity);
ctx.camera.lookAt(target);
},
systems: [{ on: 'authority', run: battle }],
});- For a weapon, use
applyDamageand explicitly destroy enemies when your death rule fires: Health reaching zero alone does not remove an actor.autoStart: falselets a trigger callspawnerWaves(spawner).start(ctx). Stopping waves stops future spawns, leaving living children in the world. The actor runtime saves wave progress and child tracking automatically.
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: without K, alive reaches 2 and no second wave starts. Destroy both: the second wave waits one second and starts. Its count is 3, but maxAlive: 2 keeps at most two alive at once; remove one to allow the third spawn. Remove all three and finished becomes true. Later spawns can share TargetPoint poses.