Three bugs I still think about, from making Sky Arena
Three bugs I still think about, from making Sky Arena
Hi. Sky Arena: Biplane Combat is a 2D dogfighter for phones: one biplane, one machine gun, some rockets, and three minutes to shoot everyone else down. You can play 1v1, 2v2, or a free-for-all up to eight — online with a room code, or over a single Wi-Fi with no internet at all.
Since this is the first entry, I'd rather not list features. Here are three things that broke along the way. They cost me the most.
1. A gun that makes you worse
Weapon upgrades started out the way they usually do: pick one up, get stronger. Boring. After a couple of test matches it was obvious the whole game had collapsed into "who reaches the orange pickup first".
So I made every upgrade a trade.
| damage | range | fire rate | thrust | turn rate | |
|---|---|---|---|---|---|
| base | 10 | 45 | ×1 | ×1 | ×1 |
| orange | 15 | 80 | ×0.8 | ×0.89 | ×0.86 |
| cyan | 20 | 100 | ×0.7 | ×0.78 | ×0.72 |
The cyan gun hits twice as hard and reaches furthest, but it fires slower and the plane carrying it is heavier: slower to accelerate and noticeably worse in a turn. And these aren't numbers in a vacuum — the handling changes the instant you grab the pickup. You feel the plane become a different aircraft.
Rockets became a separate, fourth weapon: one hit kills, its own ammo counter, a smoke trail behind it. In exchange you get very few, and they spawn rarely.
The side effect I like more than the original idea: taking the upgrade is no longer automatic. If you're good in a turning fight, the base gun is objectively the better tool.
2. Google Play decided the game was incompatible with almost every phone
The worst discovery of the release. I upload the build and the console says: ~3,000 supported devices. On Android that's nothing. I open the store page on a Xiaomi 14 — "not supported on your device". A flagship. Running the game perfectly fine from an APK.
The cause wasn't in my code at all. Unity 6 on Android writes this into the generated manifest by itself:
<supports-gl-texture android:name="GL_IMG_texture_compression_pvrtc" />
PVRTC is a texture compression format for PowerVR GPUs — that is, for iOS. My textures are ASTC/ETC; there isn't a single PVRTC asset in the project. But Google Play reads that tag literally and filters out every device without PowerVR, which is essentially all of modern Android.
The fix is a build hook on
IPostGenerateGradleAndroidProject that strips every <supports-gl-texture> tag from the manifest before the AAB is packed. Twenty lines. Half a day to work out that the thing to look for wasn't mine.
If you ship Unity on Android: check your device coverage in the console before you publish, not after.
3. "It lags over Wi-Fi" turned out to be the garbage collector
Friends kept saying local matches stuttered. Ping wasn't the culprit — over Wi-Fi it's near zero, and the stutter was on the host.
Profiler: 9.5 KB of garbage every frame. The source was
FindObjectsByType, used by pickups, enemy indicators, hazard zones and the HUD to find living planes. Every. Frame. With fourteen pickups on the map the host was doing up to seven hundred scene scans per second.
I replaced the searches with a plain registry: a plane adds itself to a static list when it spawns and removes itself when it despawns.
Before and after (host, 4 planes, 300 frames):
- garbage: 9.5 → 0.4 KB per frame
- worst frame: 9.35 → 1.52 ms
Stutter gone. Zero gameplay changes, zero new features — I just stopped looking for something I already knew.
Bonus: players were getting two planes
Short, but it stung. Two independent code paths led to respawn, and both fired if you finished off a pilot during the ~1.2 seconds where he'd already run to the hangar and was waiting for a new aircraft. Result: two of your planes in the air, an extra death on the victim's stats and an extra kill for the shooter.
Fixed with a one-shot flag on the client and a server-side guarantee of "one death, exactly one spawn". One trap worth knowing: the flag has to be cleared at match start and end, not inside the coroutine —
StopAllCoroutines never lets the coroutine get that far.
What's next
Modes, mostly. Right now there's 1v1, 2v2 and free-for-all; I want something built around a shared objective instead of just "shoot everyone".
And one question for you, since you read this far. In this game you can bail out with a parachute on purpose — ditch a smoking plane, walk away from a fight you're losing, and run to a fresh aircraft. But once you've actually been shot down, that's it: no ejecting. I like the asymmetry, because it turns retreating into a decision instead of an undo button. I've also heard it called too harsh.
What do you think? Let me know if you've played.


Discussion