Gessa Docs
Product · Explanation

Concept

Explanation: Entity Component System

How Gessa models built-in components as typed atomic contracts, why authored state reaches every runtime subsystem only through materialized artifacts, and what the model deliberately does not build.
engine v1.0.234since v1Copy for LLM
engine v1.0.232

Last verified 2026-09-03 against engine v1.0.232.

An entity in Gessa is a bag of components, and each component is a small, typed, fixed-shape contract. This page explains the model as it is implemented: how a component is defined, how a field carries meaning, how authored state crosses into the runtime, why registration lives in exactly one place, and where the model draws hard lines it will not cross. It frames the why. The canonical inventory of every built-in component, its fields, packs, consumers, and replication policy lives in the generated Component Type Definitions reference, which this page links down to and never restates.

Two framings from Backend Authority carry through everything here: the server owns the world, and a component's authored value is not the same thing as the runtime state a subsystem consumes. The ECS unit is the discipline that keeps those two honest.

ReferenceComponent Type DefinitionsResolved signature, schema and example

One record per component

Every built-in is a ComponentTypeDefinition (packages/ecs/src/index.ts). The record carries the component's type, its human label and description, a category, an authored contractVersion, an addMode that governs how it is admitted, its defaults, a replication declaration, and a schema that validates its authored value. Optional facets (visibility, multiplicity, verbs, variants, scene relevance, authoring assistance) hang off the same record.

The value schemas are Zod objects, and they are unioned into one discriminated union keyed on type, ComponentSchema (packages/ecs/src/index.ts), with a parallel by-type lookup map COMPONENT_SCHEMAS_BY_TYPE. Because the discriminant is the component type, a payload can only ever validate against one arm, and an unknown type fails closed rather than falling through to a permissive default.

A category is one of a fixed union declared on ComponentTypeDefinition.category. The categories cover spatial, rendering, physics, gameplay, input, scripting, runtime, audio, and networking concerns; naming the field's category is how the palette and the inspector group a component without a second taxonomy drifting away from the schema.

Not every built-in is authorable by a creator or the in-product AI. INTERNAL_ENGINE_COMPONENT_TYPES (packages/ecs/src/index.ts) marks ForceComponent and VelocityComponent as engine-internal and palette-hidden; the public authorable set, PublicAuthorableBuiltInComponentType, excludes them by construction.

One encoding per concept

A three-dimensional position, a rotation, a color, a duration, an asset handle: each of these has exactly one sanctioned encoding. VOCABULARY_PRIMITIVE_NAMES (packages/ecs/src/primitives.ts) fixes the primitive constructors (vec3, quat, rotationDegrees, colorSrgb, seconds, millis, ticks, assetRef, entityRef, vertexBuffer) as the only vocabulary an authored field may use, and a second encoding of the same concept fails at the syntax level. This is why two components that both hold a position hold it the same way, and why a consumer never has to guess which of several representations it was handed.

Field meaning is data, not comments

What a field means to the engine, whether it affects rendering, simulation, both, or neither, its units, its coordinate space, is itself generated data. COMPONENT_FIELD_SEMANTICS (packages/ecs/src/fieldSemantics.ts) holds those annotations, and applyFieldSemantics (packages/ecs/src/index.ts) merges them into the registry when it loads. An inline annotation on a definition wins over the generated map, so a component can migrate its semantics inline later without touching any consumer. This is how TransformComponent declares its pose to be local-space, its rotation to be in degrees, and its scale to be a multiplier, in a form the inspector, the runtime, and the documentation all read from one source.

The materialization boundary

This is the load-bearing part of the model. A consumer subsystem never reads a component's authored value directly. The only thing it is allowed to read is an Artifact (packages/component-artifacts/src/index.ts), the boundary-safe shape a component's module materializes from that authored value. The set of consumer subsystems (ConsumerSubsystem) and the set of artifact kinds (ArtifactKind) are both closed unions, so render, simulation, collision, navigation, spatial query, and audio each reach authored state through a materialized artifact and through nothing else. There is no side channel.

Because there is no side channel, a component is not two half-defined things at once. Every module is exactly one of two structural kinds, inline or resource-backed-thin-ref (ComponentKind, packages/component-intents/src/index.ts). An inline module materializes straight from its authored fields; a resource-backed module carries a thin reference and resolves the heavy payload through the resource system. There is deliberately no hybrid dual-source kind that reads from both an inline value and a resource at once, because that would reintroduce the ambiguity the boundary exists to remove.

The boundary is evidence, not attestation. Each effectful field or verb names the concrete reader that consumes it, by file and symbol, through DeclaredConsumer (packages/component-intents/src/index.ts). A field that claims to affect the runtime must point at the code that reads it.

JSON
{
  "kind": "inline",
  "materialize": "authored fields -> Artifact",
  "declaredConsumers": [
    { "field": "<authored field>", "reader": "<file>#<symbol>" }
  ]
}

No Potemkin components

A component that declares an effect but produces no artifact change would be a lie the type system cannot catch. The engine catches it at test time instead. runModuleConformance (packages/component-modules/src/conformance.ts) drives every registered module and proves, per module, that each declared effectful field changes only its own coordinate and changes the emitted artifacts, that each effectful verb changes the artifacts, and that materialize() is twin-run deterministic. A vacuous module, one whose verb changes nothing, fails. The runner is proven to have teeth by an explicit Potemkin negative control in componentModuleConformance.test.ts.

Registration lives in one place

There is a single site where a module becomes part of the engine: COMPONENT_MODULES (packages/component-modules/src/registry.ts), a frozen record. The registered set is required to equal the ECS component surface exactly, with nothing unported, and unportedComponentTypes() reports the shrinking gap while any exists. An inline module does not redefine the schema it validates against; it binds to the ECS source of truth through inlineFieldModule (packages/component-modules/src/inlineModule.ts), as transformModule and rigidBodyModule do. One registration site, checked against one component surface, is what keeps the module layer from becoming a second, quietly divergent catalog.

Local pose, composed world truth

An authored TransformComponent holds a local pose. The world-space pose of an entity that sits in a hierarchy is not stored on the component; it is composed at runtime by RuntimeHierarchyTransformSystem (server/src/modules/runtime/runtimeSystems.ts), whose tick composes world transforms for the hierarchical entities and emits a world-transform state patch tagged with a runtime-hierarchy source. It does this only for entities that pass entityHasRuntimeHierarchy (a parent reference, a parent anchor, or hierarchy flags). An entity with no runtime hierarchy keeps its local pose as its world pose, and nothing is emitted for it. Structural hierarchy is projected from entity fields; there is no authored hierarchy component, only the removed contract that used to be one.

Replication is declared and fail-closed

How a component reaches other clients is a typed, per-type policy, ComponentReplicationDeclaration (packages/ecs/src/index.ts): an audience, a priority tier, whether delivery is reliable, a frequency, a payload shape, and an interest rule. When the wire value is projected, it is built one field at a time by projectComponentReplicationValueForType (packages/ecs/src/index.ts), which drops any unknown field, the discriminant, the replication metadata, and anything malformed, and counts what it rejected. A field is replicated because the policy names it, not because it happened to be present on the object.

What the model does not build

The model is honest about its edges. Naming them is part of the contract.

  • No hybrid module kind. There are two structural kinds and no third that reads inline and resource state at once (ComponentKind, packages/component-intents/src/index.ts).
  • Two components are engine-internal. ForceComponent and VelocityComponent are palette-hidden runtime primitives (INTERNAL_ENGINE_COMPONENT_TYPES). Force is not creation-map authorable because its queue is per-tick runtime commands; velocity is.
  • Custom components are project-local data, not engine atomics. A project may define its own component, but it is validated project-local data, never a new engine atomic, and it is not exposed to the in-product AI. The non-exposure is asserted by check:ecs-contract-surface-closure (scripts/check-ecs-contract-surface-closure.ts).
  • No first-class camera modes. CameraComponent.firstPerson is forbidden and fails the closure gate; the camera is oriented by the client look controller, not by an authoritative flag.
  • Gameplay patterns are scripts, not components. Health, damage, spawn pools, and collectors are script patterns under ADR 0020, not ECS atomics. The generated reference lists the pattern names that were deliberately stripped from the atomic set.
  • Removed contracts persist only as migration metadata. The former HierarchyComponent and StateMachineComponent survive only inside REMOVED_COMPONENT_CONTRACTS (packages/ecs/src/index.ts), so old content migrates rather than silently breaking.
  • Some declared fields are representation-only. NetworkProfileComponent carries an ownership selection that is representation-only and inert today: no runtime subsystem reads it yet. It is admitted through a time-boxed field-consumer exemption rather than pretending to have a consumer.

Where to go next

For the surfaces, invariants, proof surfaces, and version-pinned numbers, each bound to a symbol, read the ECS reference. For a reader's guide to the generated catalog itself, read Reference: ECS Components. The canonical, machine-generated inventory is always the Component Type Definitions reference.

Was this helpful?Report an issueContact support

On this page