@oxecs/ecs
v0.1.0-alpha.6
Published
oxecs runtime and documentation skeleton
Readme
@oxecs/ecs
@oxecs/ecs is a data-oriented Entity Component System for JavaScript and TypeScript.
The runtime is the source of truth. Compiler passes, worker executors, and
SharedArrayBuffer backends are acceleration layers, not prerequisites. The
core must still work without them.
What you get
- typed tokens for components, resources, events, and relations;
- Bevy-style queries, systems, schedules, commands, and world access;
- dense archetype storage for hot data;
SparseSetstorage for direct entity-indexed data;- deterministic command playback and schedule validation;
- explicit parallel-safety in the type surface;
- referential cleanup for
eidfields.
Install
npm install @oxecs/ecsQuick Start
1. Create a world and run a query
import { createWorld, component, f32, query } from "@oxecs/ecs";
const Position = component({ x: f32, y: f32 });
const Velocity = component({ x: f32, y: f32 });
const world = createWorld();
world.spawn(
[Position, { x: 0, y: 0 }],
[Velocity, { x: 1, y: 2 }],
);
const moving = query(Position, Velocity);
world.query(moving).each((position, velocity) => {
position.x += velocity.x;
position.y += velocity.y;
});2. Move the same logic into a system
import { component, f32, query, system } from "@oxecs/ecs";
const Position = component({ x: f32, y: f32 });
const Velocity = component({ x: f32, y: f32 });
export const Move = system([query(Position, Velocity)], (movement) => {
movement.each((position, velocity) => {
position.x += velocity.x;
position.y += velocity.y;
});
});3. Run the same logic in a schedule
import { createWorld, createSchedule } from "@oxecs/ecs";
import { Move, Position, Velocity } from "./shared/movement";
const world = createWorld();
world.spawn([Position, { x: 0, y: 0 }], [Velocity, { x: 1, y: 2 }]);
createSchedule().add(Move).run(world);The Usual Flow
- Define component schemas with
component(...). - Create a world with
createWorld(). - Spawn entities with typed component values.
- Run ad-hoc query loops or move the same logic into systems.
- Add systems to a schedule and run it against the world.
Parallel Execution
Parallel execution is explicit and workload-aware.
- Serial execution is the default.
- Dense chunk workloads can be split across workers.
- Small or sparse-heavy workloads may stay serial when that is cheaper.
- The world behavior stays deterministic in both serial and parallel modes.
The query loop below:
world.query(moving).each((position, velocity) => {
position.x += velocity.x;
position.y += velocity.y;
});is the serial form. It does not parallelize by itself.
Current host support
- Node.js: supported.
- Browser: not available yet.
- React Native: not available yet.
If you need parallel execution today, treat Node.js as the only supported runtime target.
Optional Node parallel setup
You do not need this for normal ECS usage. Keep using schedule.run(world)
when you want the simplest serial path.
When you want Node-backed parallel execution, move the reusable system into a worker-entry module and let the bootstrap plugin load it at worker start.
// src/shared/movement.ts
import { component, f32, query, system } from "@oxecs/ecs";
const Position = component({ x: f32, y: f32 });
const Velocity = component({ x: f32, y: f32 });
export const Move = system([query(Position, Velocity)], (movement) => {
movement.each((position, velocity) => {
position.x += velocity.x;
position.y += velocity.y;
});
});
export { Position, Velocity };// src/workers/physics.ts
import { registerSystem } from "@oxecs/ecs";
import { Move } from "../shared/movement";
registerSystem("move", Move);// src/main.ts
import { createWorld, createSchedule, SchedulerMode, ExecutionFallback } from "@oxecs/ecs";
import { createExecutorPlan, createNodeExecutor, createNodeRunner } from "@oxecs/ecs/extensions";
import { Move, Position, Velocity } from "./shared/movement";
const world = createWorld();
world.spawn([Position, { x: 0, y: 0 }], [Velocity, { x: 1, y: 2 }]);
const schedule = createSchedule().add(Move);
const plan = createExecutorPlan(schedule.plan(), SchedulerMode.Auto, ExecutionFallback.Serial);
const runner = createNodeRunner(createNodeExecutor());
runner.run(plan);That is the general pattern:
- move the logic from
world.query(...).each(...)into a reusablesystem(...); - keep the system definition in a shared module;
- create a worker-entry module that imports the system and registers it with
registerSystem(...); - use the same system in
createSchedule()on the main thread; - build a plan with
createExecutorPlan(...); - execute the plan with
createNodeRunner(createNodeExecutor()).
Bootstrap Plugin
@oxecs/ecs also ships an optional build-time unplugin helper for generating a
worker bootstrap module.
Use it when you want the bundler to emit a Node worker bootstrap file from a set of ordinary worker-entry modules.
Those modules are just normal TypeScript files. They usually contain worker setup code, such as system registration, schedule construction, or other top-level initialization that should run when the worker starts.
In practice, a worker-entry module often imports reusable systems and registers
them with registerSystem(...).
import { createBootstrapVitePlugin } from "@oxecs/ecs/compiler";
export default {
plugins: [
createBootstrapVitePlugin({
imports: ["./src/workers/physics.ts", "./src/workers/render.ts"],
}),
],
};The plugin is a build-time convenience only. It does not replace the runtime executor.
Build and Verify
npm run check
npm run build
npm run test
npm run perf-gatePublishing
The repository uses Git tags to drive publishing.
For an alpha release:
- Update
package.jsonto the next prerelease version, for example0.1.0-alpha.0. - Commit the change.
- Create a tag that matches the version, for example
v0.1.0-alpha.0. - Push the tag to GitHub.
The GitHub Actions workflow publishes the package to npm with Trusted Publishing and creates a GitHub Release from the tag.
Contributing
See CONTRIBUTING.md for the local workflow, PR rules, and release process.
API Reference
License
Licensed under the MIT License.
