add : finish readme, git lfs and gitignore
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
# Git LFS for 3D assets
|
||||
# Track large binary files to avoid repo bloat
|
||||
|
||||
*.glb filter=lfs diff=lfs merge=lfs -text
|
||||
*.gltf filter=lfs diff=lfs merge=lfs -text
|
||||
|
||||
# Textures
|
||||
*.png filter=lfs diff=lfs merge=lfs -text
|
||||
*.jpg filter=lfs diff=lfs merge=lfs -text
|
||||
*.jpeg filter=lfs diff=lfs merge=lfs -text
|
||||
*.webp filter=lfs diff=lfs merge=lfs -text
|
||||
*.hdr filter=lfs diff=lfs merge=lfs -text
|
||||
*.exr filter=lfs diff=lfs merge=lfs -text
|
||||
*.ktx2 filter=lfs diff=lfs merge=lfs -text
|
||||
*.ktx filter=lfs diff=lfs merge=lfs -text
|
||||
|
||||
# Audio
|
||||
*.wav filter=lfs diff=lfs merge=lfs -text
|
||||
*.mp3 filter=lfs diff=lfs merge=lfs -text
|
||||
*.ogg filter=lfs diff=lfs merge=lfs -text
|
||||
|
||||
# Video (cinematics)
|
||||
*.mp4 filter=lfs diff=lfs merge=lfs -text
|
||||
*.webm filter=lfs diff=lfs merge=lfs -text
|
||||
+27
-11
@@ -1,24 +1,40 @@
|
||||
# Dependencies
|
||||
node_modules/
|
||||
|
||||
# Build
|
||||
dist/
|
||||
dist-ssr/
|
||||
*.local
|
||||
|
||||
# Environment
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Logs
|
||||
logs
|
||||
logs/
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
# Editor
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
.idea/
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Debug
|
||||
*.debug
|
||||
|
||||
# 3D Assets Cache (drei, GLTFJSX)
|
||||
.drei/
|
||||
.glitchdrei-cache/
|
||||
@@ -48,6 +48,7 @@ la-fabrik/
|
||||
└── src/
|
||||
├── world/ # Single persistent 3D world
|
||||
│ ├── Map.tsx # Base map, always mounted
|
||||
│ ├── Lighting.tsx # Ambient, directional, point lights
|
||||
│ ├── Environment.tsx # HDRI, fog, sky
|
||||
│ ├── PostFX.tsx # Bloom, SSAO, chromatic aberration
|
||||
│ ├── zones/ # Spatial zones — LOD per zone
|
||||
@@ -96,7 +97,7 @@ la-fabrik/
|
||||
│
|
||||
├── utils/
|
||||
│ ├── Debug.ts # lil-gui panel
|
||||
│ ├── EventEmitter.ts # Decoupled event bus between managers
|
||||
│ ├── EventEmitter.ts # Simple pub/sub for manager-to-manager events
|
||||
│ └── Dispose.ts # traverse() + dispose() helper
|
||||
│
|
||||
├── App.tsx # Canvas + UI superimposed
|
||||
@@ -105,11 +106,31 @@ la-fabrik/
|
||||
|
||||
## 🏗 Architecture Patterns
|
||||
|
||||
These patterns are **mandatory across the entire codebase**. Every class, every 3D object, every manager follows the same conventions. Consistency over cleverness.
|
||||
The project uses **two complementary patterns**:
|
||||
|
||||
### 1. Singleton Pattern
|
||||
- **Singleton service classes** for orchestration and side effects
|
||||
- **Declarative React components** for all 3D scene objects
|
||||
|
||||
Every Manager and core utility uses the same singleton pattern. One instance, shared everywhere — no prop drilling, no context, no stray `new` calls
|
||||
This distinction is intentional. Scene elements such as the map, lights, environment, zones, and player are implemented as **React Three Fiber components** and mounted through `<Canvas>`.
|
||||
Global systems such as gameplay flow, cinematics, audio, and debug tooling are implemented as **manager classes**.
|
||||
|
||||
Consistency matters, but the codebase does **not** force the same lifecycle pattern on scene components and global services.
|
||||
|
||||
---
|
||||
|
||||
### 1. Singleton Pattern for Global Managers Only
|
||||
|
||||
Only cross-cutting services use the singleton pattern.
|
||||
|
||||
Examples:
|
||||
- `GameManager`
|
||||
- `CinematicManager`
|
||||
- `AudioManager`
|
||||
- `ZoneManager`
|
||||
- `Debug`
|
||||
- `EventEmitter`
|
||||
|
||||
These services must exist once, be accessible from anywhere, and coordinate the experience globally.
|
||||
|
||||
```ts
|
||||
// stateManager/GameManager.ts
|
||||
@@ -119,7 +140,6 @@ export class GameManager {
|
||||
cinematic!: CinematicManager
|
||||
audio!: AudioManager
|
||||
zone!: ZoneManager
|
||||
npc!: NPCManager
|
||||
|
||||
static getInstance(): GameManager {
|
||||
if (!GameManager._instance) {
|
||||
@@ -132,73 +152,95 @@ export class GameManager {
|
||||
this.cinematic = CinematicManager.getInstance()
|
||||
this.audio = AudioManager.getInstance()
|
||||
this.zone = ZoneManager.getInstance()
|
||||
this.npc = NPCManager.getInstance()
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
this.cinematic.destroy()
|
||||
this.audio.destroy()
|
||||
this.zone.destroy()
|
||||
this.npc.destroy()
|
||||
GameManager._instance = null
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
// Usage — anywhere in the codebase
|
||||
Usage:
|
||||
|
||||
```ts
|
||||
const game = GameManager.getInstance()
|
||||
game.startMission('workshop')
|
||||
```
|
||||
|
||||
Apply the **exact same pattern** to every Manager and utility class (`CinematicManager`, `AudioManager`, `Debug`, `Sizes`, `EventEmitter`)
|
||||
**Important:** scene objects such as `Map`, `WorkshopZone`, `Lighting`, or `Environment` are **not** singletons and must remain standard React components.
|
||||
|
||||
---
|
||||
|
||||
### 2. Class Interface — `load` / `update` / `destroy`
|
||||
### 2. Scene Objects Are React Components, Not Manager Classes
|
||||
|
||||
Every 3D class implements the same three-method lifecycle. No exceptions.
|
||||
All 3D scene objects are implemented as **declarative React components**.
|
||||
|
||||
```ts
|
||||
// Enforced interface for all world objects
|
||||
interface WorldObject {
|
||||
load(): Promise<void> // Load assets, build scene graph
|
||||
update(delta: number): void // Per-frame logic (called from useFrame)
|
||||
destroy(): void // Clean GPU memory — mandatory
|
||||
}
|
||||
```
|
||||
This includes:
|
||||
- maps
|
||||
- lights
|
||||
- environments
|
||||
- player controllers
|
||||
- zones
|
||||
- interactive props
|
||||
- postprocessing layers
|
||||
|
||||
```ts
|
||||
This keeps the code aligned with the R3F runtime instead of rebuilding a parallel imperative engine.
|
||||
|
||||
Example:
|
||||
|
||||
```tsx
|
||||
// world/zones/WorkshopZone.tsx
|
||||
export class WorkshopZone implements WorldObject {
|
||||
private group: THREE.Group = new THREE.Group()
|
||||
private mixer: THREE.AnimationMixer | null = null
|
||||
import { useEffect, useRef } from 'react'
|
||||
import * as THREE from 'three'
|
||||
import { useFrame } from '@react-three/fiber'
|
||||
import { useGLTF } from '@react-three/drei'
|
||||
|
||||
async load(): Promise<void> {
|
||||
const gltf = await loadGLTF('/models/workshop/ebike.glb')
|
||||
this.group = gltf.scene
|
||||
this.mixer = new THREE.AnimationMixer(this.group)
|
||||
this.scene.add(this.group)
|
||||
}
|
||||
export function WorkshopZone() {
|
||||
const root = useRef<THREE.Group>(null)
|
||||
const gltf = useGLTF('/models/workshop/ebike.glb')
|
||||
const mixer = useRef<THREE.AnimationMixer | null>(null)
|
||||
|
||||
update(delta: number): void {
|
||||
this.mixer?.update(delta)
|
||||
}
|
||||
useEffect(() => {
|
||||
mixer.current = new THREE.AnimationMixer(gltf.scene)
|
||||
|
||||
destroy(): void {
|
||||
this.mixer?.stopAllAction()
|
||||
Dispose.object(this.group) // ← always traverse before remove
|
||||
this.scene.remove(this.group)
|
||||
return () => {
|
||||
mixer.current?.stopAllAction()
|
||||
mixer.current = null
|
||||
}
|
||||
}, [gltf.scene])
|
||||
|
||||
useFrame((_, delta) => {
|
||||
mixer.current?.update(delta)
|
||||
})
|
||||
|
||||
return <primitive ref={root} object={gltf.scene.clone()} />
|
||||
}
|
||||
```
|
||||
|
||||
Per-frame values such as movement, interpolation, camera smoothing, and physics must stay in:
|
||||
- `useRef`
|
||||
- `useFrame`
|
||||
- Rapier bodies
|
||||
- other frame-based systems
|
||||
|
||||
They must **never** go through React state.
|
||||
|
||||
---
|
||||
|
||||
### 3. State Management — Single Source of Truth
|
||||
### 3. Single Source of Truth for Durable Gameplay State
|
||||
|
||||
The project uses a single authoritative `GameManager` for durable gameplay state. React components subscribe to this state through thin custom hooks. **High-frequency values such as movement, camera interpolation, or physics never go through React state and stay in refs or frame-based systems.**
|
||||
The project uses a single authoritative `GameManager` for durable gameplay state.
|
||||
|
||||
React components subscribe to that state through thin hooks.
|
||||
Other managers communicate through `GameManager`, which acts as the main gameplay orchestrator.
|
||||
|
||||
High-frequency values such as movement, camera interpolation, or physics never go through React state and stay in refs or frame-based systems.
|
||||
|
||||
```ts
|
||||
// GameManager.ts — single source of truth
|
||||
// stateManager/GameManager.ts
|
||||
type Phase = 'loading' | 'intro' | 'exploring' | 'cinematic' | 'outro'
|
||||
type ZoneId = 'workshop' | 'powerGrid' | 'farm' | null
|
||||
|
||||
@@ -281,53 +323,87 @@ export function useGameState() {
|
||||
}
|
||||
```
|
||||
|
||||
All other managers (Cinemactic, Audio, Zone) remain as side effects that communicate through `GameManager`
|
||||
This keeps the architecture simple:
|
||||
- **GameManager** owns durable gameplay state
|
||||
- **other managers** handle side effects
|
||||
- **React components** render that state
|
||||
- **R3F frame systems** handle fast-changing values
|
||||
|
||||
---
|
||||
|
||||
### 4. Memory Management — `traverse()` + `dispose()`
|
||||
### 4. Side Effects Stay in Specialized Managers
|
||||
|
||||
**Every `destroy()` must call `Dispose.object()` before removing anything from the scene.** Skipping this leaks GPU memory (VRAM) silently — no error thrown, just a crash after a few zone transitions. **Rule: traverse first, remove second. Always.**
|
||||
Managers other than `GameManager` should not become secondary state stores.
|
||||
|
||||
Their role is to manage side effects and specialized runtime logic, such as:
|
||||
- GSAP timelines
|
||||
- audio playback
|
||||
- zone entry detection
|
||||
- interaction triggers
|
||||
- camera lock/unlock
|
||||
- temporary event coordination
|
||||
|
||||
They can read from `GameManager`, react to its state, or notify it of important transitions.
|
||||
|
||||
Example flow:
|
||||
|
||||
```text
|
||||
Component / Hook
|
||||
↓
|
||||
GameManager.getInstance()
|
||||
├── startMission('workshop')
|
||||
├── cinematic.play('intro_workshop')
|
||||
├── audio.playAmbience('workshop')
|
||||
└── zone.setActive('workshop')
|
||||
```
|
||||
|
||||
This keeps the dependency graph understandable while avoiding duplicated durable state.
|
||||
|
||||
---
|
||||
|
||||
### 5. Memory Management — Dispose Only What You Own
|
||||
|
||||
GPU memory must be cleaned carefully.
|
||||
|
||||
However, the project does **not** blindly deep-dispose every object on unmount.
|
||||
Only resources explicitly created and owned by the current component or manager should be disposed.
|
||||
|
||||
This includes things like:
|
||||
- custom materials
|
||||
- render targets
|
||||
- postprocessing passes
|
||||
- manually created geometries
|
||||
- manually created textures
|
||||
- temporary clones with owned resources
|
||||
|
||||
Shared or cached assets must **not** be blindly disposed.
|
||||
|
||||
```ts
|
||||
// utils/Dispose.ts
|
||||
import * as THREE from 'three'
|
||||
|
||||
export class Dispose {
|
||||
/**
|
||||
* Recursively disposes all geometries, materials, and textures
|
||||
* from an Object3D and its entire subtree.
|
||||
*
|
||||
* Always call this before scene.remove() to prevent VRAM leaks.
|
||||
*/
|
||||
static object(obj: THREE.Object3D): void {
|
||||
obj.traverse((child) => {
|
||||
if (!(child instanceof THREE.Mesh)) return
|
||||
|
||||
// 1. Dispose geometry buffers
|
||||
child.geometry.dispose()
|
||||
|
||||
// 2. Handle single and multi-material meshes
|
||||
const materials = Array.isArray(child.material)
|
||||
? child.material
|
||||
: [child.material]
|
||||
|
||||
for (const mat of materials) {
|
||||
// 3. Dispose every texture referenced by the material
|
||||
for (const value of Object.values(mat)) {
|
||||
static material(material: THREE.Material): void {
|
||||
for (const value of Object.values(material)) {
|
||||
if (value instanceof THREE.Texture) {
|
||||
value.dispose()
|
||||
}
|
||||
}
|
||||
// 4. Dispose the material itself
|
||||
mat.dispose()
|
||||
}
|
||||
})
|
||||
material.dispose()
|
||||
}
|
||||
|
||||
static mesh(mesh: THREE.Mesh): void {
|
||||
mesh.geometry?.dispose()
|
||||
|
||||
const materials = Array.isArray(mesh.material)
|
||||
? mesh.material
|
||||
: [mesh.material]
|
||||
|
||||
for (const material of materials) {
|
||||
if (material) this.material(material)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispose a WebGL render target and its textures.
|
||||
*/
|
||||
static renderTarget(rt: THREE.WebGLRenderTarget): void {
|
||||
rt.texture.dispose()
|
||||
rt.dispose()
|
||||
@@ -335,20 +411,35 @@ export class Dispose {
|
||||
}
|
||||
```
|
||||
|
||||
Usage pattern — identical in every `destroy()`:
|
||||
Example usage:
|
||||
|
||||
```ts
|
||||
destroy(): void {
|
||||
Dispose.object(this.mesh) // Frees VRAM: geometries, materials, textures
|
||||
this.scene.remove(this.mesh) // Then removes from scene graph
|
||||
}
|
||||
useEffect(() => {
|
||||
const material = new THREE.ShaderMaterial({
|
||||
vertexShader,
|
||||
fragmentShader,
|
||||
})
|
||||
|
||||
meshRef.current.material = material
|
||||
|
||||
return () => {
|
||||
Dispose.material(material)
|
||||
}
|
||||
}, [])
|
||||
```
|
||||
|
||||
**Rule:** disposal is ownership-based, not automatic and not blind.
|
||||
|
||||
---
|
||||
|
||||
### 5. Debug Utility
|
||||
### 6. Debug Utility
|
||||
|
||||
Activate the debug panel by appending `?debug` to the URL (`http://localhost:5173?debug`). Never scatter `if (isDev)` blocks across files — all debug logic flows through `Debug.ts`.
|
||||
The debug panel can be activated by appending `?debug` to the URL:
|
||||
|
||||
`http://localhost:5173?debug`
|
||||
|
||||
All debug logic is centralized in `Debug.ts`.
|
||||
Do not scatter debug checks across the codebase.
|
||||
|
||||
```ts
|
||||
// utils/Debug.ts
|
||||
@@ -379,15 +470,16 @@ export class Debug {
|
||||
}
|
||||
```
|
||||
|
||||
Usage:
|
||||
|
||||
```ts
|
||||
// Usage in any class
|
||||
const debug = Debug.getInstance()
|
||||
|
||||
if (debug.active) {
|
||||
debug.gui!.add(this.mesh.position, 'y', -5, 5).name('Height')
|
||||
debug.gui!.add(params, 'bloomIntensity', 0, 3).name('Bloom')
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## 🚀 Getting Started
|
||||
|
||||
```bash
|
||||
|
||||
Reference in New Issue
Block a user