Hey everyone! This is my first official Zombox update in a long, long time.
I left off in March, having finished modelling most of the craftable structures in the game. But one big thing was looming pretty far over my head….
If any of you are game developers yourselves, you’ll know that ‘draw calls’ are the bane of many developers existences (especially those targeting mobile platforms). A draw call is basically an instruction sent to a device’s video card, that tells it to render some geometry to the screen. So, for example, if you have five separate objects you’d like to render, then that will require five separate draw calls (assuming the game engine doesn’t implement any type of batching system). Draw calls are ‘expensive’, which means that they take a lot of processing time to send to the graphics card, and for that reason you always want as few draw calls as possible.
Unity’s rendering pipeline includes a batching system, that can group objects and send them together as a single draw call. The batching system has many quirks, the biggest one being that it can only attach a very small amount of geometry together for each ‘batch’. Up until now it was what I was relying on to render the Zombox world.
However, with the addition of the crafting system into Zombox….I was presented with a pretty major problem: if the user is able to create objects anywhere in the world, then there is nothing stopping them from creating tons of different objects on screen in the same area. Typically, any location within the city in Zombox (where the user hasn’t crafted anything) takes about 30-40 draw calls to render (that’s excluding draw calls for the GUI). That’s already pretty heavy….but if the user were to craft a bunch of objects in that area, that number could increase way past what most mobile devices could handle at an acceptable framerate (even with built-in batching). We’re talking possibly over a hundred draw calls just to render the scene geometry alone! That’s not including zombies or NPCs or even the main character! That’s totally unacceptable and has been a major roadblock in the game’s development.
Earlier in the year I attended a Unity meetup in Toronto where Jim McGinley spoke about procedural generation in his game Endlight. He presented a novel solution for quickly updating meshes in Unity with thousands of changing vertices and arbitrary vertex counts (a process that is normally very slow when vertex counts change): at the start of the game, he generated a separate mesh for each possible vertex count, up to a max vertex count (so if he knew his biggest mesh would have 1000 vertices, he generated 1000 meshes and set their vertex counts from 1 to 1000, respectively). Then when he quickly needed to generate a mesh with, say, 874 vertices, he would grab the pre-generated mesh with 874 vertices, pop his new vertices into it, and hide all the other meshes. This prevented him from having to re-send new meshes to the GPU each frame because all of those pre-generated meshes were already in the GPU, just waiting for their vertex lists to be updated. And the whole thing would only take 1 draw call to render.
Now, that solution worked great for him, but it is also quite memory intensive — and Zombox needs to conserve memory to make room for all of the other procedural content that gets loaded. So generating 1000 meshes isn’t a viable option for me….but it did lead me to come up with the solution I’m using now. Here’s how my current solution works:
- The scene geometry is imported with a custom tool to automatically cache all base city meshes for quick reading of their vertex/triangle lists
- When the game starts, I generate about a dozen meshes in a mesh pool, each with a few thousand triangles/verts. The lower the vertex count of each mesh, the less processing time required to update it. However, the more meshes in the pool, the higher the resulting draw call count will be. So, setting the total triangle/vert count to a few thousand is a good balance.
- Each of the triangles in the mesh are added to a pool of “free” triangles
- When the city begins to load, instead of displaying each object with an individual MeshRenderer component (which would then be batched and sent to the GPU when it’s time to render), each object instead grabs however many triangles it needs from the pool, and positions the vertices of those triangles to match all of the vertices in its cached mesh. It does the same for its texture verts (UVs). Any triangles used to generate the object are then removed from the “free” triangle pool
- If, at any point, the “free” triangle pool ever gets used up before all base city meshes are displayed, I simply add a new mesh to the mesh pool and pop its triangles into the triangle pool, and keep going. No matter how many objects a user crafts on-screen, I’ll never need more than about 20 pooled meshes to draw them all, considering that on-screen vertex counts can only ever get so high before there’s no more room to craft things in screen-space
- Any time an object is unloaded from the game, its pool triangles are added back to the “free” triangle pool, and all of the verts that were used are set to be degenerate (which causes them not to render anymore)
- Any time an object is updated, I cycle through its pool triangles and just update their vertex locations
Now, that’s a pretty big mouthful of information — what does it entail? Well it means that I can render the entire Zombox world, no matter how many objects the user crafts, in about 12-20 draw calls….which is pretty amazing!
That said, there is a performance hit that occurs by doing all my custom batching/pooling operations. But, by spreading those operations out over a few frames and not doing all of the geometry calculations at once, the hit is small enough that it doesn’t affect the game’s framerate. So it’s a pretty great solution to a pretty massive problem! Caching all of my mesh data beforehand also means that I don’t need to do any allocations per frame in order to read mesh triangle/vertex data. The downside to caching is that it means you have to create a duplicate of your mesh data in memory, on top of what Unity already caches under the hood. My solution to that was to scrap FBX import of the city geometry all together and write my own mesh import system, which means I’m only ever holding 1 copy of each city mesh in memory at once, and I still don’t need to do any extra allocations to read geometry data.
So, as you can see, I’ve been busy despite my silence! But the results have been worth it.
Not everything I’ve been working on has been under the hood, though. I’ve also been working on the crafting interface and getting all the new craftable structures implemented into the game! The crafting interface (that allows you to place and rotate objects you want to craft) is still a work in progress, but you can see a result of some construction in the image at the top of this post.
Thanks to everyone for you continued support, and look forward to more Zombox updates soon!