feat: game/engine split refactor + skybox
This commit is contained in:
parent
581e5892b0
commit
eeedcb8e48
34 changed files with 610 additions and 218 deletions
155
AGENTS.md
155
AGENTS.md
|
|
@ -26,8 +26,14 @@ rules live in `.agents/rules/*.md`.
|
|||
frame; on-screen solids also **backface-cull**. This is what keeps a dense world
|
||||
(thousands of trees/rocks) affordable — off-screen content costs ~nothing.
|
||||
- **2D assets only.** Sprites/billboards (PS1-style), **no 3D model loading**.
|
||||
- **Engine is headless.** `engine/` has no DOM types and could run server-side;
|
||||
all browser glue (canvas, input, image decode) lives in `app/`.
|
||||
- **Three layers, one-way deps: `engine` ← `game` ← `app`.** `engine/` is the
|
||||
reusable, content-agnostic **mechanism** (rasterizer, framebuffer, culling, the
|
||||
`Actor`/`Material` interfaces) — no DOM, no game content, could run server-side.
|
||||
`game/` is **this game's content** (the creatures/props, terrain, level, scene
|
||||
assembly + render orchestration) built on the engine interfaces. `app/` is
|
||||
**browser glue** (canvas, input, image decode, the worker render driver + loop).
|
||||
Enforced: `engine/` imports nothing from `game`/`app`, `game/` nothing from `app`
|
||||
(`tests/layering.test.ts`). Both `engine` and `game` are DOM-free (tsconfig).
|
||||
|
||||
## Stack & tooling
|
||||
|
||||
|
|
@ -48,15 +54,16 @@ rules live in `.agents/rules/*.md`.
|
|||
- `bun run bench:browser` — Playwright: drive headless Chromium through the
|
||||
`?bench=st`/`?bench=mt` flythrough, print single-thread vs worker frame timings
|
||||
(median/p95/max). The real-browser profiler; run it on the target machine.
|
||||
- `bunx tsc --build tsconfig.app.json` — **typecheck the app+engine graph. Use
|
||||
- `bunx tsc --build tsconfig.app.json` — **typecheck the engine+game+app graph. Use
|
||||
this**, not `bun run check` (see Caveats).
|
||||
- `bunx oxlint engine app` — lint.
|
||||
- `bun test` — tests (none yet).
|
||||
- `bunx oxlint engine game app` — lint.
|
||||
- `bun test` — tests (registry id-order + engine↛game layering guards).
|
||||
- `bun run serve` — Bun server (`server/server.ts`, a stub for now).
|
||||
|
||||
## Layout
|
||||
|
||||
- `engine/` — headless engine, consumed by `app/` via tsconfig project ref.
|
||||
- `engine/` — content-agnostic mechanism (no DOM, no game content); consumed by
|
||||
`game/` then `app/` via tsconfig project refs.
|
||||
- `math/` — `Vec2`, `Vec3`, `Mat4` (column-major, OpenGL-style; verified).
|
||||
- `render/` — `Color` (packed RGBA, little-endian = canvas ImageData order),
|
||||
`Framebuffer` (Uint32 color + Float32 1/w depth; `quantize` = color-depth +
|
||||
|
|
@ -69,74 +76,70 @@ rules live in `.agents/rules/*.md`.
|
|||
- `scene/` — `Camera` (fps yaw/pitch; far plane reaches the outdoor peaks),
|
||||
`Mesh` (indexed tris; verts stored flat: `STRIDE` floats x,y,z,u,v per vertex,
|
||||
no per-vertex objects — cache-friendly + alloc-free to draw), `Sprite`
|
||||
(Y-axis billboard), `Terrain` (procedural
|
||||
heightfield around the room: flat clearing in the center, rolling hills, tall
|
||||
edge peaks. `Terrain.patch` builds one ground patch over a rectangle -- called
|
||||
per chunk, aligned so patches weld crack-free, with a hole for the room;
|
||||
`Terrain.height` is the shared ground-height sampler for the player), `Actor`
|
||||
(the `Entity` interface — geometry + behavior + bounds — that each mob kind
|
||||
implements), `Tree` (procedural low-poly oak/spruce/birch, sapling..full via a
|
||||
`growth` knob; each species a `TreeSpecies` in `trees/<Kind>.ts`, assembled by a
|
||||
registry — see the Trees section), `Boulder`
|
||||
(procedural low-poly rock: a squashed, jittered, part-buried sphere;
|
||||
`Boulder.build` appends into a shared mesh), `Bush` (cluster of small leaf
|
||||
blobs, shares the oak leaf texture/mesh), `Flower` (thin stem + colored bloom;
|
||||
samples a 2x2 color-atlas texture, drawn double-sided), `Mob` (a **roaming**
|
||||
creature — `frog` hops the ground, `bee` hovers/darts, `robin` mostly hops but
|
||||
now and then takes a short powered flight — the engine's only moving geometry.
|
||||
Unlike the baked props, a mob's low-poly mesh is built once per kind in **local
|
||||
space**; `Mob.build` bakes the canonical meshes, `Mob.update` steps the wander
|
||||
AI (leashed to a home anchor, deterministic via an evolving per-mob seed) each
|
||||
frame, and the live `position`/`heading`/`scale` become a per-frame model matrix
|
||||
at draw time. New kinds extend the `MobKind` union + `MOB_KINDS` order).
|
||||
- `app/` — browser glue.
|
||||
- `main.ts` — game loop: input, sim, preset switching, per-frame culling, then
|
||||
the non-blocking pump (`renderer.dispatch`/`done`) + `present` (GPU/CSS
|
||||
upscale) + a multi-line frame HUD (`work + present` critical-path ms, vsync
|
||||
interval, visible chunks / LOD-aware tri count — the real profiler in play).
|
||||
Also owns the **mob sim**: each frame it steps `Mob.update` for every mob,
|
||||
rebuilds the near-player mob colliders into `level.colliders`, and culls the
|
||||
mobs (`visibleMobs`) so only the visible transforms get dispatched.
|
||||
- `renderer.ts` — the render driver. When the page is cross-origin-isolated it
|
||||
runs a pool of `render-worker.ts` threads (`MAX_WORKERS`) over a
|
||||
`SharedArrayBuffer` framebuffer, each owning a disjoint row band, synced by a
|
||||
lock-free `Atomics` barrier; otherwise it renders inline. `dispatch`/`done`
|
||||
are non-blocking so the caller paces on rAF. Per-frame inputs ride shared
|
||||
arrays: camera/matrix/visible-chunk list, plus the visible **mob transforms**
|
||||
(`mobState`, count in the `MOBVIS` control slot). `?bench=st|mt` A/Bs the paths.
|
||||
- `renderScene.ts` — `renderBand(fb, scene, …, mobDraws, …, y0, y1)`: the single
|
||||
source of render truth (sky + room + culled chunks + sprite + roaming mobs +
|
||||
quantize for a row band). Used full-height by the inline path, per-band by each
|
||||
worker. `Scene` bundles the static meshes/textures (incl. the two canonical mob
|
||||
meshes) so it clones to a worker whole; each mob is drawn double-sided through
|
||||
its own `viewProj × Mat4.compose(...)` model matrix, and `visibleMobs`
|
||||
frustum-culls the moving mobs per frame.
|
||||
- `assets.ts` — load `/assets/*.png` → `Texture` (zero-copy; ImageData bytes
|
||||
are already the `Color` layout).
|
||||
(Y-axis billboard), `Actor` (the generic `Entity<State, World>` interface —
|
||||
build + update + bounds — that a content kind implements; the engine dispatches
|
||||
through it, never a `kind` switch).
|
||||
- `game/` — this game's content + world assembly, on the engine interfaces
|
||||
(headless: no DOM, imports nothing from `app`).
|
||||
- `actors/` — the placeable things. `Mob` (a **roaming** creature — `frog` hops
|
||||
the ground, `bee` hovers/darts, `robin` mostly hops but now and then takes a
|
||||
short powered flight — the only moving geometry; each kind an `Entity` in
|
||||
`mobs/<Kind>.ts` + shared `mobs/mobkit.ts`, assembled by the thin `Mob` registry.
|
||||
Its local-space mesh is built once per kind; `Mob.update` steps the wander AI
|
||||
(leashed to a home anchor, deterministic per evolving `seed`) each frame and the
|
||||
live `position`/`heading`/`scale` become a per-frame model matrix at draw.
|
||||
`MOB_KINDS` is the SAB id order). `Tree` (oak/spruce/birch, each a `TreeSpecies`
|
||||
in `trees/<Kind>.ts` + `trees/treekit.ts` — see the Trees section), `Boulder`
|
||||
(squashed jittered part-buried sphere), `Bush` (leaf-blob cluster, shares the
|
||||
leaf mesh), `Flower` (stem + colored bloom, 2x2 atlas, double-sided). Baked props
|
||||
append into shared per-material meshes; mobs draw live.
|
||||
- `Terrain.ts` — procedural heightfield around the room (flat clearing, rolling
|
||||
hills, tall edge peaks). `Terrain.patch` builds one ground patch over a rectangle
|
||||
(per chunk, welds crack-free, hole for the room); `Terrain.height` is the shared
|
||||
ground sampler for the player + mobs.
|
||||
- `level.ts` — builds the playground: a flat stone-floored room (three thick
|
||||
walls via `slab`, north side open) always drawn, in the center of a big grassy
|
||||
`Terrain` world (~20x across). Props are placed first (`placeTrees` /
|
||||
`placeBoulders` / `placeBushes` / `placeFlowers` → instance lists + colliders;
|
||||
`TREE_/BOULDER_/BUSH_/FLOWER_COUNT`/`_SEED`/`_REACH`) and the roaming mobs
|
||||
scattered (`placeMobs`; `FROG_/BEE_COUNT`, `MOB_SEED`, `MOB_REACH` — mobs move,
|
||||
so they carry no baked colliders), then `buildChunks` bakes
|
||||
terrain + props into a `CHUNK_GRID` x `CHUNK_GRID` grid of `Chunk`s (each = a
|
||||
tight AABB + two `DrawGroup[]` lists, `near`/`far`, where a `DrawGroup` is a baked
|
||||
mesh + its `Material` = texture + cull; the renderer just loops them and knows no
|
||||
content by name) that `main` frustum-culls; bushes fold into the leaf mesh,
|
||||
flowers get their own (double-sided) group. Trees + boulders are baked **twice** —
|
||||
full geometry into `near` and a low-poly impostor into `far` (via the builders'
|
||||
`lod` arg) — so a far chunk swaps to the cheap group set with no per-frame work
|
||||
(see `chunkFar` / `RenderConfig.lodDistance`). `buildLevel(textures)` binds the
|
||||
ground/prop materials once and shares them across chunks. `Aabb` colliders (walls, crate, grown trunks, big
|
||||
boulders), NPC position, `TERRAIN`/`TERRAIN_SUBDIV`/`GROUND_UV`, sky/cloud config. The stone floor is lifted by `FLOOR_LIFT` (a z-bias) so it
|
||||
stays clean over the terrain skirt that laps under the room edges. Room surfaces
|
||||
are single flat quads -- no subdivision needed since texturing is
|
||||
perspective-correct.
|
||||
- `player.ts` — feet-cylinder player: gravity/jump + Shift-run
|
||||
(`RUN_MULTIPLIER`) + circle-vs-AABB/-circle collision, substepped so fast
|
||||
running can't tunnel walls; ground height from `Terrain.height` (plus
|
||||
standable AABBs).
|
||||
scattered (`placeMobs`; `FROG_/BEE_/ROBIN_COUNT`, `MOB_SEED`, `MOB_REACH` — mobs
|
||||
move, so no baked colliders), then `buildChunks` bakes terrain + props into a
|
||||
`CHUNK_GRID` x `CHUNK_GRID` grid of `Chunk`s (each = a tight AABB + two
|
||||
`DrawGroup[]` lists `near`/`far`; the baker accumulates one mesh per **material
|
||||
key** (`MAT_ORDER`) and routes each prop by its declared material, so it names no
|
||||
texture) that `main` frustum-culls. Trees + boulders bake **twice** — full into
|
||||
`near`, a low-poly impostor into `far` — so a far chunk swaps to the cheap set
|
||||
with no per-frame work (`chunkFar` / `RenderConfig.lodDistance`).
|
||||
`buildLevel(textures)` binds the ground/prop `Material`s once + shares them.
|
||||
Also: `Aabb` colliders, NPC position, `TERRAIN`/`TERRAIN_SUBDIV`/`GROUND_UV`,
|
||||
sky/cloud config, `FLOOR_LIFT` (a z-bias lifting the stone floor over the terrain
|
||||
skirt). Room surfaces are single flat quads (texturing is perspective-correct).
|
||||
- `renderScene.ts` — `renderBand(fb, scene, …, mobDraws, …, y0, y1)`: the single
|
||||
source of render truth (sky + room + culled chunk draw-groups + sprite + roaming
|
||||
mobs + quantize for a row band). Used full-height by the inline path, per-band by
|
||||
each worker. `Scene` bundles the static meshes/textures (incl. the canonical mob
|
||||
meshes) so it clones to a worker whole; each mob draws double-sided through its
|
||||
own `viewProj × Mat4.compose(...)` model matrix, and `visibleChunks`/`visibleMobs`
|
||||
frustum-cull per frame. `textures.ts` holds the `Textures` palette type.
|
||||
- `player.ts` — feet-cylinder player: gravity/jump + Shift-run (`RUN_MULTIPLIER`)
|
||||
+ circle-vs-AABB/-circle collision, substepped so fast running can't tunnel
|
||||
walls; ground height from `Terrain.height` (plus standable AABBs).
|
||||
- `app/` — browser glue only (top layer; depends on `game` + `engine`).
|
||||
- `main.ts` — game loop: input, sim, preset switching, per-frame culling, then
|
||||
the non-blocking pump (`renderer.dispatch`/`done`) + `present` (GPU/CSS upscale)
|
||||
+ a multi-line frame HUD (`work + present` critical-path ms, vsync, visible
|
||||
chunks / LOD-aware tris). Owns the **mob sim**: steps `Mob.update` for every mob,
|
||||
rebuilds near-player mob colliders into `level.colliders`, culls mobs
|
||||
(`visibleMobs`) so only visible transforms dispatch.
|
||||
- `renderer.ts` — the render driver. When the page is cross-origin-isolated it runs
|
||||
a pool of `render-worker.ts` threads (`MAX_WORKERS`) over a `SharedArrayBuffer`
|
||||
framebuffer, each owning a disjoint row band, synced by a lock-free `Atomics`
|
||||
barrier; otherwise inline. `dispatch`/`done` are non-blocking so the caller paces
|
||||
on rAF. Per-frame inputs ride shared arrays: camera/matrix/visible-chunk list +
|
||||
visible **mob transforms** (`mobState`, count in `MOBVIS`). `?bench=st|mt` A/Bs
|
||||
the paths.
|
||||
- `assets.ts` — load `/assets/*.png` → `Texture` (zero-copy; ImageData bytes are
|
||||
already the `Color` layout); returns the `game` `Textures` palette.
|
||||
- `index.html` — Vite entry at repo root; holds the `#screen` canvas and the
|
||||
`#fps` meter div (styled inline).
|
||||
- `scripts/gen-assets.ts` — procedurally draws the placeholder textures and
|
||||
|
|
@ -206,10 +209,10 @@ front-out — a culled mesh that renders inside-out has its index order flipped
|
|||
(well inside `fog.far`, so far detail is already fog-dimmed at the switch),
|
||||
`Infinity` on `clean` to disable LOD. Lower it for more headroom (more pop),
|
||||
raise it for more far detail (more tris).
|
||||
- **`app/level.ts` `GROUND_UV`** (0.25) — outdoor ground texture tiles per world
|
||||
- **`game/level.ts` `GROUND_UV`** (0.25) — outdoor ground texture tiles per world
|
||||
unit. Lower = the stone tiles bigger and less busy = less far-distance moire
|
||||
(there are no mipmaps); higher = finer but shimmerier.
|
||||
- **`app/level.ts` `CHUNK_GRID`** (12) / `TERRAIN_SUBDIV` (5) — spatial-cull
|
||||
- **`game/level.ts` `CHUNK_GRID`** (12) / `TERRAIN_SUBDIV` (5) — spatial-cull
|
||||
granularity and terrain resolution. World terrain divisions = `CHUNK_GRID *
|
||||
TERRAIN_SUBDIV`. Smaller cells cull tighter (draw less off-screen) but cost more
|
||||
per-cell tests/bounds. This is the lever if a dense world still lags.
|
||||
|
|
@ -272,12 +275,12 @@ by a `CloudLayer` discriminated union `kind`:
|
|||
- **`fancyCumulus`** — domain-warped + heightfield-shaded fake volume,
|
||||
~5 lookups/pixel (pricier; watch the FPS meter).
|
||||
|
||||
Both are exported presets in `app/level.ts`; the active one is set in
|
||||
Both are exported presets in `game/level.ts`; the active one is set in
|
||||
`buildLevel`'s `sky.clouds`. Add new cloud types by extending the union and
|
||||
branching in the cloud shader. Cost scales with sky resolution — fine at
|
||||
`standard`, heavy at `clean` (mitigate: fewer fbm octaves or half-res sky).
|
||||
|
||||
## Trees (`engine/scene/Tree.ts` + `engine/scene/trees/`)
|
||||
## Trees (`game/actors/Tree.ts` + `game/actors/trees/`)
|
||||
|
||||
Procedural low-poly geometry, faceted flat-shaded like everything else. Each species
|
||||
is a `TreeSpecies` definition in its own `trees/<Kind>.ts` module (geometry +
|
||||
|
|
@ -298,13 +301,13 @@ canopy blobs (oak/birch) / tiers (spruce); `seed` gives each tree its own wobble
|
|||
A species declares its `trunk`/`foliage` **material keys** (e.g. birch → white
|
||||
`birch` trunk, oak `leaf` foliage); the chunk baker (`level.ts`) accumulates one
|
||||
mesh per material key and routes each tree via `Tree.species(kind)` — so a forest
|
||||
still batches into a few draw calls and the baker names no texture. `app/level.ts`
|
||||
still batches into a few draw calls and the baker names no texture. `game/level.ts`
|
||||
`placeTrees` seeds the forest and rolls the species. **Add a species** = add a
|
||||
`trees/<Kind>.ts` module (its geometry + material keys) + one entry in the `Tree`
|
||||
registry; only a genuinely new material also needs a `Material` in `buildLevel` +
|
||||
its key in `MAT_ORDER`.
|
||||
|
||||
**Boulders** (`engine/scene/Boulder.ts`) work the same way: `Boulder.build`
|
||||
**Boulders** (`game/actors/Boulder.ts`) work the same way: `Boulder.build`
|
||||
appends a squashed, per-vertex-jittered low-poly sphere (seam/pole-safe so it
|
||||
never cracks) into one shared rock mesh, sunk partway into the ground.
|
||||
`scatterBoulders` sizes them small→big (biased small) and drops colliders on the
|
||||
|
|
@ -314,7 +317,7 @@ colliders; add a new prop type by cloning the pattern (generator + scatter).
|
|||
|
||||
## Controls
|
||||
|
||||
WASD move · **Shift** run (speed ×`RUN_MULTIPLIER` in `app/player.ts`) · mouse
|
||||
WASD move · **Shift** run (speed ×`RUN_MULTIPLIER` in `game/player.ts`) · mouse
|
||||
look (click canvas to pointer-lock) · **Space** jump · **1/2/3** switch look
|
||||
presets. FPS shown bottom-right. The room's north wall is open — walk out onto
|
||||
the terrain and toward the peaks.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue