← All multiplayer Lenses
Territory capture · Paper.io Playable build · internal

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.

11,891
Lines of TypeScript
38
Gameplay scripts
~800
Hex tiles on the board
~57,000
Vertices in the floor mesh
Gameplay

The game

  1. 01

    You start on a home base of 19 hex tiles. Inside your own land you are safe.

  2. 02

    Move out and a trail ribbon begins painting behind you. The joystick steers; you never stop.

  3. 03

    Close the loop back onto your own territory and every tile you enclosed floods and becomes yours.

  4. 04

    Cross your own trail and you die, though the two tiles right behind your head are exempt, so a hard 180 is survivable.

  5. 05

    Cut through someone else's trail and you eliminate them, releasing all their land back to neutral.

Controls

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.

Scoring

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 & respawn

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.

Rounds

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.

In the camera
Claimed ground with its jagged capture border, and a rival mid-run with the trail that can be cut

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

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

A trail ribbon drawn as one constant-width mesh, rebuilt locally from the owner's synced head position

Networking

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.

Client-authoritative
Every client owns itself
  • Its own head position and heading
  • Its own trail and captures
  • Its own territory, published as deltas
  • Its own death and land release
Host-authoritative
One host owns the world
  • All bot players: steering and combat
  • Bot territory, tagged per bot id
  • Food top-up
  • The game state machine and match restart
What travels over the wire
ChannelOwnerPayloadCadence
taddTerritoryManagerNewly-owned tile ids as a chunked delta150 tiles/frame
tclrTerritoryManagerRelease all my landOn death
elimTerritoryManagerEliminate a player whose trail was cutOn cut
btadd / btclrTerritoryManagerSame, host-authored, per bot idHost
botStateBotManagerAll bots' position, heading, score and boost0.15 s
scoreLeaderboardOwn territory countHeartbeat
grace / death / boostSnakeBodyManagerSpawn shield, corpse mark, boost glowOn change
currentStateGameStatesSynchronizerState machine index via StoragePropertyHost-gated
Swipe the table →
Chunked deltas with a keepalive that replaces

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
Remote trails cost nothing

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 board is data, not objects

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.

Under the hood

Engineering highlights

Three hand-written mesh renderers

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
A measured performance pass

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
Generated audio

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
Interface drawn in code

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.

Build log

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.

Territory sync, three attempts
Problem

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.

Fix

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.

A 54 ms stall nobody could see
Problem

The game hitched periodically with no obvious cause. The profiler attributed it to tile repainting, but only when repaints happened on consecutive frames.

Fix

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.

Long trails ate the frame budget
Problem

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.

Fix

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.

Ghost trails after respawning
Problem

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.

Fix

Record the path taken while graced separately and replay it at grace end, so the trail starts from where the player actually is.

Interaction components could not be trusted
Problem

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.

Fix

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.

By the numbers
38
Gameplay scripts
11,891
Lines in Assets/Scripts
23,484
Total TypeScript incl. packages
5.22.0
Lens Studio
23
SessionController call sites
14
Generated audio files
Core scripts
SnakeBodyManager.ts 2,288 lines Player and bot registry, heads, badges, collisions, death sequence, boost, grace
TerritoryManager.ts 1,374 lines The capture core: trails, flood fills, cuts, elimination and delta tile sync
BotManager.ts 906 lines Host-simulated AI players in one compact broadcast
PlayerSpawner.ts 733 lines Networked instantiation, joystick steering, respawn and edge bounce
DeathScreen.ts 708 lines Runtime SDF-baked game-over card with rank, stats and countdown
Leaderboard.ts 655 lines Roster and score heartbeat, ranked on territory percentage
HexFloorRenderer.ts 352 lines The whole board as one 57k-vertex vertex-coloured mesh
AudioManager.ts 296 lines Pooled SFX and music hub with a null-guarded static API
TrailRibbon.ts 260 lines Incremental constant-width ribbon mesh
HexBorderRenderer.ts 203 lines Extruded 3D hex map wall
HexGrid.ts 179 lines Axial hex data model and neighbour table
Packages
ConnectedFrameworkBitmoji Player PackageSimpleVertexBaseColorBox BlurBitmoji 3DCharacter ControllerCamera ControllerJoystickEventFeedPresenceIndicatorGameManagerSyncTweenTweenUI Button
Project details
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
Up next
Next generation
Mob Siege
Strategy · .io battler
Open →