Why Does My Godot Game Chug When the Screen Fills Up? A Brotato Experiment Has Answers

An independent experimenter ran Brotato on a weak ARM handheld, saw 5 to 10 FPS from around wave 20, failed first with a custom physics server, then rebuilt the test harness and found three small causes: unseparated collision layers, repeated per-frame work, and unbatched blood particles. The FPS gains are the author's own harness results, not official Brotato benchmarks.

Why Does My Godot Game Chug When the Screen Fills Up? A Brotato Experiment Has Answers
Guide chapters5 chapters

On a weak ARM handheld, Brotato fell to 5 to 10 frames per second from around wave

20. The fix did not come from rewriting the physics engine first. It came from rebuilding the test harness, then finding three small, checkable problems.

Brotato key art: the potato hero, guns out, surrounded by enemies. Official Steam store art.

At a glance

  • Brotato, a top-down arena shooter by Blobfish, built on Godot 3, not Godot 4.

  • The case: an independent experiment, not a Blobfish postmortem. One player-developer tested Brotato on a weak handheld and a weak phone.

  • The symptom: 5 to 10 FPS from around wave 20 on an Anbernic RG34XXSP handheld, as reported by the experimenter.

  • The first attempt failed: a custom physics server and batching scripts gave no gain beyond measurement noise on a Samsung A51 phone.

  • The three causes found: unseparated collision layers, repeated per-frame work, and one-particle-per-draw-call blood effects.

  • All FPS numbers in this article are the experimenter's own test-harness results, not official Brotato benchmarks.

  • Source: illarn, Game Optimization Methodology, DEV Community, October 20, 2026.

  • Last verified: October 27, 2026.

A handheld that could not keep up, and a first fix that did nothing

Brotato is a small game with a big late-game problem for weak hardware. One potato hero wields up to six weapons at once, waves last 20 to 90 seconds, and a run finishes in under 30 minutes. The game launched in 1.0 on June 23, 2023, costs 4.99 dollars, and is Steam Deck Verified. None of that is the story here. The story is what one experimenter, illarn, did when the game chugged on his own weak device: an Anbernic RG34XXSP, an ARM Linux handheld, where Brotato dropped to 5 to 10 frames per second from around wave 20 onward.

His first instinct was the big one: write a custom physics server that rebuilt a hash grid every frame, and add batching scripts. On a second weak device, a Samsung A51 phone, the retest was brutal. The gains did not rise above measurement noise, and the custom physics server made two test scenarios worse. Three of the five experiment days then went into rebuilding the test harness instead of more optimization: fixed scenarios, dry runs, run metadata, test length controlled by physics tick count instead of wall-clock time, and median, P95 and P99 instead of a bare average. Only then did the real causes show up.

Brotato in action at wave 6: a top-down arena filling with enemies and damage numbers. Official Steam store screenshot.

Cause one and two: collisions that should never meet, and work done twice

The first cause was collision layers. Enemy hitboxes and enemy collision areas were not separated, so the broad phase generated huge numbers of invalid enemy-versus-enemy pairs, when the pairs that mattered were player-versus-enemy. Moving enemy hitboxes onto their own collision layer cut that waste. The lesson transfers to Godot 4, where the same idea lives in the Inspector as Collision Layer (which layer an object is on) and Collision Mask (which layers it checks), with code-side helpers set_collision_layer_value and set_collision_mask_value in Godot 4.1. The layer numbers are yours to assign in your own project; this article does not hand you Brotato's layer numbers, because the experiment does not publish a copy-paste layout.

The second cause was small, repeated per-frame waste: a static map size recalculated every frame, fire-spread queries run for characters that were not burning, and already-disabled areas disabled again every frame. A few guard checks removed them. Neither cause required a new engine. Both required a harness that could tell a real gain from noise, which is why the harness came first and the fixes came second.

Cause three: one particle effect, hundreds of draw calls

The third cause was rendering. The experimenter reports that blood effects accounted for about 30 percent of draw calls, and that batching them through MultiMeshInstance2D, drawing many instances in one call, lifted the slingshot wave 200 scenario from 32.30 to 40.11 FPS, and wave 249 from 51.95 to 55.7 FPS. In Godot 4, the equivalent choices are MultiMeshInstance2D or GPUParticles2D, as the experiment article itself notes for Godot 4 and later. The broad-phase experiment points the same way: the author says Brotato's developers had moved the broad phase to a hash grid, and switching back to BVH helped the AFK scenario but slightly hurt the slingshot scenarios, so he kept it only because the AFK gain was larger. That is a per-project measurement, not a rule. Do not copy the conclusion; copy the test.

The headline numbers, all from the author's own harness, tell the same story in one line. The slingshot wave 200 scenario went from 16.08 to 32.30 FPS, wave 249 from 45.33 to 51.95 FPS, and the AFK wave 150 scenario from 14.02 to 44.02 FPS. The total change was under 200 lines. None of this is a Blobfish patch note, and none of it should be read as Brotato's official performance. Brotato's own patch history is separate: patch 1.1.8.0 moved the game to Godot 3.6 with explosion and effect performance work, and the 2026 Paws and Claws update moved it to Godot 3.7 dev-1, according to wiki patch records and GamingOnLinux. The experiment is a method lesson, not a release note.

What to do Monday morning

  • Build one fixed heavy scene in your own project and record its baseline before changing anything.

  • Fix the harness before the code: fixed scenarios, dry runs, run metadata, length by physics tick count, and median with P95 and P99.

  • Change one thing at a time, and keep only changes that improve both the median and P95 on the same scene.

  • Separate collision layers so objects that never need to interact stop generating pairs.

  • Add guard checks for repeated per-frame work: skip what is already done, static, or not applicable.

  • Batch repeated particle draws with MultiMeshInstance2D or, in Godot 4, GPUParticles2D, and measure the draw-call count before and after.