Skip to content

Writing code by hand

Everything you place with the editor's Inspector — a Transform, a Health component, a system that ticks every frame — is backed by plain TypeScript you can write, read, and test directly. The editor is a tool for placing entities in a scene; it isn't a requirement for the game logic itself. This page gets you writing and running real engine code with nothing but a terminal.

Not on npm (yet)

@modoki/engine isn't published as a standalone npm package today — the engine ships as source inside its own repo, consumed via Vite. "Writing code by hand" means working inside a checkout of modoki-engine, not npm install-ing it into an arbitrary project. See Getting started for cloning + install.

The core loop, with no renderer at all

The engine's verification harness (createTestWorld) wraps world creation, ticking, and teardown into one call — it's built for headless testing, but it's also the fastest way to see the ECS mechanics with zero project setup. Save this as quickstart.ts at the repo root and run it with npx tsx quickstart.ts:

ts
import { trait } from 'koota';
import { createTestWorld, registerTrait, Transform } from '@modoki/engine/runtime';

// A trait is just data — define it with koota's `trait()`, then register it
// so the editor/serializer can discover it. Registration is optional for a
// pure headless script like this one, but it's what makes a hand-defined
// trait a first-class citizen everywhere else in the engine.
const Health = trait({ current: 100, max: 100 });
registerTrait({ name: 'Health', trait: Health, category: 'component', fields: {} });

// A system is a function `(world) => void`, run once per tick in priority order.
function damageSystem(world: import('koota').World) {
  for (const entity of world.query(Health)) {
    const h = entity.get(Health);
    if (h.current <= 0) continue;
    entity.set(Health, { current: h.current - 1 });
  }
}

const tw = createTestWorld({ systems: [{ name: 'damage', fn: damageSystem }] });

const enemy = tw.spawn(Transform, Health);
tw.step(5); // advance 5 fixed-dt ticks deterministically

const health = tw.trait<{ current: number; max: number }>(Health, enemy);
console.log(health.current); // 95

tw.dispose(); // tear down every global the harness installed

That's the whole loop: define a trait → register a system → spawn an entity → tick → read a trait back. No React, no Three.js, no PixiJS, no editor process. Everything the editor's Inspector shows you is this same data — it just also draws it.

How this maps onto a real, running game

A running Modoki game is the same traits and systems, wired into the app shell so they get a renderer, an input loop, and a real requestAnimationFrame driver instead of tw.step(). That wiring is a game.ts exporting a single GameDefinitiondon't hand-write this file or its scene JSON; the scaffolder mints the whole skeleton (identity, config, a first scene with correct asset GUIDs) so nothing is missed:

bash
node engine/scripts/scaffold-project.mjs games/my-game "My Game"

Inside the scaffolded project, runtime/setup.ts is where you register your own traits/systems/managers — the exact same registerTrait/registerSystem calls from the snippet above, just called from registerSystems() instead of run top-to-bottom in a script:

ts
// games/my-game/runtime/setup.ts
export function registerSystems() {
  registerTrait({ name: 'Health', trait: Health, category: 'component', fields: {} });
  registerSystem('my-game/damage', damageSystem, SYSTEM_PRIORITY.GAME);
}

From there, placing an entity that carries Health in a scene — whether you do it by hand-editing scene JSON or by adding it in the editor's Inspector — is just data for the traits and systems you already wrote to act on.

Where to go next

Built with Modoki.