JellyFish is a 3v3 first-person multiplayer control point game. One team tries to raise the facility temperature to trigger a meltdown, the other fights to keep it below the threshold until time runs out. I served as Game Director, Level Designer, Game Designer, and Programmer, and designed Sector B (Blueberry).

Role: Game Director, Level Design, Multiplayer Programmer

Team: 6 developers

Project length: 7 weeks

Tools: Unity (FishNet + Steamworks), later recreated in UEFN (Verse)

: Toronto Game Expo, Futures Forward Program, CNE

Level Design

Fight for control of the Reactor Core room

What I wanted to achieve

I wanted a competitive map that creates constant mid-conflict, while also rewarding smart flanking and objective play.

Design goals

  • A readable 3-lane layout with flanks and a mid that naturally attracts fights

  • Objectives that are not just “stand here” points, but change how the map plays

  • Strong orientation through a central landmark and themed spaces, so players build a mental map fast

How I wanted to achieve it

Layout philosophy

  • Mirrored, flipped map for fairness, with bases on opposite sides and lanes that fan outward into intersections

  • A strong mid landmark so players always know where they are relative to the map state

  • Room theming, even for connectors, so callouts become natural: cafeteria, mech room, electrical, jelly testing

Objectives that reshape traversal

The three machines are the control points, and each one changes traversal based on whether it is cooling or heating:

  • Coolant Machine: cooling floods a flank with lethal water, heating drains it and opens the path

  • Fans Machine: cooling pulls players through vents one direction, heating reverses the vent routes

  • Reactor Machine (mid): cooling powers the train crossing mid, heating stops it. The reactor anchors navigation and pacing

This let the match feel alive. Teams are not only fighting over points, they are fighting over how the map functions.

Greybox to Kitbash Comparisons

Bubble Diagram of Sector B

Process and iteration

1) Paper to LDD to greybox

I started with lane diagrams and room flow in my LDD, then greyboxed quickly to validate scale, routes, and sightlines before art.

Visual slots

  • Lane diagram + route options

  • Early greybox top-down

  • “Machine state” diagrams showing how paths open/close

2) Playtest loops

I playtested and iterated continuously, focusing on:

  • route usage (what players ignored)

  • clarity (where players got lost)

  • time-to-mid and time-to-objective

  • whether machine effects created meaningful decisions

3) Key changes from feedback

Redundant fan path to mid

  • Players rarely used it, and the drop made it unclear where the reactor button was

  • I removed the path and simplified the reactor room by removing upper platforms

  • Result: cleaner reads, better mid fights, less “Where am I?” moments

Fans room was visually and mechanically flat

  • I added stairs and an upper platform with the button

  • Risk-reward: players could use the fans to reach the platform faster, but could also get punished if the machine flipped to “suck”

  • Result: the room became a real encounter space, not just a hallway with a button

4) Kitbash pass and readability test

After greybox validation, I kitbashed with asset packs and playtested again to confirm that the art pass did not distort gameplay readability or player navigation.

What I learned

  • The best multiplayer layouts are simple to read, but deep to play

  • Objectives are more interesting when they change movement and routing, not just scoring

  • Playtests tell you what players actually do, not what you hoped they would do

  • Clarity fixes often come from removing or simplifying, not adding more

Code samples: (Link to GitHub repo or code snippets)

  • GameManager initialization flow

  • TeamManager authority + syncing

  • Train rider offset logic

  • First-person render separation

  • Remote animation prediction / smoothing

  • Optimization notes and profiler screenshots

Problems I solved

1) Reliable match start (solving multiplayer initialization order)

Problem: In networked games, UI/state can initialize before all players are actually ready, causing missing UI or desynced starts.
Fix: I added a server-side acknowledgement step that waits until every client confirms load, blocks duplicates, then triggers a single observers call to initialize UI and start the match timer.
Result: Match start became deterministic and repeatable.

Show this code: ServerAcknowledgeSceneLoad() and Clients_InitializeUI() plus your short flow diagram (Client spawn → Server ack → All loaded → Observers init).

2) Fair spawns without spawning into danger

Problem: Players spawning on top of enemies or teammates creates instant frustration.
Fix: I built a “safe spawn” selector that filters spawn points by distance to enemies and teammates, and falls back gracefully if no safe spawn exists.
Result: Respawns felt fair and reduced immediate chain-deaths.

Show this code: FindSpawnPoint() and the avoid-radius checks.

3) Networked coolant hazard that matches game design intent

Problem: The coolant lane needed to feel like a real map state change for both teams, not a local-only visual.
Fix: I synced coolant fill/drain using a ServerRPC → ObserversRPC pattern, then smoothed the actual surface movement client-side and paired it with FMOD spatial audio so the state change reads instantly.
Result: The objective had clear, shared consequences, and players understood when the lane was safe or lethal.

Show this code: FillPoolServer/Observer and EmptyPoolServer/Observer, plus a GIF of the lane changing state.

4) Train traversal that stays stable in multiplayer

Problem: Moving platforms are tricky in multiplayer. If timing or state changes are sloppy, players feel jitter or the train feels unpredictable.
Fix: I drove movement on FishNet ticks and built an explicit state machine (Idle/Accelerating/Moving/Decelerating/Stopped) with smooth damp acceleration and a timed stop routine.
Result: The train became readable, consistent, and “gameplay reliable,” which matters in a control-point shooter.

Bonus micro-system: Trigger volumes can stop the train for a controlled duration.
Show this code: HandleTrainMovement() and StopAndWait() plus the trigger script.

5) First-person camera correctness (local vs remote presentation)

Problem: In multiplayer, you must render the local player differently than remote players. If you do not separate layers and components, you get broken visuals and duplicated audio.
Fix: For non-owners, I disable the camera and minimap objects and force the mesh into a specific third-person layer. For the owner, I enable camera/audio components and store the local camera reference.
Result: Clean first-person experience for the player, correct third-person readability for everyone else.

Show this code: OnStartClient() and the recursive layer setter.

UEFN recreation

Later, we recreated Sector B in UEFN to test mechanics with a larger, younger audience and get real feedback faster. UEFN let us prototype machine interactions quickly, rely on built-in networking, and kitbash with high-quality assets without rebuilding the entire multiplayer stack.

Technical Design and Programming

What I owned

  • Match flow and client initialization (UI + game start timing)

  • Team tracking (server-side)

  • Spawn selection + respawn safety (avoid enemies/teammates, reset state)

  • Networked objective state: coolant fill/drain with audio feedback

  • Train system: tick-driven spline motion + explicit state machine + stop triggers

  • First-person camera setup in multiplayer

Hello, World!

Show: a simple sequence diagram + snippet(s) from your GameManager that demonstrates the final flow.

Performance Improvements

Problem: Low frame rates late in development.
Investigation: Profiling showed too many expensive skeletal meshes and high triangle counts.
Fixes:

  • Replaced high-cost skeletal meshes with optimized static meshes where possible

  • Reduced tri counts on heavy assets

  • Set up occlusion culling

  • Baked lighting for stability

  • Result: 13 FPS to Solid 60 FPS (capped)

Before After
Before After
Before After