Lindsay Edwards

The constraint is the aesthetic

The mobile game I was building had to run on cheap phones. Not flagship handsets, genuinely low-end hardware with a tight frame and memory budget. My backend brain filed that under “constraint to fight”, the way I would fight a slow query or a memory ceiling on a server.

So I spent a while resenting it. Fewer polygons than I wanted. Lower internal resolution than I wanted. Compressed textures I would rather have kept crisp. Every choice felt like giving something up to stay under the ceiling. Miss that budget, though, and the game stutters on exactly the cheap phones most of your players actually own, and a game that stutters gets uninstalled fast.

Then it clicked that I was describing the art style. The exact same words.

The optimisation and the look are one list#

Write down what a cheap phone forces you to do. Low polygon counts. A low internal resolution. Compressed, low-colour textures. A dither pattern to hide the colour banding that compression causes.

Now write down the aesthetic of old console games. Low polygon counts. Chunky low resolution. Flat, low-colour textures. Visible dithering.

It is the same list. The retro look is not decorated on top of the performance budget. It is the performance budget, seen from the other side. Fewer polygons is the style and the optimisation in one move. Lower resolution is the style and the optimisation. The scanline-and-dither treatment hides banding and sells the era.

Once I saw that, the work stopped feeling like a per-frame fight. I was not sacrificing fidelity to hit a number. I was leaning into a look that happened to be cheap, which is a much better mood to build in.

When a resource ceiling is unavoidable, make the cheapest thing to render your intended look. Then the constraint stops being a fight and starts being a decision you already made.

Make the budget announce itself#

Reframing the art was half of it. The other half was refusing to find regressions the way I usually do on a side project, which is on my own nice phone, too late.

So I built an always-on overlay. Every frame it sampled three numbers: frames per second, draw calls, and memory. It graphed them live, with two reference lines drawn across the frame rate graph. A target line where I wanted to sit, and a warning line at the floor I refused to drop below. The moment the frame rate crossed the floor, or memory pushed past its ceiling, the overlay threw a visible warning.

if fps < FPS_FLOOR or mem_mb > MEM_CEILING:
warn("budget breached: %d fps, %d mb" % [fps, mem_mb])

Nothing fancy. The value was that it was always on. I could not add a too-heavy effect or leak a texture without the overlay lighting up in the same session I made the mistake, instead of a player finding it for me on hardware I did not own.

Two habits came out of this, and both travel well past games.

The first is to stop treating a hard ceiling as pure loss. Look at the cheapest thing you are allowed to do and ask whether it can be the intended design rather than the fallback. Sometimes the constrained version is genuinely the better version, and naming it the goal changes how the whole build feels.

The second is to make the limit self-reporting. A budget you have to remember to check is a budget you will breach quietly. A budget with an always-on, thresholded readout tells on itself the instant you cross it. That overlay is just a dashboard with an alert line, which is a pattern I trust everywhere. Recast the constraint as the aesthetic, then wire it up to shout when you break it.

Keep reading