at play · case study

TIKI DASH

How a 3D endless runner gets built for the browser — product, agents, gameplay, art, and the GPU budget, from empty repo to shipped.

TIKI DASH cover art — the runner chasing golden coconuts down a jungle beach path toward a waterfall
play tiki dashtiki-dash.com · free · no install · works offline
role
design & code
stack
Three.js · TypeScript · Vite
built
solo, with agents
status
live — free, in the browser

The product, before the polygons

An endless runner is a dopamine loop — swipe, near-miss, one more run — and the browser makes the install a link: no store, no download screen. After the first visit a service worker keeps all ~13.9 MB playable offline, installable like any app. Those two choices set the engineering bar in one sentence: as smooth as the genre's giants, on the phones people actually hold, without dimming the jungle.

A process with gates

The build follows a playful-production discipline: prove the fun first, gate every phase, let the gates say no.

01
prototype — one test to pass: a stranger finishes a run and starts another, unprompted
02
vertical slice — the core loop at shippable feel — the gate where the game earns production
03
design macro — scope locked in writing; from here it may deepen, never grow
04
alpha — feature-complete in greybox — systems before beauty
05
content — river crossing, cave bore, tiki shrine, tutorial, missions
06
art slice — one purchased palette, one licence audit, one shared atlas
07
performance — instruments in the build, verdicts from the real phone
08
ship — PWA, one build for two channels, deploy on push

Built with agents

The workforce is one person directing LLM agents. The repo carries a standing working agreement — a persona, hard mobile-performance rules, a shorthand vocabulary for recurring jobs — so every agent session starts already knowing the frame budget, the zero-allocation render loop, and the pipeline's bounds. Decisions and lessons persist in append-only project logs that agents read and write: the project's memory outlives any single session. Research runs at fan-out scale — the art-sourcing survey was a 105-agent sweep before a dollar was spent. And nothing merges on an agent's word: the phone has the last one. This page, too, was distilled from those logs by an agent.

Designing the run

Game feel is numeric, not vibes. Touch follows newest-input-wins: a jump swiped mid-slide cancels the slide instantly. Swipes chain — one touch can fire several actions, the gesture origin re-anchoring under the finger, with a chained action requiring more travel (36 px) than a first swipe (28 px) so intent stays unambiguous. Collision is a fairness contract: the hit window must stay inside the lane midline, so a player who has committed to the next lane can never be clipped by an obstacle they visually cleared —

THE TRAPobstacle · half-width 0.7lane midline · 1.15hit window · 1.2the trap: a window wider than the half-gap clips players who already left the laneSHIPPEDobstacle · half-width 0.7lane midline · 1.15hit window · 1.1
The contract in units: obstacle half-width + player half-width must stay under the 1.15 lane midline. A window of 1.2 is the trap; the shipped 1.1 leaves a margin of safety on the player's side. And because the world moves up to ~3 units a frame at dash speed, collision tests the swept path since the last frame — never a point sample.

The look, on a budget

The style is modern toon: the whole world UV-maps into one 1024² palette atlas, colored by a single toon material with a four-band ramp — one texture, one shader, one draw philosophy. The line work is a whole-scene screen-space ink outline: the scene renders offscreen, and a composite pass draws a line wherever depth breaks, tinted with the surface's own color. The bushes and grass are the palm crowns themselves — cropped and dome-squashed — so all vegetation speaks the same painted language. Even sharpness is a dial: mobile renders at pixel ratio 1.35, tuned by eye to where the flat-fill style stops gaining from more pixels. The art direction and the frame budget are the same decision seen from two sides.

Two budgets, one frame

A frame pays two bills: the CPU bill, charged per draw call, and the GPU bill, charged per triangle, per shaded pixel, per megabyte of texture touched. The frame ships when the slower bill clears — so the architecture is built to keep both small from the start.

CPU billone fee per draw callGPU billtriangles × pixels × texture bandwidththe frame ships when the slower bill clears →
frame time = max(CPU bill, GPU bill) — optimizing the cheap side buys nothing.

The CPU bill is paid with instancing: hundreds of trees, rocks and collectibles render as a few instanced meshes — even the coconuts, spawning and despawning constantly, are one instanced draw fed from an object pool. The GPU bill is paid with the shared atlas, flat toon shading, and per-asset triangle budgets — ~205k triangles a frame, all instanced volume: the 72 shipped models total only ~112k in their files.

naive scene — every object its own draw771instanced forest338merged decals & UI160shipped~70
What the batching architecture buys: the same scene drawn naively needs 771 draw calls a frame — shipped, it needs about 70.

Measured on the real thing

Every performance verdict comes from the target phone, not a desktop GPU — under a standing protocol (rested, charged, nothing in the background), because thermal state alone can move a reading by ~5 fps. The build carries its own instruments: a frame-time flight recorder shipping ring-buffer logs off the device, and a bench mode that A/B-cycles render variants in interleaved segments so every comparison shares the same thermal reality. The instruments earn their keep by protecting the art: the sweep proved jungle density costs nothing on mobile — so the phone gets the full jungle — and named fill rate the one lever that matters, which is exactly where the pixel-ratio dial sits. Development instruments are stripped from public builds at compile time.

60fps, vsync-locked, on a 2020 mid-range Android — measured over nine runs and 669 recorder rows, not one frame under 45. In a browser tab.

Shipping it

One build goes everywhere: relative asset paths let the same dist/ run at the root of tiki-dash.com and inside itch.io's nested CDN. The game ships as a progressive web app — 102 precached files, fully playable offline. Audio ships as 192 kbps AAC: 2.9 MB of source WAV becomes 310 KB. Licences are audited, not assumed — purchased pack, CC BY credits, CC0 audio. And deploying is a push: merging to main triggers CI, which builds once and delivers to both channels.

The spec

60 fps
vsync-locked on a 2020 mid-range phone
~70
draw calls per frame
~205k
triangles per frame, instanced
1024²
one palette atlas colors the world
~13.9 MB
the whole game, offline-capable

Next

The music and ambience pass, menus and settings, the final art polish. The game is playable today — polish lands in place, because deploying is a push.

play tiki dashtiki-dash.com · free · no install · works offline