Details, please!
Sure! This may run a little long, though. So my game involves a geodesic sphere dual (hexagon and pentagon tiles).
Note: not like my avatar. I'm not ready to join Screenshot Saturday, but if anyone wants a look, I'm happy to send a screenshot in confidence via PM. Each tile is layered and players can add or remove layers. Each layer is one of several types, such as dirt or sand. There are hundreds of tiles and each tile can have several layers, so it would be taxing to render each layer as a separate mesh. Layers of the same type share the same render material, so instead I store every layer of the same type in a single mesh. Because players can add or remove layers, the meshes have to be modified at runtime. Unfortunately, the method that did this was super slow. At 2700ms, I was way over the 16ms maximum for 60fps.
S
TEP 0 - Status Quo
It was even more inefficient than this in the past, but that was before I had Unity Pro (thanks, Nintendo =] ). Once I figured out how to use the profiler, I was able to identify potential areas of improvement.
Time: 2700ms
STEP 1 - Removing Redundancy
To calculate the vertices for a layer, I need the vertices of the tile on the surface of the geodesic sphere. The vertices of a tile are used several times during mesh generation. I was using a function call for this, so the vertices were being calculated multiple times. I improved upon this by storing the vertices in a variable so I could retrieve them without recalculating.
Time: 300ms
STEP 2 - Increased Rendering Overhead
Each layer in a hexagon tile consists of a hexagon for the top and rectangles on each side. The bottom layer in a tile does not have sides because they will always be invisible. The top layer in a tile is the only layer whose top is rendered, because the tops of the other layers are concealed. Pentagons are special tiles that cannot have layers added to them, so they are only rendered with a single Pentagon. With this setup, I had to recreate all of the meshes whenever a layer was added or removed. By including the tops of concealed layers when creating meshes, I only needed to update the mesh containing layers of the same type as the layer that was just added or removed. I thought this would increase the rendering time, but I was still under 1ms for rendering time, so it was no sweat. The culler probably takes care of them anyway.
Time: 200ms
STEP 3 - Reduced Mesh Count
I lied. I actually had two meshes per layer type. One for the hexagons on tops of layers and one for the rectangles on the sides. I also had one mesh for the pentagon tiles. The reason for this is it let me generate the UVs for the entire mesh at once. I changed this to one mesh per layer type. This reduced the number of draw calls by 2, but seemed to have no impact on performance at all. The UVs for the sides got really messed up as a result of this, but hey, I can fix that later. My code ended up a lot cleaner so I kept it.
Time: 200ms
STEP 4 - Time Slicing
It had gotten pretty fast since the last step. 200ms is quick enough to feel responsive to the player. The problem was that the framerate crashed in this moment. So why not divide the task up over several frames? I used coroutines to handle this. I could spread out the mesh generation over as many frames as required. Unfortunately, there was another 150ms that could not be time sliced.
Time: 150ms
STEP 5 - Many Mesh Colliders
Some of you may be wondering why I am generating meshes instead of using prefabs. Although they look regular, the tiles of a geodesic sphere are irregular, so mesh generation is necessary. For the same reason, I must use mesh colliders for tile selection. I generated one giant mesh collider that encompassed the entire geodesic sphere and its layers. I raycast from the mouse into the collider and used the index of the hit triangle to determine the tile. I had to recreate this mesh whenever I added or removed a tile. Generating the mesh was quick, but afterwards the collider automatically triggered "physX rebake" so it could use the updated geometry. This procedure was responsible for nearly all of the 150ms. This was an internal Unity feature, so I could not time slice it. The solution was to just make a mesh collider for every tile. This way it only had to rebake the physX for a small mesh. I figured that having hundreds of mesh colliders in the scene would make rendering slow, but it was fine. Actually, it made the editor playback view really choppy, but this choppiness was not present in builds. The time for adding/removing a layer was now negligible.
Time: Instant (Okay, the time isn't 0.00ms, but I haven't bothered to calculate it because the game is steady 300+fps on my four-year-old laptop. It happens QUICKLY.)
STEP 6 - Deep Refactoring
I spent several hours refactoring the way I store tiles. Before, I stored tiles as integers, and used their indices in function calls to get their properties. I went ahead and gave them their own class and stored them as Tile objects, with the properties stored inside. This made my code a lot cleaner but didn't increase performance of the game. As a side effect, this somehow got the editor view to a normal framerate. Not sure how that happened.
Time: Still Instant