Skip to main content

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:

ImportWhat it isPage
@/engine/uiThe game UI kit: Icon, Counter, PressScale, motion helpers, SheetImageHUD 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, kingdomGame 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​

FileWhat it doesPage
index.tsThe public API: everything @/engine exportsthis page
math.tsclamp, lerp, damp, angleDelta, TAU, formatTime, makeRngUtilities
shell.tsexitToShell, onShellClosed, isShellActive, onShellActive, useShellActive: the game's side of the app around itPlatform layer
shellModule.tsWhat the app gives the game on phones (an optional native module, absent in plain Expo)Platform layer
shellModule.web.tsThe same contract in a browser, over postMessage with the page that frames the gamePlatform layer

Folders​

FolderWhat it doesPage
render/EngineCanvas (the 3D surface), the WebGPU renderer, Environment (sky, sun, fog), PostFX (bloom, grade), glow, WaterCanvas and rendering
core/GameDefinition, GameScreenProps and EngineErrorBoundaryCanvas and rendering
input/TouchControls (racing), Joystick, CharacterControls, input state objects, the web keyboardInput
hud/Gauge (a Skia speedometer) and HUD chrome: TopBar, RoundButton, FinishCard, Banner, DebugRowHUD and UI
ui/The game UI kit (@/engine/ui): SF Symbol icons, bundled fonts, rolling counters, press and pop motionHUD and UI
audio/AudioMixer and useAudioMixer (buses, positional sound, ducking, music), RaceAudioAudio
physics/Rigid bodies on cannon-es, triggers, joints, rays, Vehicle, GPU DebrisPhysics
models/Model (bundled .glb with a fallback), Scenery (one model placed many times), Billboard, animation graph and IKModels and scenery
gpu/TypeGPU on three's device: Particles, Trail, Grass, LivelyGround, GpuStrip, the root for your own computeGPU effects
feel/Camera shake, hit-stop, slow motion, flash, squash and stretch, tweens, juice presetsGame feel
2d/GameCanvas2D and the 2D kit: stage units, pointer, loop, sprites, tile maps, platformer bodies, haptics, one-shot soundsThe 2D layer
character/The character controller, cameras, CharacterMesh, ripples and flocksCharacter and cameras
level/Levels of boxes, pieces (ramps, stairs, arches), grid maps, LevelMeshes, Extruded, LathedCharacter and cameras
combat/Health, weapons, hitscan, projectiles, aim assist, statuses, combat HUDCombat
bots/Nav grid, pathfinding, senses, steering, BotSquad, roster fillBots
save/Versioned saves, progression, shop, settings, share cardSaves
net/Leaderboards, presence, rooms, launch links, the game backend URLNet
story/Dialogue with portraits, choices, objectives, chaptersStory
turns/TurnRules, chess, search, useOpponent, useTurnRoomTurns
paths/Curves in metres, followers, moving platforms, camera dollies, path meshesUtilities
zones/Trigger zones without physics, and typed signals (an event bus)Utilities
debug/The development readout: fps, frame time, draw calls, module linesUtilities
web/The browser boot (bootWeb) and Skia's font fallback for CanvasKitPlatform layer
modules/Genre modules: racing, shooter, puzzle, kingdomGame 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.ts files, Platform.OS checks), 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.ts re-exports whole folders with export *. 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 over src/engine/**/*.test.ts). Type errors do not stop Metro, so also run npm run typecheck.