Skip to content

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

ConceptWhat it isTicks every frame?HoldsExample
Entityan id — a thing in the worldnothing (just identity)a chess piece, a UI button
Component (Trait)pure data on an entitytyped fieldsTransform, UIElement
Worldthe container of entitiesall entities + their traitsthe active scene's world
Systemper-frame update(world)yesnothing (transforms ECS)a damage-over-time system
Projectionmirrors a store into ECSon change (or a tick)nothinga chat-log projection
Managerevent-driven logic ownernolong-lived state + methodsa scene controller, an AI opponent
Storereactive state containerthe data the UI rendersa Zustand store
ServiceSDK / platform wrappera connection/handleads, audio, analytics
Utilitypure functionnothinglayout 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.

ts
registerSystem('my-game/shipShake', shipShakeSystem, SYSTEM_PRIORITY.GAME + 3);

Priorities run in tiers — TIMEINPUTGAMEANIMATIONTRANSFORM_PREPASSPHYSICSLATE_UPDATETRANSFORMAUDIOMATERIALPROJECTION (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

  1. Is it data? → a Component/Trait. (No behavior in traits.)
  2. Does it react to time passing (a different result next frame with no input change)? → a System.
  3. Does it mirror a store into ECS? → a Projection (event-driven, unless it needs a per-frame tick → System).
  4. Is it reactive state something else renders? → a Store.
  5. Does it wrap a platform/SDK? → a Service.
  6. Is it a pure function? → a Utility.
  7. 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

Built with Modoki.