Skip to content

Plugins

Reference documentation for creating and registering ECS plugins.

Plugins are self-contained bundles of world configuration. A plugin can register components, systems, and resources as a single unit, making it easy to share and reuse whole features — a physics plugin, a networking plugin, a rendering plugin — without manually wiring each piece together.

Plugins are immutable. Every method on a plugin builder returns a new instance rather than mutating the original.

Use Define.plugin(name) to start a plugin builder, then chain methods to configure it.

const PhysicsPlugin = Define.plugin('physics').build((world) => {
world.registerComponent(Transform, Velocity);
world.registerSystem(Schedule.FixedUpdate, GravitySystem);
world.registerSystem(Schedule.FixedUpdate, CollisionSystem);
});

A plugin must have a name and a build function. The build function receives the World instance and is called once when the plugin is installed.

const RenderPlugin = Define.plugin('render')
.defaults({ resolution: 1, antialias: true })
.build((world, config) => {
world.bindResource('renderConfig', config);
world.registerSystem(Schedule.Render, RenderSystem);
});

The .defaults() method provides a typed configuration object. When no .defaults() is set, the build function receives only the World argument.

For reusable plugins that accept application-owned components or generate systems, see Third-Party Plugins.

Use world.registerRequiredComponents(...) when a requirement exists only because a plugin is installed. Missing required components are added with their own schema defaults whenever the trigger component is added in that world.

const TransformPlugin = Define.plugin('physics:transform').build((world) => {
world.registerRequiredComponents(Position, Transform, PreviousPosition);
});

Plugins can declare dependencies on other plugins or plugin groups. The engine resolves them in topological order before running the plugin’s build function.

const GamePlugin = Define.plugin('game')
.dependencies(PhysicsPlugin, RenderPlugin)
.build((world) => {
/* Runs after PhysicsPlugin and RenderPlugin */
});
  • Dependencies are auto-installed if they are not already present.
  • Circular dependencies are detected and throw an error.
  • Transitive dependencies are resolved automatically.
  • Diamond dependencies (a shared dependency required by multiple plugins) are deduplicated — the shared dependency is installed once.
  • A plugin can depend on a plugin group, in which case all plugins in that group are installed first.

Plugin groups let you bundle multiple plugins together and selectively disable or override individual ones.

const DefaultPlugins = Define.pluginGroup().with(PhysicsPlugin, NetworkPlugin, RenderPlugin);
world.addPlugin(DefaultPlugins);
// Skip RenderPlugin
const MinimalPlugins = DefaultPlugins.disable(RenderPlugin);
world.addPlugin(MinimalPlugins);
const RestoredPlugins = MinimalPlugins.enable(RenderPlugin);

Overriding a plugin’s config within a group

Section titled “Overriding a plugin’s config within a group”
const CustomPlugins = DefaultPlugins.set(RenderPlugin.config({ resolution: 2 }));
world.addPlugin(CustomPlugins);

Groups can contain other groups. The engine flattens them recursively before installation.

const CorePlugins = Define.pluginGroup().with(PhysicsPlugin, NetworkPlugin);
const FullPlugins = Define.pluginGroup().with(CorePlugins, RenderPlugin);
world.addPlugin(FullPlugins);

Use world.addPlugin() to install a plugin or group.

// Single plugin
world.addPlugin(PhysicsPlugin);
// Multiple plugins
world.addPlugin(PluginA, PluginB);
// Plugin group
world.addPlugin(DefaultPlugins);
// Configured plugin
world.addPlugin(NetworkPlugin.config({ port: 8080 }));
// Chaining is supported
world.addPlugin(PhysicsPlugin).addPlugin(RenderPlugin);
  • Name-based deduplication: Plugins are deduplicated by name. Installing the same plugin twice (or the same plugin via two different dependency paths) runs its build function only once.
  • First install wins: When the same plugin is added with different configurations, the first configuration used during installation is the one that takes effect.
  • Group flattening: When a plugin group is flattened, if the same plugin name appears more than once, the last occurrence wins within that group. However, cross-group deduplication still uses first-install-wins.
  • Installation order: Plugins are installed in the order they are declared within a group, respecting dependency resolution.

Call .config() on a plugin to provide partial overrides for its defaults.

const plugin = Define.plugin('network')
.defaults({ host: 'localhost', port: 3000 })
.build((_ecs, config) => {
connect(config.host, config.port);
});
// Override just the port
world.addPlugin(plugin.config({ port: 8080 }));
// Build function receives: { host: 'localhost', port: 8080 }

.config() can be chained multiple times — overrides accumulate.

const configured = plugin.config({ port: 8080 }).config({ debug: true });
// Build function receives: { host: 'localhost', port: 8080, debug: true }
MethodDescription
Define.plugin(name)Creates a new plugin builder with the given name.
.build(fn)Sets the build function. Required before installation.
.defaults(obj)Sets the default configuration object. Changes the build function signature to (world, config).
.config(overrides)Returns a new plugin with merged configuration overrides.
.dependencies(...plugins)Declares plugins or groups that must be installed first.
MethodDescription
Define.pluginGroup()Creates a new plugin group builder.
.with(...plugins)Adds plugins or groups to the group.
.set(...plugins)Replaces plugins by name in the group (and nested groups).
.disable(...plugins)Marks plugins as disabled.
.enable(...plugins)Re-enables previously disabled plugins.
function DefaultPlugins(config: { target?: string } = {}) {
const group = Define.pluginGroup().with(DomPlugin);
if (!config.target) return group;
return group.with(
ViewportPlugin.config({ target: config.target }),
InputPlugin.config({ target: config.target }),
);
}
world.addPlugin(DefaultPlugins({ target: 'my-canvas' }));

Deriving a minimal group from a full group

Section titled “Deriving a minimal group from a full group”
const FullPlugins = Define.pluginGroup().with(PhysicsPlugin, NetworkPlugin, RenderPlugin);
const MinimalPlugins = FullPlugins.disable(RenderPlugin);
world.addPlugin(MinimalPlugins); // Only PhysicsPlugin and NetworkPlugin are installed

Plugins are fully typed. The configuration object inferred from .defaults() is passed to the build function without explicit type annotations. Module augmentation can be used to extend the IoC registry for resources bound by a plugin.

declare module '@shell/ecs' {
interface IocRegistry {
renderConfig: { resolution: number; antialias: boolean };
}
}
  • packages/ecs/src/plugin.ts