← Back to all projects

Wonderland

Luau · Game · Private

YouTubeTikTokCommunity

Wonderland is the game I co-founded, project-led and lead-engineered at ScaryPlay, the Roblox studio I own together with the founder of The Mimic. It launched in August 2026, peaked at over 72,000 concurrent users and has passed 37.9 million visits, and I have supported it through live operations since.

I led a multidisciplinary team of eight across engineering, modeling, animation, art and music, and interviewed and onboarded the engineers who joined it.

Roblox invited me to pitch Wonderland to investors at a private, invite-only event at RDC 2026.

The game features randomly generated worlds, AI-driven monsters, item interactions and more. The final trailer passed a million views on YouTube in its first week and is now at 1.6 million, the first trailer has passed 1.3 million, related TikToks have reached tens of millions of views in total, and the ScaryPlay community has grown past 591,000 members.

I engineered all of its core technology. A few of those systems are described below.

Systems I engineered

How these are organized, with short excerpts. Full source isn't public.

Marionette: ECS NPC System

Marionette is the entity-component-style framework that drives every monster in the game; the server pulls every NPC's strings. Instead of bespoke code per enemy, an NPC is just data: a settings file, a state index and render settings, interpreted by shared state machines like Roam, Chase, Attack and Jumpscare. Adding an enemy means adding data, not new systems.

The hard part is the networking, and it's the part I'm most proud of. The system is fully server-authoritative: the server runs all AI, pathfinding and movement and is the single source of truth. Clients never simulate monsters; they replay what the server tells them, which keeps behaviour consistent across everyone and makes the whole thing cheat-resistant.

Rather than stream raw positions every frame, the server sends each NPC a path of waypoints with attached actions (Jump, Climb, …). Clients walk the NPC along that path locally and smoothly, so between waypoints there is almost no traffic. Running alongside it are an unreliable high-frequency position stream and a periodic 'path correction' broadcast that reconciles any client-side drift back to the server's ground truth.

Every byte is budgeted. Each NPC is identified by a single-byte network id; positions are quantized to 0.2-stud precision and packed as a Vector3int16, so a full position update is about 7 bytes per NPC, flushed at 20 Hz. Reliable remotes carry spawns, state changes and waypoint paths; unreliable remotes carry the firehose of position updates. Paths older than 30s fall back to position-only using a rolling, wrap-around timestamp.

  • Server-authoritative: server owns AI, pathfinding and movement; clients replay only
  • Waypoint paths (+ actions like Jump/Climb) instead of per-frame positions
  • 20 Hz position stream + 1 Hz path-correction reconciles client drift
  • Tiny packets: 1-byte ids, 0.2-stud Vector3int16 positions (~7 bytes/NPC)
  • Reliable remotes for spawns/state, unreliable for high-frequency streams
  • Data-driven: shared state machines reused across every NPC type

Replication is engineered around a strict byte budget. A 1-byte id names each NPC and its position is quantized to 0.2 studs as a Vector3int16, so a full update is ~7 bytes streamed at 20 Hz. Waypoint paths cut that further: clients replay locally and only a 1 Hz path-correction reconciles drift against the server's ground truth.

shared/NPCSystem/NetworkSettings.luau
MAX_NETWORK_ID = 255,             -- one byte names an NPC
STREAMING_FLUSH_RATE = 0.05,      -- position stream @ 20 Hz
PATH_CORRECTION_INTERVAL = 1,     -- ground-truth reconcile (s)
POSITION_QUANTIZATION_SCALAR = 5, -- 0.2-stud precision
STALE_PATH_SECONDS = 30,          -- old paths -> position-only

-- 1-byte id + Vector3int16 * 3
BYTES_PER_NPC = NETWORK_ID_BYTES + 6,

On top of that transport, a whole monster is just frozen configuration: movement speeds, AI 'interest' tuning and roam behaviour. The same engine reads these fields for every NPC type, which is what makes the system data-driven.

shared/NPCSystem/NPCSettings/Alice.luau
return DeepFreeze({
    speed = 14,
    chaseSpeed = 30,
    customData = { health = 100 },

    interestDuration = 15 :: number?,
    interestMinDistance = 10 :: number,
    redetectCooldown = 4 :: number,

    roamRoomBudget = 6,
    roamIdleChance = 0.2,
})

Cell-Based Map Generator

A procedural generator that builds large, varied levels from pure data. Rooms and hallways are authored as templates, and a pipeline selects, rotates, places and connects them into one coherent map, then manages the doors between them.

Everything lives on a uniform grid where each cell is a fixed 10 studs, but rooms are not a fixed size. Every template declares its own bounding box in cells, so a room can be whatever dimensions and whatever shape it needs, and only the cells its sections actually cover are marked occupied. Two bounding boxes are free to overlap as long as their cells do not, which is what lets an irregular room nest into the gap another one left. Designing a new map means authoring more templates; there is no hand-built geometry in the algorithm itself.

Rooms are attached door to door rather than placed and then joined up. To add a room the generator shuffles the four rotations and tries each against the door it is connecting to: the candidate door's normal has to be the opposite of the target's, and every cell the rotated template covers has to be free. The first rotation that satisfies both decides where the room lands, positioned so the two doors sit directly against each other. Most of the map is connected this way and needs no hallway at all.

Both figures below are a recreation of the generator running in your browser, not the real implementation. The real one is better in every respect: many more room templates, more rules around puzzles, exits and pacing, and tuning this version does not attempt. Treat them as a simple demonstration of the idea, mistakes included.

A finished map. Adding a room means adding data, not code.

Dashed outline: declared bounding boxLarge cells: walkable sectionsSmall cells: generated hallwayWhite square and tick: a door and the way it facesBoxes may overlap: only the cells are occupiedSimplified recreation, not the real generator

0 / 110 rooms · 0 / 192 hallway cells · 1257 cells

Level 50 asks for 89 to 123 rooms, and by then the template draw is weighted heavily toward the three and four door rooms, which is what lets a map branch this far. In game the whole layout, hallways included, lands in roughly two tenths of a second, give or take with the seed. This is a simplified recreation of that, with stand-in room templates, so it is rougher than the real thing.

  • Uniform grid: each cell is a fixed 10 studs
  • Rooms are any shape and size: each template declares its own bounding box
  • Only the cells a room covers are occupied, so bounding boxes may overlap
  • Rooms attach door to door: shuffle the four rotations, take the first that faces right and fits
  • Leftover door pairs are scored by spatial and tree distance, then joined by A* hallways
  • Length caps and a Manhattan heuristic; native-compiled for speed
  • New maps need only new data, not new code

A room is pure data: its own bounding-box size in cells (here 12×12, but every room differs; each cell is 10 studs), its walkable sections, which sides have doors and which cells are blocked. The generator only ever places templates like this, so the map can grow endlessly just by adding files.

server/MapGenerator/Components/Rooms/Door1/Door1_A.luau
local Room = {
    BoundingBox = Vector2.new(12, 12), -- in cells; rooms vary
    Sections = {
        { LocalOffset = Vector2.new(1, 1), Size = Vector2.new(10, 1) },
        { LocalOffset = Vector2.new(10, 2), Size = Vector2.new(1, 7) },
        -- ...
    },
    Doors = {
        { Side = "South", CellOffset = Vector2.new(6, 0) },
    },
}

Conductor

Conductor is the module lifecycle orchestrator the whole game boots through, shared by client and server. It has since grown past this game into the private template my projects start from, rebuilt around Blink networking, pesde and React Luau.

← Back to all projects