📜 ⬆️ ⬇️

First steps in optimizing and polishing the game on Unity3d

image After I finished my first project, I had the idea of ​​porting it to mobile devices or at least launching it on an integrated GPU . In all optimization guides, in one of the first tips, they will tell you that you shouldn’t worry about performance in advance, start optimizing after you’re done and you will gradually put everything in order. So, having initially released the game to the desktop, I decided that it would never be too late to optimize it for mobile devices. Unfortunately, I was not able to fully achieve this goal, because it seems that mobile games should be developed from the very beginning with an eye on weak iron. At the moment, for further optimization for mobile platforms, I see only the need to seriously remake the gameplay and design of the game world. However, in the current version a valuable optimization experience has been obtained for Unity3d and the resulting performance gain of more than 300% on an integrated GPU .

Let's start with the CPU


A fairly obvious list formed during the optimization:

  1. Do not use Properties ! Fields and methods are your best friends.
    ')
  2. Cache everything you get with GetComponent <> , here also includes transforms , rigidbodies , etc.

  3. Try to never refer to objects twice to get the same data. It is almost always access through Properties that you must give up. Often, you can see how in different scripts the position of the same object is requested, which is better to replace with caching that position, updating it once inside Update or even FixedUpdate in this object.

  4. Cache all math. Each call to Vector.Up will call a constructor under the hood, which is not very fast. I created a static CachedMath class in which all directions, commonly used vectors and quaternions, were folded.

  5. Try to do without using the String type. Each line requires memory allocation, and if you use strings uncontrollably, you will see how the GC stops all threads for its call. In my case, the main sources of rows were the FPS indicator and the timer during the race. The solution was to create a pool of string literals for all numbers from 1 to 100. This completely eliminated the selection of lines in each frame.

  6. Never use foreach , just replace with for if you want to save GC and precious CPU time. The use of generic methods often leads to the same consequences.

  7. LINQ is another source of load on the GC . Try to simplify your LINQ expressions, or better yet, completely replace them with simple constructs.

  8. All strings used in Animator objects should be converted to integer identifiers via Animator.StringToHash ()

  9. The instantiation of objects is very difficult operations, so it is worthwhile for frequent creatures to use a pool of objects and then reuse them.

  10. Delete all empty Update and FixedUpdate methods . Also, if your script uses both or only fixed, then you should think about transferring any possible logic from fixed to regular Update .

Of course, any optimization should be done only if you see delays in the profiler window. Most importantly, permanently allocate memory allocations.

It is also never too late to simplify some logic in your scripts, or the amount of data processed within reasonable limits. However, I repeat once again that you need good reasons obtained with the help of the profiler, that the specific method is too slow. After the changes, be sure to ensure that the profiler displays smaller numbers than before the start of the optimizations.

The worst moment of optimizations is that your structured and " ideal " code spreads into places that are not very readable. Unfortunately, this is inevitable. The main thing to remember is that this is a sacrifice for the sake of performance.

Now gpu


With the CPU tips were quite versatile and they are applicable in any project. What can not be said about GPU optimizations, which are often highly dependent on a particular scene. However, if you do not use strong magic in your shaders, then an obvious indicator is the number of GPU passes (pass-calls) .

My game contains an open world with the ocean as a base for traveling and several islands as scenery. In my case, there were more than 2000 passes, and I managed to reduce this value to about 300 .

Materials Reduce the amount of materials used as much as possible. Each change of material is a new pass, as well as each texture layer inside the material is also a new pass. Of course, I simplify a little and the passages are not so easy to form, but the fact remains - too many passes will exorbitantly load a weak GPU . For mobile devices recommend something in the region of 40-60 passes. More advanced devices can handle in the hundreds . So you have where to strive!

Visible objects. There are too many objects in my scene that are constantly on the screen. The only problem is that they must be visible! Of course, from a distance we don’t need the same detailing as close, so the obvious solution was to use LOD objects.

Imposters I chose to replace my objects with imposter (in general, this is very similar to billboards , but this is the set of textures the object received from the prerender on all sides). In the built-in Asset-Store from Unity3d there are a lot of ready-made paid solutions for LOD and imposters. However, I decided to reproduce the basic algorithm myself. I created a script extension for the editor, which created a copy of the necessary object, changed its layer, then created a camera that was limited only by this special layer, and produced an object in textures from all sides. Basic parameters were added, such as the name of the resulting folder with textures, the resolution of the resulting textures, the distance to the object, the height offset, the number of sides, and the flag for saving or turning off the light during the creation of the imposter. After all actions have been completed, the script deleted the already unnecessary copy of the object.

Sprites Now almost all objects are replaced by sprites at a certain distance from the camera. But the number of passes was still huge. Then I discovered that sprites are far from always a lightweight form to display. Each default sprite triangulates the image, creating multiple vertices. For every 900 or so (according to official documentation) vertices, a regular pass is created (officially grouping | packaging | batching - storing data from multiple objects into one instruction for the GPU - generally not applicable to SpriteRenderer objects). At the same time, it is impossible to replace all sprites with full square regions with transparency, since all transparent pixels still require rendering, and the GPU does not let them through. Also, transparency leads to problems during the rendering of all sprites due to the check for depth of rendering. The GPU will still create an extra pass for one or two sprites, between rendering the set already grouped just because depth checking requires it. The only thing that was done was to change the type of the sprite to Multiple , which changes the internal mechanism of triangulation, which creates much fewer vertices.

Sprite Packer . This is the last thing you should remember when working with sprites. To explicitly indicate to the sprite the need for packing into an atlas map, you must specify its Tag . At the time of drawing sprites from a single atlas GPU does not create additional passes, even if the order of rendering in depth is not optimal for unpacked sprites. The size of the resulting atlas is also important. By default, it is limited to 2048x2048 . This is the maximum size of the atlas, and it dynamically adjusts to the optimum, depending on the filling. In my case, this was not enough to pack all the sprites I needed on one page. Replacing the packaging algorithm with our own, which is based on the basic one, but with a modified size value of 4096x2048, significantly improved performance.

A further increase to 4096x4096 almost did not affect the number of passes, but at the same time even slightly deteriorated performance. It is worth remembering that some sprites can not be placed on the same atlas together - for this they must have the same compression settings, and some of the other parameters, otherwise they will be automatically divided into different groups. Therefore, try to group the sprites by atlases logically and visually, so that at one moment as few atlases are displayed on the screen, because each switch between them, including a non-optimal depth position, will cost you passes.

In my case, I divided atlases into UI sprites, then all the objects that are very far away - I had to use several atlases, but they were divided into diametrically opposed groups in the world, and at the same time it was difficult to see them on the screen, and all the remaining intermediate objects.

After all the changes, the performance has improved so much that turning off all the imposter objects has almost no effect on the resulting FPS .

Water. In my case, I needed to get more productive water. Initially, the stage used waterProDaytime with refraction turned on, which underwent minimal changes to support the foam along the shoreline. The camera of refractions was removed and replaced with the grabpass call. The thing is that for the correct display of the refractions, the camera had to turn off the cut-off matrix, because otherwise - all objects, above the water level, simply did not cast shadows. Due to this limitation, the camera additionally rendered the entire scene, and the grabpass call was faster in this case. The parameter LOD of the multiplier was also changed at the time of rendering reflections. Thus, imposters display a little longer in the water, which further reduces the load.

All changes increased the performance on the integrated GPU from 6-8 to 22-24 frames per second. It is still low, but it hasn’t been possible to achieve the best. I still recommend launching my game on discrete graphics.

Polishing


The new release did not want to be released only with a change in performance, so it was decided to close a few rather important points in how the game looks, namely, the UI.

Everyone who saw my game in the first release said that the UI is bad. He’s just dead, and even if I haven’t seen this before, now I understand what’s wrong with him.

There were no sounds in it, there was no movement, no life. Having launched one of the projects, I began to notice the details of the main menu, which previously were just invisible to me. So I added all the missing in the first approximation. Now the selected button is animated, focus transfer and button presses are voiced from free resources in the Asset-Store.

To animate all the elements in the game itself would be too distracting and annoying for the player, so I added an internal glowing glow that very gently reminds of UI elements.

And the last was the medal screen. He was just static, it was awfully boring to be in him. But this should be the place where the player with pleasure watches his progress. So I added a bit of life. Now it is decorated with three systems of particles creating soft iridescent balls of light similar to fireflies. Particles, unfortunately, are not available by default on the UI , so I had to create an additional camera that only draws these particles into the texture, then overlaps with the UI .

What was left behind


I could not run the game on mobile devices. Even the most powerful hardware does not reproduce the game with acceptable performance. Perhaps with the future versions of Unity3d something will change, but at the moment, as I mentioned, the development of the game with an eye on mobile devices should be done differently from the very beginning.

Also, in my case, very strange drawing artifacts appeared on mobile devices with luminous materials in some cases, literally like lasers with materials that look quite normal on the desktop.

Another goal that appeared during optimizations in the form of a quick launch of an application, or even a smooth launch, was also not achieved. A cold start in my case lasts more than a minute. Moreover, each subsequent launch reduces this time almost by half. So it seems that this is some kind of internal requirement of the Unity player. The main disadvantage is that the UI stream hangs when the scene is activated. I already use the asynchronous scene loading option, even switched it to Additive mode, however, the UI just stops after 90% loading, when you need to switch the allowSceneActivation flag. It would be great if someone prompts a workaround for Unity5.x , or something like calling an event that can thread-safe and with a top priority change Ui- objects with their redrawn, so that there is at least some indication of the application process.

PS


Of course, this is just my story and it will not magically solve all your problems. In some places it is too subjective, but I still hope that someone will find these tips useful.

Pps
I described the main thing, without going deep into technical details. The project has undergone a very large number of changes, and not everything has been described - now I can accurately remember that from the last publication they asked to deal with the water seeping through the boat - and this was definitely closed. Sounds were also added during the passage of checkpoints and receiving medals, as well as a small animation of the timer on checkpoints and color information about the time.
If someone is interested to see everything in action, unfortunately, there is no video yet, but the trial is now endless.

Source: https://habr.com/ru/post/314306/


All Articles