| Document | PRD — Laige 2.5D Multi-OS Game Engine (C++) |
| Version | 0.4 (draft) |
| Status | Proposed — pending review |
| Owner | Engine team |
| Last updated | 2026-10-04 |
Name: Laige — an acronym for Legendary AI Game Engine. Confirmed as the final name on 2026-09-10 (decision D-NAME, ADR 0001).
v0.4 change: §8.1: the isometric depth-key rebuild budget is re-baselined from ≤ 0.2 ms to ≤ 0.3 ms (mean, 10k dirty cells after a terrain edit). The absolute-target gate runs on the CI reference machine (ubuntu-24.04, Clang 18.1.3, CMake Debug — methodology §5: "CI is the gate"), where the engine's unchanged
setTilepath measures 0.194–0.267 ms across runner variance — the 0.2 ms bar (calibrated from local-machine runs of 0.081 ms) had zero margin there and flapped on master and PR lanes with identical engine code (no regression). The workload, the engine path, and the measurement method are unchanged. Evidence:docs/benchmarks/baselines/m2-iso-depth-table-budget-rebaseline.md.v0.3 change: engine name confirmed as "Laige" (acronym for Legendary AI Game Engine); §18 item 1 resolved (ADR 0001).
v0.2 change: isometric is designated the primary projection — most Laige games will be isometric. It is the default template, the reference scene for all visual/performance acceptance tests, and the focus of new first-class requirements (depth keys, picking, grid-snap camera, grid-aligned AOI).
Laige is a cross-platform (Windows / Linux / macOS) 2.5D game engine written in C++20, designed so that developers — human or AI agent — can build 2.5D games easily, fast, and stably, from small single-player titles up to large authoritative multiplayer games (MMOs). Isometric is the primary projection: most games built with Laige will be isometric, and the engine is tuned so that the isometric case is the fastest, safest, and most polished path.
The engine's three defining properties:
- 2.5D-native, isometric-first. The logical simulation is 2D (flat playfield, 2D physics, 2D state — cheap, deterministic, easy to replicate); the presentation is 3D (3D camera, depth-sorted sprites/billboards, parallax, isometric/oblique projection). Games get depth and cinematic camera freedom without paying 3D simulation or 3D networking costs.
- Performance-guarded API. The public API is flexible (custom systems, custom components, custom rendering passes, scripting hooks, unsafe escape hatches) but tight: the safe-by-default path is the only path where common performance mistakes (per-frame allocations, unbatched draw calls, variable-timestep physics, hand-rolled replication) are possible at all. Dangerous patterns are either impossible, or explicitly opt-in behind a documented, warned
unsafenamespace. - Minimal dependencies. At most a handful of small, mature, header-only or single-file third-party libraries. Everything performance- or determinism-critical (ECS, 2D physics, networking protocol, asset format) is built into the engine. Nothing ships that isn't justified.
Success = a developer can ship a playable isometric 2.5D game with the editor in a day, and a team can scale the same codebase to an MMO with thousands of concurrent players without rewriting subsystems.
Vision. One engine, one codebase, one API for: an isometric action RPG (the flagship style — most Laige games will be isometric), a 20-minute single-player platformer, a co-op dungeon crawler, a real-time strategy-lite, and a persistent 2.5D MMO — with deterministic simulation at the core in all of them.
Problem. Existing engines are either:
- General 3D engines (Unity, Unreal, Godot): 2D is a compromise layered on 3D; the API surface is so wide that developers (and AI agents writing code against it) routinely produce slow, allocation-heavy, hard-to-debug code; MMO-grade networking is bolted on, not built in.
- Lightweight 2D engines: fast and simple, but 2D-only presentation (no depth, no 3D camera), no credible path to MMO scale, thin tooling. Isometric in these engines is usually hacked together (manual z-fighting workarounds, hand-rolled picking).
There is a gap: a narrow engine that is deep — small API, opinionated defaults, hard performance ceilings by construction, first-class networking, and tooling good enough for professional use.
| Persona | Needs | Laige's answer |
|---|---|---|
| Solo/indie dev | Fast iteration, few concepts, great defaults | One editor, safe API, batteries-included (audio, physics, input, UI), templates |
| Small team (2–10) | Custom systems, profiling, reproducible builds | ECS escape hatches, built-in profiler, deterministic replays, headless server mode |
| MMO team | Authoritative server, sharding, bandwidth control | Built-in authoritative server runtime, AOI streaming, generated serialization, delta sync, lockstep mode |
| AI agent / LLM-generated code | Predictable API, machine-readable docs, safe-by-default, actionable errors | Machine-readable API manifest, deterministic behavior, guardrails that make wrong code fail loudly instead of being slow, stable versioned ABI |
Representative games in scope (all must be buildable without leaving the engine):
- Isometric 2.5D action RPG (single-player + online co-op) — the flagship. Default projection for the project template; the reference game for visual and performance acceptance.
- Single-player 2.5D platformer with parallax, tilemaps, 2D skeletal animation.
- Small-scale deterministic lockstep multiplayer (≤8 players) e.g. twin-stick arena.
- Large 2.5D MMO: hundreds of players per zone, authoritative server, client prediction, chat, trading UI, instance dungeons.
"2.5D" in Laige is a precise architectural contract, not a visual style:
- Simulation space is 2D. Entities have position/orientation in a plane (local X/Y; world Y may be "height" or "depth" depending on projection mode — see §4.2). All physics, collision, pathing, AI spatial queries, and replicated state are 2D.
- Rendering space is 3D. The camera is a full 3D camera (position, orientation, projection). Entities are rendered as depth-sorted textured quads/billboards/sprites with a world-space depth value used for z-ordering; optional lightweight 3D content (simple meshes, 3D backgrounds) is a stretch goal, not part of the core promise.
- Depth is presentation-only. The z/depth value is a rendering attribute, not a simulated physical dimension. This keeps state small, physics cheap, and networking cheap — the single most important decision for the MMO goal.
- Isometric is the primary projection — most Laige games will be isometric (ARPGs, strategy-lite, dungeon crawlers, MMO hubs). Consequences:
- Isometric is the default camera/projection for new projects (templates ship with it).
- It has the deepest feature set: deterministic depth keys derived from (x, y, tile/step height, layer), grid snapping, grid-snap camera, screen→grid picking (click-to-select / click-to-move).
- It is the reference scene for all visual and performance acceptance tests (worst-case overlap for depth sorting).
- Other projection modes are first-class, as simpler special cases of the same pipeline:
- Isometric (primary): 2:1 dimetric (pixel-art default) and true 30°/60° isometric, plus arbitrary shear per scene. Simulation stays axis-aligned 2D (no oblique simulation — keeps collision and pathing axis-aligned and deterministic); the render depth key is computed from the axis-aligned world, not from screen space.
- Side-view (2.5D platformer): camera looks at the X/Z plane; Y = height; sprites billboarded; depth = Z.
- Top-down / oblique: camera looks down; X/Y = ground plane; depth from Y (or sprite z-order); supports classic top-down and ¾-view.
- Free 3D camera over a 2D field (cinematic/cutscene mode).
- Parallax & layers: background/midground/foreground layers render with configurable parallax factors — core renderer feature, not a plugin.
Acceptance criteria (AC):
- AC-4.1: A platformer (side-view) and an isometric ARPG both run from the same engine build, differing only in camera/projection configuration.
- AC-4.2: No simulation, physics, pathing, or network code depends on the projection mode.
- AC-4.3: Depth sorting is correct (visually verified) for 10,000 overlapping sprites at 60 FPS on mid-range hardware.
- AC-4.4: Isometric first-class set: screen→grid picking is exact at all supported zoom levels; depth keys are correct with tile/step heights (stepped terrain); isometric is the default for the project template; depth-key rebuild after a terrain edit meets the §8.1 budget.
- G1: Make 2.5D game development easy — low concept count, sane defaults, working editor, excellent docs.
- G2: Be performant by construction — hard, measurable performance budgets (§8), guardrails (§9), zero steady-state allocation in hot paths.
- G3: Be stable — no-UB guarantee, deterministic simulation, graceful degradation, crash-free CI, reproducible builds.
- G4: Scale from single-player → MMO without changing architecture: one deterministic simulation core, one networking stack, one server runtime.
- G5: Flexible but tight API: rich customization surface + explicit unsafe escape hatches that are impossible to misuse silently (§9).
- G6: Minimal dependencies (§11): small approved list, all mature, each with documented justification; no runtime dependency growth without a PRD revision.
- G7: AI-agent-friendly development experience (§13).
- Full 3D engine (no 3D physics, no 3D character animation, no skeletal 3D rigs). 3D meshes for static scenery: stretch goal, M8+.
- Console targets (PC first; mobile/web stretch).
- Built-in asset store, marketplace, or online services (we provide the networking stack, not the service).
- Editor as a runtime dependency — games and servers run headless without any editor code.
- Built-in multiplayer matchmaking/accounting services.
| Platform | Priority | Notes |
|---|---|---|
| Windows 10/11 (x64, arm64 stretch) | P0 | MSVC 2022 primary, clang-cl secondary |
| Linux (x64, arm64) | P0 | glibc ≥ 2.31; major distros; MSVC-free CI path |
| macOS (arm64 + Intel) | P0 | No support for apple fanboys. Fuck apple. |
| Web (WASM + WebGL2) | P2 | M8+; desktop targets first |
| Android / iOS | P3 | Stretch; only if WebGL/WASM path is solid |
Rendering API: OpenGL 3.3 core (desktop) / OpenGL ES 3.0 (web/mobile). One renderer, version-gated feature layers. Vulkan is an explicitly optional later backend; the renderer API is defined so a second backend is a clean addition (no promise in v1).
Acceptance criteria:
- AC-6.1: One source tree compiles on all P0 platforms with the same feature set (no feature flags that silently remove subsystems).
- AC-6.2: Server mode builds headless (no GPU required) on Linux and Windows.
Requirement IDs are tracked. P0 = must ship in M1–M5, P1 = M6–M7, P2 = M8+.
- FR-1.1 Game loop: Fixed-timestep simulation (default 60 Hz, configurable 20–120 Hz) decoupled from variable rendering; render interpolation of positions (2D-aware, projection-correct) between simulation ticks.
P0 - FR-1.2 ECS core: Archetype/SoA storage; dense entity iteration; O(1) component access; deterministic iteration order; component add/remove via pool (not heap). Entity is a 32-bit handle; components identified by compile-time trait types.
P0 - FR-1.3 System framework: Systems are plain, registered functions with declared (static or auto-measured) time budgets and declared input/output component access; engine enforces iteration legality (no mutation while iterating the same storage).
P0 - FR-1.4 Determinism mode: Simulation core must be bit-identical across platforms, compilers, and run times when run in deterministic mode (see §10.3). Deterministic replays (input log → state) must be replayable and diffable.
P0 - FR-1.5 Configuration: Declarative game config (tick rate, budgets, camera defaults, asset roots) in JSON; runtime config overrides; hot-reload of non-simulation config in debug.
P0 - FR-1.6 Headless mode: The entire engine (except presentation) runs without a window/GPU — required for servers, CI, and replay tooling.
P0
- FR-2.1 Sprite pipeline: Batched textured quads; one draw call per (atlas, material, blend) group per frame; GPU-instanced; supports rotation, scale, tint, per-sprite depth, UV sub-rect, atlas UV animation (sheet frames).
P0 - FR-2.2 Depth & sorting: Deterministic, stable z-order from (layer, depth, entity id). In isometric mode the depth key = ground (x + y) contribution + tile/step height, precomputed at scene build and incrementally updated on tile/height changes (not recomputed per frame). Bucket/radix sort, no per-frame allocation.
P0 - FR-2.3 Parallax layers: Named background/mid/foreground layers with parallax factor, offset, UV scroll, blend.
P0 - FR-2.4 Camera: Full 3D camera (position, look-at, FOV/ortho), smooth follow/target, shake, zoom, screen-shake, camera constraints (rect bounds). Isometric presets (2:1, true iso), grid-snap camera mode (camera locked to grid coordinates — the standard isometric game feel), and zoom clamping.
P0 - FR-2.5 Projection modes: Isometric (primary; 2:1 default, true iso, custom shear), side-view, top-down/oblique, free-cinematic — selectable per scene/view (see §4).
P0 - FR-2.6 Tiles & tilemaps: Tilemap component (chunks), tile animation, per-tile depth/height for 2.5D stepped terrain (auto-depth from tile Y-height — the isometric staple), parallax tile layers.
P0 - FR-2.7 Particles: Lightweight GPU particle system (CPU-simulated, 2D + depth), budgeted, pooled.
P0 - FR-2.8 Text & UI: Text rendering (bitmap or SDF font,
P0for bitmap / SDFP1); immediate-mode-style UI widget tree (panel, button, text, image, list, slider, input) with a retained mode for menus; UI is a separate render pass, screen-space.P0 - FR-2.9 Custom passes: Users may add shader passes (fullscreen, layer-specific) via a stable pass API; passes are budgeted and ordered; the
unsafedirect-draw path exists but is warned and gated (§9).P1 - FR-2.10 Lighting (2D): Per-layer ambient + point light masks (cheap, baked or runtime), no real-time 3D lighting.
P1 - FR-2.11 World picking & screen transforms: Screen↔world transforms per projection mode; isometric picking (screen → ground plane → grid cell) is O(1), deterministic, and exposed in the safe API for click-to-select / click-to-move — a must-have for isometric games.
P0
Performance acceptance: ≤ 30 draw calls/frame for a busy scene (50k visible sprites — worst-case isometric overlap with tile-height depth keys — 3 parallax layers, UI); ≤ 2 ms total CPU on mid-range laptop CPU; zero per-frame allocations in the render batcher.
- FR-3.1 Rigid bodies: Kinematic/dynamic/static; point & composite (convex polygon, circle) shapes; position, velocity, angular velocity (2D plane only).
P0 - FR-3.2 Collision: SAT/Minkowski narrowphase; spatial-hash broadphase (engine-managed, auto-sized); contacts with manifolds; continuous collision for fast movers (CCD on player-critical bodies).
P0 - FR-3.3 Integration: Semi-implicit Euler at the fixed tick; fully deterministic; fixed-point (Q16.16) option for bit-exact lockstep use; floating-point path uses engine-only math ops with identical semantics on all targets.
P0 - FR-3.4 Constraints/joints: Hinge, slider, distance, motor, weld — enough for vehicles/cranes/dungeon doors; deterministic solver with fixed iteration count.
P0 - FR-3.5 Layers & masks: Collision layer/mask matrix; query APIs (raycast, overlap, sweep) that are spatial-hash-bounded.
P0 - FR-3.6 No variable-timestep API. Variable-step physics is simply not exposed.
P0
Performance acceptance: 10,000 dynamic bodies at 60 Hz ≤ 1.5 ms on mid-range laptop CPU; zero per-tick allocations; deterministic replay reproduces physics bit-exactly.
- FR-4.1 Devices: Keyboard, mouse, gamepad (GLFW backends), touch (P2).
P0 - FR-4.2 Action abstraction: Named actions mapped to bindings; remapping at runtime; saved per profile; axis + digital; deadzone config.
P0 - FR-4.3 Input state: Per-tick sampled input frame (deterministic; consumed by simulation as an input event, not polled arbitrarily).
P0 - FR-4.4 UI input routing: Input routes to UI first (focus system), then world.
P0
- FR-5.1 Playback: Music + SFX; pooled sources; bus structure (master/music/SFX/voice) with per-bus volume/filters.
P0 - FR-5.2 2D positional audio: Pan by angle, falloff by distance, occlusion flags.
P0 - FR-5.3 Streaming: Large assets stream with bounded memory.
P0 - FR-5.4 Lossless/lossy import: Import WAV/OGG/FLAC/MP3 (via decoders) into engine asset format.
P0
- FR-6.1 Sprite-sheet animation: Frame-based (per sprite), state machine or free sequencing.
P0 - FR-6.2 2D skeletal (mesh) animation: Simple 2D bone hierarchy (transform bones), keyframed clips, additive layers, weight-blended vertex animation (CPU or GPU) — for characters in 2.5D games.
P1 - FR-6.3 Animation state machine: Node/transition/parameter-based; per-entity instances; debug visualization.
P0(sheet-based) /P1(skeletal) - FR-6.4 Animation ↔ simulation contract: Animations read/notify simulation (footsteps, events) via a one-way event API; animation never mutates physics directly.
P0
- FR-7.1 Project format:
.laigeproject = versioned manifest + asset directory + scene directory; git-friendly (all text sidecars; binary blobs content-addressed).P0 - FR-7.2 Asset types: Texture (→ atlas), sprite frame, tileset, audio, font, scene, animation, script, shader.
P0 - FR-7.3 Import pipeline: Headless
laige-assetCLI (import, atlas-pack, validate, pack) — runs in editor, in game (pack only), and in CI.P0 - FR-7.4 Textures: Automatic atlas packing (with user-pinned layouts); mipmaps; texture memory budget reporting.
P0 - FR-7.5 Scenes: Binary scene format + JSON sidecar (for diffs/docs); scene = root transform, layers, entities + components; scene streaming (load/unload by region).
P0 - FR-7.6 Validation: All assets validated on import (format, size, bounds); invalid assets fail loudly at import time, never at runtime.
P0 - FR-7.7 Compression: Content-addressed chunks, miniz-compressed packs for distribution.
P0
- FR-8.1 Scene editor: 2.5D viewport (pan/zoom, projection-mode aware; isometric grid overlay with snap in isometric mode), entity hierarchy, component inspector, transform gizmo (2D + depth), selection, multi-select, undo/redo (transaction-based).
P0 - FR-8.2 Tilemap editor: Tile brush, eraser, flood, stamp, height (depth) brush, animation layers.
P0 - FR-8.3 Sprite/animation editor: Frame editor, state-machine graph editor, preview playback.
P0 - FR-8.4 Asset browser: Import, preview, atlas view, texture memory view.
P0 - FR-8.5 Play/Debug: Run game in-editor (play mode), stop, profiler overlay (per-system time, allocs, draw calls, net traffic), deterministic replay viewer (scrub + diff against live state).
P0 - FR-8.6 Script console: Evaluate script expressions against a running game (scripting on).
P1 - FR-8.7 Editor architecture: Editor is a separate binary sharing the engine core; the engine never links editor code; no editor is needed to build/run a game or server.
P0 - FR-8.8 Custom component UI: Components may register inspector UI (sliders, curves, asset refs) via a small declarative schema — no editor code required for custom components.
P1
- FR-9.1 Scriptable surface: Game logic can be written in an embedded scripting language (proposed: Lua 5.4, vendored) alongside C++; C++ remains first-class (no scripting requirement to build a game).
P1 - FR-9.2 Sandbox: Scripts run with bounded time per tick (watchdog), no direct access to engine internals beyond a stable, documented binding; memory limits per script; hot-reload in debug.
P1 - FR-9.3 Bindings are generated from the same API manifest used for docs (§13) — one source of truth.
P1
- FR-10.1 Modes: (a) Authoritative server (default; MMO path), (b) Lockstep (small deterministic multiplayer, client-side simulation, ≤ N players, input exchange), (c) P2P relay (stretch, P2).
P0for (a) and (b). - FR-10.2 Transport: UDP; reliable-ordered channels for chat/UI/commands, unreliable for movement/state; retransmission + congestion-aware send rate; NAT traversal basics (STUN-style, relay support).
P0 - FR-10.3 Entity replication: Per-component replication rules declared in component traits (replicated: server→all / client→server / both; rate; compression hints). Serialization is generated from component definitions (bitpacked); hand-written serialization is not required and not exposed in the safe API.
P0 - FR-10.4 Interest management (AOI): Spatial-hash interest regions — grid-aligned for isometric worlds, where the AOI is a rhombus on screen but an axis-aligned box in simulation space, so enter/leave streaming is cheap and deterministic per player; enter/leave streaming of entity state; per-player entity budget; occlusion by zone boundaries.
P0 - FR-10.5 Sync model: Server tick (20–100 Hz, configurable); snapshot + delta; per-tick bandwidth budget per player with automatic degradation (lower rate, coarser data) when budget exceeded; client interpolation buffer.
P0 - FR-10.6 Prediction & reconciliation: Built-in client-side prediction for player-controlled entities (movement, actions) with server reconciliation; lag compensation for hit detection via deterministic replay of recent server state (rewind window ≤ 500 ms).
P0 - FR-10.7 Chat/UI: Built-in reliable chat channel, emotes, basic UI state sync (e.g. shop selection).
P1 - FR-10.8 Server runtime:
laige-serverbinary: multi-zone host, zone = one simulation instance with its own tick; world registry for zones/instances (dungeons, lobbies); player sessions, reconnection, kick/leave; headless by definition.P0 - FR-10.9 Sharding: Zones run in separate processes (or separate simulation instances in one process, P1); cross-zone gateway for travel/teleport with state handoff; horizontal scale = more zone processes + registry (external, e.g. a small database or file-backed registry).
P1 - FR-10.10 Anti-cheat posture: Server-authoritative by default; all game-affecting state computed server-side; client input validated against rules; deterministic replay stored (bounded) for audit/dispute review.
P0 - FR-10.11 Protocol hygiene: Versioned binary protocol; forward-compat negotiation; no reflection over the wire; packet budget telemetry; protocol changes gated by tests.
P0
Scale acceptance:
- AC-10.1: One zone process sustains 2,000 concurrent players at 20 Hz tick on a single 8-core machine, p95 server tick ≤ 8 ms, p95 client state latency budget ≤ 1 RTT.
- AC-10.2: Steady-state client bandwidth ≤ 1 KB/s per idle player, ≤ 50 KB/s under heavy combat, with automatic degradation.
- AC-10.3: Lockstep mode is bit-deterministic across all connected clients.
- FR-11.1 Built-in profiler: Always-on (cheap) counters: per-system time, entity counts, alloc counts (target: 0 in sim), draw calls, texture binds, net bytes, tick time, frame time percentiles; exposed in editor overlay, CLI, and file export.
P0 - FR-11.2 Frame graph / budget report: Per-frame breakdown against declared budgets; over-budget systems flagged.
P0 - FR-11.3 Replay: Every debug run can be recorded (inputs + seed) and replayed bit-exactly; diff two replays by frame/state.
P0 - FR-11.4 Memory inspector: Live pools, per-system memory, peak tracking.
P1 - FR-11.5 Determinism checker: CI tool that runs the same seed on two targets/compilers and asserts bit-identity.
P0
- FR-12.1 No exceptions, no RTTI, no
dynamic_castin engine core or public API. Errors arelaige::Result<T, E>/laige::Status+ structured error codes.P0 - FR-12.2 Logging: Structured, leveled, sink-swappable (console, file, network); per-subsystem scopes; log volume is budgeted (no logging in hot paths by default).
P0 - FR-12.3 Debug builds are loud: In debug, every guardrail violation (§9) is asserted/logged with an actionable message (what, why, how to fix, doc link). Release builds degrade gracefully (clamp, skip, warn-once) — never crash the game for a recoverable condition.
P0 - FR-12.4 Crash reports: Opt-in minidump capture with symbolication; deterministic-replay association.
P1 - FR-12.5 Telemetry: Developer-facing perf telemetry (opt-in, local) feeding the profiler; no user/PII telemetry in the engine.
P1
| Budget | Target | Measured on |
|---|---|---|
| Frame time (render) | p95 ≤ 8.3 ms @ 1080p (60 FPS) | Mid-range laptop (2019–2023 class) |
| Simulation tick (10k entities, 2k dynamic bodies) | ≤ 3.0 ms avg, ≤ 5 ms p99 | Same |
| 50k visible sprites (worst-case isometric overlap), 3 parallax layers, UI | ≤ 30 draw calls; ≤ 2 ms CPU | Same |
| Isometric depth-key rebuild (10k dirty cells after terrain edit) | ≤ 0.3 ms | Same |
| Isometric screen→grid picking | O(1), ≤ 0.01 ms per pick | Same |
| Steady-state heap allocations in sim loop | 0 per frame (asserted in debug) | Debug builds |
| Engine base memory (empty scene, running) | ≤ 100 MB RSS | All P0 platforms |
| Cold start (game process → first frame) | ≤ 2 s on SSD, ≤ 5 s cold | P0 platforms |
| Build time (clean, engine + sample) | ≤ 10 min on CI, ≤ 5 min local (warm) | CI |
| Zone server (2k players @ 20 Hz) | p95 tick ≤ 8 ms; ≤ 4 GB RAM | 8-core server class |
Policy: budgets are part of CI. A PR that regresses any budget by > 10% (or breaches absolute target) fails CI unless the budget is revised via a PRD revision. (NFR-8.1)
- NFR-8.2 No undefined behavior. Core is ASan + UBSan + TSan clean in CI on all P0 platforms, always.
- NFR-8.3 Determinism. In deterministic mode, identical inputs + seed ⇒ bit-identical state on all P0 platforms/compilers, verified in CI every merge.
- NFR-8.4 Zero-crash targets. 0 crashes in 72 h soak (auto-generated gameplay + scripted abuse, e.g. 10k entities spawning/dying, input floods, network partitions) per release.
- NFR-8.5 Graceful degradation. Every subsystem defines its degraded mode (e.g. audio off, V-sync off, reduced AOI radius, dropped particle effects) and the engine selects degradation automatically under load; degradation events are logged and visible in the profiler.
- NFR-8.6 Reproducible builds. Same sources + versions ⇒ same binaries (pinned dependency hashes, fixed flags); release archives verified by checksum.
- NFR-8.7 Security. Network protocol is memory-safe against malformed packets (fuzzed in CI); asset import is sandboxed against malformed files (fuzzed in CI); no
system()/exec-style APIs in the engine.
- NFR-8.8 CMake ≥ 3.22; single configure; no autotools; no network access needed to build (all deps vendored).
- NFR-8.9 Static + shared library builds; sample game links statically by default (no DLL hell).
- NFR-8.10 C++20; -Wall -Werror on engine; no exceptions/RTTI in shipped API (enforced by compiler flags and static checks).
- NFR-8.11 Engine core ≤ 50k LOC (excluding vendored deps and editor); each module independently testable; module boundaries enforced by include-graph lint in CI.
- NFR-8.12 100% of public API covered by doxygen; every public symbol has an example or is marked
@experimental. - NFR-8.13 Dependency count is a tracked metric in CI (see §11).
This section is the core of the product. The API has two tiers:
What every developer and AI agent writes against. Rules:
- S-1 Handles, not pointers. Public API uses 32-bit entity handles,
ComponentRef,AssetRef,TextureRef— never raw pointers to engine-owned data. Lifetime is engine-managed; use-after-free is structurally impossible through the safe API. - S-2 No per-frame heap in the safe API. Any safe-API call that would allocate is either (a) pooled internally, or (b) refuses with
Status::BudgetExhaustedin release / asserts in debug. The safe API has nonew-equivalent; data goes through engine pools (laige::Pool<T>, arena-scoped). - S-3 Generated serialization. Network/asset serialization is generated from component trait definitions. Developers declare what replicates, not how it packs; the generator emits tight bitpacked code and a protocol manifest. There is no "just serialize this struct" escape in the safe API (that's how MMO bandwidth budgets die).
- S-4 Fixed-timestep only. The safe API exposes no variable-step simulation, no manual physics stepping, no manual frame stepping.
- S-5 Rendering goes through the batcher. Scene content is declared (layers, sprites, tiles, depth, material); the engine batches. There is no "draw this quad now" call in the safe API. (Isometric depth keys are engine-computed from the axis-aligned simulation state — users never write z-ordering code.)
- S-6 Declared budgets. Systems, passes, scripts, and network handlers declare (or inherit) time/memory/network budgets. Exceeding them is a visible, actionable event, not a silent 1% frame drop.
- S-7 Deterministic by default. Simulation code that runs in deterministic mode uses engine-provided math ops only (or fixed-point types); using raw
float/doubleinside deterministic systems is a compile-time/trait error. - S-8 Component traits are the customization surface. Custom components = data + traits (replication rules, memory class, inspector schema, serialization). The engine generates what it can from the trait; the user writes systems that operate on them. Custom components with no system are legal (data carriers).
- S-9 Fail loudly in debug, degrade in release. (§FR-12.3)
For the 5% of cases that need it (custom post FX, bespoke render paths, hand-rolled packet codecs, direct GL). Rules:
- U-1 Namespaced and documented. Everything lives in
laige::unsafe(C++) / equivalent marker in bindings; each symbol carries a doc comment stating the exact performance/safety contract the caller must uphold (e.g. "caller must not allocate; caller must batch; caller owns this GPU resource untilrelease"). - U-2 Instrumented. Unsafe calls are counted by the profiler and reported in every frame graph; an unsafe-heavy build is visible at a glance.
- U-3 Bounded. Unsafe APIs are still inside engine guardrails (budgets still apply; GPU resources tracked; memory charged to the caller's budget). "Unsafe" means you take responsibility, not no limits.
- U-4 Reviewed by design. Each unsafe API is justified in its header doc with "use when / never when" — written for humans and parseable for AI agents.
| Guardrail | Mechanism | Violation behavior |
|---|---|---|
| G-R1 Zero sim-loop allocations | Debug: allocation counter + assert. Release: pool overflow → logged degradation | assert (debug) / clamp+log (release) |
| G-R2 Draw-call budget | Batcher reports; per-pass cap configurable | warn + frame graph flag |
| G-R3 Entity-count thresholds | ECS warns at 25%/50%/100% of declared scene budget | warn (debug: with advice) |
| G-R4 Per-frame component churn | Add/remove is pooled but counted; > threshold/tick | warn (debug: "move to spawn/despawn system") |
| G-R5 Per-system time budget | Declared vs measured; rolling p99 | warn + profiler highlight; over 3× → error event |
| G-R6 Unbatched submission | Only in laige::unsafe, counted, logged |
count + log always |
| G-R7 Network bandwidth budget | Per-player per-tick budget with auto-degradation | auto-degrade + telemetry |
| G-R8 Determinism violation | Deterministic-mode code uses engine math only; trait-checked | compile error |
| G-R9 Asset size/texture budget | Import-time validation + runtime texture memory budget | import failure (never runtime surprise) |
| G-R10 Script tick budget | Watchdog: script over budget | kill script tick + loud error (debug: suspend) |
| G-R11 Hand-rolled depth sorting | Isometric depth keys are engine-owned (FR-2.2); user-settable per-sprite depth exists for overrides | per-sprite depth overrides counted + warned (debug: "prefer tile height") |
- Machine-readable API manifest (JSON) generated from the same sources as the headers.
- Deterministic, side-effect-pure documentation: every function's postconditions are stated.
- Error strings are templates:
{what} / {why} / {fix} / {doc_anchor}. - Stable versioning: minor versions are additive-only; breaking changes = major; deprecation = 2 minors.
hello.laigetemplate compiles, runs, and renders in < 100 lines of code (isometric scene by default).
laige-core determinism, math, allocators, pools, ECS, systems, config, logging, Result
laige-sim game loop, physics, input state, animation state, pathing/steering, AI hooks
laige-render GL context, 2.5D batcher, isometric depth keys, parallax, tiles, particles, text/UI, camera, passes
laige-assets asset store, import pipeline, atlas packing, validation, content addressing
laige-net transport (UDP/ENet), protocol, replication, AOI, prediction/reconciliation
laige-server zone runtime, world registry, sessions, sharding host
laige-script (optional) Lua VM + generated bindings, sandbox
laige-editor (separate binary) scene/sprite/tile editors, play mode, profiler UI, replay viewer
laige-sample reference games: isometric ARPG (flagship), platformer, lockstep arena, MMO demo zone
Dependency rule: arrows only downward; laige-core depends on nothing internal; lint-enforced (NFR-8.11).
- Simulation: single-threaded by default (determinism, simplicity, debuggability). Deterministic parallelism (fixed partitioning) is an optional M7 optimization for large zones, off by default, still bit-exact.
- Rendering: its own thread (frame pipeline: cull/batch → submit), lock-free handoff of the frame descriptor.
- Network: dedicated I/O thread; state handoff to the sim thread at tick boundaries (one copy, no locks in hot path).
- Audio/asset loading: worker pool, budgeted.
- No user-visible thread APIs in the safe API; threading is an engine implementation detail. (Custom systems are sim-thread code.)
- Fixed timestep; all simulation time is integer ticks.
- Math: deterministic mode uses engine math ops (x86-64
floatsemantics pinned by compiler flags, or Q16.16 fixed-point for cross-ISA bit-exactness — fixed-point is the default for lockstep/MMO). - Iteration order fixed (archetype order, entity id order); no unordered containers in sim hot paths (hash tables use deterministic hash + fixed iteration, or are banned in sim).
- Randomness: seeded engine PRNG (xorshift128+ or similar), per-substream; seed is part of the replay.
- Floating point: if used at all in deterministic paths, only via engine ops; never platform intrinsics outside the engine.
- Engine-owned pools for: entities/components, contacts, particles, audio sources, network packets, strings (string table with
StringRefhandles — no per-framestd::stringin sim). - User data via
laige::Pool<T>/ arena (budgeted, reset-per-frame where applicable). - All memory is accounted in the profiler (NFR-8.11, FR-11.4).
Rule: every third-party dependency is vendored, mature, small, and justified in this table. Adding a dependency requires a PRD revision + justification. Dependency count is a CI metric (target ≤ 10, of which ≤ 3 are compiled code; the rest header-only/single-file).
| Dependency | Why (not reinventing the wheel) | Form |
|---|---|---|
| GLFW | Windowing + input (keyboard/mouse/gamepad) across all P0 OSes; mature, tiny, battle-tested. Writing native window code on 3 OSes is the exact wheel we don't invent. | Compiled, vendored |
| GLAD | OpenGL loader (generated header for our GL version) | Header-only, vendored |
| GLM | Math foundation for the rendering side (cameras, projection matrices — incl. isometric camera matrices). Sim uses its own fixed-point math (§10.3). | Header-only, vendored |
| stb_image / stb_image_write / stb_truetype | Texture decode/encode, font rasterization — public domain, single-file, zero-risk | Single-file, vendored |
| miniz | Asset pack compression (zlib-compatible, no build) | Single-file, vendored |
| miniaudio | Audio playback backend (WASAPI/CoreAudio/ALSA/Pulse) in one file | Single-file, vendored |
| ENet | Reliable-sequenced UDP channels for the network stack (chat, commands); our layer adds app semantics on top | Compiled, vendored |
| Lua 5.4 (optional module) | Embedded scripting (§7.9) | Compiled, vendored, off by default |
| GoogleTest (dev-only) | Unit/integration tests; never shipped | Dev-only |
Explicitly not dependencies: ECS (ours), 2D physics (ours — determinism is a first-class requirement Box2D doesn't guarantee for our use), serialization (generated, ours), UI framework (ours, 2.5D-scale), asset format (ours), server runtime (ours), isometric projection/picking/depth keys (ours — the primary projection gets native support, not a plugin).
Versioning: dependency versions pinned in deps.lock; CI builds from deps.lock hashes (reproducible builds, NFR-8.6).
The MMO path is not a plugin — it's the same deterministic simulation core run in an authoritative zone process with the replication layer on top.
- Zone model. A zone = one simulation instance (own tick, own entity universe, own AOI). Typical zone capacities: 200 (dungeon), 500–1000 (city), 2000 (persistent hub) per process. Zones communicate via the world registry (gateway): travel, instance invites, global events.
- State model. Replicated state is 2D (position, velocity, facing, animation id, a handful of component payloads). This is why 2.5D scales: idle player state is tens of bytes, not kilobytes. For isometric zones, positions replicate as grid-aligned 2D coordinates — smaller still.
- Bandwidth contract. Per-player budgets (idle 1 KB/s, combat 50 KB/s, AC-10.2) are enforced by the replication layer with automatic degradation: lower update rate → coarser precision (fixed-point bit-count reduction) → region shrinkage. The degradation ladder is declared per component.
- Latency contract. Client prediction for local player + reconciliation; interpolation buffer for others (adaptive 100–250 ms); hit detection via server rewind (≤ 500 ms window, FR-10.6).
- Persistence boundary. The engine does not include a database. It exposes a clean persistence seam:
EntitySnapshotexport/import + event stream (commands), so game projects plug in any store (SQLite/Postgres/external service) without touching engine code. - Ops surface.
laige-serveris a plain process: config file, log to stdout, metrics endpoint (local HTTP, dev), graceful shutdown (flush snapshots), hot zone load/unload (P1), watchdog + auto-restart hooks. - Scale-out. Sharding = more zone processes + registry. No engine-internal distributed simulation in v1 (deliberate: keep the deterministic core single-process; scale by replication of zones, not of state).
- Testing. MMO scenarios are CI-testable: scripted load generator (N bots, movement patterns, combat bursts, join/leave churn, partition/latency injection) runs headless against a zone process every merge (subset) and nightly (full 2k-player scenario).
Developed with the expectation that a significant share of game code is written by LLM agents:
- NFR-13.1 API manifest:
laige-api.json(generated, versioned) describing every public symbol: signature, pre/postconditions, budget implications, examples,@experimentalflags. The manifest is the contract AI agents code against; it is tested to match the headers in CI. - NFR-13.2 Safe-by-default payoff: because wrong-but-legal code is visible (budgets, warnings, profiler), an agent can detect its own mistakes from runtime output — errors are the observability channel.
- NFR-13.3 Error message grammar: every engine error follows
{code} | {what} | {why} | {fix} | {doc_anchor}; parseable by both machines and humans. - NFR-13.4 Determinism & idempotency: engine behavior is deterministic and has no hidden global state not surfaced by the API — same inputs, same outputs, always.
- NFR-13.5 Templates:
hello.laige,iso-arena.laige(isometric, the default template),platformer.laige,mmo-zone.laige— minimal, fully working, heavily commented; the docs' "canonical patterns" section is written as copy-pasteable snippets. - NFR-13.6 Scripting sandbox (FR-9.x) doubles as an isolation boundary for AI-generated runtime logic.
- NFR-13.7 No magic: no implicit behavior not visible in the API (no hidden autoloads, no global singletons exposed to user code).
| Layer | What | When |
|---|---|---|
| Unit | Per-module, ASan/UBSan | Every commit |
| Integration | ECS↔sim↔render↔net contracts; replay determinism; protocol fuzz; isometric picking/depth-key golden tests | Every commit |
| Fuzz | Asset import, network packets, config parse | Every commit (bounded), nightly (long) |
| Perf | Budget suite (§8.1) on pinned CI hardware, incl. worst-case isometric scene | Every PR (subset), nightly (full) |
| Determinism | Two-target bit-identity check (seeded scenarios) | Every merge |
| Soak | 72 h auto-gameplay + abuse scenarios per release candidate | Per release |
| Cross-OS | All P0 platforms in CI; one per PR, all per merge | Per PR/merge |
| Load | MMO bot scenario (AC-10.1) | Nightly + pre-release |
| Milestone | Scope | Exit criteria |
|---|---|---|
| M0 — Foundations (2–3 wks) | Repo, CI (all P0), build system, laige-core (math, pools, alloc, Result, logging, config), dep lock, API manifest generator |
CI green on 3 OSes; core unit-tested; budget harness wired |
| M1 — Heartbeat (3–4 wks) | Game loop, ECS, systems, determinism, headless mode, profiler core, hello.laige (headless) |
10k entities @ 60 Hz ≤ 3 ms; replay bit-exact; zero-alloc assertion passes |
| M2 — 2.5D Rendering (4–6 wks) | GL context, sprite batcher, isometric depth keys + picking (primary), depth sort, parallax, camera (incl. grid-snap iso presets), projection modes, particles, text, basic UI | 50k sprites ≤ 30 draw calls on worst-case isometric scene; isometric is the default template; all AC-4.x incl. AC-4.4 pass |
| M3 — Game Feel (4–6 wks) | Physics (incl. CCD, joints), input actions, audio, sprite-sheet animation + state machines, tilemaps (height brush), asset import/atlas CLI | Physics budgets + determinism AC; playable isometric sample in-engine |
| M4 — Scripting & Customization (3–4 wks) | Lua VM, generated bindings, sandbox, custom component traits end-to-end, unsafe API v1, custom passes | Script budget watchdog works; a custom-render-pass sample runs |
| M5 — Editor MVP (6–8 wks) | Scene editor (iso grid overlay), tilemap editor (height brush), animation editor, asset browser, play mode, profiler overlay, replay viewer | A dev can build a small isometric game entirely in the editor |
| M6 — Networking (6–8 wks) | Transport, protocol, replication, grid-aligned AOI, prediction/reconciliation, lockstep mode, laige-server v1 |
Lockstep bit-exact; 100-player zone at 20 Hz; bandwidth budgets met |
| M7 — MMO Scale (8–12 wks) | Sharding, world registry, persistence seam, deterministic parallel sim (optional), load harness, anti-cheat review, 2k-player scenario | AC-10.1/10.2/10.3 met; nightly load green 2 weeks straight |
| M8 — Release 1.0 (4–6 wks) | Docs pass, samples polish, reproducible releases, security review, 72 h soak, 1.0 tag | All P0 FRs closed; budgets green; 3 reference games shipped with the engine (isometric flagship first) |
| M9+ — Stretch | WebGL/WASM target, 2D skeletal animation polish, custom component inspector UI, mobile, optional Vulkan backend, 3D mesh scenery | Per-feature proposals |
- Ease: median time from fresh clone to running
hello.laige≤ 10 min (measured with 5 external devs, humans and agent-only builds). - Performance: all §8.1 budgets green on pinned hardware at 1.0 (the worst-case isometric reference scene is the primary baseline).
- Stability: 0 crash bugs open at 1.0; determinism checker green for 30 consecutive merges.
- Scale: one reference MMO demo sustains 2,000 bot-players per zone for 72 h with degradation telemetry healthy.
- API health: 0 breaking changes during 1.0 cycle after M4; 100% of public API covered by manifest + examples.
- Dependency discipline: ≤ 10 vendored deps at 1.0; 0 new deps since M0 without PRD revision.
| Risk | Likelihood | Impact | Mitigation |
|---|---|---|---|
| Deterministic 2D physics is harder than it looks (CCD, joints, fixed-point) | Med | High | Scope physics to the M3 feature list; fixed-point first for lockstep; fuzz + replay tests from day one; cut joints to P1 if needed |
| MMO scale slips (2k/zone) | Med | High | Load harness in M6 (not M7); zone-sharding design keeps the problem bounded; budgets degrade gracefully so demos work at 1k even if 2k slips |
| Scope creep toward full 3D | Med | High | Non-goals are contractual (§5); 3D mesh support only as M9 stretch with its own PRD revision |
| Isometric feature set eats the roadmap | Med | Med | Isometric scope is bounded by design (depth keys + picking + camera presets, FR-2.2/2.4/2.11); anything fancier (e.g. arbitrary oblique lighting) is P1/stretch |
| Editor complexity explodes | High | Med | Editor is a separate binary, P0 = the six FRs in §7.8; fancy editor features are all P1+ |
| GL quirks across OSes (drivers, macOS layering) | Med | Med | Pinned CI hardware per OS; shader minimalism (no fancy GLSL); GL 3.3 core only |
| AI-generated code stresses unsafe API misuse | Med | Med | Unsafe tier instrumented by default (§9.2); error grammar (NFR-13.3) turns failures into self-correcting signals |
| Dependency creep | Low | Med | CI dep-count metric + PRD-gated additions (§11) |
| Single-maintainer bandwidth (if applicable) | Med | High | Module boundaries allow external contributions; samples-as-tests; keep M0–M2 dependency-free of editor code |
- Name & license — Resolved 2026-09-10 (ADR 0001): name confirmed as "Laige" (Legendary AI Game Engine); engine MIT, assets/samples separately licensed.
- Fixed-point default — Q16.16 for all deterministic paths, or float-pinned with fixed-point only for lockstep? (Affects M1 core math; decision needed before M1.)
- Lua vs alternative — confirm Lua 5.4 (vs. no scripting in 1.0; vs. embedded WASM for scripting — heavier).
- Editor embedded vs standalone — PRD assumes standalone binary (FR-8.7); confirm (embedded play-mode is P0 either way).
- UI framework scope — retained-mode UI widget set: confirm minimal list for M2 (panel/button/text/image/list/input) vs. defer full UI to M4.
- Persistence seam details — SQLite built-in (one more compiled dep) vs. pure external store? PRD currently: external only.
- Stun/relay — how much NAT traversal does the transport ship in M6 (STUN-only vs. STUN+TURN)?
- Isometric defaults — confirm 2:1 dimetric (pixel-art standard) as the template default vs. true isometric (30°/60°); both remain selectable per scene.
- 2.5D (Laige definition): 2D simulation + 3D presentation (§4).
- Isometric (primary projection): the default Laige projection — a 2D grid simulation presented through an oblique 3D camera; deterministic depth key from (x, y, tile height, layer) (§4, §7.2).
- Zone: one authoritative simulation instance hosting a player population.
- AOI (Area of Interest): the spatial region whose entities a client receives.
- Lockstep: all clients simulate identically from exchanged inputs; no server.
- Reconciliation: client prediction corrected by authoritative server state.
- Safe API / Unsafe API: the two API tiers (§9).
- Budget: a declared, measured, enforced resource limit (time, memory, network, allocations).
- Replay: an input log + seed that reproduces a run bit-exactly.
// What a game writes. No raw pointers, no manual serialization, no allocation.
struct Health {
int32_t current = 100;
int32_t max = 100;
};
LAIGE_COMPONENT(Health,
.replicate(laige::rep::server_to_all, /*rate_hz*/ 5, /*bits*/ {16, 16})
.inspector(laige::insp::slider("current", "max"),
laige::insp::slider("max", 1, 100000)));
// A system: deterministic, single-threaded, budgeted.
LAIGE_SYSTEM(HealthRegen, /*budget_ms*/ 0.5)
void run(laige::sim::Context& ctx) {
for (laige::iter::Entity e : ctx.each<laige::Tag<Player>, Health>()) {
Health& h = ctx.get<Health>(e);
if (h.current < h.max) h.current = laige::math::min(h.current + 1, h.max);
}
}(Illustrative — final syntax is set in M0/M1. The invariants are the point: handles, pools, generated replication, declared budgets.)