Recently, our studio has completed the development of a large update - Captain Antarctica: Endless Run - for devices on iOs. The hard work on updating affected performance, which turned out to be very low on weak devices. I struggled with this for a whole week and achieved at least 30 FPS, as well as a significant reduction in the size of the application. I want to tell you how I did it, well, and how not to do it.
The article is useful to any developers on Unity (not only project managers and technical specialists, but also just programmers, artists and designers), because it affects both optimization on Unity as a whole, and specifically optimization of 2d applications for mobile devices.

Everything's possible!
To begin with, every time I start optimization, I initially do not believe that something else can be optimized, especially if the project has already passed several optimization cycles before. But viewing the official Unity documentation, topics on forums, articles on the Internet makes me think of new possible improvements. Thus, I keep a special list in which the main ideas are written down on what can be optimized in a project on Unity, constantly updating it and first of all referring to it when it comes to optimization. These ideas I want to share with you. I hope this article will help you make your project much quicker.
Immediately indicate that the development was conducted on Unity 3.5.6, the target platform - Apple devices from iPhone 3GS and newer.
Basic rules
To begin with, I will give a few rules that I use when developing and optimizing.
1.
Do not optimize in advance .
This golden rule should be familiar to anyone who has ever done optimization. Recall the
80/20 rule :
80% of the benefit comes from 20% of the work .

The numbers are fairly arbitrary, but I meant this: most likely,
most of the optimizations that you are going to do at the initial stage of the project will most likely have
no effect on the final project as a whole .
')
However, there are a couple of exceptions to this rule, which are especially important when developing for mobile devices, because this rule is more suitable for PC projects. A PC is much more productive than mobile platforms and less limited in resources. So, exceptions:
- Suppose there is a code or construction that you already know how to write better, with less memory / processor cost, and so on. You know this from your own experience, because you have repeatedly optimized components of this kind, and you know that this leads to an increase in productivity. So why not write it right now? Usually such things are put together into a set of rules like “how to write code correctly and how to write is not necessary”, and a competent programmer constantly uses it to avoid mistakes in the future. Something similar has a place for artists, designers and so on.
- If there is an action that will be repeated many times, and you can optimize it from the very beginning, why not do it right away. Then everything will go automatically, and you will not need to correct the same thing several times. These include, for example, helper scripts that help designers speed up the tangible work with a group of objects.
2.
Find what needs to be optimized .
A few years ago I had such a situation: walk through the code, scenes, etc. and optimize everything that is possible, and then look at the performance - this is the
error of the novice optimizer .
First you need to find what slows down the system, on which there are performance jumps. This profiler helps a lot. Of course, it is quite conditional and it loads the system a little, but the benefits of it are undeniable! The Unity Pro has a built-in pro-faler, quite comfortable. But if you have a regular Unity, you can use the xCode profiler, or any other suitable one. Profiler helps to find the most loading code, shows the used memory, how much sounds load the system, DrawCall count on the end device and so on. Thus, before optimizing, run the application through the profiler. I think you will learn a lot of new things about your project)

What helped me solve a performance problem?
Even before running through the profiler, it was obvious that the weak point was in the
Draw Calls count. On average, the scene produced about 70 DrawCalls, which is fatal for devices on iPad1 and below.
Normal for them - 30-40 Draw Calls . You can see the Draw Calls count directly in the editor in the
Game-> Stats: window.

The number of Draw Calls shown in the editor is the same as that on the end devices. The profiler confirmed this. In general, it is very useful to watch these statistics, and not only for programmers, but also for designers, to find "tight" places in the game.
In our scenes, the grouping of several Draw Calls for the same material into one Draw Call worked poorly. This is called
Dynamic Batching . I started digging on the topic "how to lower the number of Draw Calls and improve their grouping." Below are the basic rules, adhering to which, you can get an acceptable amount of Draw Calls. Here are the ones that helped me a lot:
1.
Use atlases to combine multiple textures into one large one .
In fact, it is even more important that sprites / models use not only one common texture, but rather
one common material . It is in the number of different materials that the number of Draw Calls is measured (ideally). Therefore, in our project, the used images are always combined into atlases, divided into categories: objects used in all scenes, GUI objects, background and so on. Here is an example of such an atlas:

Such a partition will also be useful in the future for applying various settings to textures. But more about that later.
2.
Do not change Transform-> Scale .
Objects with a modified
Scale fall into a separate category, increasing the number of Draw Calls. I noticed this when I once again traced through the
Draw Call Batching document. That's what it means to reread;) Walking through the scene, I discovered a huge number of such objects:

- It turned out that designers have long increased some frequently used objects by 1.2 times through Scale right in the object subspace. As a result, we came to the decision to increase their size right in the texture. This also complied with the condition pixel-for-pixel , which is very important for a 2d game.
- There were objects that had the same image, but a different Scale . For such objects, a special script was written that translated the desired Scale from the Transform directly to the mesh used for the sprite, i.e. changed the mesh size and left Scale = (1, 1, 1) .
- Also, Scale was often used here to reflect an object, for example, Scale.x = -1 reflects an object from left to right. All such scales were replaced with their corresponding turns.
- Some other Scale objects were changed in animation, a couple of times unreasonably. Do not forget to check the animations , often changes in them are implicit, and can only be detected after launch.
As a result of eliminating almost all changes,
Scale managed to reduce the number of Draw Call almost
doubled ! True, these improvements took us a decent time, so
it is worth remembering Scale already at the initial stage of level design . And now in
Memo to the designers we have written in bold red:
Try to avoid changing Scale .
A few more tips (taken, including from the
Unity document ), how to reduce the number of Draw Calls:
- Static objects can be labeled as Static . Then Static Batching will be used (only in the Pro version), which will also help to reduce the number of Draw Calls.
- Try to use objects with the same material at the same distance from the camera . Example: we have differences in the distance of 10 units have already given 1-2 additional Draw Call. In this case, some special pattern was not identified, but I suspect that there is a relationship between the size of the camera, the size of objects, their distance from the camera and the number of Draw Calls. Experiment!
- Try to ensure that objects with different materials do not overlap each other . This also increases the number of Draw Call, especially for translucent objects.
- Multipass ( multi-pass ) shaders increase the number of draw call . We have not had such, but it will be useful to consider this in the future.
- Each particle system gives 1 Draw Cal l. (This is the old Unity 3.5.6 particle system, which we use to this day. What is the situation in Unity 4, I do not know). Therefore, if the screen simultaneously N particle systems - it is automatically at least N Draw Calls. Usually, the same effect can be achieved in different ways, including a smaller number of both particle systems and particles in the system. Often seen as novice effect designers use a huge number of particles (and the huge size of the particles themselves) to create a wow effect. At the same time, they do not think about performance (especially considering that all this is done in the editor on a PC) and usually fewer particles are enough to achieve the same effect.
Performance jumps

The second factor affecting performance is the so-called performance jump. Sometimes the game had “hangs” for 0.5-1 seconds, which of course was unacceptable and directly affected the gameplay. And this was observed even on the latest devices.
And in this case, the profiler helped! Here is a list of rules for reducing performance hops:
1.
Try not to use Instantiate () , especially for complex objects .
Performance jumps accounted for mainly the calls to
Instantiate () , which created new objects from the subs, or cloned existing ones. Moreover, some objects were very cumbersome, which influenced the time of their creation. Instead, we had to rewrite the system so that the objects were used again. Those. the state of the object after the end of use (or before use) was reduced to the initial one. It also helped to reduce the amount of memory used (since new objects no longer needed new memory) and the number of calls to
Destroy () .
2.
Minimize the number of calls to Destroy () .
Destroy (especially for large objects) almost always leads to memory manipulations. And it usually deplores performance. This rule is directly related to the rule above, because
Instantiate () /
Destroy () calls are usually associated. Thus, using objects anew made it unnecessary to destroy them.
3.
Minimize gameObject.SetActiveRecursively () calls .
For complex objects, the call can be very long, because it involves not just the activation of objects and their components, but in some cases the loading of the necessary resources.
4.
Minimize calls to Object.Find () .
I think it is not necessary to explain that the time of this operation depends on the number of objects on the stage. Functions like
GetComponent () are also included here.
5.
Minimize calls to Resources.UnloadUnusedAssets () and GC.Collect () .
Unity sometimes resorts to it itself if there is not enough memory to load a new resource or a request comes from the OS to free up unused memory. Thus, the first 2 rules automatically reduce the number of such calls. The best place to call
Resources.UnloadUnusedAssets manually - before loading the scene or immediately after it starts. It will also help free up additional memory for the scene, which is sometimes critical. The corresponding performance jump can be hidden, for example, by the boot screen;)
Using the rules above has resulted in the elimination of performance jumps and a much smoother gameplay and image.
Other optimizations
Next, I give other rules that can help you. Most of them I used in the previous stages of optimization.
Scripts
- Do not use GetComponent <> () in Update , FixedUpdate and other similar functions. Instead, it is better to cache the component in Awake () or Start () . If there are a lot of objects that use the script, such caching can significantly reduce the script operation time.
- Cache the built-in components of the type transform , renderer, and so on. Especially if they are used in functions like Update () . For each such call is made via GetComponent () . See rule above. This can be done, for example, as follows:
- Use Vector2 (3,4) .sqrMagnitude instead of magnitude .
- Use Color32 and Mesh.colors32 instead of Color and Mesh.colors when accessing mesh colors.
- You can use OnBecameVisible / OnBecameInvisible for scripts that can be disabled when the camera no longer sees the object.
- Use built-in arrays. These are arrays of type T [] . They are much faster than all the other collections.
- Disable the logs when the application is running on the device. Typically, printing a log requires access to the file system, and if there are a lot of logs, this can lead to dire consequences for performance.
- Disable exceptions. As a result, try not to use them in the code. Disabling exceptions will help save up to 30% performance . But do not forget that if an error occurs, and it cannot be handled by an exception, this is a crash of the application on the device. Therefore, you have to choose - either good testing and performance gain, or reliability, but the performance is slightly worse . In the case when the performance satisfies, the advantage is better to give the second option.
Physics
- The less simultaneously active rigidbody the better. Deactivate unused.
- Reduce the number of FixedUpdate per unit of time. Fixed Time = 0.03333 guarantees physics at a speed of 30 calculations per second, which is usually very good. Even 20 may be acceptable, which corresponds to Fixed Time = 0.05 .

- Minimize the use of Continuous or Dynamic collision detection . They are very resource intensive. For 2d games, they are usually not needed.
Animations
- Keep track of the number of animated objects on the screen. Less is better.
- Instead of a few simple animations on a complex object, you can do one complex, doing the same thing.
- You can try to reduce the Sample Rate of the clip. Especially if there are a lot of animations.
- Use Culling Type = Based On Renderers or Based On User Bounds . Then the animation will be played only when the object is visible on the screen.

If this, of course, suits.
Particle system
- Use Vertical Billboard instead of Billboard in Particle Renderer .
- Use shaders from Mobile-> Particles on mobile devices. This applies not only to the particle system.
- Use as few particle systems as possible and the particles themselves in the system to achieve the desired effect.
- For weak systems, you can turn off some obscure or insignificant particle systems. That will allow you to save several Draw Calls and raise their performance to the detriment of efficiency. I assure you that it is often more pleasant to enjoy the process of the game, rather than mega-cool effects.
GUI
- Do not use OnGUI () . Each such call is a few extra Draw Calls. The same applies to GUILayout . And in general, GUI support in Unity is very bad. We, for example, have our own sprite-based GUI system. There are several other very useful GUI plugins in the Asset Store.
- The size of the texture generated for the font can be reduced by adding only the characters used. In Font Settings, set the Character = Custom Set , and in the Custom Chars, include the used characters:

- You can try using separate cameras for scene objects and GUI. This will make the GUI zoom-independent, which frees Scale from being used on it to zoom in / out. Sometimes it can increase productivity.
Other
- Disable accelerometer if not in use. You can also lower the measurement frequency in Player Settings .
- You can limit the maximum FPS on older devices. For example, setting it to:
Application.targetFrameRate = 30
. This usually results in a smoother picture. Also, it reduces the drawdown of the battery, because at the same time, less processor power is required. In general, on Apple devices, FPS> 60 does not make sense, since their screen refresh rate is 60. - Sometimes periodic frequent garbage collection smooths performance. Because the system itself does this rarely and when everything is already completely bad, and a large number of objects for destruction can accumulate, which leads to a performance jump. If you do this more often, there will be fewer objects for destruction, and the release of memory will be smoother. On the other hand, for the entire duration of the scene, garbage collection may not be necessary.
Reducing the size of the application
What can it be needed for? Previously, this was done because applications
<20Mb in size could be downloaded to iOS via a 3g network. That, in principle, should increase the number of downloads, although I have not seen specific statistics. In connection with the release of iPad3 applications have become "fatter", and the threshold was raised to
50Mb . Do not forget that after uploading the application in the AppStore, it will be increased in size by an average of
4Mb . To check how much the application will weigh after filling in the AppStore in xCode in
Organizer-> Archives , a special button
Estimate Size has even appeared:

Everything you need to reduce the size of the build is described in the documents Unity:
I will describe here what I used myself. Let's start with what affects performance:
1.
Use the correct texture format .
This will also reduce the amount of texture memory used, and thus improve performance. Sometimes it is enough to use the texture format of
16 bits , especially if all the graphics are drawn in just a few colors. Compare:


For monotonous textures, you can only use its gray and alpha components and sculpt the finished object from them using a specially written shader and multiplication by color:

For backs and fuzzy objects you can use compression. Now PVRTC-compression on iOS is quite advanced. It is worth remembering that
the more texture - the better its quality after compression . On small textures, the use of compression may be unacceptable.
To make it all make sense, you need to divide objects into groups of the “background” type, GUI, game objects, which I already wrote about. Then for each type you can create your own atlas and use different texture format settings.
2.
Use the correct sound format .
I have not thought about it before. The use of compression on sounds allowed me not only to reduce the size of the application, but also the amount of memory used by it. I myself use the following rules:
- Use Audio Format = Native for very short and small sounds ( <100 Kb ).
- For the rest, use compression. Compression = 96 Kbps - this is already very acceptable. Below - noticeable distortion.
- Use Load Type = Compressed in Memory for most hard-coded sounds. This will reduce the amount of memory used, but may affect performance, as it requires unpacking during playback.
- For background music, use Load Type = Stream from disk , especially on systems with fast HDD. This will save a lot of memory.
- Use Decompress on Load in all other cases. The sound will take up more memory, but practically will not “plant” the CPU, which will sometimes help to get rid of performance jumps.
- Use Hardware Decoding for background music. The built-in decoder in iOS devices allows you to reduce CPU usage when playing background music, but this can only be done for one track at a time.
- Use Force to Mono if stereo is not needed. Or if it is present in the file, but not distinguishable. This will reduce the amount of used space by 2 times (both in memory and on disk).
Other steps to reduce the size of the build follow. Most of the settings are done in Player Settings:

- Set in the project settings Stripping Level = Use micro mscorlib . This is only for Pro owners. Don't use units from System.dll and System.Xml.dll without need. They are not compatible with Use micro mscolib .
- Install API Compatibility Level in .Net 2.0 subset . But sometimes after that the code may not work if the corresponding classes / functions and so on are not included in the .Net 2.0 subset. What once happened in my case.
- You should also get rid of dependencies on unnecessary libraries.
- Set Script Call Optimization Level to Fast but no exceptions to disable exceptions. This will also reduce the size of the build.
- Install the Target Platform in armv6 (OpenGL ES1.1) . If you don't need armv7. Or vice versa, in armv7, but not both at the same time. Given that Apple is less supporting devices with armv6, it makes sense to leave only armv7.
- Do not use JS arrays. Better not to use JS at all, use C #. Usually, after rewriting the script code from JS to C #, the application weighs less.
Sources of Inspiration Used
Primarily, Unity documents were used — the most useful resources from the Unity developers themselves. They need to be read in the first place, preferably several times, and after a while again, because they are constantly updated.
Separately, I have already mentioned documents to reduce the size of the application:
Other sources:
Conclusion
The optimization carried out by me allowed us to significantly improve the performance of the game and playability in general. As an added bonus, the size of the application has been reduced;) I hope my article will help you to make your application on Unity even better.
Perhaps someone will be interested to read my previous articles:
Most likely, my next article will be an article on how to simplify work in Unity and minimize the number of errors.
If someone has questions, suggestions, amendments - I am always ready to listen and discuss.