arch-nexus-ecs
v0.9.0
Published
Arch Nexus Entity Component System
Maintainers
Readme
Arch Nexus ECS
Arch-Nexus-ECS is a lightweight Entity Component System (ECS) library for TypeScript, built on an archetype-based storage model for cache-efficient iteration.
Installation
npm install arch-nexus-ecs
# or
bun add arch-nexus-ecsCore concepts
| Concept | Role | |---|---| | Entity | Unique ID that ties components together | | Component | Pure data — no logic | | System | Logic that runs every frame, queries for entities | | World | Central hub: manages entities, systems, events, and resources | | Archetype | Internal grouping of entities that share the same component set |
Components
Two styles work interchangeably:
import { component, Component } from "arch-nexus-ecs";
// decorator style (no base class needed)
@component
class Position {
constructor(public x: number, public y: number) {}
}
@component
class Velocity {
constructor(public x: number, public y: number) {}
}
// extends style (still works)
class Health extends Component {
constructor(public value: number) { super(); }
}Systems
Class-based system
import { System, World, Query } from "arch-nexus-ecs";
class MoveSystem extends System {
private q!: Query<[typeof Position, typeof Velocity], []>;
startUp(world: World) {
this.q = world.createQuery<[typeof Position, typeof Velocity], []>()
.with(Position, Velocity);
}
update(world: World) {
for (const [pos, vel] of this.q.findAll()) {
pos.x += vel.x;
pos.y += vel.y;
}
}
}Function-based system with defineSystem
import { defineSystem, query, queryAll, queryFirst, resource, subscribe } from "arch-nexus-ecs";
class GameConfig {
constructor(public speed: number) {}
}
@event
class PlayerDied {
constructor(public entityId: number) {}
}
const moveSystem = defineSystem(
{
movers: queryAll(Position, Velocity), // ResultTuple<T>[] — iterate directly
leader: queryFirst(Position), // ResultTuple<T> | undefined
config: resource(GameConfig), // Resource<T> | undefined
deaths: subscribe(PlayerDied), // EventBuffer<T> — events this frame
},
({ movers, leader, config, deaths }) => {
const speed = config?.get().speed ?? 1;
for (const [pos, vel] of movers) { // no .findAll() needed
pos.x += vel.x * speed;
pos.y += vel.y * speed;
}
if (leader) {
const [pos] = leader; // no .findFirst() needed
console.log("leader at", pos.x);
}
for (const e of deaths) {
console.log("player died:", e.entityId);
}
}
);All descriptors are resolved automatically on startUp and are fully typed:
| Descriptor | Resolved type | Notes |
|---|---|---|
| query(A, B) | Query<[typeof A, typeof B], []> | cached query object, call .findAll() / .findFirst() |
| queryAll(A, B) | ResultTuple<[A, B]>[] | refreshed every frame, iterate directly |
| queryFirst(A, B) | ResultTuple<[A, B]> \| undefined | refreshed every frame |
| resource(T) | Resource<T> \| undefined | |
| subscribe(E) | EventBuffer<E> | cleared after each update() |
All descriptors support .without() for exclusions:
queryAll(Position).without(Dead)
queryFirst(Position, Velocity).without(Frozen)
query(Position).without(Velocity)Lifecycle hooks
const sys = defineSystem(
{ movers: queryAll(Position, Velocity) },
({ movers }) => { /* update — runs every frame */ },
{
startUp: ({ movers }, world) => { /* runs once after descriptors are resolved */ },
destroy: ({ movers }, world) => { /* runs on world.destroy() */ },
}
);Queries
Query object (class systems or manual use)
const q = world.createQuery().with(Position, Velocity);
const alive = world.createQuery().with(Position).without(Dead);
q.findAll(); // ResultTuple<T>[]
q.findFirst(); // ResultTuple<T> | undefined
q.findLast(); // ResultTuple<T> | undefinedConvenience methods (one-off queries)
// shorthand — creates and executes a query in one call, no caching
const all = world.queryAll(Position, Velocity); // ResultTuple<T>[]
const first = world.queryFirst(Position, Velocity); // ResultTuple<T> | undefinedFor queries that run every frame, prefer
query()/queryAll()/queryFirst()descriptors insidedefineSystem— they cache the query object and only re-execute when archetypes change.
Include Entity in the query to access the entity ID:
const q = world.createQuery().with(Entity, Position);
for (const [entity, pos] of q.findAll()) {
console.log(entity.id, pos.x);
}World setup
import { World } from "arch-nexus-ecs";
const world = new World();
// add resources before startUp
world.addResource(new GameConfig(2));
// register systems (class or function-based)
world.addSystems(SpawnSystem, MoveSystem);
world.addSystem(moveSystem);
world.startUp();
// game loop
const loop = () => {
world.update();
setTimeout(loop, 16); // ~60fps
};
loop();Entity lifecycle
const e = world.addEntity(new Position(0, 0), new Velocity(1, 0));
world.addComponentToEntity(e, new Health(100));
world.removeComponentFromEntity(e, new Velocity(0, 0));
world.removeEntity(e);Events
import { event } from "arch-nexus-ecs";
@event
class PlayerDied {
constructor(public entityId: number) {}
}
// subscribe
world.addSubscriber(PlayerDied, (e: PlayerDied) => {
console.log("player died:", e.entityId);
});
// dispatch
world.addEvent(new PlayerDied(42));
// unsubscribe
world.unsubscribe(PlayerDied, handler);To handle events inside
defineSystem, use thesubscribe()descriptor — it buffers events per frame and cleans up automatically ondestroy().
Resources
Global singleton data accessible from any system:
class Score {
constructor(public value: number) {}
}
world.addResource(new Score(0));
const score = world.getResource(Score); // Resource<Score> | undefined
score?.get().value; // number
world.removeResource(Score);Task Scheduler
Run time-sequenced async tasks inside the game loop:
import { WaitAmountOfSeconds } from "arch-nexus-ecs";
function* spawnWave(world: World) {
yield new WaitAmountOfSeconds(3);
world.addEntity(new Position(0, 0), new Velocity(1, 0));
yield new WaitAmountOfSeconds(3);
world.addEntity(new Position(5, 0), new Velocity(-1, 0));
}
world.addTaskScheduler(spawnWave);The generator function itself is the identifier — no strings, no manual instantiation:
world.pauseTaskScheduler(spawnWave); // pauses without destroying the generator
world.resumeTaskScheduler(spawnWave); // resumes from where it stopped
world.removeTaskScheduler(spawnWave); // stops and removes from the scheduler listPlugins
Bundle groups of systems and resources into reusable plugins:
import { IPlugin, World } from "arch-nexus-ecs";
class PhysicsPlugin implements IPlugin {
build(world: World) {
world.addResource(new PhysicsConfig());
world.addSystems(CollisionSystem, GravitySystem);
}
}
world.addPlugin(PhysicsPlugin);Scripts
bun test # run tests
bun run typecheck # type-check without emitting
bun run build # full tsc buildLicense
MIT
