Pocket City 2 is my upcoming city-building game for mobile (and eventually PC). It is close to completion and I hope to launch it in the next couple of months. Working as a solo dev using Unity, there was a lot of experimentation involved in trying to find a way to best render large city grids on mobile. Here are the techniques I tried and what ended up working the best. Maybe you'll find them useful or at least provide some insight into the behind the scenes work. Enjoy!
Strategy 1: Billboards for LOD buildings
Replacing full 3D meshes with 2D billboards for far away cameras is a way to reduce poly count using static images. I made billboards for each of my buildings which would show when the camera was far enough. However there were 4 billboards for each possible building rotation, times 4 possible seasonal colors, times each different building, equaling way too many draw calls. Even though the camera poly count was lower, the performance was bad. The memory footprint of the sprites was also scaling badly.
Strategy 2: Atlased billboards for LOD Buildings
I atlased my building sprites and ensure the billboard had UV settings to show specific sprites in the atlas, and then combine all the quads into a single mesh at runtime, this would allow all the sprites to be drawn in a single draw call. While this improved the frame rate, it didn't fix the memory issues and made the sprite quality was generally worse since I had to fit them all into a atlas. Additionally, some testers reported the sprites not loading at all (probably due to memory issues and using large textures on mobile).
Strategy 3: Generate a combined world mesh procedurally at runtime
At this point, I tried a new approach generate a single large world mesh at runtime to allow the city to be drawn in a single call. For larger cities, this needed to be chunked into separate blocks so that the vertex count of each chunk did not exceed the max count in Unity (about 65k). The function I used for this is Unity's built-in Mesh.CombineMeshes by iterating over each building's mesh and putting it into a new combined mesh. This also requires each building to use the same material, so I had to edit the textures of many buildings in Blender to use the same sheet.
Final Strategy: Combined world mesh at runtime with LODs
Expanding on the combined runtime mesh in strategy 3, I created lower poly versions of each building so that each building has 3 LOD meshes. When creating the combined chunks of my city at runtime, I also create additional LODs for each chunk. This way, when the player zooms out, we don't show the highest detailed chunk, but instead show a lower poly chunk. I decided to manually create LOD meshes in Blender since auto-generated LODs looked bad. During gameplay, the poly count was quite high, but the draw calls were much lower and more performant than showing individual buildings meshes or using billboards.
As an additional optimization, I only generate the chunks of the world that the player can see, and delete the other chunks that the player cannot see in order to free up memory. This causes a bit more CPU usage when the player moves around the map as the chunks need to be rebuilt, but it frees up a good amount of memory and was a good trade-off.
When the player is in free-roam mode, the combine building meshes are cleared and the original individual building meshes are shown again. I found that using the combined meshes in free roam was actually worse than just letting the perspective camera cull individual buildings.
Trees and foliage are actually billboards. These work well as billboards because they are all identical with each other and are instanced easily, unlike building billboards which are all different. The billboard quads are all combined into a single mesh as well. For different seasons, I just change the texture of the trees and bushes to a different image.
Thanks for reading!
I hope that provided some behind-the-scenes insight, or was helpful for fellow developers looking to for optimization ideas. Stay tuned for more Pocket City 2 news!