@glom/ecs v1.0.0

Reactivity

Systems usually run every frame and check for entities that match a query.

Sometimes you only want to do something when a state changes, like playing a sound when a unit is created or stopping an effect when a buff ends. Glom uses In and Out transition queries to handle this.

State Changes vs. Polling

Performing side effects with regular All queries is possible, but you’d need to read component values to avoid repeating the effect every frame.

Transition queries identify which entities changed since the last time the system ran. You could just use In<typeof Shield> if you wanted to add a ShieldVFX when an entity gets a Shield.

Reacting to Component Additions

Define queries with In to match entities that just started matching your criteria.

import { Add, All, Entity, In, Read, defineComponent } from "@glom/ecs"

const Shield = defineComponent<{ power: number }>()
const ShieldVFX = defineComponent<{ intensity: number }>()

const onShieldAdded = (
  added: In<Entity, Has<typeof Shield>>,
  addVfx: Add<typeof ShieldVFX>
) => {
  // yields only entities that just received a shield
  for (const [entity] of added) {
    addVfx(entity, { intensity: 1.0 })
  }
}

The loop only runs for entities that moved into the “has Shield” state in the current frame.

Reacting to Component Removal

Out matches entities that no longer match a component signature.

import { Entity, Out, Remove } from "@glom/ecs"

const onShieldRemoved = (
  // yields only entities that just lost its shield
  removed: Out<Entity, Has<typeof Shield>>,
  removeVfx: Remove<typeof ShieldVFX>
) => {
  for (const [entity] of removed) {
    removeVfx(entity)
  }
}

Transition queries can also manage separate entities. This example demonstrates spawning a laser when a player begins attacking, and despawning it when they stop.

import { Despawn, Entity, In, Out, Spawn, defineRelation, defineTag } from "@glom/ecs"

const Attacking = defineTag()
const LaserBeam = defineTag()
const EmitsFrom = defineRelation()

// spawn a beam and link it
const onAttackStarted = (
  added: In<Entity, typeof Attacking>,
  spawn: Spawn<typeof LaserBeam>
) => {
  for (const [player] of added) {
    spawn(LaserBeam, EmitsFrom(player))
  }
}

type Query = Out<Join<All<Entity>, All<Has<typeof Attacking>>, typeof EmitsFrom>>

// find the beam and despawn it
const onAttackStopped = (
  removed: Query,
  despawn: Despawn
) => {
  for (const [beam] of removed) {
    despawn(beam)
  }
}

The world API equivalents for these operations:

// spawning with initial components
const beam = spawn(world, LaserBeam, EmitsFrom(player))

// despawning
despawn(world, beam)

The Out query yields entities that no longer emit from attacking entities in onAttackStopped, which means they can be cleaned up.

How it works

Transition queries subscribe to changes in the Entity Graph. The graph notifies the queries when an entity moves between nodes.

Each query keeps track of which entities entered or left its scope. These lists are cleared after the system runs.

Manual Definition

Provide the in or out descriptors in the system metadata if you aren’t using the transformer.

import { In, Read, defineSystem } from "@glom/ecs"

const onPositionAdded = defineSystem((added: In<typeof Position>) => {
  for (const [pos] of added) {
    // ...
  }
}, {
  params: [
    { in: [{ read: Position }] }
  ]
})