ecs

Structure game state with the EliCS entity-component-system: components, systems, queries, predicates, lifecycle, and the Input/State/View architecture. Use when organizing game logic with multiple interacting objects and state-driven update loops.

Skill file

Preview skill file
---
name: ecs
description: "Structure game state with the EliCS entity-component-system: components, systems, queries, predicates, lifecycle, and the Input/State/View architecture. Use when organizing game logic with multiple interacting objects and state-driven update loops."
---

# Entity Component System

We use EliCS, a lightweight, high-performance Entity Component System framework for TypeScript. It uses a data-oriented design that prioritizes composition over inheritance, with efficient bitmasking and typed array storage.

**Core concepts:** Entities (unique objects), Components (data containers), Systems (logic processors), Queries (entity selectors), and World (the container managing everything).

**When to reach for it:** a game with many interacting objects that share behaviors (enemies, projectiles, pickups, units) and a state-driven update loop benefits from ECS — it keeps logic in systems and state in components instead of a tangle of classes. For a tiny scene with a handful of bespoke objects, ECS is overhead; plain objects and a render loop are fine. The sections below show enough of the EliCS API to build with it; for exact signatures and the full type surface, read the installed `elics` types rather than relying on this page, which can lag the package.

## Recommended Architecture

Use 3 layers: Input, State, and View.

- mapping Input to State (Input Systems: receive user input, normalize it, and apply it to State. Complex inputs can be split across multiple systems (e.g., raw events → normalized actions → state updates). Each input system must provide methods to imperatively invoke all inputs enabling simulating and testing user input programmatically.)
- advancing State each frame (State Systems: advance the game state every frame (e.g., enemies chasing the player).)
- synchronizing State to the View (View Systems: reflect State in the View. Subscribe during initialization to entity creation/removal when needed (e.g., to create/destroy 3D objects or UI), and update frequently changing data each frame.)

### Recommended File Structure

- \`src/components/\*.ts\`: one component per file (e.g., position.ts, velocity.ts). Components hold state only; behavior belongs in systems.
- \`src/systems/\*.ts\`: one file per system (e.g., input.ts, physics.ts). Each system exports initialization and per-frame update functions.
- \`src/seed.ts\`: constructs startup entities and attaches their components so the game is immediately playable.
- \`src/main.ts\`: composes the game. Loads seed, registers systems, runs initialization, then advances all state each frame and syncs to the View.

## Creating a World

The "world" is where all ECS elements coexist.

```typescript
import { World } from 'elics'

const world = new World()
```

## Defining Components

Components are data containers that define entity properties.

```typescript
import { createComponent, Types } from 'elics'
import { world } from './world.ts'

// Available Types:
// - Scalars: Types.Int8, Types.Int16, Types.Float32, Types.Float64, Types.Boolean, Types.String
// - Vectors: Types.Vec2, Types.Vec3, Types.Vec4, Types.Color (RGBA 0-1)
// - Entity reference: Types.Entity
// - Enum: Types.Enum
// - Types.Object holds any JS value (a Three.js mesh, an array) — not an ECS reference

// Define enums for type-safe state management using const assertions
export const UnitType = {
  Infantry: 'infantry',
  Cavalry: 'cavalry',
  Archer: 'archer',
} as const

export const CombatState = {
  Idle: 'idle',
  Moving: 'moving',
  Attacking: 'attacking',
  Defending: 'defending',
} as const

export const Position = createComponent(
  'Position',
  {
    value: { type: Types.Vec3, default: [0, 0, 0] },
  },
  '3D position coordinates',
)
//register components directly after creating them
world.registerComponent(Position)

export const Velocity = createComponent(
  'Velocity',
  {
    value: { type: Types.Vec3, default: [0, 0, 0] },
  },
  '3D velocity vector',
)
world.registerComponent(Velocity)

export const Health = createComponent(
  'Health',
  {
    value: { type: Types.Float32, default: 100 },
  },
  'Entity health points',
)
world.registerComponent(Health)

export const Unit = createComponent(
  'Unit',
  {
    type: { type: Types.Enum, enum: UnitType, default: UnitType.Infantry },
    state: { type: Types.Enum, enum: CombatState, default: CombatState.Idle },
    damage: { type: Types.Float32, default: 10, min: 1, max: 100 },
    armor: { type: Types.Int16, default: 0, min: 0, max: 50 },
    morale: { type: Types.Float32, default: 1.0, min: 0.0, max: 1.0 },
  },
  'Military unit with combat statistics',
)
world.registerComponent(Unit)

// Entity references
export const Target = createComponent('Target', {
  entity: { type: Types.Entity, default: null },
})
world.registerComponent(Target)
```

> **Important:** Vector types (`Vec2`, `Vec3`, `Vec4`, `Color`) must be accessed via `getVectorView()`, not `getValue()`/`setValue()`. EliCS throws at runtime if you use the wrong accessor.

## Creating Entities

Instantiate entities and attach components:

```typescript
const unit = world.createEntity()
unit.addComponent(Position, { value: [10, 20, 30] })
unit.addComponent(Velocity)
unit.addComponent(Health)
unit.addComponent(Unit, {
  type: UnitType.Infantry,
  state: CombatState.Moving,
  damage: 15,
  armor: 20,
  morale: 0.8,
})

// Reading and writing scalar values
const damage = unit.getValue(Unit, 'damage') // returns number
unit.setValue(Unit, 'state', CombatState.Attacking)

// Reading and writing vector values (returns TypedArray subarray)
const pos = unit.getVectorView(Position, 'value')
pos[0] = 100 // Modify X directly

// Check component existence
if (unit.hasComponent(Health)) {
  /* ... */
}

// Remove component or destroy entity
unit.removeComponent(Velocity)
unit.destroy() // entity.active becomes false
```

## Creating Systems

Systems contain the core logic.

```typescript
import { createSystem, Entity } from 'elics'
import { Object3D } from 'three'
import { scene } from './scene.ts'
import { unitModel } from './unit-model.ts'

const movementQueryConfig = {
  movables: { required: [Position, Velocity] },
}

export class MovementSystem extends createSystem(movementQueryConfig) {
  update(delta: number, time: number) {
    this.queries.movables.entities.forEach((entity: Entity) => {
      const position = entity.getVectorView(Position, 'value')
      const velocity = entity.getVectorView(Velocity, 'value')
      position[0] += velocity[0] * delta
      position[1] += velocity[1] * delta
      position[2] += velocity[2] * delta
    })
  }
}

const unitViewQueryConfig = {
  units: { required: [Unit, Position] },
}

export class UnitViewSystem extends createSystem(unitViewQueryConfig) {
  public readonly models = new Map<Entity, Object3D>()

  init() {
    const create = (entity: Entity) => {
      const newModel = unitModel.clone()
      scene.add(newModel)
      this.models.set(entity, newModel)
    }
    const destroy = (entity: Entity) => {
      const oldModel = this.models.get(entity)!
      scene.remove(oldModel)
      this.models.delete(entity)
    }
    // Loop through existing and subscribe to new query entities
    this.queries.units.entities.forEach(create)
    this.queries.units.subscribe('qualify', create)
    this.queries.units.subscribe('disqualify', destroy)
  }

  update(delta: number, time: number) {
    this.queries.units.entities.forEach((entity) => {
      const model = this.models.get(entity)!
      const pos = entity.getVectorView(Position, 'value')
      model.position.set(pos[0], pos[1], pos[2])
    })
  }

  destroy() {
    // Cleanup when system is unregistered
  }
}

// Register with priority (lower runs first)
world.registerSystem(MovementSystem, { priority: 0 })
world.registerSystem(UnitViewSystem, { priority: 10 })
```

## Accessing Systems

Retrieve a registered system instance using `world.getSystem(...)`:

```typescript
const enemySystem = world.getSystem(EnemySystem)!

// Access system properties and methods
enemySystem.spawnEnemy(position)
```

## System Resource Loading

The framework calls `init()` (on `registerSystem`) and `update()` (each `world.update`). It has **no `load()` lifecycle hook** — only those two. For async resource loading, give the system your own `load()` method and call it yourself after registering, before the animation loop. It is an ordinary method, not a framework hook: nothing calls it unless you do.

```typescript
export class EnemySystem extends createSystem(queryConfig) {
  private model!: Group

  init() {
    /* called by registerSystem() — synchronous only */
  }

  async load() {
    // your own method — you must await it yourself (see below)
    this.model = (await new GLTFLoader().loadAsync('/models/enemy.glb')).scene
  }

  update(delta: number, time: number) {
    /* called every frame by world.update */
  }
}
```

In `main.ts`, register (which runs `init()`), then await your `load()` before the loop:

```typescript
world.registerSystem(EnemySystem) // runs init()
await world.getSystem(EnemySystem)!.load()
renderer.setAnimationLoop(() => { ... })
```

## System Lifecycle Control

Pause and resume systems dynamically using `stop()` and `play()`:

```typescript
const enemySystem = world.getSystem(EnemySystem)!

// Pause the system (e.g., during cutscenes or menus)
enemySystem.stop()

// Check if system is paused
if (enemySystem.isPaused) {
  console.log('Enemy system is paused')
}

// Resume the system
enemySystem.play()
```

> **Example Use case:** Pause physics during dialogue sequences, then resume when the player regains control.

## Query Predicates

Filter entities by component values using `where` clauses:

```typescript
const queryConfig = {
  lowHealth: {
    required: [Health, Unit],
    excluded: [Target], // Exclude entities with this component
    where: [{ component: Health, key: 'value', op: 'lt', value: 30 }],
  },
}
```

**Operators:** `eq` (equal), `ne` (not equal), `lt` (less than), `le` (less or equal), `gt` (greater than), `ge` (greater or equal), `in` (in array), `nin` (not in array).

## Updating the World

Update the world with `delta` and `time`.

```typescript
function render() {
  ...
  const delta = clock.getDelta()
  ...
  world.update(delta, clock.elapsedTime)
  ...
}
```

Source

Creator's repository · drawcall-ai/skills

View on GitHub

Security

Security checks in progress
Results will appear here once audits complete
Checked by 3 independent security firms
Does it try to trick the AI?Not yet checkedPending · Gen Agent Trust Hub
Does it sneak in hidden code?Not yet checkedPending · Socket
Does it have known bugs?Not yet checkedPending · Snyk