One scale manager, 70 watches
Every Garmin model has a different screen, so a single scale manager was built to size every sprite, font and hitbox off the device resolution: a 4,500-line refactor that made multi-resolution support possible at all.
From the workshop
Tiny screen. Big fun.
A growing catalogue of pixel-perfect arcade games for Garmin watches.
Retro endless runner. Jump, dodge, sprint through pixel-perfect obstacles.
Featured by Garmin Connect IQ
Space trading across the solar system. Upgrade your ship, trade goods, dock at 25+ stations.
A full action RPG on your wrist. Ten quests across floating islands, permadeath, no phone needed.
A virtual pet on your wrist. Hatch eggs, raise creatures, discover dozens of species.
The build story
A Garmin watch is a spectacularly bad games console. The frame budget is 67 milliseconds, 15 FPS on a good day. The memory ceiling is measured in double-digit kilobytes, and the screen is a circle. SkyGames exists because that sounded like a fun set of rules rather than a reason not to try. It is now a catalogue: SkyRunner (endless runner, 50 handcrafted levels), SkyLander (space trading), SkyLing (a virtual pet on your wrist) and SkyRift (a full action RPG, which took two goes), all sharing one Laravel backend and one set of hard-won lessons.
SkyRunner’s game loop runs player movement, two spawning systems, six kinds of collision detection and a full render pass inside that 67ms window, with a profiler wrapped around each stage so a regression shows up as a number rather than a feeling. Between them the games target dozens of watch models across nine screen sizes, from a 218-pixel Forerunner to a 466-pixel Fenix 9 Pro. Everything is authored once at the largest size, and every pixel literal in the codebase passes through a scale manager at runtime.
Levels are plain text files, compiled by a Python step into Monkey C data arrays before every build. The watch never parses a level. It reads structs that were code all along.
SkyRunner hit the Connect IQ store in November 2025. The online level editor (build a level in the browser, play it on your wrist) followed eleven days later. By January the Pro paywall was gone and everything went free. By April there was an in-game diamond shop. Somewhere in there Garmin featured it on Connect IQ, and the player counts at the top of this page stopped being hypothetical. They are live numbers from the same events pipeline the games report into, from players in 160-odd countries.
SkyLing pushed the catalogue somewhere stranger: a pet that keeps living while the watch sleeps. Hunger and mood are extrapolated server-side, capped at 24 hours and politely paused during your sleep window. Its companion watch face syncs through a pairing scheme built for a device that cannot hold secrets: a one-time four-digit code, rate-limited hard so the tiny keyspace cannot be scanned, swapped on first use for a long-lived 96-bit key. The analytics behind all three games had a growth moment of their own. The day the events table got big enough to OOM the dashboard, aggregation moved from PHP into SQL, and my phone now pings at every fiftieth new device.
SkyRift: Relics of the Aether was 97% of a game for five months. It is a real-time action RPG set in the SkyLands world: a ten-quest campaign across procedurally generated floating islands, a town hub with a shop, an inn and three quest givers, eight enemy types with a boss at the end of every quest, 12 weapons and 14 armour pieces across five tiers, 13 rings, fog-of-war exploration, permadeath runs kept on a local scoreboard, and save and resume anywhere. No phone, no account, no network.
What it was not, for those five months, was playable. On a real watch the frame rate sat near six per second where the simulator flew, the watchdog timer killed island generation before a map finished building, and the render cache meant to share sprites between entities quietly refused to cache anything.
All three were fixable, and fixing them was the whole second half of the project. Island generation was sliced into async chunks behind a loading screen, and start-up split into phases, so the watchdog always gets a breath. The sprite cache was repaired, then given pre-scaled wall tiles and cached font handles so the hot path stops asking the system for things it already has. Last, the render loop stopped allocating: a reusable camera bounds object instead of one per call, reusable buffers for the depth sort instead of a fresh array and a tuple per entity per frame, a 680-line bitmap font system deleted outright because every supported watch has vector fonts, and release builds compiled with full optimisation. Frame time went from 83ms to 66ms. Six frames per second became fifteen, which on this platform is all there is.
It went to the Connect IQ store in September 2026, free, no ads and nothing to buy, for 44 touchscreen AMOLED watches from 390x390 up to the 466x466 Fenix 9 Pro. MIP screens and button-only watches sat this release out, deliberately: you move by touching where you want to go, and a small monochrome screen cannot hold an RPG. SkyRift on Connect IQ.
None of these shipped in a straight line. A mascot got fired, a ship grew five new bodies, a virtual pet refused to get hungry, a font had to be drawn one pixel at a time for the cheaper watches, and an RPG spent five months at 97%. Below is each game’s development as it happened: the turns, redesigns and platform fights, pulled from the commit history. Each track scrolls sideways.
Every Garmin model has a different screen, so a single scale manager was built to size every sprite, font and hitbox off the device resolution: a 4,500-line refactor that made multi-resolution support possible at all.
The Garmin starter project shipped with a sample “monkey” asset that never actually rendered. The first real player was this scrappy little hand-drawn runner, dropped in just to start testing.
Sheep, kiwi, kākā and a bee joined the obstacle roster, each needing a hand-tuned hitbox so collisions matched the visible pixels, not the bounding box.
A Python step compiles authored levels into Monkey C data the watch just reads, plus auto-generated preview images and a stack of validation rules for ledge spacing, reachable jumps and safe landings.
→
v1.4.0 redrew the dude, and every other sprite, into a proper six-frame run cycle with new celebrate, death, jump and slide states. Same scrappy runner, glow-up applied.
For a while the free version literally hit a wall: a brick paywall that spawned across the track at 9.8km and stopped Lite players cold. v1.5.0 tore it down: every Pro licence check ripped out, all levels and the browser editor unlocked for everyone.
The diamonds players had been hoarding finally became a currency: a shop selling Endless-Mode power-ups like the slingshot, bubble and extra life. (A wallet bug shipped with it, hot-fixed two weeks later.)
The opening art drop: three workhorse ships (the nimble Mosquito, the cargo Mule and the Vector) drawn through a custom AI asset pipeline and packed across seven watch resolutions.
Flight feel jumped when the ship started rotating to face its velocity vector, with the thrust flames re-aligned to match. Flat top-down drifting suddenly read as real flying.
The economy moved from flat zone multipliers to fuel-cost-per-AU, progressive distance burn, cargo-weight penalties and a mandatory landing reserve, so every trade route became a fuel-vs-profit sum.
→
The Mosquito grew a five-tier upgrade ladder (Reinforced, Racer, Combat, Elite, Legendary), each a distinct sprite, the plain grey fighter becoming a gold-and-purple cruiser.
To test buying every skin, starting credits got bumped from 1,000 to 100,000, then sheepishly walked back to 1,000 the same day. Leftover debug money, narrowly un-shipped.
Cheaper watches like the Venu 3 have no scalable fonts, so a font manager rendering 5×7 and 6×9 pixel glyphs was written from scratch, later given borders and shadows to stay legible.
The new toast notifications spawned a timer each and hit Garmin’s hard timer limit; the fix routed every auto-dismiss through one shared timer. A very watch-platform kind of war story.
SkyLing didn’t start as a roster. It started as Drago, a single red dragon, the art-pipeline guinea pig. A cast of named characters followed (including Zorro, the family border collie) before the whole lot was cut and rethought.
Those named characters, and a three-egg picker, got scrapped for a single Mystery Egg. Every Skyling now starts identical, and which of ~27 species you get is decided at hatch time by how active you’ve been.
An eight-hour offline test exposed that hunger never decayed while the app was closed. The fix pinned the real-time-to-game-time ratio and hardened the decay maths against nulls: the first real offline-simulation milestone.
A single batch generated configs and sprites for thirteen new Skylings at once, growing the roster toward its eventual ~27 species across bio, cosmic, synthetic and oddball families.
→
A wholesale art pass regenerated every frame of the Buzz creature (idle, eat, emerge) for a clean before/after on the very same sprite.
Older watches were rendering text wrong, so a font manager and bitmap fonts were added with per-context scaling (menu, important, detail) to keep type sharp across the device zoo.
The companion watch face arrived: the app makes a short pairing code, pushes signed state to the backend, and the face polls to mirror your live pet. The code shrank from six letters to a four-digit PIN to make it typeable on Garmin Connect.
Friendly Mode (reduced decay, no death) and Pause-During-Sleep landed, so your Skyling’s hunger and mood freeze overnight, with a 24-hour cap so a long-unsynced pet can never come back already starved.
Frame timing and draw-call counts went in during the first week, before the game had a shop, a quest or a second kind of monster. Every performance decision after that was a number rather than an opinion.
Generating a whole island in one go took longer than Garmin lets an app think, so the watchdog shot it. Generation was cut into async chunks behind a loading screen, and start-up into phases, so the map builds a slice at a time.
Each entity had been loading its own bitmaps. A shared sprite cache preloads them once, joined by pre-scaled wall tiles and cached font handles, which is most of the gap between the simulator and the wrist.
Death is permanent, so death needed a screen worth reading: level, kills, quests completed and time survived, and a local high-score table where a winning run is marked in gold.
The frame-rate fight, won in one sitting: reusable camera bounds and depth-sort buffers instead of per-frame allocations, a 680-line bitmap font system deleted because every supported watch has vector fonts, and release builds compiled with full optimisation.
Round one of feedback was all about bodies: enemies stopped walking onto the player, packmates sidestep instead of freezing, blocked enemies get one budgeted A* detour a frame, and a teleport got an animation so it stopped reading as lag.
The Fenix 9 Pro 51mm brought a new 466x466 screen and caught an old bug: backgrounds were scaled by width only, leaving 12 pixels of white along the bottom. Art regenerated per resolution, 44 devices verified, submitted to the store.
SkyRift came out of the SkyLands universe, a dark-fantasy world of islands shattered across a breathable void that I built out far past any single game. One other swing at that world is still sitting on the bench, and it is the bigger of the two. Parked, not dead.
Endless text RPG · Darklands-inspired
A data-driven, endless RPG in the spirit of Darklands: branching vignettes, reputation, turn-based combat, all authored as data and compiled onto the watch. The engine works. What ran away was the world: a 500,000-word lore bible across 200-odd wiki pages, declared ‘complete’ three days in and promising six hundred quests, against a game with exactly one quest and one location actually playable. It didn’t get out of hand so much as start out of hand. Shelved, with enormous fondness.
SkyRift proved the world reads on a wrist. SkyLands is the version of it I still intend to come back to.
Because constraints are a forcing function. There is no room for a framework, an asset store or an unconsidered allocation. Every feature has to justify its bytes. The catalogue keeps growing, with more Sky-somethings in various stages of playable, and every one of them starts from the question that started SkyRunner: how much fun fits in 67 milliseconds?
Working with a brutal platform constraint that still has to feel effortless? Here’s how we could work together.