Engine overview
The engine is the folder src/engine/ inside every game repo made from the template (templates/crescent-game/). It is plain TypeScript and React Native: a game imports what it needs from @/engine and composes it in src/game/. There is no npm package; each game carries its own copy of the engine.
This page is the map. Each folder has its own page with examples and reference tables.
How a game imports from the engine
@/ is a path alias for src/ (set in the template's tsconfig.json), so @/engine is src/engine/index.ts. Almost everything comes from there:
import React from 'react';
import { View } from 'react-native';
import { EngineCanvas, Environment, TopBar, type GameDefinition, type GameScreenProps } from '@/engine';
const Screen = ({ onExit }: GameScreenProps) => (
<View style={{ flex: 1 }}>
<EngineCanvas camera={{ position: [0, 3, 8] }}>
<Environment preset="day" />
<mesh position={[0, 1, 0]} castShadow>
<boxGeometry args={[1, 1, 1]} />
<meshStandardMaterial color="#E8ECF4" />
</mesh>
</EngineCanvas>
<View style={{ position: 'absolute', left: 0, right: 0, top: 0 }} pointerEvents="box-none">
<TopBar title="My game" status="Ready" onExit={onExit} />
</View>
</View>
);
const game: GameDefinition = {
id: 'my-game',
title: 'My game',
description: 'A box in the sun.',
Component: Screen,
};
export default game;
Three other entry points are imported by path, not from @/engine:
| Import | What it is | Page |
|---|---|---|
@/engine/ui | The game UI kit: Icon, Counter, PressScale, motion helpers, SheetImage | HUD and UI |
@/engine/ui/fonts/<face> | One bundled font face per module (@/engine/ui/fonts/nunitoBlack) | HUD and UI |
@/engine/modules/<name> | A whole genre from a config: racing, shooter, puzzle, kingdom | Game modules |
Fonts and modules stay out of @/engine on purpose: a game's bundle carries only the faces and modules it imports.
The map of src/engine
Top-level files
| File | What it does | Page |
|---|---|---|
index.ts | The public API: everything @/engine exports | this page |
math.ts | clamp, lerp, damp, angleDelta, TAU, formatTime, makeRng | Utilities |
shell.ts | exitToShell, onShellClosed, isShellActive, onShellActive, useShellActive: the game's side of the app around it | Platform layer |
shellModule.ts | What the app gives the game on phones (an optional native module, absent in plain Expo) | Platform layer |
shellModule.web.ts | The same contract in a browser, over postMessage with the page that frames the game | Platform layer |
Folders
| Folder | What it does | Page |
|---|---|---|
render/ | EngineCanvas (the 3D surface), the WebGPU renderer, Environment (sky, sun, fog), PostFX (bloom, grade), glow, Water | Canvas and rendering |
core/ | GameDefinition, GameScreenProps and EngineErrorBoundary | Canvas and rendering |
input/ | TouchControls (racing), Joystick, CharacterControls, input state objects, the web keyboard | Input |
hud/ | Gauge (a Skia speedometer) and HUD chrome: TopBar, RoundButton, FinishCard, Banner, DebugRow | HUD and UI |
ui/ | The game UI kit (@/engine/ui): SF Symbol icons, bundled fonts, rolling counters, press and pop motion | HUD and UI |
audio/ | AudioMixer and useAudioMixer (buses, positional sound, ducking, music), RaceAudio | Audio |
physics/ | Rigid bodies on cannon-es, triggers, joints, rays, Vehicle, GPU Debris | Physics |
models/ | Model (bundled .glb with a fallback), Scenery (one model placed many times), Billboard, animation graph and IK | Models and scenery |
gpu/ | TypeGPU on three's device: Particles, Trail, Grass, LivelyGround, GpuStrip, the root for your own compute | GPU effects |
feel/ | Camera shake, hit-stop, slow motion, flash, squash and stretch, tweens, juice presets | Game feel |
2d/ | GameCanvas2D and the 2D kit: stage units, pointer, loop, sprites, tile maps, platformer bodies, haptics, one-shot sounds | The 2D layer |
character/ | The character controller, cameras, CharacterMesh, ripples and flocks | Character and cameras |
level/ | Levels of boxes, pieces (ramps, stairs, arches), grid maps, LevelMeshes, Extruded, Lathed | Character and cameras |
combat/ | Health, weapons, hitscan, projectiles, aim assist, statuses, combat HUD | Combat |
bots/ | Nav grid, pathfinding, senses, steering, BotSquad, roster fill | Bots |
save/ | Versioned saves, progression, shop, settings, share card | Saves |
net/ | Leaderboards, presence, rooms, launch links, the game backend URL | Net |
story/ | Dialogue with portraits, choices, objectives, chapters | Story |
turns/ | TurnRules, chess, search, useOpponent, useTurnRoom | Turns |
paths/ | Curves in metres, followers, moving platforms, camera dollies, path meshes | Utilities |
zones/ | Trigger zones without physics, and typed signals (an event bus) | Utilities |
debug/ | The development readout: fps, frame time, draw calls, module lines | Utilities |
web/ | The browser boot (bootWeb) and Skia's font fallback for CanvasKit | Platform layer |
modules/ | Genre modules: racing, shooter, puzzle, kingdom | Game modules |
What goes in src/game and what goes in src/engine
The entry chain is fixed: index.ts (phones) or index.web.ts (browser) registers src/App.tsx, and App.tsx renders the Component of the GameDefinition that src/game/index.tsx default-exports. Keep that contract.
Put in src/game/:
- Everything about this game: its rules, level data, palette, fonts, HUD screens, sounds list and the
GameDefinition. - Code that only this game needs, even if it is large.
Change src/engine/ only when:
- The game needs something the engine does not have and it would be useful to any game (a new control scheme, a new effect). Add it as a new export and keep existing exports working.
- Something differs per platform. Platform differences live in the engine (
*.web.tsfiles,Platform.OSchecks), never in a game. See Platform layer.
Rules that keep a game working everywhere:
- Read per-frame values from mutable objects inside
useFrame(input state, character state), not from React state. React state is for things that change a few times a second at most. - Take input through the engine's controls or react-native-gesture-handler, so a keyboard and mouse work in a desktop browser too.
- Load every asset with
require()and the engine's loaders. Do not build file paths or URLs by hand. - Draw 3D through
EngineCanvas. There is no WebGL path.
Gotchas
src/engine/index.tsre-exports whole folders withexport *. Two folders exporting the same name would clash, so engine additions use distinct names.- Inside the hosted Moonspell app, the template's engine files are refreshed in existing games when a new version runs, unless the game changed them. In your own repo nothing refreshes them: copy engine changes from the template yourself.
- Tests for the pure parts of the engine run with
npm test(Node's test runner oversrc/engine/**/*.test.ts). Type errors do not stop Metro, so also runnpm run typecheck.