In part 3 of our series on Bubble Byte’s QR Invaders app, Jon Cebula talks about tweaking performance graphics to give users a consistent, unique gaming experience. Check out part 1 and part 2 if you missed them!
Jon Cebula: Level loading for QR Invaders gets more complex for later stages and larger QR Codes, as they introduce unique pattern variations you don’t generally find on smaller codes. This in itself introduced a cool feature and tricky challenge.
In the initial beta phase, the background QR Code was made up of hundreds of tiny sprites. On a very simple QR Code that the game reads as QR Level 1, there are 23×23 tiles, or 529 sprites. The invaders (made up of 36 tiny sprites to allow for explosion effects) add another 1,440 sprites. Factoring in bullets and other sprites, we have around 2,000 sprites for even the simplest QR code. An issue arose when pushing all of this data onto the vertex buffers.
On the lower levels, initializing vertex buffers took 2-5 seconds. On QR Level 13 (71×71 tiles), we were creating around 5,700 sprites, which ended up taking three minutes to load into the vertex buffer while dropping the frame rate to around 15 FPS. As you can imagine, this doesn’t produce a very positive gaming experience for the user.
Handling these large QR Levels was going to be a challenge. I decided to test the biggest QR Code you can create, a QR Level 40. This was a huge 179×179 tiles, meaning it would take 32,041 sprites just to render the background QR Code. It took about 35 minutes for QR Level 40 to load on the devices and ran at around 1 FPS.
The entire process needed a revamp.
The challenge was that the QR Code in the background isn’t just a big, static piece. Each square actually opens to allow the invaders to fly out from, so how could I make such a huge dynamic mesh?
The second issue was that each square actually has 4 triangles because they are double sided; so the mesh would end up being 128,164 triangles: a huge mesh to try and make dynamic.
If I could solve these two challenges, then it would improve in-game performance and load time dramatically. Here is how I did it:
First, I had to work out the optimal performance by looking at the variations of different enemies, taking into account the number that would show onscreen at any one time.
On QR Level 40, we have 12 different enemies:
- 2 of the 12 take up 1 square;
- 3 take up 2 squares;
- 3 others take up 4 squares; and
- 4 take up 9 squares.
I could allow for 315 sprites to cover all the different hatches that could open in a single attack phase. These would form all the hatches that will open, the remaining hatches would be created with a series of dynamic meshes. I had to use multiple dynamic meshes for 2 reasons:
- Unity has a max number of vertices limit of 65,000, and my mesh for a QR Level 40 has 256,328.
- I needed to maintain performance.
I found the best performance was around the 13,000 vertex mark, and used a formula to determine the best number, which varied slightly by QR Level so that each mesh ends up creating a perfect rectangle rather than an equal segment.
Using this approach, I could animate 315 sprites until I run out of one of the enemy batches and then dynamically recreate the batches. Each mesh was recreated over a series of frames to keep the CPU times low and maintain a good frame rate.
The result changed loading times from 35 minutes to 2-5 seconds.
When you initially play your first game, the load is closer to the 3-5 second mark, but I combat this by keeping all of the sprites as a child of my Game class. This class is created on the splash screen and, using DontDestroyOnLoad, keeps this Game object and any of its children available to use throughout all my scenes.
When someone plays their second game, all of the sprites are already created and in GPU memory, dropping load times to less than 1 second.
I have plans to optimize this further by changing the invaders to use dynamic meshes, but for saving the few seconds of load time this can wait until a later release.
I came across a strange situation with one of the shaders that only affected normal mapped shaders. Normal mapping allows you to create the illusion of tiny bumps and variations on the surface of a mesh. They can really bring your game to life and can perform very well. During boss battles there are normally around 10 normal mapped meshes on screen at once.
The issue I was getting was that in the Unity editor, these shaders were bright and shiny, but once deployed to a BlackBerry device they became dark and dull.
Unity has a great feature that lets you work around these sort of quirks called Platform Dependent Compilation (though they are commonly called Compiler Directives).
Using Compiler Directives, you can write a piece of code and tell Unity to run a slightly altered version depending on the platform. Here is what I updated to fix my shader:
#if UNITY_BB && !UNITY_EDITOR
//Added because shader was very dark on blackberry.
fixed diff <b>=</b> clamp<b>(</b>max <b>(</b>0<b>,</b> dot <b>(</b>s<b>.</b>Normal<b>,</b> lightDir<b>)),</b>0.25<b>,</b>1.0<b>);</b>
fixed diff <b>=</b> max <b>(</b>0<b>,</b> dot <b>(</b>s<b>.</b>Normal<b>,</b> lightDir<b>));</b>
fixed nh <b>=</b> max <b>(</b>0<b>,</b> dot <b>(</b>s<b>.</b>Normal<b>,</b>halfDir<b>));</b>
fixed spec <b>=</b> pow <b>(</b>nh<b>,</b> s<b>.</b>Specular<b>*</b>128<b>)</b> <b>*</b> s<b>.</b>Gloss<b>;</b>
As you can see, #if specifies which platform the code should target, so in essence, I am saying:
If targeting UNITY_BB (BlackBerry 10) and not UNITY_EDITOR (for when you are testing in the editor window) use the modified code, else use the original.
This allows you to cater for any quirks you come across. Some might say this should be handled internally, but to be honest, I prefer to have the control. Ultimately the issue here is caused by the differences in the way OpenGL and DirectX handle the shader, so the same fix should apply to all OpenGL based devices.
I want to note that all of the previously mentioned issues affected pretty much every device, not just BlackBerry 10 devices; with other platforms there are further issues still to contend with.
I’ve released for the touch-only BlackBerry 10 devices, but other devices are in the pipeline. I found Unity a pleasure to work with and was able to get great results in a very short timeframe.
The best part about building for BlackBerry was that the game worked just as it did within the editor for the main pieces. That, combined with the great support you get from the BlackBerry Developer Relations team, made publishing my first game a great experience.
My main advice for anyone looking to build their own game is to set realistic goals and timescales, and then double them. You’ll always hit some unexpected issues!
Remember, a mobile phone is different from the typical gaming console, so come up with a control system that works well for mobile, not just for consoles. There is nothing more frustrating than a great game that’s difficult to play.
I’ve already started my next project and BlackBerry 10 is already on my build list.
Thanks for reading!
About Bubble Byte