So, I threw together a quick map proof of concept and some mechanics for populating it with buildings and randomly spawned NPCs. Then I implemented a camera system with zooming, and honestly? It was way easier than I expected. Barely took any code at all. Python’s ability to iterate fast is an absolute game-changer. But, of course, that speed comes with a double-edged sword—make a wrong move, and suddenly you’re in performance hell. Especially when FPS starts dipping.
The FPS Drop
I ran into a big problem when adding NPCs. I was only at around 200-300 of them, and my FPS tanked to 30. And this was before adding any real game logic—just rendering them. Clearly, something was very wrong. No way this would be viable for even a small simulation.
Here’s a short animation showing the issue

Profiling the Performance and Optimizing
I fired up cProfile to figure out what was happening. There were some silly calculations to be fixed. I managed to get the same FPS (25) for 1000 NPCs. Still a far cry from anything workable. After profiling it more, turns out, the vast majority of time was spent in blit()—just rendering the NPCs. That’s bad news because it means even with optimized logic, the rendering alone would be a massive bottleneck. Here’s what the profiler logs looked like:

Even though blit() was the main culprit, there was still room for improvement. After several iterations, I optimized the conversion from grid to isometric coordinates and refined the y-sorting algorithm (more on that later). These changes made a significant impact—I managed to push the FPS up to 77 while rendering over 4000+ NPCs.

Relatively speaking, that’s a huge improvement. But there’s a catch—this is only for rendering graphics on screen. There’s no actual game logic running yet, just pure rendering calculations. In fact, even the rendering pipeline isn’t fully done yet. I still need to implement pathfinding (a notorious CPU hog!) and animated frames.
On top of it, the moment I introduce even a tiny bit of game logic, the FPS will take another big hit. I have to ensure there’s enough performance headroom left for all the non-rendering aspects of the game—because that’s where the real gameplay magic will happen.
Sad truth is blitting wasn’t going to cut it.
The Next Move: SDL2 Wrapper in Pygame
Pygame has an experimental module that wraps SDL2 instead of using its older blitting-based rendering. This should, in theory, provide a significant FPS boost. I’m about to dive into that now and see if it makes a difference.