Performance is critical for mobile games, especially for fast-paced action games. As you probably know, we are currently making a pinball game for iOS / Android. It features fast physics combined with fully animated characters and a 3D world. Any pinball game needs 60 frames per seconds of performance, and we need to squeeze every drop out of the little CPUs of our target devices to achieve that.
So the challenge for us is to keep the game running smoothly at (at least) 60 frames per second. Any drop below 60 fps will cut the framerate in half! Switching from 60 fps to 30 fps would mean a serious hick-up, and it would kill the “feel” of the game if it happens too much.
To top it off, we want to support the ancient iPhone 3GS, and we need at least a steady 30 frames per second for that.
Above: Momo and Fry are beating up two owl bandits – Four characters on screen at the same time.
So performance is critical. How do you approach this when you are making an iPhone / Android game with Unity? Here are four Unity tools we used to optimize the framerate of the iPhone / Android versions of Momonga.
1. Use The Performance Profiler
The first thing to look at when you want to improve the performance game is the Unity Profiler. It is a Unity Pro feature that lets you analyze performance bottlenecks. The Profiler is an invaluable tool. With it, you can determine where any framerate issues are coming from. You run the game on your target device, and run the profiler on your PC. When you launch the game, the Profiler starts pumping out performance data.
The good part about this is that even when profiling an iPhone or iPad, you can run the Profiler from a windows machine.
To use the Profiler on your mobile devices, simply build the game in Developer mode. From the Unity documentation:
To be able to connect to a player, the player must be launched with the Development Build checkbox found in the Build Settings dialog. From here it is also possible to tick a checkbox to make the Editor and Player Autoconnect at startup.
The Profiler shows a graph of the CPU usage while you play the game. Simply keep your device connected to your development machine, and play the game. It shows all your bottlenecks and hick-ups, in real-time. The Profiler categorizes the activity in a couple of areas: Rendering, Scripts, Physics, GarbageCollector, VSync and Others.
For mobile games, the issues are usually related to rendering. For Momonga, the rendering takes up roughly 40% of the CPU. This is an area we have to be careful with: we try to cut back on vertex and poly count.
There is the occasional spike in scripts when a scene is loading – but it is not unusual to have a lower framerate in a loading screen. You can still see some spikes, which are mostly related to physics. This will be our next focus area if we want to further improve the performance.
2. Static batching
Static batching is a feature of Unity that saves a lot of CPU cycles. It works like this.
Every time an object gets rendered there is a “Draw Call” – basically a command to the CPU or GPU that the object needs to get rendered. Unity issues several drawcalls and overlays them on top of each other. This is what makes the scene complete. However, each Draw Call has CPU overhead, and you want to minimize the Draw Calls as much as possible. Compare a draw call to a blank canvas on which an object gets painted. Would you rather have 1 canvas to paint on, or 50? I thought so.
This is where Batching comes in. It makes sure that no unnecessary Draw Calls are used. Batching comes in two flavours: Static and Dynamic.
Static gives you the best performance, so we always try to use Static Batching. Learn the details of it here.
To effectively use static batching you want to have as few different materials as possible. To achieve this you could combine all your materials in one big texture. We have opted to use vertex colors and have one pattern texture for all objects. Vertex coloring allows you to give each vertex a colour, which removes the need for real-time lighting. The colors are painted directly on the models by our artists. This means all our props and environment objects use the same material. And that’s great for Static Batching.
Above: Static Batching in action. Note these objects are rendered with a single Draw Call.
Using vertex colors only looks a bit dull. So we have a couple of pattern textures which are multiplied with the vertex colors to give the objects a bit more “texture”.
Above: Vertex colors and a pattern texture.
The last step is to add a lightmap to the scene. Because we hardly use any texture memory for the objects themselves, we can have a fairly detailed lightmap without running into memory issues.
Static Batching requires you to set the flag “Static” in the Object Properties panel. It can only be used on objects that do not move, rotate or scale in the scene.
3. Dynamic Batching
When Static Batching is not an option, Dynamic Batching can save the day. As a matter of fact, it is always active on objects that are not static. To use it you just have to use as little objects as possible with as little vertices as possible.
Batching dynamic objects has certain overhead per vertex, so batching is applied only to meshes containing less than 900 vertex attributes in total.
Our shader is using Vertex Position, UV and Colors. So we can have upto 300 verts per object.
Some useful tips from the Unity manual:
- Batching dynamic objects has certain overhead per vertex, so batching is applied only to meshes containing less than 900 vertex attributes in total.
- If your shader is using Vertex Position, Normal and single UV, then you can batch up to 300 verts and if your shader is using Vertex Position, Normal, UV0, UV1 and Tangent, then only 180 verts.
- Don’t use scale. Objects with scale (1,1,1) and (2,2,2) won’t batch.
- Uniformly scaled objects won’t be batched with non-uniformly scaled ones.
- Objects with scale (1,1,1) and (1,2,1) won’t be batched. On the other hand (1,2,1) and (1,3,1) will be.
- Using different material instances will cause batching to fail.
- Objects with lightmaps have additional (hidden) material parameter: offset/scale in lightmap, so lightmapped objects won’t be batched (unless they point to same portions of lightmap)
- Multi-pass shaders will break batching. E.g. Almost all unity shaders supports several lights in forward rendering, effectively doing additional pass for them
- Using instances of a prefab automatically are using the same mesh and material.
And a little tip on top of that: If an object has an animation but there are parts that never move, you can mark that part as static and it will not interfere with the animation.
Dynamic Batching is very useful for star pickups and other small objects that are animated but are otherwise the same in the scene.
4. Audio Optimizations
We suffered from some serious performance issues, and after some digging it turned out that the audio was causing the trouble.
The Unity Component Documentation has this to say about audioclips:
As a general rule of thumb, Compressed audio (or modules) are best for long files like background music or dialog, while Native is better for short sound effects.
This is good advice, but there is a catch. We found that having the incorrect compression settings can still mess up your memory usage or cause CPU spikes while playing.
Carefully reading the unity manual helped us locate the problems:
“Note that only one hardware audio stream can be decompressed at a time.”
We were seeing CPU spikes because the Iphone can only decompress one audiofile at a time using the hardware decoder.
This can be solved by using the “Decompress On Load” setting. Performance increases, but it causes the memory footprint to skyrocket. So only use this when it’s really needed.
These are our rules of thumb for optimal performance when it comes to different types of audio:
- Short Clips – Native
- Longer (or looping) clips – compressed in memory
- Music – Stream from disc
- Files which consistently cause CPU spikes – Decompress on load
If you follow these rules, audio should not give you much trouble. Except if you launch a whole armada of sound files all at once.
The most important thing for mobile games is to keep your triangles, vertices and drawcalls as low as possible. And if you run into performance troubles, Unity provides a lot of tools to get it back under control. It will surely pay off to read the documentation.
Those are all the tips we have for now!
What are your little tricks to improve performance? Let us know in the comments!