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
| Technology | Purpose |
|---|---|
| roblox-ts | TypeScript to Luau compilation |
| Flamework | Services, Controllers, Components, DI |
| jecs | Entity Component System for enemies |
| Charm | Reactive state management (atoms) |
| Charm-Sync | Client-server atom synchronization |
| Remo | Type-safe networking (remotes) |
| Iris | Immediate-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 componentsclient/— Controllers and client componentsshared/— Remotes, types, and shared utilities
Core Game Loop
- Player spawns in lobby, selects Quirkymal, queues for a run
- Teleported to game server — spawns at trail start
- 30 trail markers through procedurally generated forest chunks
- Detect → React → Survive against escalating enemies
- Safe zones at markers 10 and 30 (final)
- Death = teleport back to lobby
- Reaching marker 30 = run complete, teleport to lobby
Key Architectural Patterns
- Components register with Services — Components call
service.register()inonStart(), 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 work —
task.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