It is a widely held axiom among programmers that premature optimization is poison. If you let it get in your head, you can end up spending months optimizing code that isn’t even a problem and waste countless hours better spent actually writing code. Being the level-headed individual I am, I decided to engage in such a behavior over the past couple of weeks. I didn’t arrive at this point by impulse or recklessness though, and the results are pretty incredible so far. Let me explain how I got here.
Tiles, United
I have made my fair share of tile-based engines in the past. I’ve used a variety of different languages and frameworks to do it, and I thought Unity would be no different. In a way I was right, just not in the way I expected. Up to this point I had been using Unity’s sprite renderer component to draw all of my images onto the screen. After learning that I was limited to one such component per object, I immediately took the brute force approach and just started adding more game objects to the scene, each with a renderer for a single tile. With the lights turned off, this worked fine, and Unity batched all of the renders together into a single pass, and everything was fast and efficient. Unfortunately, I’m making heavy use of lighting for this game, and when you turn the lights on with this approach, things start to fall apart.
Initially it wasn’t all bad. I only had one renderer per terrain feature, which was just stretched across the size of feature. This made everything look terrible, and was a hack to get the scene up and running. I started making more materials, one for each unique size of terrain, and telling the shader to tile the image out. This worked great, but I ended up with an unmanageable amount of materials, and it didn’t seem like a very scalable solution. I opted to switch to a single game object with a child object for each tile, each holding a single sprite renderer. A much more viable approach to the problem, but I noticed a peculiar spike in my draw call count and a dip in my frame rate. It wasn’t much, so I ignored it; premature optimization is the devil, after all.
I intend to use about five layers to render the scenes in this game, and up to this point I had simply been using a sprite layer and the terrain layer. A solid black background makes for boring screenshots, and since I am trying to spread the word about this game early and often, I wanted something a bit more interesting to share. As I started to put in background, middle, and parallax layers, the funny little draw call spike turned into a hulking monster. One room, with only a handful of lights and a single character, was spiking to unreasonable levels. The draw call count was well over 300, and the frame rate on my computer was dropping dangerously close to 60. I have a pretty powerful computer, I can play AAA 3d titles with the settings maxed out and never drop below my refresh rate. My 2d game should not be running at 60 fps on this computer, and if it does, it probably won’t run very well on a more standard desktop. Something had to be done.
The Cusp of Trouble
I think it had to be done, anyway. I did no profiling on this, I didn’t test it on other computers to see if it was really that slow. For all I know it could have been just a fluke. Regardless, I panicked. I had no idea how to approach this problem, I didn’t understand how Unity’s sprite renderers did their batching or what the potential ramifications of pumping 10k game objects per room into the scene were. I started looking into it, and while it seemed to be a common problem, nobody ever really had a solution that would work with my lighting system. The only thing I could think of was to stop using Unity’s built-in 2D tools and just write a tile renderer myself.
After digging through Unity’s sparse documentation on the matter, and then looking up several examples of people building mesh geometry through code, I figured out how to build the meshes and send them to Unity to be rendered. Essentially what I have now is a series of textured quads that are all built as part of the same mesh, with each quad representing a single tile on the screen. If this sounds like a simple and obvious solution to the problem, it is, but that’s part of why I’m confident in this decision.
Worth It
This comes with a few nice benefits on the side; answers to problems I hadn’t even thought about addressing yet. First and foremost, it lets me create an atlas for my tiles. I wasn’t sure how to do this in Unity without creating a separate material for each sprite in the atlas. This would mean that the extra textures for lighting would have to be split apart, which defeats the purpose of using an atlas in the first place. It also gives me a nice, single object to use as the view for each layer of map data. This cuts down on extra game objects in the scene, extra components in the code, and the extra complexity of grouping and managing tiles within a room to help decide when to stop processing that area because it’s too far away.
I really wanted to let Unity handle this for me, but it seems their sprite system and 3d lighting just don’t mix. At this point I’m not really using any of Unity’s 2d tools any more, which is a bit frustrating as this was supposed to save me time. If it makes for a better product though, I guess I’m alright with that. I’m hoping this is the last bit of infrastructure I have left, and I should be able to start focusing on making this into an actual game instead of a tech demo with a story.