Stateic Potential - optimizing tile map in Unity 3D
While we have made game called Stateic Potential at Slavic Game Jam 2015. After 24h we had developed simple working game prototype and started thinking about proper ballance. There was about 12h to the end of jam and we have faced even greater problem - our game run at ~10FPS at most. Normally you would bring PC with GTX and i7 to the presentation and optimize later when ideas are approved, but we had none of above and we were quite sure about the mechanics. I have taken the challenge and the results were quite stunning.
The problem was that we had map composed from tiles, about 60x60. Map was loaded from Texture and read by GetPixel. Each tile was a pixel in big picture - unit of territory - and was glowing in numerous colors depending on the nation it belonged. Since it was jam and we had act quick whole map was assembled from prefabs in Unity. Literally each tile was a GameObject with Renderer and run some logic in script every Update. Each tile also had Collider so points flying over the map could make raycasts and know on whose territory they are.
Things that I have tried:
1. Unity build-in Occlusion Culling
Early on camera seen only part of map. That caused first red bulb to fire and after short glance at Rendering Stats it became clear that number of drawn objects is an issue. It was first thing I have tried because required minimum effort and changes in project - jam ended in several hours! Unity occlusion culling works only on static objects placed in editor. Tiles were generated dynamically at start but heck, lets generate some and save as prefab for testing. On map 10x10 effect was very appealing - stable frame rate at ~60 FPS and reasonable amount of draw calls. But on map 60x60 it was not. Occlusion culling was even grouping together multiple tiles in clusters but visible tiles were heavy enough to freeze game at ~22 FPS. If you have many small object always seen by camera occlusion culling is not so helpful.
2. Checking if Unity can Batch Geometry
Double check if Unity can do the hard work. I would gone mad if one magical check box could solve the riddle and make our game run smoothly at ~30 FPS. Tiles were Quads so were small enough to be batched and used only 2 materials, we could even use 1. Tested on prefab created for occlusion because Static Batching has less constraints over Dynamic Batching and tiles actually remain constant transform. Everything seemed to be in place but FPS was still low. It was not very effective.
3. Cutting corners with Screen Effect
Think out of the box - maybe it is possible to make something light that look just like big pixelated tiles. The idea was dropped because game's appeal was too dependent on accurately glowing pixels and their color.
4. Draw whole map on one quad
Nothing easy worked so far I came up with craziest idea. Do you know shadertoy? Yep! It could work but I realized that there is no way to input so big matrix/vector of uniforms for changing tile's color - nation possession changing through game. But.. there was a way, a bit different...
5. Final approach - Generate Mesh from script and update vertex color
Plan was to create whole mesh from script, update frame update vertex color according to our formula for glowing pixels and draw it with custom shader. Mesh required vertex(position), triangles(indicies), uv and additionaly color to supply data needed for shader for rendering. All in format and order familiar to OpenGL or DirectX. Each tile was made from two triangles. To get flat shading on faces and avoid color interpolation between adjacent tiles vertices were duplicated for each tile. Standard Unity shaders do not support vertex color but it is available in appdata_full and can be used with Surface Shaders.
Mesh generation was pretty fast even for greater dimensions like 100x100 it went in a blink of an eye. Reading pixels by Texture.GetPixel() took on average 7ms - 12ms (lowest measured was 3ms) and creating whole map took even half minute, very slow.
In fact we were reading two textures - one for nations colors and second for land/sea mask. I ended up storing mask in alpha channel of nations image so had one texture read instead of two but the numbers stayed the same - maybe textures were already buffered and it will be felt after fresh start.
Jam made me focus only on runtime, but I do have some ideas for loading too, maybe next time. The way Unity detects mesh array made me lose some hours of trials and errors. It seems only assignment of new array works. I tried directly setting values by index in color array, assigning same list I reused after generation or reusing same array every Update and nothing worked.
It was faster than I expected. Such generated mesh turned out to be amazingly fast in rendering. At about 120k vertices there was nearly none overhead, 1 MeshRenderer, 1 Material = 1 draw call, 60+ FPS, Whoo ho! I was stunned how effective it turned out to be. Jam version of game now runs ~30FPS because at end he 3D chart of victims appear and in each tile and every bars still is separate GameObject... But wait. In Unity 5, UV3 and UV4 channels were introduced to Mesh and even if we utilize UV1 and UV2 for per tile texture(1) and whole map texture(2) we can dynamically control the height of each tile in very elegant way. I just have to do some research in using additional UVs in Unity shaders.
For now I am extremely pleased with the results and tile map generator technique seems to be quite universal. Only downside is game must be refactored to use this mesh and some code got duplicated for now. Improving speed of rendering gives a lot of fun and satisfaction. There are more things to find out especially in Polytegy.
PS: I know there is profiler and that stats measurements may be off but sometimes it is enough just to know the estimation in orders of magnitude.