Skip to content
E
Egmatic
2d game performancegame optimizationdraw callssprite batchingobject pooling60 fps

How to Optimize 2D Game Performance: A Practical Guide to 60 FPS

A 2D game that drops frames usually loses to one of four things: too many draw calls, per-frame memory allocations that trigger garbage collection, work done off-screen that no one sees, and a simulation coupled to the frame rate. The fix is always the same sequence — measure first, then cut draw calls with sprite batching and atlases, eliminate allocations with object pooling, stop rendering what the camera cannot see, and run the simulation on a fixed timestep. This guide walks each step against the 16.67 ms budget a 60 FPS frame gives you, with the profiling habits that separate a smooth game from a stuttering one. No invented benchmarks — every technique is grounded in how renderers and the .NET runtime actually behave.

Vladislav KovnerovJuly 9, 20268 min

A 2D game that stutters almost never has a mysterious cause. Once you measure it, the frame budget points at one of four usual suspects: too many draw calls burning CPU time on communication overhead, per-frame memory allocations triggering garbage collection pauses, work performed for objects the camera cannot see, and a simulation tied to the frame rate so the game runs faster or slower on different machines. The cure is the same in every engine: measure first, then attack those four in order.

A frame at 60 FPS has a budget of 16.67 milliseconds. Everything — input, simulation, rendering — has to finish inside that, or the frame is dropped and the player sees a hitch. Optimization is the discipline of fitting your game into that budget and keeping it there. This guide is the practical version, written for 2D games and grounded in how renderers and the .NET runtime actually work, not in invented speed-up percentages.

If your slowdown happens at build time rather than in play, that is a different problem — see our guide to fixing slow build times. This article is about the frame you ship to the player.

Step one is always: profile, do not guess

The single most expensive mistake in optimization is changing code without measuring. Most games have one or two functions that dominate the frame, and they are rarely the ones you suspect. The fix is always to find them first.

ToolWhat it showsWhen to reach for it
Frame time graphPer-frame millisecond cost, with spikesAlways start here — reveals stutters an average FPS counter hides
CPU profilerTime spent in each functionWhen the frame is CPU-bound and you need the culprit
GPU profiler / frame debuggerDraw calls, overdraw, shader costWhen the frame is GPU-bound or fill-rate limited
Memory profilerAllocations per frame, GC eventsWhen the game stutters but the FPS counter looks fine

Read the numbers, not your intuition. A function that "feels expensive" but takes 0.1 ms is not your problem; a boring loop that takes 6 ms is. Every optimization below is only worth applying where the profiler says it matters.

Cut draw calls with batching and atlases

Every draw call is one instruction from the CPU to the GPU: "draw this geometry, with this material, at this transform." The cost is not the drawing — modern GPUs draw millions of pixels instantly — it is the fixed overhead of setting up each call. A scene that draws a thousand sprites one at a time spends most of its frame budget on that overhead, and the GPU sits mostly idle waiting for the next instruction.

Sprite batching fixes this by combining many sprites that share the same material into a single draw call. For batching to work, the sprites must share a texture, which is why the companion technique is the sprite atlas: a single large texture that packs many smaller sprites side by side. With an atlas, a whole level's tiles, a character's animation frames, or a swarm of identical enemies can collapse into one or a few draw calls instead of hundreds.

SceneNaive (one draw call per sprite)With atlas + batching
500 tiles~500 draw calls~1 draw call
Animated character (8 frames)1 per frame visible1
Bullet hell (300 projectiles)~300~1 (same atlas)

The practical rules: keep sprites that appear together on the same atlas, avoid breaking batches by switching textures or blend modes mid-scene, and pack atlases at power-of-two dimensions where the platform expects them. These are documented behaviors of every major 2D renderer, not engine-specific tricks.

Stop allocating every frame: use object pooling

In a garbage-collected runtime like .NET (which Unity and MonoGame both use), memory you allocate eventually has to be reclaimed. The garbage collector does that work, and when it runs it pauses your game for a few milliseconds — long enough to drop a frame and produce a visible stutter. The trick is to stop allocating during play, and the biggest offender is creating and destroying objects on the hot path.

Object pooling is the standard answer. Instead of creating a new bullet when the player fires and destroying it when it hits, you keep a pre-allocated set of bullets. Firing takes an inactive one, activates it, and resets its position. Impact deactivates it and returns it to the pool. No allocation, no destruction, no garbage collection.

Pool anything spawned and discarded frequently:

  • Projectiles and particles
  • Enemies in a wave-based game
  • Damage numbers and floating text
  • Audio sources for repeated sound effects

The deeper habit is to avoid per-frame allocations in general: skip LINQ and string concatenation in hot loops, avoid boxing value types, and prefer reusing collections over creating new ones. A frame that allocates nothing runs smoothly; a frame that allocates a few kilobytes a hundred times will eventually hitch.

Don't render what the player can't see

Rendering off-screen objects is wasted work the GPU still has to accept and the CPU still has to submit. Two simple techniques remove most of it:

  • Culling: skip objects entirely outside the camera's view. If an enemy is three screens away, it does not need to draw this frame.
  • Layering and draw-order discipline: group static background art so it can be drawn once or cached, rather than reprocessed every frame.

The same logic applies to logic, not just rendering. An enemy off-screen does not need its full AI update every frame; a distant particle system can update at half rate. Spend the budget where the player is looking.

Decouple simulation from frame rate

If your game moves objects by "advance position by speed each frame," then the game runs faster on a 144 Hz monitor and slower on a 30 FPS device. That is not optimization — it is correctness — but it directly affects perceived smoothness, because a fixed simulation rate combined with interpolation looks and feels stable on any display.

The established pattern is the fixed timestep with an accumulator: the simulation advances in fixed slices (say, 60 times a second) regardless of how fast the screen refreshes, and the renderer interpolates between the last two simulation states for the current frame. Glenn Fiedler's widely cited article Fix Your Timestep! is the canonical explanation. The payoff is twofold: deterministic, reproducible physics, and a game that feels the same on every machine.

Keep the frame budget in mind, always

A frame has to finish in 16.67 ms to hold 60 FPS. That number is the whole game. Here is roughly where a healthy 2D frame spends it:

PhaseTypical share of 16.67 msGoes wrong when
Input + simulation2–5 msToo many active objects, per-frame allocations
Culling + batch preparation1–3 msNo batching, frequent texture switches
GPU draw + present2–6 msHigh overdraw, large transparent particles
Headroomthe restStutter eats this first

The headroom column is the point. A frame that uses 16 ms has no room for a spike, so the next allocation, the next particle burst, or the next garbage collection drops it. Aim to spend well under the budget, not right up to it.

The mistakes that undo all of this

  • Optimizing before measuring. You will tune the wrong thing.
  • Chasing the average FPS. A flat frame-time graph matters more than a high number. Watch for spikes.
  • Ignoring garbage collection. The number one cause of "high FPS but it still stutters."
  • Over-optimizing cold code. A menu that loads once a minute does not need pooling.
  • Coupling game speed to frame rate. It breaks the feel on different displays and undermines every other effort.
  • Adding memory instead of fixing logic. More cache helps only if the cache is warm; a cold cache re-imports everything.

Where Egmatic fits

Egmatic is a 2D editor and engine built on a MonoGame foundation. That choice matters for performance in two honest ways. First, MonoGame is a lean C# runtime: it gives you direct control over the draw pipeline without a heavy abstraction layer sitting between you and the hardware, so when the frame budget is tight the bottleneck is your own code, not the engine fighting you. Second, Egmatic's live preview lets you change a value and watch the frame-time impact immediately, which turns profiling from a slow edit-build-run loop into a fast feedback cycle — exactly the habit the rest of this article keeps recommending.

Egmatic targets desktop, mobile, and consoles through its MonoGame foundation, so the same optimization work holds across platforms. If you are new to the underlying engine, our guide to MonoGame explains what that foundation is and why it is lean.

Conclusion

A smooth 2D game is not the product of secret tricks. It is the product of measuring against a 16.67 ms budget, cutting draw calls with atlases and batching, removing per-frame allocations with object pooling, culling what the camera cannot see, and running the simulation on a fixed timestep. Do those five things in that order, guided always by the profiler rather than by intuition, and the frame budget stops being a fight and becomes a habit.

Related Posts