A couple of years ago, Michiel wrote a blog post about how Powargrid is drawn on screen. It still is a good introduction to how video games get drawn and what kind of optimizations are possible, so it certainly deserves a spot in Best of the Blog.
Paint ALL THE THINGS (and make it quick)
What exactly does it take to draw one screenful of Powargrid? Quite a lot actually, which is why I've been working on making it faster. To make the game run smoothly, we would like to redraw the screen 60 times per second. A screen redraw is also called a "frame", so it's often referred to as "60 fps". Most monitors only update 60 times per second, so faster than that doesn't really buy you anything.
Simplifying a little (well... a lot), drawing the screen involves sending the geometry of everything in the scene to the graphics card, then telling it how to colour in the bits.
Simplifying a little (well... a lot), drawing the screen involves sending the geometry of everything in the scene to the graphics card, then telling it how to colour in the bits.
This involves your CPU repeatedly telling your graphics card, "take this can of paint and paint that wall." Now this is magic cartoon paint: you can have plain coloured paint, checkerboard paint, grass-and-flowers paint, and it can be dull paint, glossy paint, transparent paint, etc. In computer graphics, it's called a "material".
Each of these instructions to draw something using a particular material is a "draw call". Now, the graphics card is a very, very fast painter, but each draw call (changing paint cans) involves a noticeable amount of work. In short, it's much faster to paint many walls with the same paint (material) than it is to change cans for every wall.
In the beginning, we were painting every object on the screen with its own can of paint. That's not just each building - each building consists of at least four legs and a body. Even though all the legs of a building have the same colour, they still got their own can of paint, so we were quickly up to thousands of draw calls, making the game run quite slowly unless you had a fast CPU. In the meantime, the graphics card was sitting there, waiting for the CPU to hand it more paint cans.
The first step we took was to redo our models and make sure all the matching bits (like the legs) actually share a single material. Then, Unity can use dynamic batching to draw all the blue bits with the same paint can (one single draw call). Then, all the red bits get drawn in one go, etc. Big win!
Each of these instructions to draw something using a particular material is a "draw call". Now, the graphics card is a very, very fast painter, but each draw call (changing paint cans) involves a noticeable amount of work. In short, it's much faster to paint many walls with the same paint (material) than it is to change cans for every wall.
In the beginning, we were painting every object on the screen with its own can of paint. That's not just each building - each building consists of at least four legs and a body. Even though all the legs of a building have the same colour, they still got their own can of paint, so we were quickly up to thousands of draw calls, making the game run quite slowly unless you had a fast CPU. In the meantime, the graphics card was sitting there, waiting for the CPU to hand it more paint cans.
The first step we took was to redo our models and make sure all the matching bits (like the legs) actually share a single material. Then, Unity can use dynamic batching to draw all the blue bits with the same paint can (one single draw call). Then, all the red bits get drawn in one go, etc. Big win!
Shadows
However, then we wanted to use shadows, because shadows look neat. In our case, with a single light (the sun) casting shadows, everything needs to be drawn twice: once from the viewpoint of the sun, to know which parts of the scene are in shadow, and then from the viewpoint of the camera.
Unfortunately, with dozens of buildings on the play field, all consisting of several pieces, rendering shadows for all those little bits meant it was still too slow. We couldn't just rely on Unity combining the draw calls for us. We needed to draw less things! We don't actually have to leave anything out, because graphics cards are so fast these days. We just need to draw bigger chunks at a time.
Unfortunately, with dozens of buildings on the play field, all consisting of several pieces, rendering shadows for all those little bits meant it was still too slow. We couldn't just rely on Unity combining the draw calls for us. We needed to draw less things! We don't actually have to leave anything out, because graphics cards are so fast these days. We just need to draw bigger chunks at a time.
Mesh Combining
So that's what we do now: we take all the bits that make up the game grid, and combine them into larger parts that share the same material. For example, all the powered red legs get combined into one big "everything that's bright red" object.
When something changes (say, a building gets blown up) we quickly rebuild the combined mesh to take the changes into account. Some details were a bit tricky, and I was occasionally looking at "the ghost of buildings past" because something had been destroyed but was still in the combined mesh, or stuff would turn invisible for a fraction of a second as the mesh was rebuilt. However, in the end it was only about 200 lines of code, and the results are nothing to sneeze at:
And that's just a simple scene, at the start of the first campaign mission. Without combining objects, the CPU had to make 574 draw calls, even though 733 were prevented by dynamic batching. With combining, the exact same scene takes only 179 draw calls!
So there you have it. Powargrid should run a whole lot smoother, with nice shadows, even if you don't have a very fast PC. Enjoy!
-- Michiel
So there you have it. Powargrid should run a whole lot smoother, with nice shadows, even if you don't have a very fast PC. Enjoy!
-- Michiel