跪拜 Guibai
← All articles
Frontend · AI · Three.js

Wulai: A React + Three.js Browser Action Game with Procedural Art and a Mobile Performance Budget

By 莫西干式秃头 ·
Read original on juejin.cn ↗ Google Translate ↗ Alt translation

A full 3D action game can ship in the browser without a game engine, as long as performance is budgeted numerically; the same discipline makes AI coding agents stop repeating mistakes by encoding every fix into enforceable project rules.

Summary

Wulai is a full 3D action game that runs in the browser, built with React 18, React Three Fiber v8, and three.js r168. No game engine, no external art assets: the character, terrain, trees, rocks, monsters, and skill effects are all procedural geometry from code. The only imported model is the SU7. The map is 260m × 260m with a winding path, 165 monsters, six weapons, two bosses, and a drivable car with a nitrous system.

Performance is treated as a numeric budget. A permanent pool of four point lights prevents shader recompilation stalls; shaders pre-warm during browser idle; repeated objects go through InstancedMesh; and terrain is a single pure function queried by the ground, vegetation, spawns, and vehicle. A built-in probe at ?perf=auto runs the full move set and reports metrics, with acceptance criteria of zero new shaders, four lights, max ~20ms frame time, and draw calls capped at 660.

Five production-only pitfalls are documented: Vite circular chunks that break initialization, a Suspense fallback that duplicates the canvas build, stale visualViewport reads on iOS rotation, Xiaomi long-press spawning system UI, and gltf-transform optimize flattening wheel nodes. Each fix is encoded into project rules and skills that guide Cursor's coding agent, so recurring mistakes fade. The combination of numeric performance gates and AI-rule enforcement is what makes the game playable on phones.

Takeaways
— The map is 260m × 260m with 165 monsters, 6 weapons, and 2 bosses (HP 4200 and 4800).
— The ultimate "Wulai" has a 10x damage multiplier, a 13m shockwave, and leaves a 6.5m radius magma crater for 5 seconds.
— All geometry except the SU7 is procedurally generated; code is the art.
— A permanent pool of 4 point lights avoids shader recompilation when light count changes.
— Shaders are pre-warmed during browser idle to prevent first-use stalls.
— All repeated objects use InstancedMesh; particles separate per-instance alpha attribute.
— Monster click detection uses a capsule approximation, not mesh raycast.
— Terrain is a single pure function used by ground, vegetation, spawns, player height, and wheels.
— Mobile fidelity: terrain segments 176→120, shadow map 1024→512, grass 880→520, pixel ratio fixed to 1.
— First-screen synchronous build budget is ~110ms on a dev machine; new features add ≤20ms or get deferred.
— Performance probe at ?perf=auto runs all actions; acceptance: 0 new shaders, 4 lights, ≤20ms frame, draw calls ~214 normal and ~660 during ultimate.
— Pitfalls: Vite circular chunks produce `Circular chunk` warnings; Suspense fallback with canvas causes double build; iOS `resize` reads stale viewport; Xiaomi long-press needs `touchstart` cancellation; gltf-transform optimize flattens wheel nodes.
— AI rules in `.cursor/rules/*.mdc` and `.cursor/skills/` encode every bug fix to prevent recurrence.
Conclusions

Keeping per-frame state in module singletons outside React is a clean pattern for any animation-heavy React app; it avoids re-render storms without extra libraries.

The light pool and shader pre-warm are specific to three.js but the underlying principle (prevent runtime shader compilation) applies broadly to WebGL/GPU apps.

Turning each bug fix into a rule for the AI coding agent works because it turns tribal knowledge into executable constraints; that's a transferable practice for any AI-assisted development.

Building art via code, with anchor tables and loft profiles, makes "changing art" as fast as changing code, but it requires an unusual tolerance for debugging geometry.

Concepts & terms
React Three Fiber
A React renderer for three.js that lets you describe 3D scenes declaratively, used here with drei helpers.
InstancedMesh
A three.js technique that renders many copies of the same geometry with per-instance transforms, drastically reducing draw calls.
Procedural generation
Creating assets algorithmically via code instead of hand-authored art files.
Shader variant
A compiled version of a shader for specific material property combinations; changing properties at runtime can force new compilations and cause stalls.
manualChunks
A Vite/Rollup config to split code into separate bundles for caching; misusing it can create circular dependencies.
Suspense
A React feature for loading, falling back to a placeholder; using a heavy subtree as fallback can double work.
Source: juejin.cn ↗ Google Translate ↗ Backup ↗