(pronounced "ex-presso")
v0.18.0
A type-safe, modular ECS framework for TypeScript
import ECSpresso from 'ecspresso';
interface Components {
position: { x: number; y: number };
velocity: { x: number; y: number };
health: { value: number };
}
// Component, event, and resource types flow through the chain — you
// never hand-annotate a system's callback; it all comes from here.
const world = ECSpresso.create()
.withComponentTypes<Components>()
.withResource('regenRate', 5) // heals 5 hp/sec — also previews resources
// .withEventTypes<{ ... }>() // events slot in the same way
.build();
// `mutates: ['position']` auto-stamps change-detection after each tick
// and narrows `velocity` to Readonly at the type level — writing to it
// would be a compile error.
world.addSystem('integrate-velocity')
.setProcessEach(
{
with: ['position', 'velocity'],
mutates: ['position'],
},
({ entity, dt }) => {
// entity.components.position is { x: number; y: number } — inferred.
entity.components.position.x += entity.components.velocity.x * dt;
entity.components.position.y += entity.components.velocity.y * dt;
},
);
// Same `mutates` contract; the outer for…of is yours to write.
world.addSystem('regen-health')
.addQuery('injured', {
with: ['health'],
mutates: ['health'],
})
.withResources(['regenRate'])
.setProcess(({ queries, dt, resources: { regenRate } }) => {
for (const entity of queries.injured) {
entity.components.health.value += regenRate * dt;
}
});
// the Systems guide for `fixedUpdate`, `render`, and phase ordering.
const player = world.spawn({
position: { x: 0, y: 0 },
velocity: { x: 10, y: 5 },
health: { value: 80 },
});
const loop = (last: number) => (now: number) => {
world.update((now - last) / 1000);
requestAnimationFrame(loop(now));
};
requestAnimationFrame(loop(performance.now()));