Outcome: you’ll learn the one rule that prevents most ECS bugs: don’t change entity structure while iterating. You’ll reproduce the problem safely, then fix it using Commands and flush points (via Schedule). The library explicitly supports this workflow: defer structural operations with world.cmd() and apply them with world.flush() / Schedule phase boundaries.
constunsafeDespawnInsideQuery:SystemFn=(w:WorldApi)=>{for(const{e,c1:pos}ofw.query(Position)){if(pos.x>8){// ❌ Structural change during iteration (may throw)w.despawn(e);}}}
Now call it once (inside a try/catch so the tutorial keeps going):
123456
try{unsafeDespawnInsideQuery(world);console.log("unsafe: no error (but still not safe)");}catch(err:any){console.log("unsafe: error as expected ->",String(err.message??err));}
The lib will warn that structural changes during query iteration can throw and instructs to use cmd() + flush() instead.
flush() applies queued commands (and update() also flushes automatically at the end).
Option B — Flush at phase boundaries (recommended)¶
Use Schedule, which flushes after each phase:
1 2 3 4 5 6 7 8 910111213141516
constsched=newSchedule();sched.add("sim",(w:WorldApi)=>{// movefor(const{c1:pos,c2:vel}ofw.query(Position,Velocity)){pos.x+=vel.x;}});sched.add("cleanup",(w:WorldApi)=>{// safely despawn based on updated positionssafeDespawnInsideQuery(w);});// Flush happens after each phase automaticallyconstphases=["sim","cleanup"];
Schedule.run(world, dt, phases) runs phases and calls world.flush() after each phase.
import{World,Schedule}from"archetype-ecs-lib";classPosition{constructor(publicx=0){}}classVelocity{constructor(publicx=0){}}constworld=newWorld();functionspawnMover(x:number,vx:number){conste=world.spawn();world.add(e,Position,newPosition(x));world.add(e,Velocity,newVelocity(vx));returne;}spawnMover(0,2);spawnMover(5,-3);spawnMover(9,1);constunsafeDespawnInsideQuery:SystemFn=(w)=>{for(const{e,c1:pos}ofw.query(Position)){if(pos.x>8){w.despawn(e);// ❌ may throw}}}try{unsafeDespawnInsideQuery(worldasany);console.log("unsafe: no error (but still not safe)");}catch(err:any){console.log("unsafe: error as expected ->",String(err.message??err));}constsafeDespawnInsideQuery:SystemFn=(w)=>{for(const{e,c1:pos}ofw.query(Position)){if(pos.x>8)w.cmd().despawn(e);// ✅ deferred}}functionlogPositions(w:WorldApi,label:string){constitems:string[]=[];for(const{e,c1:pos}ofw.query(Position)){items.push(`e${e.id}:${pos.x.toFixed(1)}`);}console.log(label,items.join(" | ")||"(none)");}constsched=newSchedule();sched.add("sim",(w:WorldApi)=>{for(const{c1:pos,c2:vel}ofw.query(Position,Velocity)){pos.x+=vel.x;}});sched.add("cleanup",(w:WorldApi)=>{safeDespawnInsideQuery(w);});constphases=["sim","cleanup"];logPositions(world,"before");for(leti=0;i<5;i++){sched.run(world,0,phases);// flush after each phaselogPositions(world,`after tick ${i+1}`);}