Skip to main content

Woodlands

Woodlands is a multiplayer survival horror game built on Roblox using roblox-ts and the Flamework framework. Players select a Quirkymal character, enter a procedurally generated forest trail, and survive 30 trail markers of escalating danger.

Tech Stack

TechnologyPurpose
roblox-tsTypeScript to Luau compilation
FlameworkServices, Controllers, Components, DI
jecsEntity Component System for enemies
CharmReactive state management (atoms)
Charm-SyncClient-server atom synchronization
RemoType-safe networking (remotes)
IrisImmediate-mode debug UI

Project Structure

src/
├── apps/ # Feature modules
│ ├── chunks/ # Chunk generation, forest, pacing, danger zones
│ ├── enemies/ # Enemy spawning, ECS behaviors
│ ├── global/ # Death, respawn, player data, audio
│ ├── lobby/ # Queue system, matchmaking
│ ├── quirkymals/ # Player character system
│ └── devtools/ # Debug utilities
├── client/ # Client runtime, UI components
├── server/ # Server runtime, tests
├── shared/ # Cross-boundary modules
│ ├── data/ # Data store, atoms, sync
│ ├── ecs/ # jecs world, components, systems
│ ├── lod/ # Level-of-detail system
│ └── utils/ # Seeded random, helpers
└── types/ # Roblox type declarations

Each app follows a consistent structure:

  • server/ — Services and server components
  • client/ — Controllers and client components
  • shared/ — Remotes, types, and shared utilities

Core Game Loop

  1. Player spawns in lobby, selects Quirkymal, queues for a run
  2. Teleported to game server — spawns at trail start
  3. 30 trail markers through procedurally generated forest chunks
  4. Detect → React → Survive against escalating enemies
  5. Safe zones at markers 10 and 30 (final)
  6. Death = teleport back to lobby
  7. Reaching marker 30 = run complete, teleport to lobby

Key Architectural Patterns

  • Components register with Services — Components call service.register() in onStart(), services never scan
  • No per-frame CollectionService lookups — Cache tagged parts in Sets via component registration
  • One operation per heartbeat tick — Queue-based processing, never create AND unload in same frame
  • Defer expensive worktask.defer() chains for cell parsing, forest gen, danger zone cleanup
  • Client batch rendering — 25 MeshPart clones per frame max
  • Heartbeat throttling — ChunkService 0.25s accumulator, visibility 0.1s interval