Mob Siege
Mob Siege is a top-down army battler. You steer your own 3D Bitmoji with a joystick while a ring of marching soldiers orbits you. That ring is your score, your health and your weapon all at once. Walk over neutral soldiers to recruit them. Walk your ring into someone else's and both armies start dying at five units a second until one of you breaks.
Sixteen towers sit across the island. Standing in a tower's radius trades your soldiers against its garrison one for one until it flips to you, after which it regrows and starts feeding you reinforcements. It is the most systemically complex of the three games, and it was only buildable because the networking, the procedural rendering pipeline and the performance discipline were already solved.
The game
- 01
Steer your Bitmoji with the joystick. A ring of soldiers orbits you, and its size is your score.
- 02
Walk over neutral soldiers scattered across the island to recruit them into your ring.
- 03
Push your ring into another player's to engage. Both armies burn five units per second until someone breaks off or dies.
- 04
Stand inside a tower's radius to trade soldiers against its garrison one for one. At zero it flips to you.
- 05
Held towers regrow to a garrison of 30, then send you a free soldier every three seconds.
Joystick movement at 250 cm/s. There is no attack button; engaging is purely positional, which means the whole game is played through where you put your body.
Live army size, broadcast on a half-second heartbeat and ranked on a leaderboard with Bitmoji portraits. Milestone banners fire at 15, 30, 50, 80, 120, 180 and 250 soldiers with escalating emoji tiers.
Your army reaching zero while engaged kills you. Your Bitmoji plays a fall animation, a one-second grayscale death cam runs, your army scatters as up to twenty pickups for everyone else, and a stats card shows your peak army, recruits, towers captured and rank. Respawn is automatic after ten seconds with a 3.5-second grace period, broadcast network-wide, so every client sees you pulse while you are untouchable.
Endless, with the same hop-on/hop-off rationale as Hex. The win-condition machinery is fully built and deliberately dormant.
A live session: bots and players ranked together, with a kill posted to the shared event feed
The death card, drawn at runtime from supersampled SDF shapes rather than imported as art
Tiles, trees and marching soldiers, every one of them generated by MeshBuilder at runtime
Multiplayer architecture
The code calls it the proven Hex split. Each client is authoritative over itself (its own pickups, its own losses, its own death) and the host is authoritative over the world. Both sides of a fight burn units on their own client and reconcile on the score heartbeat, so counts can never diverge. Every host duty is written re-entrant per tick, which makes host migration a non-event: the new host simply resumes from the last state it received.
- →Its own recruit pickups
- →Its own unit losses in combat
- →Its own death and grace period
- →Its own hero position
- →Bot AI simulation and bot deaths
- →Tower ownership, garrison and gifting
- →Neutral soldier top-up on the field
- →The game state machine
| Channel | Owner | Payload | Cadence |
|---|---|---|---|
| score | Leaderboard | Own army count | 0.5 s, every client |
| twr | TowerManager | Full state of all 16 towers: owner and garrison | 0.5 s, host |
| gift | TowerManager | Aggregated soldier grants, serial-deduped | 1 s, host |
| botState | BotManager | Bot position, heading and score | 0.15 s, host |
| ko | CombatSystem | Kill notification to the killer | On kill |
| grace / death | SnakeBodyManager | Spawn invulnerability, harmless-corpse mark | On change |
| deathDrop | PointsSpawner | Every drop position from one death in one packet | On death |
| currentState | GameStatesSynchronizer | State machine index via StorageProperty | Host-gated |
Sixteen towers fit in 200 to 350 bytes. So rather than tracking what changed, the host rebroadcasts every tower's owner and garrison twice a second whether anything moved or not. Late joiners and lost packets converge within one beat, and there is no delta bookkeeping that can corrupt. It is the opposite call from Hex's territory sync, made deliberately because the state set is three orders of magnitude smaller.
Self-healing by construction: late joiners and lost packets converge within one beat, and there is no delta bookkeeping to corrupt.Project source
An army can be eighty rendered units, each with a position, a facing and a walk frame. None of it is networked. Every client rebuilds every army from the hero transform and the synced count using deterministic ring maths seeded from the player's colour rather than a random number, so all clients independently compute an identical formation. Colours themselves are assigned by sorting connection ids, meaning everyone agrees on who is which colour without a single negotiation packet.
Both sides of an engagement burn their own units on their own client, then reconcile on the half-second score heartbeat. Engagement uses hysteresis, 60 cm to engage and 160 cm to disengage, so a fight on the boundary does not flicker on and off, and no client has to ask another whether it is currently in combat.
Engineering highlights
There is no imported 3D art in the gameplay at all. 35 MeshBuilder call sites produce every soldier, tower, tree and tile in the game.
- ·The soldier is built at exactly 1.0 units tall so world scale equals height in centimetres, with a white-toned vertex-colour bake that a runtime tint turns into any army's colour, so one mesh serves every player
- ·Its four-frame marching cycle is produced by shearing the legs into three shared meshes that units swap between: an assignment, not an upload, so the whole walk animation costs three mesh builds total
- ·The island is roughly 1,880 tiles as chunked vertex-coloured meshes, 192 per chunk, so a repaint only re-uploads its own chunk
- ·Towers come in four archetypes chosen deterministically from their grid cell, with garrison bars, a 48-segment trade-radius ring and clumped pine forests
The bots run a five-mode goal stack (cruise, recruit, attack, flee and capture) with real thresholds rather than randomness: flee armies 1.25× your size within nine metres, attack armies below 0.77× within eleven, capture towers you can afford within twenty-one, otherwise recruit, weighted by how clustered the nearby pickups are.
- ·Steering decisions are staggered at 0.1 s so bots never all think in the same frame
- ·Non-host clients dead-reckon between the 0.15 s state packets rather than waiting for them
- ·Population is adaptive: bots fill the session up to five participants and get parked as real players join, shedding simulation cost at exactly the moment a full lobby needs the headroom
- ·Parked bots are detected by packet staleness rather than an explicit despawn message, so a dropped packet cannot leave a ghost behind
The death screen, leaderboard rows, loading screen and mute button are all drawn at runtime into procedural textures using 2×2 supersampled signed-distance painters: rounded cards, rings, gradients, trophies and a point-in-polygon crown laid out on a 24×18 design grid. Every shape is baked once and shared across instances.
Custom shader graphs cover the tint pipeline, cartoon water with animated bands and shore foam, a shadow catcher, and a colour grade, alongside a GPU crop that masks Bitmoji portraits into circles.
- ·The vertex-coloured floor is unlit and therefore cannot receive shadows, so an invisible shadow-receiver quad sits 0.25 cm above the tiles
- ·The sun is deliberately placed low, so shadow length lands at about 0.87× height, because a steeper sun hides a tree's shadow completely under its own canopy
- ·Shadow configuration is authored in the scene rather than set at runtime, where it applies inconsistently
Every sound is algorithmically generated: a music bed plus capture, claim, countdown, death, kill, respawn and UI variants. The manager exists because of two measured engine constraints.
- ·AudioComponent has no pitch control and plays one sound at a time, so playback runs a round-robin voice pool with two voices for rapid-fire sounds and pre-baked variant files for escalation
- ·Calling play() costs a probe-measured 15–19 ms, so one-shots queue and at most one plays per frame, with real sounds jumping ahead of ambient ticks. One frame of latency is inaudible
PerfProbe carried over from Hex and stayed in use. Hot paths are allocation-free by construction, and almost every system runs on an explicit per-frame budget.
- ·Collision circles and food positions are snapshot once per frame. Rebuilding them per query allocated tens of thousands of short-lived objects a second, and the resulting GC pauses showed up as periodic movement hitches
- ·Budgets: 80 rendered units with the true count always shown on the label, 3 new units per update, 10 rotations per frame, 7 drops per frame, 4 network spawns per tick
- ·The 1–2 second freeze on entering the playing state is hidden behind a black fade by intercepting the state switch, which also discards the freeze frame's oversized delta time
The field border and every tower and tree push the player out by clamping position directly rather than using colliders. A character controller pressed against a static collider receives vertical depenetration nudges, which the follow camera turns into rapid zoom jitter. Obstacle pushback runs before the border clamp so it can never shove a player off the map.
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.
Tower ownership and garrison change constantly and matter to everyone. Delta-syncing them meant tracking what each client had already seen, and any gap left a client showing a tower as the wrong colour indefinitely.
Rebroadcast all sixteen towers in full twice a second regardless of change, at 200 to 350 bytes. Every error self-corrects within half a second, and the bookkeeping disappears entirely. The opposite of the Hex approach, chosen because the state is small enough to make it the simpler correct answer.
Dying scattered up to twenty soldiers as networked pickups. Creating them in one frame flooded the realtime store and dropped the connection; spreading them over seconds looked broken.
One broadcast containing every drop position, turned into purely local objects on each client, with instantiation capped at seven per frame. Spread across three frames it reads as instant.
The death screen scavenged an existing material from the scene to draw with. On one build that handed it an additive glow clone and the entire card blew out to solid white.
Clone the material passed in explicitly, never one found by searching the scene.
Spawning at or below the settle height sometimes started the capsule underneath the ground plane, and the player dropped straight through the island. The scene's authored spawn point sat so high the Bitmoji visibly fell out of the sky instead.
A fixed spawn height of 15 cm above ground, plus a safe-spawn picker that places new players clear of existing fights.
Tuning constants exposed as script inputs get serialised into the scene. From then on the scene's stored value wins and editing the code changes nothing, a failure that looks exactly like the code not running.
Every tuning constant that matters lives as a private static readonly rather than an input. Called out twice in the codebase, in the two places it had already cost time.
Anything anchored to a player's head drifted visually off target for remote players only. Local heads report the capsule centre while remote heads report the ground root.
Anchor to a fixed height instead of the reported head transform, so local and remote render identically.
- Platform
- Snapchat in-camera Lens
- Clients
- Mobile · Camera Kit
- Contexts
- Live camera · Reply camera · Video chat
- Lens Studio
- 5.22.0
- Tracking
- None (synthetic 3D world under a fixed −65° camera)
- Session
- Remote / matchmade
- Players
- 2–4 matchmade
- Bots
- Up to 5 total participants, adaptive
- Offline
- Mocked singleplayer session