Skip to content

Game Hacks

Draw-call batching for 2D pixel games: the cheap wins

Game Hacks 4 min read

The first time most indie devs notice their game is GPU-bound, the instinct is to reach for shaders. Almost always wrong — 2D pixel games rarely shade hard. What they do is submit hard. Every sprite you draw is potentially its own draw call, and at sixty frames a second across two thousand sprites that's a hundred and twenty thousand calls a second. Most of them duplicates.

Batching is the fix. Same texture, same material, same render state — collapsed into a single call. Here are four batching wins for 2D pixel games that pay off without writing a single line of shader code.

1. Atlas everything

Sprites that share a texture can be batched into one draw call. Sprites in separate textures cannot. So: put everything that draws at the same time into one atlas. Your player, the bullets they fire, the enemies they shoot, the particles those enemies leave behind — one atlas. UI gets its own atlas. Tilemaps get their own atlas. Three atlases for a typical scene is a reasonable target.

The mistake is to atlas by category (one atlas for "enemies", one for "props", one for "weapons") rather than by render order. Render order is what dictates batching. If your player and bullets render between enemy bodies, splitting them across two atlases means the GPU swaps textures every layer change — and "swap" means "new draw call."

2. Material discipline

Same texture, same material gets you a batch. Same texture, different material doesn't. The classic break: someone puts a damage-flash shader on the player ("hit" tint), uses the default material on everything else, and now every time the player takes damage the GPU draws the rest of the frame, swaps material, draws the player, swaps back. Three draw calls where there was one.

Fix it by giving every sprite that uses any custom shader the same material with a per-sprite parameter (via a property block, or vertex colour, or a uniform — pick your engine). The flash tint becomes a vertex colour applied to specific frames. No material swap, one batch, no perf cliff.

3. Understand dynamic batching's small print

Unity's dynamic batching looks like a free lunch — it batches sprites at runtime that share the same material. But it's only happy with meshes under a vertex threshold (around 300), it costs CPU time to rebuild the batch each frame, and it gives up the moment your sprites have wildly different scales (it has to transform each vertex on the CPU).

For pixel games this usually works in your favour — sprites are simple quads, two triangles, four vertices. But for tilemaps you'll get more reliable batching from a static mesh you build yourself than from leaning on the engine's per-frame batcher.

4. Don't draw what nobody sees

The cheapest draw call is the one you didn't issue. Off-screen sprites still go through some of the engine's submission pipeline; visibility culling stops that early. Most engines do this for you automatically when sprites are children of a 2D camera — but it's worth checking. Spawn a debug visualiser that draws the camera frustum and tints any sprite outside it red. If you see red sprites that aren't culling, the engine isn't doing what you assumed and a per-scene investigation is worth your morning.

The other half of this: if you've got a swarm of bullets, a tile field, or any other "many small drawables", group them under one parent transform and cull at the parent level. One bounds check beats two hundred.

Measure first, then atlas

Profilers exist for a reason. Unity's Frame Debugger, RenderDoc, or whatever your engine's equivalent — open it before you start optimising. You're looking for two numbers: total draw calls and how many of those are "batched" vs "broken." If you've got fifty broken batches, fix the four causes above and most will collapse into a handful of calls.

None of this requires you to write a shader. None of it requires a custom render pipeline. It does require you to think about render order when you set up your atlases — which is the kind of upfront work that doesn't feel productive on day one and pays out every frame after that.

The cheap wins are real. Take them.

← Back to blog