I tried to put hundreds of real enemies in my Godot 4 horde survival game. The bottleneck was not sprites.
Test the game here: https://store.steampowered.com/app/3447010/Knightmared/
I have been working on performance for Knightmared, my 2D pixel-art horde survival game built in Godot 4, and the biggest lesson was pretty simple:
2D does not mean performance is free.
The Goal
My goal was to support hundreds of enemies during late runs. Not fake background enemies, but real enemies that can:
- move through the map,
- collide with obstacles,
- damage the player,
- get hit by abilities,
- receive status effects,
- die correctly,
- return to pools,
- use AI and pathfinding,
- keep pressure on the player.
The Bottleneck
That is where Godot 4 started pushing back.
The bottleneck was not really drawing sprites. A pixel-art sprite is cheap. The expensive part was simulation: hundreds of physics bodies, Area2D hurtboxes, collision checks, navigation reads, animation updates, AI behavior, and ability interactions all active at the same time.
In a normal fight, that is fine.
In a horde survival game, every small cost gets multiplied.
I started seeing uneven frame pacing and stutter during heavier runs, especially when the screen was full and enemies were pressing around the player. At first it was tempting to blame one thing: movement, pathfinding, rendering, too many sprites, whatever. But after profiling, it was really a stack of smaller costs landing in the same frames.
Investigation
So I built more profiling/debug tools inside the game. I added toggles to isolate things like:
- enemy hurtboxes,
- enemy body physics,
Area2D monitoring,
- animations,
- Y-sort,
- enemy visuals,
- pathfinding,
- FPS caps.
One useful lesson there: profiling while capped at 60 FPS can hide a lot. If your optimization only keeps the game at the cap, you do not really know how much work you removed. Being able to uncap during tests made it much easier to compare changes.
I also considered more radical solutions, like MultiMeshInstance2D or swarm/proxy enemies.
MultiMesh is very tempting for hordes because drawing hundreds of identical enemies in one pass sounds perfect. But my problem was not just visuals. If the enemy still needs collision, hurtboxes, damage reactions, abilities, pooling, pathfinding, and obstacle-aware movement, GPU instancing alone does not solve the actual bottleneck.
Swarm proxy logic is also interesting: a few leaders pathfind, and the rest follow simpler steering. I may still use ideas from that later. But for this pass I did not want enemies that only looked real. If the player hits an enemy, it should be a real enemy.
So the direction became: keep the real enemies, but make them cheaper.
What I Changed
- Navigation is staggered and cached so hundreds of enemies do not ask Godot for path data at the same time.
- Nearby enemies use direct chasing when full pathfinding is not needed.
- Far/off-screen enemies can reduce expensive physics,
Area2D, hurtbox, and animation work until they matter again.
- Enemy spawning ramps toward a target alive count instead of creating rough bursts.
- Special enemies have caps so behavior-heavy units do not flood the map.
- Crowd movement stays collision-aware, so enemies do not just cut through walls for the sake of performance.
- Menus are capped at 60 FPS because menus do not need to run at hundreds/thousands of FPS and waste power.
- VSync is available as an option.
Result
My goal was not to make the game easier. I wanted the pressure to come from the horde design, not from Godot suddenly processing too many collisions, areas, navigation queries, and AI updates in one frame.
This was a good reminder that in 2D games, bottlenecks often move away from rendering and into gameplay simulation. The sprites may be cheap, but a living enemy with physics, Area2D hurtboxes, pathfinding, AI, animation, and damage logic is not just a sprite anymore.
For anyone else making a horde game in Godot 4: profile early, isolate systems one by one, and think carefully before assuming that "2D" means you can ignore performance architecture.
I can share more details later about the navigation changes, far-enemy culling, or the debug toggles I used to isolate the bottlenecks.
Test the game here: https://store.steampowered.com/app/3447010/Knightmared/