Core concepts
Modoki is an ECS engine (built on koota) wrapped in rendering, UI, and lifecycle layers. This page defines every building block you'll reach for when writing game code by hand, and — crucially — when to reach for which. It's the conceptual companion to Writing code by hand; the full design rationale lives in the reference.
At a glance
| Concept | What it is | Ticks every frame? | Holds | Example |
|---|---|---|---|---|
| Entity | an id — a thing in the world | — | nothing (just identity) | a chess piece, a UI button |
| Component (Trait) | pure data on an entity | — | typed fields | Transform, UIElement |
| World | the container of entities | — | all entities + their traits | the active scene's world |
| System | per-frame update(world) | yes | nothing (transforms ECS) | a damage-over-time system |
| Projection | mirrors a store into ECS | on change (or a tick) | nothing | a chat-log projection |
| Manager | event-driven logic owner | no | long-lived state + methods | a scene controller, an AI opponent |
| Store | reactive state container | — | the data the UI renders | a Zustand store |
| Service | SDK / platform wrapper | — | a connection/handle | ads, audio, analytics |
| Utility | pure function | — | nothing | layout math, color helpers |
The one question that separates the two active roles: does it produce a different result on frame N+1 with no input change? (easing, oscillation, time) → System. Only reacts to events? → Manager.
The data layer
Entity
An entity is just an id — koota packs a world id, generation, and local id into a number. It has no data of its own; everything about it lives in the components attached to it.
Component (Trait)
A component is a bag of typed data attached to an entity. In koota's vocabulary (and Modoki's) it's called a trait. Two kinds:
- Data traits carry fields:
Transform(position/rotation/scale),Renderable3D(mesh + material references),UIElement,Camera,Light,ParticleEmitter, … - Tag traits carry no fields and just mark an entity:
Persistent,Paused.
Traits are pure data — they contain no behavior. Behavior that reads or writes a trait lives in a System (per frame) or a Manager (on events). Define one with koota's trait({ ... }), then hand it to registerTrait so the rest of the engine (Inspector, serialization) can discover it — see Writing code by hand for a runnable example.
World
A world holds every entity and its traits. There's always an active world (getCurrentWorld()), and Modoki swaps to a new one atomically when a scene loads — see Scene Loading.
The logic layer — five roles
Logic never lives in a trait. It lives in one of five roles. Naming all five is what keeps a codebase's logic from sprawling into a junk-drawer init.ts.
System
A System is a function (world) => void registered in the pipeline at a priority and run every frame. Use it for anything that must react to time passing: animation, physics, easing, oscillation, time accumulation.
registerSystem('my-game/shipShake', shipShakeSystem, SYSTEM_PRIORITY.GAME + 3);Priorities run in tiers — TIME → INPUT → GAME → ANIMATION → TRANSFORM_PREPASS → PHYSICS → LATE_UPDATE → TRANSFORM → AUDIO → MATERIAL → PROJECTION (the full SYSTEM_PRIORITY set, detailed in Managers & Systems). When the sim isn't running, tiers below TRANSFORM are skipped, so game time freezes on pause. A System may also own named actions ({ actions }) that a UI button can dispatch into it.
Manager
A Manager is an event-driven logic owner with no tick. It holds long-lived state and a method surface: scene navigation, an AI controller, a model-download lifecycle, app-level commands. Full scope rules (scene / game / app lifetime) are in Managers & Systems.
Projection
A Projection mirrors a store into ECS, so a reactive value shows up on the entities the renderer draws. Event-driven (registerProjection) for a pure store→ECS mirror; if the sync needs a genuine per-frame tick, or runs the reverse direction (ECS → store readback), it's a System instead — and named *System, not *Projection, so the name always tells you which one you're looking at.
Store
A Store is a reactive state container (Modoki uses Zustand) — pure data + setters, no logic, no tick. It's the subscribable surface UI components re-render from. A Manager owns the logic and writes the store; UI and Projections read it.
Service & Utility
- Service — a stateful wrapper around a platform/SDK (ads, audio, analytics). Event-driven, async, no tick.
- Utility — a pure function with no state: layout math, color/coordinate helpers.
Choosing where logic goes
- Is it data? → a Component/Trait. (No behavior in traits.)
- Does it react to time passing (a different result next frame with no input change)? → a System.
- Does it mirror a store into ECS? → a Projection (event-driven, unless it needs a per-frame tick → System).
- Is it reactive state something else renders? → a Store.
- Does it wrap a platform/SDK? → a Service.
- Is it a pure function? → a Utility.
- Otherwise (event-driven logic, lifecycle, commands, controllers) → a Manager.
If you're about to add a bare handler or free function to a setup script, stop — that's almost always a Manager method or a System in disguise.
Where to go next
- Writing code by hand — a runnable ECS example using the vocabulary above.
- Managers & Systems — the full design: scope rules, the read/write action registries, the decision log.
- Engine Architecture — the ECS core, the three render layers, the frame driver.
- API reference — every exported trait, function, and type.