Third-Party Plugins
Factories let a reusable package construct a plugin around choices made by the consuming application. The factory runs before plugin installation and returns a normal plugin builder, so installation still uses world.addPlugin(...).
Plugin boundaries
Section titled “Plugin boundaries”A reusable plugin should own its internal components and resources. Import shared canonical engine components directly when the plugin and application already agree on their meaning and schema.
Use a factory when the application must choose a component, resource, strategy, or generated system. Capture that choice when constructing the plugin rather than adding mutable type bindings to the world.
Configurable plugin factory
Section titled “Configurable plugin factory”A factory can capture ordinary values and use them while constructing a system:
import { Define, Mut, Optional, Schedule, StoreType } from '@shell/ecs';
const PendingDamage = Define.component('PendingDamage').withSchema({ amount: { type: StoreType.Float32 },});const CriticalHit = Define.component('CriticalHit');
export function createDamagePlugin(options: { criticalMultiplier: number }) { const DamageQuery = Define.query({ data: [Mut(PendingDamage), Optional(CriticalHit)], }); const ApplyDamage = Define.system() .queries({ damage: DamageQuery }) .update(({ queries }) => { queries.damage.forEach((entity) => { if (entity.$CriticalHit) { entity.$PendingDamage.amount *= options.criticalMultiplier; } }); });
return Define.plugin('damage').build((world) => { world.registerSystem(Schedule.Update, ApplyDamage); });}
world.addPlugin(createDamagePlugin({ criticalMultiplier: 2 }));Factories select types and construct systems or resources. In contrast, .defaults() and .config() customize values on an already constructed plugin shape. Use either approach for values, but use a factory whenever a value changes which systems or component types the plugin creates.
Accepting an application-owned component
Section titled “Accepting an application-owned component”Use ComponentRequiring to describe the fields a generated system needs while preserving the concrete component builder selected by the application:
import { type ComponentRequiring, Define, Mut, Schedule, StoreType } from '@shell/ecs';
type PositionLike = ComponentRequiring<{ x: number; y: number }>;
const Velocity = Define.component('Velocity').withSchema({ x: { type: StoreType.Float32 }, y: { type: StoreType.Float32 },});
export function createMovementSystem<P extends PositionLike>(Position: P) { const Moving = Define.query({ data: [Mut(Position), Velocity] }); return Define.system() .queries({ moving: Moving }) .update(({ queries, time }) => { const deltaSeconds = time.delta / 1000; for (const batch of queries.moving.iter()) { const position = batch.getComponent(Position); for (let i = 0; i < batch.size; i++) { position.x[i] += batch.$Velocity.x[i] * deltaSeconds; position.y[i] += batch.$Velocity.y[i] * deltaSeconds; } } });}
export function createMovementPlugin<P extends PositionLike>(Position: P) { const ApplyMovement = createMovementSystem(Position);
return Define.plugin('movement').build((world) => { world.registerSystem(Schedule.Update, ApplyMovement); });}
const GamePosition = Define.component('GamePosition').withSchema({ x: { type: StoreType.Float32 }, y: { type: StoreType.Float32 }, z: { type: StoreType.Float32 },});
world.addPlugin(createMovementPlugin(GamePosition));Use batch.getComponent(Position) for a parameterized component whose key is not known to the package. Fixed components retain their normal fields, such as batch.$Velocity. The plugin and application access the same component storage and archetypes; no duplicate component or synchronization system is created.
Providing defaults
Section titled “Providing defaults”Export a canonical component, the factory, and a ready-made plugin when most consumers should use the package’s schema:
export const Position = Define.component('Position').withSchema({ x: { type: StoreType.Float32 }, y: { type: StoreType.Float32 },});
export const MovementPlugin = createMovementPlugin(Position);Consumers can choose the convenient default or inject their own compatible component:
world.addPlugin(MovementPlugin);// Or:world.addPlugin(createMovementPlugin(GamePosition));Multiple component parameters
Section titled “Multiple component parameters”Prefer an object parameter when a plugin accepts several application-owned components:
type RotationLike = ComponentRequiring<{ angle: number }>;type SizeLike = ComponentRequiring<{ width: number; height: number }>;
type SpatialComponents = { position: PositionLike; rotation: RotationLike; size: SizeLike;};
export function createSpatialPlugin({ position, rotation, size }: SpatialComponents) { const SpatialQuery = Define.query({ data: [position, rotation, size] }); const SyncSpatialData = Define.system() .queries({ spatial: SpatialQuery }) .update(({ queries }) => { for (const batch of queries.spatial.iter()) { const positions = batch.getComponent(position); const rotations = batch.getComponent(rotation); const sizes = batch.getComponent(size); // Submit positions, rotations, and sizes to the integration. } });
return Define.plugin('spatial-integration').build((world) => { world.registerSystem(Schedule.Update, SyncSpatialData); });}The named fields document each role and avoid a long positional argument list.
Exposing generated systems
Section titled “Exposing generated systems”Each factory call creates systems with new identities. If consumers need ordering constraints, return the plugin and exact generated builders together:
export function createMovementDescriptor<P extends PositionLike>(Position: P) { const ApplyMovement = createMovementSystem(Position); const plugin = Define.plugin('movement').build((world) => { world.registerSystem(Schedule.Update, ApplyMovement); });
return { plugin, systems: { ApplyMovement } };}
const movement = createMovementDescriptor(GamePosition);const ResolveMovement = Define.system() .after(movement.systems.ApplyMovement) .update(() => { // Runs after this descriptor's generated movement system. });
world.addPlugin(movement.plugin);world.registerSystem(Schedule.Update, ResolveMovement);Return only the plugin when generated systems are internal implementation details.
Limitations
Section titled “Limitations”Structural component injection requires compatible field names and storage shapes. Prefer canonical shared components when packages are maintained together, and introduce explicit adapters only when incompatible schemas genuinely need integration.
Component selection is fixed when the factory creates its systems. Make that selection before installing the plugin; changing it later requires constructing a different plugin and system set.