Skip to main content

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

FilePurpose
chunk-controller.tsQueue processing, render batching, culling
forest-generator.tsDeterministic tree placement algorithm
quadtree.tsRectangle 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:

  1. When chunk N arrives, register its baseplate but don't generate forest yet
  2. When chunk N+1 arrives, generate forest for chunk N (now knowing N+1's exclusion zone)
  3. 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

  1. Ring Computation — Subtract baseplate rect from outer rect (300 stud padding) to get 4 ring rectangles
  2. Cell Parsing — Grid cells at 6-stud intervals within ring rects
  3. Exclusion — Reject cells inside any known baseplate (15-stud padding, rotation-aware via PointToObjectSpace)
  4. Shuffle — Deterministic Fisher-Yates shuffle
  5. Spacing — Spatial hash enforces 30-stud minimum between trees
  6. OutputForestResult[] 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.defer chain
  • 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.