Skip to content

Components, Phases, Systems, and Events in ecs-js -- Doctrine #17

Description

@PJensen

Technical Memo: Components, Phases, Systems, and Events in ecs-js

Purpose

Record the intended separation of responsibilities between components, phases, systems, and events.

This memo is architectural doctrine for ecs-js usage. It is not an implementation patch by itself.

The central decision:

Components carry truth.
Phases advance truth.
Systems process rules.
Events describe what happened.
Extensions attach observers/adapters.

Declared Purpose of Components

Components represent durable simulation state.

They are the facts the world owns.

Examples:

Position
Vitality
Inventory
Equipment
ActiveEffects
Threat
Score
Posture
SleepState
Projectile
Faction
ItemInfo

A component should be used when a fact must survive beyond the current reaction moment.

Use components for:

Persistent world facts
Entity state
Rule inputs
Rule outputs
State that must be queryable
State that must be serializable
State that participates in tick-based progression

Avoid using events as a substitute for component state.

If a later system must know something during a phase, prefer durable state, changed markers, phase ordering, or transient rule components over event-listener side effects.

Declared Purpose of Phases

Phases represent ordered passage through simulation time.

They are the coarse lifecycle of a tick.

Examples:

ai
intents
movement
combat
effects
cleanup
presentation

Use phases when ordering matters.

A phase answers:

What kind of simulation work is happening now?
What systems are allowed to mutate state now?
What durable state should already exist by this point?
What durable state will later phases consume?

Phases are the primary mechanism for deterministic system-to-system rule progression.

Declared Purpose of Systems

Systems are rule processors.

They read components, write components, create/destroy entities, and emit post-fact events when useful.

Systems should not call other systems directly.

Systems communicate through:

Component state
Changed markers
Phase ordering
Explicit queries
Occasional emitted events for observation/presentation

A system is the right place for simulation truth changes.

Examples:

MovementSystem mutates Position.
CombatSystem mutates Vitality.
EffectSystem mutates ActiveEffects.
ThreatSystem mutates Threat.
ScoreSystem mutates Score.
CleanupSystem destroys dead or expired entities.

Declared Purpose of Events

Events are runtime-only descriptions of things that happened.

They are not durable state.

They are not serialized.

They are not rule truth.

They are best used as presentation / observation exhaust from the simulation.

Events are ideal for:

Rendering
Display
Audio
VFX
UI notifications
Floating text
Debug tracing
Telemetry
External observers
Presentation bridges

Examples:

EntityMoved
DamageApplied
AttackDodged
AttackParried
ItemPickedUp
ActorDied
SpellCast
TileScorched
ProjectileFired
SoundEmitted
StatusFloated

Events answer:

What just happened?
Where did it happen?
Who caused it?
What might a display/audio/VFX/debug layer want to know?

Events should generally be emitted after the rule outcome has been determined.

Rule Boundary

Events can describe rule outcomes.

Events should not determine rule outcomes.

This means event listeners should not usually be where core simulation facts are produced.

Suspicious event usage:

Damage resolution
Turn order
AI state mutation
Inventory truth
Persistent status effects
Score truth
Map mutation
Combat modifiers
Rule vetoes

Good event usage:

Play hit sound
Show damage number
Spawn blood effect
Shake screen
Show dodge text
Log debug trace
Notify UI
Animate projectile path

No Mutable Rule Hooks

Avoid mutable event hooks such as:

world.emit("beforeHit", mutableContext);
world.emit("hit", mutableContext);

That pattern lets rules escape the phase model.

If a combat outcome needs staged modification, model that inside rules space:

AttackIntent
CombatResolution
DamageApplication
Aftermath

Then process those through named systems/phases.

Example staged model:

combat:start
    Create CombatResolution from AttackIntent

combat:accuracy
    Resolve miss / dodge / parry

combat:modifiers
    Apply posture, weapon, affix, blindness, shrine, coating, status modifiers

combat:damage
    Commit Vitality changes

combat:aftermath
    Apply secondary rule outcomes

presentation
    Emit DamageApplied, AttackDodged, ActorDied, etc.

The event is the receipt, not the mechanism.

System-to-System Communication Doctrine

Use this decision table:

Hard order dependency
    Use phases / before / after

Persistent data dependency
    Use components

Transient rule staging
    Use transient components or explicit phase-local records

Post-fact observation
    Use events

Display/audio/VFX/UI/debug bridge
    Use events consumed by world extensions

Runtime setup
    Use world extensions

Do not let event listeners become hidden systems.

A listener that mutates simulation truth is probably a system trying to escape scheduling.

Presentation Boundary

A clean frame looks like this:

Simulation phases
    Mutate durable state.
    Emit descriptive events.

Presentation bridge
    Consumes events.
    Produces rendering/audio/VFX/UI effects.
    Does not mutate simulation truth.

Example:

movement phase
    MovementSystem mutates Position.
    MovementSystem emits EntityMoved.

effects phase
    TileStepEffectSystem reads Position / tile state.
    TrapSystem reads Position / trap state.
    AutoPickupSystem reads Position / pickup state.

presentation phase or external adapter
    Renderer consumes EntityMoved.
    Audio consumes EntityMoved.
    VFX consumes EntityMoved.
    UI consumes EntityMoved.

Relationship to World Extensions

A world extension is runtime setup around the world.

Extensions are appropriate for:

Render event bridge
Audio event bridge
VFX event bridge
UI event bridge
Debug event log
Telemetry bridge
Script router
External adapter
Diagnostics

Extensions may install event listeners.

Extensions should be treated with suspicion if they mutate rule truth through event listeners.

If an extension contains real simulation logic, consider converting it into a system and assigning it to a phase.

Compact Rule

Components are memory.
Phases are time.
Systems are law.
Events are traces.
Extensions are attachments.

Acceptance Criteria for Future Design Work

When adding new behavior, classify it first:

Is it durable truth?
    Component.

Is it ordered rule progression?
    System in a phase.

Is it a temporary rule-resolution artifact?
    Transient component or explicit staged record.

Is it presentation, sound, VFX, UI, debug, telemetry, or observation?
    Event.

Is it runtime wiring around a world?
    Extension.

If the answer is unclear, default toward components + phases for simulation rules, and events for presentation.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions