Hex
Hex is a networked territory-capture game in the Paper.io and Hexanaut tradition, played on a pointy-top hex grid of roughly 800 tiles. Leave your land and you start trailing a ribbon behind you; close the loop back into your own territory and everything you enclosed floods and becomes yours. Run into your own trail and you die. Run into someone else's and they do.
It began as a direct fork of Snake, and the two projects still share a scene id, but it is where the engineering got serious. Territory is a far harder thing to synchronise than a score, and solving it produced the chunked-delta-plus-keepalive model, three hand-written mesh renderers, a fully generated audio pipeline and a measured performance pass with a purpose-built profiler.
The game
- 01
You start on a home base of 19 hex tiles. Inside your own land you are safe.
- 02
Move out and a trail ribbon begins painting behind you. The joystick steers; you never stop.
- 03
Close the loop back onto your own territory and every tile you enclosed floods and becomes yours.
- 04
Cross your own trail and you die, though the two tiles right behind your head are exempt, so a hard 180 is survivable.
- 05
Cut through someone else's trail and you eliminate them, releasing all their land back to neutral.
Identical to Snake: constant forward motion, joystick steering, dash button. Keeping the control scheme meant the harder territory mechanic was the only thing players had to learn.
Percentage of the entire board you hold. It replaces Snake's raw point count, so the leaderboard reads as a live map of who is winning rather than who has been playing longest.
Death releases every tile you own back to neutral. That is the single largest state change in the game, and the reason the sync model had to be rebuilt. Respawn runs a tile-aware safe-spawn picker: 30 candidate positions, rejecting any disk clipped by the map edge, any spot within four tiles of a live trail, and anywhere within 1600 cm of where you just died.
Endless by design. The code is explicit about why: it is a hop-on/hop-off world so friends drop in and out freely and nobody's session is interrupted by someone else's match ending. A timed mode exists but ships switched off.
Claimed ground with its jagged capture border, and a rival mid-run with the trail that can be cut
The death card, scoring the run on percentage of the map covered rather than points
A trail ribbon drawn as one constant-width mesh, rebuilt locally from the owner's synced head position
Multiplayer architecture
The same Connected Framework foundation as Snake, extended with five new channels for territory. Ownership of a tile follows the player who captured it; the host owns bots, bot territory, food top-up and the state machine. Territory is the interesting problem: a capture can flip hundreds of tiles at once, and a death releases all of them.
- →Its own head position and heading
- →Its own trail and captures
- →Its own territory, published as deltas
- →Its own death and land release
- →All bot players: steering and combat
- →Bot territory, tagged per bot id
- →Food top-up
- →The game state machine and match restart
| Channel | Owner | Payload | Cadence |
|---|---|---|---|
| tadd | TerritoryManager | Newly-owned tile ids as a chunked delta | 150 tiles/frame |
| tclr | TerritoryManager | Release all my land | On death |
| elim | TerritoryManager | Eliminate a player whose trail was cut | On cut |
| btadd / btclr | TerritoryManager | Same, host-authored, per bot id | Host |
| botState | BotManager | All bots' position, heading, score and boost | 0.15 s |
| score | Leaderboard | Own territory count | Heartbeat |
| grace / death / boost | SnakeBodyManager | Spawn shield, corpse mark, boost glow | On change |
| currentState | GameStatesSynchronizer | State machine index via StorageProperty | Host-gated |
Sending your whole territory every time it changed produced one enormous payload per capture and dropped the session. Sending only deltas meant a single lost packet left a client permanently wrong. Hex does both: captures enqueue only the newly-owned tiles, streamed 150 per frame, while a slow four-second keepalive re-publishes the entire territory with replace semantics. Deltas keep it responsive; the keepalive makes every error temporary.
Captures enqueue only the NEWLY-owned tiles… a slow keepalive re-enqueues my whole territory so late joiners and any dropped delta self-heal.Project source
A trail is a long, constantly growing polyline, exactly the sort of thing that looks unavoidable to network. It is not. Every client reconstructs every remote trail locally from that player's already-synced head position, so the ribbons on screen add nothing to the wire at all.
The hex grid is a pure-data axial lattice built from parallel typed arrays (coordinates, centres, state, owner and trail each in their own buffer) with a precomputed flat neighbour table. The capture flood-fill walks it through O(1) array reads instead of map lookups, which is what makes flipping hundreds of tiles in a frame affordable.
Engineering highlights
All of the board geometry is generated in code with MeshBuilder, vertex-coloured, with no textures anywhere.
- ·The entire floor is one mesh of roughly 57,000 vertices, 19 per tile, arranged as a centre, an inner fill ring and two outline rings so fill and outline can be coloured independently and stay crisp at any zoom
- ·Owned tiles lift 7 cm so your trail visibly tucks under your own land
- ·The map wall is an extruded 3D hex border, built as prisms with a bright top face and sides shaded to 60% to fake lighting under an unlit material, capped at the 65,535-vertex index limit
- ·Trail ribbons pre-allocate their full buffer once, 1200 points × 3 vertices, and append with zero garbage collection
This is the generation where profiling became part of the project. PerfProbe is a purpose-built frame-time attribution profiler that tags each system and prints a sorted cost breakdown every five seconds, at literally zero cost when disabled. Two findings changed the architecture.
- ·Flushing vertex writes on consecutive frames stalled about 54 ms, because the first write of a frame blocks on the previous frame's still-in-flight mesh upload. Tile repaints were rebuilt to flush in bursts with idle frames between them
- ·Trail rendering rebuilt the whole ribbon every frame, which became the dominant frame cost once trails got long. Replaced with an append-only write plus a six-point tail rewrite
Fourteen sound effects and a music bed, all synthesised algorithmically by Node scripts written for the project: marimba and bell voices on a C-major pentatonic. No samples, no licences, nothing to clear.
- ·AudioComponent has no pitch control and plays one sound at a time, so playback runs through a round-robin component pool
- ·Escalation is baked rather than modulated: small, medium and large capture chimes as separate files
- ·The whole API is static and null-guarded, so it silently no-ops if the manager is not wired into a scene
The death screen bakes every shape it needs as a procedural texture painted with 2×2 supersampled signed-distance functions: a rounded card, a gold rank pill, and a glowing gradient respawn button with a countdown ring spinning around it. The loading screen animates a honeycomb claim wave. None of it exists as an asset.
What broke, and why
Every entry below is a real failure that shaped the code. They are documented in the project source itself, next to the fix.
Publishing the full tile set on every capture produced huge payloads and dropped the session. Publishing pure deltas was small and fast, but one lost packet left a client permanently showing the wrong map, with no way to notice or recover.
Run both. Deltas stream at 150 tiles per frame for responsiveness, and a four-second keepalive republishes the whole territory with replace semantics. Errors become self-correcting instead of permanent, and late joiners get a correct board without any special-case code.
The game hitched periodically with no obvious cause. The profiler attributed it to tile repainting, but only when repaints happened on consecutive frames.
The first vertex write of a frame blocks on the previous frame's mesh upload still being in flight. Repaints were batched into bursts with deliberate rest frames between them, which removed the stall entirely at about 0.08 ms per tile.
Trail ribbons were rebuilt from scratch every frame. Early in a life that was free; by the time a player had a long trail out it was the single most expensive thing in the game.
Pre-allocate the vertex buffer once and write it append-only, rewriting just the last six points each frame to keep the tail smooth. Cost went from proportional to trail length to effectively constant.
Movement during the spawn grace period was still being recorded, so the moment grace ended a phantom line was painted from where the player died to where they respawned.
Record the path taken while graced separately and replay it at grace end, so the trail starts from where the player actually is.
Lens Studio's interaction components need the exact UI camera, and depending on depth filtering they would swallow or miss taps outright. Writing visuals from inside a tap callback could blank the image entirely.
Detect taps with a global touch event plus a screen-transform region test, and reconcile button state every frame instead of writing it once, so a wrong state repairs itself on the next frame rather than sticking.
- Platform
- Snapchat in-camera Lens
- Clients
- Mobile · Camera Kit
- Contexts
- Live camera · Reply camera · Video chat
- Lens Studio
- 5.22.0 (migrated from 5.21)
- Tracking
- None (synthetic 3D world, front and back camera)
- Session
- Remote / matchmade
- Players
- 2–4 matchmade
- Bots
- 3 host-simulated
- Offline
- Mocked singleplayer session