DEV Community

Cover image for Shipping a Godot 4 game to cheap Android phones: what actually fixed my performance
Orlalalala
Orlalalala

Posted on

Shipping a Godot 4 game to cheap Android phones: what actually fixed my performance

Everything ran fine in the editor. Smooth, 60fps, no complaints. Then I installed the APK on a cheap Android phone and it basically fell apart.

That was probably the worst month of the whole year I spent on this game. I'm a student, I built it alone in Godot 4 in the evenings, and I honestly thought the hard part was done. It wasn't.

The game is Garlictos, a wave survival roguelike (yes, Brotato-like, I'm not hiding it). Lots of enemies on screen, lots of projectiles, damage numbers everywhere. Basically the worst case for a low end GPU.

Wave 12, a lot of stuff on screen

Here's the stuff that actually moved the needle, in the order I found it. Not a tutorial, just what happened.

Draw calls: 204 down to 67

First thing I did was open the monitors and look at draw calls. 204 on a busy wave. On desktop you don't care. On a cheap phone GPU you really care.

The dumbest culprit was shadows. Every enemy had a little shadow sprite using show_behind_parent. Looks nice, but it breaks 2D batching, because now the renderer has to go enemy, shadow, enemy, shadow... instead of drawing all shadows then all enemies. I moved the shadows to their own layer with a very low z_index (-50) and the count dropped a lot just from that.

Label outlines were the other surprise. I had outline size 8 on the damage numbers because it looked chunky and readable. Dropped it to 2. Nobody noticed visually, the GPU definitely did.

The flickering enemies thing

This one took me way too long. Pooled enemies would randomly flash or show the wrong color on some phones. Only on some phones, which is the worst kind of bug.

Turned out I was using shader instance uniforms for the hit flash, and there's a limit of 4096 on those. With pooling, instances get reused and on some drivers it just goes weird once you're near the limit. The fix was boring: one shared material for all enemies, and when an enemy gets hit I swap to a flash material for a few frames and swap back. Less clever, works everywhere.

Ghosting with physics interpolation

I run physics at 30 Hz to save CPU and turned on physics interpolation so it still looks smooth. Mostly works great. But anything I moved in _process instead of _physics_process started to ghost, like a smeared double image, especially stuff that teleports (pooled bullets respawning).

For those nodes I turned interpolation off and call reset_physics_interpolation() when I teleport them. That's it. Annoying to track down, tiny fix.

The freezes I missed before release

This is the embarrassing one. After launch people on older phones (2-3 GB RAM) told me the game froze for a second when big waves spawned. I didn't see it on my test phone so I shipped it.

The reason: I was creating and destroying Timer nodes on the fly for all kinds of small things. Hundreds per wave. Moved everything to pooled timers / a simple tick counter in 1.7 and the freezes went down by around 90% in my testing. Lesson learned, pool everything that spawns during gameplay, not just the enemies.

Final boss fight

Where it's at

Version 1.7.1 is out, free on Android. 20 waves plus endless, 157 items, weapons merge when you collect two of the same one. There are two short ads in a run (after wave 8 and when you go back to the menu after dying), revive ads are optional.

Trailer: https://www.youtube.com/watch?v=3GNlJ_Z0nb4

Google Play: https://play.google.com/store/apps/details?id=com.garlictos.omer

If you're doing Godot on mobile and hit something similar, ask in the comments, happy to go deeper on any of this. And if you try the game I'd love to know which wave killed you first, and an honest review on the store really helps a solo dev.

Top comments (0)