Organize related systems and resources into reusable plugins:
import ECSpresso, { definePlugin, type WorldConfigFrom } from 'ecspresso';
interface PhysicsComponents {
position: { x: number; y: number };
velocity: { x: number; y: number };
}
interface PhysicsResources {
gravity: { value: number };
}
const physicsPlugin = definePlugin<WorldConfigFrom<PhysicsComponents, {}, PhysicsResources>>({
id: 'physics',
install(world) {
world.addSystem('applyVelocity')
.addQuery('moving', { with: ['position', 'velocity'] })
.setProcess(({ queries, dt }) => {
for (const entity of queries.moving) {
entity.components.position.x += entity.components.velocity.x * dt;
entity.components.position.y += entity.components.velocity.y * dt;
}
});
world.addSystem('applyGravity')
.addQuery('falling', { with: ['velocity'] })
.setProcess(({ queries, dt, ecs }) => {
const gravity = ecs.getResource('gravity');
for (const entity of queries.falling) {
entity.components.velocity.y += gravity.value * dt;
}
});
world.addResource('gravity', { value: 9.8 });
},
});
// Register plugins with the world — types merge automatically
const game = ECSpresso.create()
.withPlugin(physicsPlugin)
.build();
When multiple plugins share the same types (common in application code), use pluginFactory() on the builder or built world to capture types automatically:
// types.ts — builder accumulates all types
export const builder = ECSpresso.create()
.withPlugin(createPhysicsPlugin())
.withComponentTypes<{ player: boolean; enemy: EnemyData }>()
.withResourceTypes<{ score: number }>();
// Types flow from the builder — no manual imports or extends chains
export const definePlugin = builder.pluginFactory();
// movement-plugin.ts — no type params needed
import { definePlugin } from './types';
export const movementPlugin = definePlugin({
id: 'movement',
install(world) {
world.addSystem('movement')
.addQuery('moving', { with: ['position', 'velocity'] })
.setProcess(({ queries, dt }) => { /* ... */ });
},
});
You can also pass a world type directly to definePlugin as a one-off alternative:
type MyWorld = typeof ecs; // derive from a built world
const plugin = definePlugin<MyWorld>({
id: 'my-plugin',
install(world) { /* world is fully typed */ },
});
Plugins can declare that certain components depend on others. When an entity gains a trigger component, any required components that aren't already present are auto-added with default values:
const transformPlugin = definePlugin<WorldConfigFrom<TransformComponents>>({
id: 'transform',
install(world) {
world.registerRequired('localTransform', 'worldTransform', () => ({
x: 0, y: 0, rotation: 0, scaleX: 1, scaleY: 1,
}));
},
});
const world = ECSpresso.create()
.withPlugin(transformPlugin)
.build();
// worldTransform is auto-added with defaults
const entity = world.spawn({
localTransform: { x: 100, y: 200, rotation: 0, scaleX: 1, scaleY: 1 },
});
// Explicit values always win — no auto-add if already provided
const entity2 = world.spawn({
localTransform: { x: 100, y: 200, rotation: 0, scaleX: 1, scaleY: 1 },
worldTransform: { x: 50, y: 50, rotation: 0, scaleX: 2, scaleY: 2 }, // used as-is
});
Requirements can also be registered via the builder or at runtime:
// Builder
const world = ECSpresso.create()
.withComponentTypes<Components>()
.withRequired('rigidBody', 'velocity', () => ({ x: 0, y: 0 }))
.withRequired('rigidBody', 'force', () => ({ x: 0, y: 0 }))
.build();
// Runtime
world.registerRequired('position', 'velocity', () => ({ x: 0, y: 0 }));
spawn, addComponent, addComponents, spawnChild, command buffer)The Transform plugin registers localTransform → worldTransform. The Physics 2D plugin registers rigidBody → velocity and rigidBody → force.