When working with mobile platforms the performance can be a nightmare, especially when you have to maintain support for older crap devices. With some good practices during the development, you can avoid some problems in the future.
If you have a framework or a common / shared library, then making it performant is essential but sometimes you can actually be losing time as the improvement has no real effect or it can also have the opposite effect. With this in mind, always test your improvements.
– Use 30 FPS on Application.targetFramerate (change on a Script) if possible. This will help to save some battery as you don’t need to render everything 60 times per second (Unity render the entire screen at X FPS, does not matter if everything in the screen is totally static). If your game uses a lot of animations your game can look laggy as a side effect so change it and compile to a device to see if it looks nice.
– Go to Player Settings > Resolution and Presentation and tick “Disable Depth and Stencil” if you don’t need them (if you have a 2D game for example and if you don’t use 2D masks);
– If you use more solid colours (without gradients for example) and you don’t see colour banding (the gradients are not smooth), you can also untick “Use 32-bit Display Buffer”.
Draw Calls: keep them as low as possible:
Since I’ll talk more about mobile platforms, it is more common to be fillrate bound. In this case your screen pixels will be taken into account and also which shader you use, how deep you are drawing stuff on top of each other (overdraw).
– I like to think about the rendering process like digging through layers of rock. For every individual texture, you’ll have a separate layer. If you group as many textures as you can on a texture atlas you’ll end up with fewer layers, which means that it will be possible to render more effectively.
An example of good use of this technique would be frame-by-frame animations that use several different textures. So make sure to use a sprite sheet that contains all your frames in one texture (as mentioned before: atlas). Unity already have a tool called Sprite Packer which does this job for you;
– Combine meshes into one. Maybe this value is higher already, but when I first wrote this article, it was considered good practice to keep a value of 4000 tris or below per group and combine those meshes into one, same reason as above … grouping stuff to be more effective when processing through it;
– Use the simplest shader available. I always like to try the highly optimised mobile shaders;
Realtime Lights and shadows – mobile nightmare:
– Maybe you can achieve some acceptable performance using low res. hard-shadows. Usually, this is very hard to optimise and achieve a nice effect using the standard techniques, think about other ways of doing your shadows … for example using a simple black, transparent circular PNG texture instead, maybe that’s enough to give the feel that you need for your game? You can also paint some light effects directly on your textures so you can fake the real time lights (light baking). Be creative 🙂
– If you don’t need realtime lights, just use a mobile unlit shader and be happy.
Textures – keep them compressed:
– Use power of two atlases to be able to use compression;
– Textures generally are the main villains when it comes to file size. This will obviously imply in other problems too, like memory usage, been able to use OTA (Over The Air) downloads etc. Keep your textures as small as possible using a small maxSize, also use the highest acceptable compression. Usually for textures with predominant solid colours I use 16 bits compression which keeps the quality and reduces a lot of the texture impact on the final file size, for textures with gradients the 16 bits compression will not work well as you will lose a lot of color information, try to use a more powerful compression algorithm like PVTRC for iOS (or the ETC equivalent for Android) and test the result, if it looks like crap, use a lower compression level until you have the best balance between compression vs quality. The important thing is to always test on different devices, especially on Android.
Applying compression will help you with the file size and also with memory usage.
– Always group your textures by compression and texture type / format. What I usually do here if to group all my RGB textures under the same tag for the Sprite Packer. I usually override the texture settings for each platform in order to be able to set a specific compression format.
Example: Textures without alpha channel will use a RGB 16 bit compression format and textures with alpha channel, RGBA 16 bit.
When playing sounds:
– Avoid playing lots of sounds at the same time;
– Use the correct compression for each type of sound. For example for long soundtracks use “Streaming”, for small sound effects use “Compressed in memory”. Keep in mind that you need to balance between performance and memory usage;
– Avoid using this as everything inside this folder will be included in the build – doesn’t matter if you use it or not in your game. This can represent a significant increase in build size. Another reason to avoid this is a performance hit when your app startup as Unity will go through every single asset inside the resources folder to prepare them for use … this will happen in the first frame and it will get worse as you have more and more assets in the Resources folder.
TL;DR don’t use it, but should be OK if you have only some small files to hold settings for example.
Doing stuff repeatedly, do you really need to do it?
– Avoid calculating unnecessary stuff when you don’t need it… this is pretty obvious but is one of the most common mistakes. Cache what is possible to cache when you know the values beforehand, specially for complex mathematical operations inside a for loop.
– Sometimes I like to use Co-Routines or Invokes, with a bigger interval between the loops where possible. For example a Score label animation: you don’t need to update it at each frame, right? Maybe a 0.3f interval will do just fine? *Careful about Co-Routines though as they do not work if the GO is not active;
– Using a lot of “new” this and “new” that on your loops? Again, try to create your stuff outside your loop when possible and avoid extra costs for creating new objects on every single loop cycle;
Static GameObjects, Rigidbodies and Colliders:
– Static GameObjects are GameObjects with a collider that are not intended to move, like walls, floor, buildings and so on.
The golden rule here is: DON’T MOVE, SCALE OR ROTATE IT. Attach a Rigidbody with Kinematics activated if you plan to move your static GameObject and be sure to use rigidbody.MovePosition.
– If you have a GameObject that will not move, scale, rotate etc. (like buildings, floor, walls) be sure to also set them as “Static“. There’s a checkbox in the top right corner of Unity Editor or you can use gameObject.isStatic = true;
– If you will not use physics or collisions on your object, there’s no need for a Collider, neither a Rigidbody;
– If you need a Collider in your GameObject to send events through OnTriggerEnter, keep isTrigger activated and the collider will be ignored by the physics engine;
– If you need to manipulate your Rigidbody, do it inside FixedUpdate method;
– Always create local caches for your transforms, rigid bodies, materials etc. Accessing them directly will cost you a lot;
– Disable your Scripts when your object is not visible using the OnBecameInvisible Method;
Creating and Destroying? No! Use pooling:
– Avoid Destroying / Creating Colliders, use collider.enable instead and cache / pool your Colliders / GOs, if this is the case. By doing this you’ll work with the same instances and will not spend processing time on creating and destroying new ones;
– You can use static references but make a local cache of them if you are using them a lot;
Canvas and Unity UI:
This one probably deserves an unique post just for it … but the main optimisations that I’ve found so far are:
– Don’t use the “Best Fit” feature on Text fields as this will recreate the font atlas. I like to stick with 3 or 4 different font size as that also contributes for a better and smoother interface.
– Avoid deepness in the canvas hierarchy. Deeper = harder to process as the relation between all transforms will need to be recalculated over all the hierarchical tree. This is not only a performance thing, having a super deep hierarchy reduces the maintainability as well.
Another problem that you can have with deep hierarchies is an increase in the Draw Calls. Unity will render it all in layers in order to present it correctly on the screen. Sometimes you’ll be able to reduce the Draw Calls if you manage to put your text fields together, on top of all your textures in the hierarchy order (so the text fields will be rendered together if they share the same font texture and material) but if you have overlapping objects that contain text fields and images you’ll see a considerable increase in Draw Calls as soon as your objects start to overlap because Unity will be checking for overlapping meshes in order to draw them in the correct order.
– Avoid having a lot of canvases active at the same time as they will increase the amount of Draw Calls for the same reason as above.
– Avoid using several different font types. I usually keep it at maximum of 2 fonts. (as a bonus, your game will probably look nicer with less fonts)
– Avoid using several different font sizes as well. If the atlas is generated at runtime, you may have some spikes.
_ I’ll keep this updated as soon I remember or learn another tip (or have time to post them here)
_ You should always test your improvements as sometimes they can have the reverse effect on performance.
_ If you haven’t checked the Unity’s learning resources yet, do it now. There’s a bunch of good information there and with a lot of depth as well. Check this “Unity Best Practices” tutorials for example.