Forest Generation
Forest generation runs entirely on the client using deterministic seeded random. All clients produce identical forests from the same game seed.
Why Client-Side?
The original server-driven system sent forest packets over the network, causing race conditions where trees appeared on chunk paths. The client-side system eliminates this by generating forests only after all relevant baseplates are known.
Key Files
| File | Purpose |
|---|---|
chunk-controller.ts | Queue processing, render batching, culling |
forest-generator.ts | Deterministic tree placement algorithm |
quadtree.ts | Rectangle subtraction for ring computation |
Generation Pipeline
Chunk tagged "Gameplay_Chunk"
→ Added to pendingChunkQueue
→ _scheduleQueueProcess()
→ _processChunkQueue()
Phase 1: Register baseplates (cull existing trees on registration)
Phase 2: Generate forest for previously-waiting chunks
Phase 3: Queue new chunks to wait for next neighbor
→ _enqueueResults() → _renderForestBatch() (25 trees/frame)
Await-Next-Chunk Strategy
To prevent trees from overlapping with future baseplates:
- When chunk N arrives, register its baseplate but don't generate forest yet
- When chunk N+1 arrives, generate forest for chunk N (now knowing N+1's exclusion zone)
- Exception: if multiple chunks arrive in one batch, earlier ones generate immediately
Deterministic Seeding
function deriveSeed(gameSeed: number, cframe: CFrame): number {
const pos = cframe.Position;
return gameSeed + floor(pos.X) * 73856093 + floor(pos.Z) * 19349663;
}
Same baseplate position + same game seed = identical forest every time.
Tree Placement Algorithm
- Ring Computation — Subtract baseplate rect from outer rect (300 stud padding) to get 4 ring rectangles
- Cell Parsing — Grid cells at 6-stud intervals within ring rects
- Exclusion — Reject cells inside any known baseplate (15-stud padding, rotation-aware via
PointToObjectSpace) - Shuffle — Deterministic Fisher-Yates shuffle
- Spacing — Spatial hash enforces 30-stud minimum between trees
- Output —
ForestResult[]with position, asset index, scale (5-10x), yaw
Retroactive Culling
When a new baseplate registers, _cullRenderedTreesOnBaseplate() directly scans Workspace.Forest children and destroys any overlapping trees. This catches trees from earlier chunks whose rings extended into the new baseplate's area.
Render Batching
- 25 MeshPart clones per frame via
task.deferchain - Render-time double-check against all known baseplates (15-stud padding)
- LRU eviction at 500 tree cap (oldest destroyed first)
- Trees registered in a bucket grid (12-stud cells) for spatial queries
Void Fill
When a chunk unloads, generateForestForVoid() fills the gap with trees, using the same deterministic algorithm but centered on the void area.