Engine patterns — Unity-isms in Three.js + R3F
Catalogue of game-engine patterns familiar from Unity and how they map onto Three.js + R3F. Two purposes: (1) translation guide when porting ideas from MyProject; (2) honest inventory of what Three doesn’t give us for free and we have to build or pull from the ecosystem.
Conventions:
- Built-in = comes with Three / R3F / drei out of the box
- Library = install a community package, minimal glue
- Own = code we write and maintain (with size estimate)
AI-fluency note. Where a Unity-ism’s natural R3F equivalent would tax an AI coding assistant (visual JSX rewrite, shadow scene file, registry-by-name indirection, drag-reference-onto- field), vibesmith picks the idiomatic alternative — even when it costs some authoring ergonomics — and names the refusal in Principled non-features. The framework’s bet is that AI legibility compounds; this translation table biases toward shapes the assistant reads end-to-end without crawling a registry. The Inspectable parameters cookbook shows the pattern in practice for game scripts.
Scene composition
Section titled “Scene composition”Unity: GameObject hierarchy + Components. Prefabs as serialized hierarchies. ScriptableObjects for shared data.
Three.js + R3F: Declarative JSX. The scene IS the React component tree. A “prefab” is a React component returning a Three subgraph; a “prefab variant” is just prop overrides.
// MyProject's SettlementComposer maps to:function Settlement({ recipe }: { recipe: SettlementRecipe }) { return ( <> <Terrain recipe={recipe.terrain} /> {recipe.buildings.map(b => <Building key={b.id} recipe={b} />)} {recipe.npcs.map(n => <NPC key={n.id} placement={n} />)} </> );}The recipe→composer pattern from MyProject ports near-verbatim. The data layer changes (Zod-typed JSON instead of ScriptableObject).
Status: built-in.
Game loop and tick
Section titled “Game loop and tick”Unity: Update() per frame, FixedUpdate() at fixed timestep,
LateUpdate() after all Updates.
R3F: useFrame((state, delta) => ...) per render frame. No
FixedUpdate equivalent. No LateUpdate either, but useFrame accepts a
priority to order callbacks within a frame.
Server tick (Colyseus): authoritative ~0.6s tick (OSRS-shape). Client never simulates canonical state — it interpolates between server-sent snapshots.
What we own:
- Fixed-timestep accumulator at the top of the scene tree if we ever
need deterministic client-side simulation (e.g. predicted projectile
paths). Pattern:
let accumulator = 0;const FIXED_DT = 1 / 30;useFrame((_, delta) => {accumulator += delta;while (accumulator >= FIXED_DT) {fixedTick(FIXED_DT);accumulator -= FIXED_DT;}});
- Snapshot interpolation buffer for incoming server state. ~100ms playback delay; renders interpolated state between two most-recent server ticks. Standard MMO client pattern; ~150 lines. Reference: Gabriel Gambetta’s snapshot interpolation series.
- System priorities for
useFrameordering: input (-100), simulation (0), animation (50), camera (100). Use the second arg touseFrame.
Size: ~200 lines total for accumulator + interpolation + system prioritization conventions.
Animation state machine
Section titled “Animation state machine”Unity: Animator Controller — visual state machine + blend trees + avatar masks + transition conditions.
Three: AnimationMixer + AnimationAction (crossfade primitives).
drei’s useAnimations wraps the mixer ergonomically.
What we own:
AnimationControllerclass wrappinguseAnimationswith named states, crossfade durations, one-shot overlays that auto-return. ~100 lines. AI codegen produces this from a spec in one shot.- Per-character animation map (data, not code): which clips exist, which
is idle, default crossfade times. Lives in
packages/content/.
Escalation paths:
- XState v5 if combat / dialogue grows interrupt/cancel/queue complex.
- Avatar masks (upper-body action while lower-body walks) — Three’s
AnimationAction.weight+ manual bone targeting. Build when needed.
Status: own (small).
Pathfinding (NavMesh)
Section titled “Pathfinding (NavMesh)”Unity: NavMeshAgent + baked NavMesh.
Three: nothing built-in.
Library: @recast-navigation/three — Recast/Detour port to JS (WASM).
Same algorithm Unity uses under the hood. R3F bindings included.
Workflow:
- Bake NavMesh from world geometry at world-gen time (or runtime in dev).
- Query path with
navMesh.computePath(start, end)on click-to-move. - Move agent along path; resync to server state on tick.
Status: library, minimal glue.
Camera control
Section titled “Camera control”Unity: Cinemachine — free-look, framing transposer, virtual cameras.
Three + drei: <OrbitControls />, <PerspectiveCamera />,
<CameraShake />. Plenty for stock cases.
OSRS-style follow camera: drei doesn’t ship this exact behaviour.
Pattern: lerp camera position toward player.position + offset,
look-at player, optional pitch/yaw control via mouse drag. ~30 lines.
Status: own (tiny).
Physics
Section titled “Physics”Unity: built-in PhysX (Rigidbody, Collider, CharacterController, triggers).
Three: nothing built-in.
Library: @react-three/rapier (Rapier physics + R3F bindings).
OSRS-style call: for movement, no physics engine needed — NavMesh-driven movement + raycasts handle it. Install Rapier only when we hit physics-needing features: projectiles, ragdolls, dropped items with bounce, destructible scenery.
Status: library, install when needed.
Unity: Input System with action maps, rebindable bindings, multi-device.
Web: raw pointerdown / keydown / Gamepad API.
What we own:
InputActionsmap — named actions (“move”, “interact”, “openInventory”) bound to keys / mouse / gamepad. JSON-defined, runtime-overridable for rebinding. ~150 lines including gamepad polling. Lives inpackages/shared/input/.- Hooks:
useAction('interact', () => ...),useAxis('move').
Status: own (small).
Coroutines / async sequences
Section titled “Coroutines / async sequences”Unity: StartCoroutine + yield return.
JS: async/await is the equivalent. Cancellation via
AbortController.signal; check between awaits.
Pattern:
async function playDialogue(line: string, signal: AbortSignal) { presenter.show(line); await wait(line.length * 50, signal); if (signal.aborted) return; presenter.fade();}Helper: small wait(ms, signal) utility that throws on abort. ~10 lines.
Status: own (trivial).
Object pooling
Section titled “Object pooling”Unity: ObjectPool<T> built-in (modern).
Three: nothing built-in.
What we own: generic pool utility for ~any reusable object — damage numbers, projectiles, particle bursts, chat bubbles. ~50 lines.
class Pool<T> { constructor(private factory: () => T, private reset: (t: T) => void) {} acquire(): T { ... } release(t: T): void { ... }}Status: own (trivial).
Zone streaming (additive scenes)
Section titled “Zone streaming (additive scenes)”Unity: SceneManager.LoadSceneAsync additive.
Three: no built-in. Each zone is a .glb (or set of .glbs) loaded
via useGLTF; mount/unmount via React conditionals.
What we own:
ZoneManager— tracks player position, computes nearby zone keys, triggers loads on entry-edge and unloads on exit-edge with hysteresis. Async via Suspense boundaries per zone.- Asset cache strategy: rely on
useGLTF.preload+ browser HTTP cache for v0. Custom IndexedDB asset cache if quota becomes the bottleneck.
Status: own (medium — ~300 lines including hysteresis + cache).
Particles + VFX
Section titled “Particles + VFX”Unity: Shuriken particle system + VFX Graph.
Three: nothing built-in.
Library: three.quarks — TS port of Unity’s Shuriken to Three.
Genuinely a Shuriken port; same mental model.
Status: library, deferred until VFX matter.
Postprocessing
Section titled “Postprocessing”Unity: Post-processing stack (URP/HDRP).
Library: @react-three/postprocessing — bloom, depth-of-field,
SSAO, color grading, vignette. Declarative <EffectComposer>.
Status: library, deferred until visual polish phase.
Unity: AudioSource + AudioListener + spatial.
Three: PositionalAudio + AudioListener. drei has
<PositionalAudio />. Web Audio API underneath.
What we own: @vibesmith/audio-runtime — scene-graph-aware
wrapper above WebAudio. <AudioEmitter> scene-node components,
camera-driven AudioListener sync, fixed five-bus mixer (master /
music / sfx / dialogue / ambient) with ducking, manifest-driven
buffer cache, recipe adapter, scenario capture + replay,
deferred-init autoplay gate. WebAudio nodes stay reachable via
emitter.raw(). See Audio runtime.
Status: built-in wrapper.
UI / HUD
Section titled “UI / HUD”Unity: UI Toolkit / UGUI.
Web: HTML/CSS/React. Strictly better for 2D UI.
Status: built-in (React + Tailwind).
Timeline / cutscenes
Section titled “Timeline / cutscenes”Unity: Timeline asset.
Library: theatre.js — keyframed animation authoring with a Studio
GUI; outputs JSON sequences.
Status: library, deferred until cutscenes happen.
Inspector / dev tooling
Section titled “Inspector / dev tooling”Unity: Editor + Inspector + Hierarchy panels.
Stack:
r3f-perf→<Perf />overlay for FPS, drawcalls, GPU timeleva→useControls({...})runtime property panels per-componenttriplex→ visual JSX scene editor (optional, install if manual placement workflows demand it)- React DevTools → scene as component tree
Status: library × 3, dev-only.
Data assets
Section titled “Data assets”Unity: ScriptableObject.
Our pick: TS modules + Zod schemas + per-entity JSON files.
export const VoiceCard = z.object({ id: z.string(), registerTags: z.array(z.string()), exampleLines: z.array(z.string()), ...});export type VoiceCard = z.infer<typeof VoiceCard>;
// packages/content/data/voice-cards/hella.json{ "id": "hella", "registerTags": ["warm", "deadpan"], ... }Validated at load time. AI-friendly. Diff-friendly. Survives engine pivots.
Status: own (schemas + loader, ~200 lines).
Server-authoritative networking
Section titled “Server-authoritative networking”Unity (MyProject): Mirror — tick-based, NetworkBehaviour, SyncVar.
Colyseus equivalents:
Room↔ Mirror’sNetworkManagersceneSchemastate ↔SyncVar(with delta encoding built in)onMessage↔CommandRPCsclock.setIntervalfor tick loopMapSchema<Player>for per-player state
Pattern: client never holds canonical state. Subscribe to Colyseus state changes → push into Zustand store → R3F components read from store → render. Client predicts movement locally for responsiveness; reconciles when server confirms.
What we own:
- Snapshot interpolation buffer (above, under Game loop).
- Client-side prediction + reconciliation for player movement. ~200 lines.
- Lag compensation in server hit-detection (later, when combat lands).
Status: library (Colyseus) + own (prediction/reconciliation).
Profiling
Section titled “Profiling”Unity: Deep profiler, frame debugger.
Web:
- Chrome DevTools Performance tab (CPU + GPU traces)
r3f-perf— in-game numbers- Spector.js — WebGL frame capture, draw-call inspector
Status: library × 2 + browser devtools.
Lighting
Section titled “Lighting”Unity: directional / point / spot / area lights; light probes; lightmaps; reflection probes; URP/HDRP shadow cascades.
Three: DirectionalLight, PointLight, SpotLight,
AmbientLight, HemisphereLight. drei: <Environment /> for IBL,
<Sky /> for procedural sky, <ContactShadows />,
<AccumulativeShadows /> for baked-feel ground shadows on static
scenes.
Approach for target visual baseline:
- One directional sun + hemisphere ambient + drei
<Environment>(HDRI cubemap for subtle reflections) covers ~90% of scenes PCFSoftShadowMapfor the sun; one shadow cascade, tuned bias- No lightmap baking (skipping the Unity equivalent — matte materials + unlit-ish materials are fine without precomputed GI)
Status: built-in + drei. ~50 lines of setup in a <SceneLighting>
component shared across scenes; per-scene overrides via props.
Skybox / environment
Section titled “Skybox / environment”Unity: Skybox material + procedural sky.
Three + drei:
<Sky />— procedural Hosek-Wilkie sky (sun position, turbidity)<Stars />— night-sky particles<Cloud />/<Clouds />— volumetric-ish cloud layers<Environment files="..." />— HDRI environment map for image-based lighting + skybox
Status: built-in. Pick procedural for time-of-day variation, HDRI for a fixed look.
Terrain rendering
Section titled “Terrain rendering”Unity: Terrain system — heightmap, splat maps, detail mesh, trees.
Three: nothing built-in. Build it:
- Mesh:
PlaneGeometrysubdivided to grid resolution + vertex displacement from heightmap (CPU on load, or GPU vertex shader) - Splat-mapped material: custom shader sampling 4-8 ground textures weighted by per-vertex (or per-pixel) splat map; output blended diffuse. ~200 lines of GLSL.
- Tiling: chunked terrain at zone scale. Per-chunk mesh + draw call. Stitch via shared edge normals.
- Detail mesh (grass, rocks): instanced meshes scattered via
Poisson-disk on terrain surface. drei has
<Instances>for instanced rendering primitives.
Status: own (~400-600 lines including shader). Build alongside the region generator that produces heightmaps. the chosen asset pack-pack-style “polygon ground tiles” are a viable alternative for the cute aesthetic — no heightmap, just tile placement. Pick at first zone.
Vegetation / scatter
Section titled “Vegetation / scatter”Unity: terrain trees + grass + Vegetation Studio packages.
Three: instanced meshes scattered by procgen rules. drei’s
<Instances> + <Instance> makes it tractable. For grass: custom
shader on instanced quads / blades with wind via vertex shader. There
are MIT examples; pick one rather than writing from scratch.
Procgen integration: scatter rules live in the region generator
(MyProject’s RegionRecipe.scatterSpec ports over). Output is just
more entities in the composition: { type: 'instanced-mesh', asset: 'tree-01', positions: [...] }.
Status: own (~200 lines instancing wrappers) + lib (grass shader when grass matters).
Memory management
Section titled “Memory management”Unity: Resources.UnloadUnusedAssets() + GC. Mostly hands-off.
Three: manual. This is the single most-bitten Three pitfall.
Geometries, materials, textures all hold WebGL resources that must be
explicitly .dispose()’d when no longer used. Without discipline,
long-running MMO clients leak GPU memory until tab crash.
Discipline:
- Use
useGLTFanduseTexturefrom drei — they instance-count and dispose on last-unmount automatically - For manual loaders, hold disposables in a
DisposeRegistryscoped to the zone / scene; drain on unmount - Never
new THREE.Material()outside auseMemo+ cleanup pair in React components - For dynamic content (procgen meshes, edited compositions), the composition unmount lifecycle is the natural disposal trigger
Tooling:
- Chrome DevTools Memory tab catches Three GPU leaks indirectly (growing heap) — first signal
renderer.info.memory.{geometries,textures}numbers — log in dev, any unbounded growth across zone transitions is a bugr3f-perfshows these live
Status: discipline + ~50-line DisposeRegistry util. Catch in code review / Tier-0 long-run perf probe.
Splines / paths
Section titled “Splines / paths”Unity: SplineContainer.
Three: CatmullRomCurve3, CubicBezierCurve3. drei has
<CatmullRomLine> for rendering.
For paths in scenes (roads, NPC patrol routes), generator emits an array of control points; renderer / pathing system samples the spline.
Status: built-in.
Localization (i18n)
Section titled “Localization (i18n)”Unity: Localization package.
Web: i18next + react-i18next. Standard, mature, widely
supported.
Pattern:
- String tables as JSON per locale in
packages/content/data/i18n/<lang>/<namespace>.json - Authoring locale: English. Other locales via translation pipeline (LLM-assisted batch translation + theme-critic per locale → bake).
- Locale-aware Theme Critic — the “warmth / register / tone” verdict applies per locale, not globally. culturally-specific humour translated to another culture may need re-anchoring.
- RTL support: Tailwind’s
rtl:variant + logical CSS properties. - Pluralization: i18next’s plural rule support.
Aspect-ratio agnostic + mobile-first (already a standing rule) intersects with localization — German strings are ~30% longer than English; designs must flex.
Status: library + ~150 lines of bootstrap. Defer the content of localization until there’s a game to localize.
Telemetry + crash reporting
Section titled “Telemetry + crash reporting”Unity: Unity Analytics, Crashlytics.
Web:
- Crash / error reporting: Sentry (web SDK). Source-map aware, ties errors to releases. Free tier covers solo dev volume.
- Product analytics: PostHog (self-hostable) or Plausible (simpler, hosted). Event tracking for “player progressed thread X”, “player entered zone Y”, “client FPS dropped below threshold”.
- Server logs: structured logging via
pino→ log aggregator (later — Loki / Logtail / etc.).
Status: install when there’s first external playtest. Free / cheap SaaS tier; no work to build.
Build + distribution pipeline
Section titled “Build + distribution pipeline”Unity: Cloud Build, IL2CPP, platform builds.
Web:
pnpm buildruns Vite production builds forapps/clientandapps/server- Client bundle → static assets (HTML, JS, WASM, KTX2, GLB) with content-hashed filenames; uploaded to CDN (Cloudflare Pages / R2)
- Server bundle → single Node entry point; deployed to a VM (Hetzner / Fly.io) or container (Fly.io Apps, Railway, Render)
- Asset CDN URLs baked into manifest at build time
- Environment configs via
.env.<environment>+ Vite’s env handling - Source maps uploaded to Sentry; not served to clients in prod
Versioning: semver on the client; server tracks min-client-version and serves an upgrade prompt. Critical because MMO clients in the wild diverge from server schema.
Status: standard web deploy. Defined in code from scaffold time —
see docs/reproducibility.md for the full
contract (justfile / Docker / Terraform / mise / drizzle-kit / CI).
Build infra structure first, populate when external playtest matters.
Persistence / save system
Section titled “Persistence / save system”Unity: PlayerPrefs, file I/O, scripted serialization.
Our setup:
- Authoritative state is server-side. Postgres is the cloud save. No client-side save files for player progress.
- Client-local state — settings, keybinds, recently-cached
composition snapshots, draft chat messages — lives in IndexedDB via
idb-keyval. Survives reloads, doesn’t sync across devices unless the server replicates it. - Anonymous → authed transition — client may start a session unauthed (browse around, play tutorial); on login, local progress migrates to server.
Status: server-side (Drizzle + Postgres handles it); ~50 lines of IndexedDB wrappers for client-local prefs.
Multiplayer matchmaking + lobby
Section titled “Multiplayer matchmaking + lobby”Unity: UGS Matchmaker, Lobby.
Colyseus: built-in. JoinOrCreate with filter predicates handles
matchmaking; rooms expose metadata for a lobby browser if/when one is
built.
For OSRS-shape MMO with shared persistent world (one or few rooms per
shard, not many small lobbies), matchmaking is trivial: joinRoom("world").
Status: library, near-zero glue.
Patterns we deliberately don’t copy
Section titled “Patterns we deliberately don’t copy”The sections above index patterns vibesmith does implement (often under a renamed surface). This section inverts that — patterns established engines ship as load-bearing primitives that vibesmith refuses to copy, with one-line reasoning and the vibesmith equivalent. Catalogued here so the absence is intentional and discoverable, not an oversight. Sibling of Principled non-features — that doc names what we don’t ship; this section names what we don’t copy.
| Pattern | Engine | vibesmith reason | vibesmith equivalent |
|---|---|---|---|
Magic method names (Awake / Start / Update / LateUpdate / OnDestroy looked up by string) | Unity | Refactor-hostile (rename = silent breakage); IDE can’t “find references” reliably; no compile-time contract. | Explicit factory registration via defineGameScript({ id, onStart, onUpdate, onDestroy }). The hook is a property the type system enforces. |
GetComponent<T>() reflection | Unity | Runtime lookup with no compile-time guarantee the component exists; diffuse coupling across sibling components on the same GameObject; null-check ceremony everywhere. | Typed ctx dependency-injection bag (ctx.physics, ctx.audio, ctx.animator(id)). Cross-script communication is ctx.emit(signalName, payload), not GetComponent + method call. |
| MonoBehaviour class inheritance | Unity | Composition > inheritance; “extends MonoBehaviour” couples every script to engine internals; no first-class way to share behaviour without diamond-problem ceremony. | Factory registration. defineGameScript({...}) returns a typed value; behaviour-sharing is plain function composition + shared ctx capabilities. No base class. |
Coroutine IEnumerator DSL (yield return new WaitForSeconds(1f)) | Unity | A bespoke DSL that mirrors what TS / JS already provide natively via async/await. Adds a separate mental model that doesn’t compose with Promises / AbortController / Promise.race. | Native async/await + ctx.wait(ms, signal?) + AbortController for cancellation. Same expressive power, zero DSL. |
| IMGUI / OnGUI immediate-mode debug UI | Unity | Immediate-mode debug GUIs leak into shipped games; per-script OnGUI allocates every frame; styling lives in C# not CSS; impossible to share with the dev-shell’s panel surface. | Standard-extension panels + leva for runtime parameter tweaking. Debug UI lives in the dev shell, not the game. |
[SerializeField] reflection (private-field inspector exposure) | Unity | Couples inspector visibility to language-level access modifiers; can’t carry validation, defaults, range constraints, or grouping without an attribute soup; the same schema isn’t reusable for AI assistants / scaffolding / docs. | Zod-schema-declared parameters on defineGameScript. One schema feeds the inspector, AI-assistant context, default values, validation, and serialised snapshots. |
@export reflection (Godot’s equivalent of [SerializeField]) | Godot | Same problem as Unity’s [SerializeField] — relies on language-level reflection (GDScript-only, not portable to typed languages); schema duplicated across runtime / editor / save formats. | Same zod-schema surface. |
GDScript magic _process / _ready / _physics_process | Godot | Magic-name lifecycle hooks share Unity’s refactor-hostile shape plus GDScript’s dynamic typing makes the contract even looser. | Explicit onUpdate / onStart / onFixedUpdate — typed by zod-declared parameters + TS types. |
| Blueprint visual scripting | Unreal | Diff-hostile (binary asset), tooling-hostile (no grep), AI-assistant-hostile (vision-only), pedagogy-hostile (no transferable mental model). The “designer-friendly” pitch is undercut by the reality that any non-trivial Blueprint reaches the readability ceiling fast. | Text TS + zod schemas. Designer-friendliness comes from the dev-shell inspector + cmd+P quick actions + AI assistant, not a parallel visual language. |
| UFUNCTION / UPROPERTY / UCLASS macro ceremony | Unreal | Macro-driven code-generation that exists because C++ has no reflection. TS gets the same affordance from import + types + zod — no macros required; no parallel header generation. | Plain TS exports + zod schemas. The build step is tsc; no header tool. |
| Class-based UObject inheritance (UPawn / ACharacter / UActor) | Unreal | Same composition-over-inheritance argument as Unity’s MonoBehaviour, escalated — Unreal pushes deep hierarchies (UObject → AActor → APawn → ACharacter → MyCharacter). | Composition. A “character” is a <RigidBody> + <Animator> + defineGameScript triple — assembled, not inherited. |
| Tags + Layers + Gameplay Tags as string-keyed lookup | Unity / Godot / Unreal | Name-string fragility (typos compile fine); flat namespace fights modular design; “GameObject.Find” lookups defeat tree-shaking. | Typed ECS-shape components (miniplex / koota); query by component type, not by name. |
GameObject.Find / get_node("Path/To/Node") / GetActorOfClass | Unity / Godot / Unreal | String-keyed scene-graph lookup is the same fragility class as tags. Breaks on rename; can’t be statically analysed; encourages spooky-action-at-a-distance. | Explicit refs (React useRef); store subscriptions (zustand); typed ctx access. |
SendMessage / call_group / Blueprint Message Bus | Unity / Godot / Unreal | String-named dispatch with no compile-time contract on payload shape; degrades to runtime errors. | Typed ctx.emit(signalName, payload) — payloads are zod-typed; signal names are TS literal-union types per script declaration. |
| Per-engine custom build / cook / package pipeline | Unity (IL2CPP + Addressables) / Unreal (Cook + Stage) / Godot (Export Templates) | The web platform already has a deterministic, well-tooled, AI-fluent build pipeline (Vite + esbuild + HTTP). Re-inventing it inside the framework loses the ecosystem. | pnpm build → Vite → static assets + content-hashed filenames. No “Cook & Stage” mental model; no custom asset bundler. |
Why the absences are surfaced here
Section titled “Why the absences are surfaced here”Onboarding agents (LLM + human refugees from established engines) habitually scan for the patterns they know. Without this section, the absence reads as “they haven’t built it yet” when the truth is “they’ve decided not to.”
The discipline is also AI-relevant. An LLM trained on Unity
scripts will, given the chance, invent GetComponent<T>()
calls against vibesmith’s ctx surface. The section above
gives the assistant the corrective context to fall back on
idiomatic vibesmith shape.
Things we don’t need to think about
Section titled “Things we don’t need to think about”These exist in Unity, don’t exist in our stack, and don’t need replacement:
- Asset bundles — Vite handles asset bundling; HTTP/browser cache is the streaming primitive
- Build platforms — one platform (browser)
- Player Settings, Quality Settings — runtime config via JSON
- Tags + Layers — components and types are richer
- GameObject.Find — refs and store subscriptions are explicit
- SendMessage — typed events / store dispatch
Summary: what we own vs install
Section titled “Summary: what we own vs install”Own (small — total ~1500-2000 lines):
- AnimationController (~100)
- Fixed-timestep + interpolation buffer (~200)
- Camera follow (~30)
- InputActions (~150)
- ObjectPool (~50)
- ZoneManager (~300)
- audio-runtime: emitter + mixer + listener-sync + autoplay gate (~400; see Audio runtime)
- Content loader + Zod schemas (~200)
- Client prediction + reconciliation (~200)
- Misc glue (~300)
Install:
- three, @react-three/fiber, @react-three/drei (core)
- @recast-navigation/three (pathfinding)
- @react-three/rapier (physics, when needed)
- three.quarks (particles, when needed)
- @react-three/postprocessing (visual polish, when needed)
- theatre.js (cutscenes, when needed)
- r3f-perf + leva (dev tooling)
- colyseus.js (networking)
- react + tailwind + zustand (HUD)
The “own” total is well under what a single composer in MyProject grew to. Three + R3F’s “barebones” reputation overstates the gap; the ecosystem fills most of it, and what’s left is small enough to fit in a single dev’s head.