ServicesECS

ECS

An Entity Component System for game state, simulations, and any fleet of objects with typed, queryable state: schema-validated components on entities, derived archetypes, parent-child hierarchies, queries with field-level filters, shared per-world resources, and systems driven by a tick coordinator. Every example on this page was executed against a live server.

When to reach for it: game worlds, agent or physics simulations, device fleets - anywhere many objects share typed state shapes that you query by shape ("all entities with Position and Velocity") and mutate with optimistic locking.

When not to: plain JSON storage keyed by name belongs to KV Store; distributed locks and compare-and-swap over a shared value belong to Distributed State.

Concepts

  • Index - a namespace isolating one ECS world, passed as -x/--index on almost every command. Everything below except schemas lives inside an index; two indexes never see each other's entities.
  • Schemas are global and mandatory - a component type must have a JSON Schema before any entity can carry it. Global (no index) and versioned.
  • Entity - an ID plus components (JSON documents of schema-defined types), tags, an optional parent, and a version that increments on every mutation (optimistic locking).
  • Archetype - the set of component types on an entity, identified by a hash. Derived automatically; you never create or name one.
  • Resource - shared singleton JSON per index (world config, clocks), not attached to any entity, with versioned updates.
  • Systems and the coordinator - external workers matched to entities by a stored query and driven by a tick loop; they have their own page.

Schemas

Schema commands take no index. Component data is validated against the latest schema version at write time - verified by relaxing a required field in a v2 and watching a previously rejected write succeed:

snug ecs schema create --type Position --version 1 \
  --schema '{"type":"object","properties":{"x":{"type":"number"},"y":{"type":"number"}},"required":["x","y"]}'
snug ecs schema list
snug ecs schema get --type Position              # latest; add --version 1 for a specific one
snug ecs schema delete --type Position --version 1 --force

Writing a component type that has no schema fails with 404 schema_not_found; data that violates the schema fails with 400 schema_validation_failed and the validator's message ("y" is a required property).

Entities and components

snug ecs entity create -x game1 --id player1 \
  --components '{"Position":{"x":10,"y":20},"Health":{"current":100,"max":100}}' \
  --tags player,active
snug ecs component set -x game1 --entity player1 --type Velocity --data '{"x":1,"y":0}'
snug ecs component get -x game1 --entity player1 --type Velocity
snug ecs component remove -x game1 --entity player1 --type Velocity
snug ecs entity get -x game1 --id player1
snug ecs entity tags -x game1 --id npc1 --tags hostile,elite
snug ecs entity delete -x game1 --id player1 --force

Creating an ID that already exists in the index fails with 409 entity_already_exists. entity tags replaces the whole tag set with the supplied list - it does not append.

Batch operations create from a JSON array and delete by query, not by ID list. The CLI's batch-delete exposes only --has-all; the HTTP endpoint accepts a full query object:

snug ecs entity batch-create -x game1 --entities \
  '[{"entity_id":"crate1","components":{"Position":{"x":0,"y":0}}},{"entity_id":"crate2","components":{"Position":{"x":1,"y":1}},"tags":["loot"]}]'
snug ecs entity batch-delete -x game1 --has-all Position --limit 100 --force

Queries

The CLI matches on component presence, with --count and cursor pagination (pass the returned cursor back via --cursor):

snug ecs query -x game1 --has-all Position,Velocity --limit 10
snug ecs query -x game1 --has-all Position --has-none Velocity --count

The HTTP endpoint adds field-level filters (eq|ne|lt|lte|gt|gte|between|in|contains), plus select and order_by that the CLI does not surface. The query lives under a where key whose four arrays are all required, even when empty:

curl -X POST -H "Authorization: Bearer $SNUG_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"where":{"has_all":["Health"],"has_any":[],"has_none":[],"filters":[{"component":"Health","field":"current","op":"lt","value":50}]}}' \
  http://localhost:4000/api/v1/ecs/game1/query
{
  "status": 200,
  "msg": "OK",
  "data": {
    "entities": [
      {
        "entity_id": "npc2",
        "components": {
          "Health": { "current": 30, "max": 30 },
          "Position": { "x": 50, "y": 8 }
        },
        "tags": ["npc"],
        "parent": null,
        "archetype_hash": "92ba98566c2e1f6a",
        "version": 1
      }
    ],
    "total": 1,
    "cursor": null,
    "has_more": false
  }
}

Archetypes

Read-only views over the component-set groupings the server derives:

snug ecs archetype list -x game1
snug ecs archetype get -x game1 --hash 92ba98566c2e1f6a

The hash is a pure function of the component set - verified by adding a component (hash changed) and removing it again (original hash returned).

Hierarchy

Single parent, many children. Set the parent at create time with --parent, or later:

snug ecs hierarchy set-parent -x game1 --id sword1 --parent player1
snug ecs hierarchy children -x game1 --id player1
snug ecs hierarchy remove-parent -x game1 --id sword1

Setting a new parent replaces the old one; a parent chain that loops back is rejected with 422 circular_reference.

Resources

snug ecs resource create -x game1 --name GameConfig --data '{"gravity":9.8,"max_players":8}'
snug ecs resource get -x game1 --name GameConfig
snug ecs resource update -x game1 --name GameConfig --data '{"gravity":10.0,"max_players":8}' --expected-version 1
snug ecs resource delete -x game1 --name GameConfig --force

--expected-version is optimistic locking: a stale version fails with 409 version_conflict (expected 1, found 2) and leaves the resource untouched. Systems can request resources in their tick payloads via --include-resources - see Systems and the coordinator.

Change subscriptions

Subscription records declare what to watch - one entity, one component type everywhere, or a query:

snug ecs subscription create -x game1 --subscription-type entity --entity-id player1
snug ecs subscription create -x game1 --subscription-type component --component-type Position
snug ecs subscription create -x game1 --subscription-type query \
  --query '{"has_all":["Position"],"has_any":[],"has_none":[],"filters":[]}'
snug ecs subscription delete -x game1 --subscription-id <sub-id>

Notifications are delivered over the WebSocket endpoint GET /api/v1/ecs/{index}/subscribe, authenticated with a Bearer header or a ?token= query parameter - verified: the handshake upgrades (101) with a token and is rejected (401) without. The CLI manages only the records; the message protocol is in the Subscriptions API reference.

Limits and configuration

Defaults: 64 components per entity, 64 KiB per component payload, batches of 100, query pages of 100 (clamped at 1000), and a 100 ms default tick - all tunable via the ECS_* variables in the ECS CONFIG reference.

Reference

On this page