Best optimization tips by Unity engineers at Unite
Compilation of best Unity tips by Unity engineers gathered at Unite conference. I spent a bunch of time rewatching these talks so you don’t have to.
There is an amazing series of talks at Unite about optimization by the awesome speaker Ian Dundore.
I personally watched it a few times myself over the years. Once at work I needed to provide the source for the optimization tip I had given, but of course, I didn’t remember off the top of my head in which talk I had seen it. I recalled that I had seen it in one of the talks listed in this blog post, but still had to spend some time looking for the exact tip.
So I decided to create a short list of takeaways from these videos, so I myself and everyone else can get back to it and refresh their knowledge without spending hours of time on x2 playback speed. All points have time codes, so you can go directly to where he talks about it and get more info about the performance issue and how to resolve it.
Obviously, some of these videos are kind of old now and not all advice can be applied, because some performance issues Ian talks about are already fixed in Unity internally. But most of these tips are still relevant and not circulating over the internet constantly. So without further ado, let’s get to the list.
Unite 2016 – Let’s Talk (Content) Optimization
04:45 | Unity serializes everything you have in a scene and prefabs. There are no duplicate data checks and no compression. |
07:20 | All serialized data is included in builds. |
08:15 | AddComponent is pretty heavy. It spends most of the time looking up the scripting class it must attach, also it spawns a C++ backing class. |
13:10 | Use nested prefabs for repetitive elements. |
18:00 | Move configuration data to ScriptableObjects from MonoBehaviors to avoid serialization of repetitive data inside each prefab or scene file (got the blog post recently mentioning this point and some others too). |
19:35 | Pass a parent transform into GameObject.Instantiate(…) method instead of setting parent manually later via transform.parent property setter. Check the next tip for more details. |
20:05 | Since Unity 5.4 transforms are stored in a contiguous buffer sorted depth-first, so if you need to iterate all children of a root transform, then make sure to use the depth-first algorithm. |
23:35 | Set transform.hierarchyCapacity beforehand if it is known to avoid reallocations and buffer copying time. |
23:55 | Do not use transforms as “folders” in your scenes. |
24:00 | Do not reparent objects when using the Object Pool pattern unless absolutely necessary. At least in release builds. |
26:10 | Apply changes to transforms at once, adding them up if you have several changes in one frame to avoid an internal OnTransformChanged callback being fired many times. Should be changed in 5.6 to the pull model to avoid this issue. |
28:10 | Use Optimize Game Object box on skinned meshes to greatly increase performance by allowing Unity to parallelize the processing of bones and improve loading times since it could reduce the number of transforms in a scene significantly if skinned meshes are used heavily. |
33:15 | Disable the parent Canvas component instead of GameObject. Text component is especially heavy with its OnEnable since UI Text dirties itself in OnEnable. Do not forget to disable MonoBehaviour callbacks like Update() manually in that case. |
37:30 | Merge layered UI elements. All Unity UI is drawn as Transparent geometry, so every pixel in every quad will be sampled and no occlusion culling is done there. |
39:00 | Split canvases. Keep dynamic and static elements in separate canvases. Keep a balance between extra draw calls vs the cost of batching. |
Unite Europe 2017 – Squeezing Unity: Tips for raising performance
03:00 | Profile your code first, before any optimization attempt. The thing I mention heavily in my blog post on one micro-optimization https://gamedev.center/array-vs-dictionary-lookup-in-net/ |
08:15 | Reduce the amount of API calls that return an array. Make sure to use a non-allocating equivalent if available. |
10:00 | Know how your data structures work internally. When to use Array, List, Dictionary, HashSet, etc. |
13:30 | You can use double collection when you need to iterate frequently over the collection, but also need key access. |
15:50 | If you need Dictionary with MonoBehaviour or ScriptableObject key, then use id = MonoBehaviour.GetInstanceId() and make it an int-keyed Dictionary using this id as a key. |
17:45 | The C# integer-keyed dictionary is highly optimized with hand-written IL code by the Xamarin team. But don’t use it every time, use only for dictionaries that are indexed 100-1000s times per frame. |
22:00 | Use inlining for very hot path methods. You can even consider manual inlining, but profile first. After .NET 4.6 inlining via attribute should be fine too. |
23:00 | Use fields instead of properties in a hot path. |
25:00 | What happens when Canvas rebuilds its mesh: recalculate layout, regenerate mesh, regenerate materials. |
25:15 | When a UI element is dirty the mesh for it will be regenerated even if the alpha is 0. |
26:17 | Changing the color of a graphic only dirties its vertices. Changing Fill* properties also dirties only vertices. So no Canvas mesh rebuild is needed. |
26:40 | UI elements are sorted by depth, yes, it includes sort() operation call, which is an O(n log n) operation. Again the tip from the previous year: UI is transparent, so everything is rendered all the time – overdraw can be a problem. |
28:35 | Nesting canvases is a good practice, as dirtying a child canvas does not dirty the parent. Child canvases maintain their own geometry and perform their own batching. |
29:00 | When splitting canvases, group elements that are updated simultaneously. Separate dynamic elements from static ones. |
30:15 | Use object pooling for large ScrollRects with many elements. Add a separate sub Canvas for large ScrollRects. Turn off pixel perfect for Canvas that wraps a large ScrollRect. Modify the code to stop ScrollRect when velocity is low. |
33:40 | Turn off the “Raycast Target” box whenever possible. |
34:40 | Always assign the Event Camera property in Canvas component. |
36:20 | FindObjectWithTag() is not as bad as FindObjectOfType() . But obviously, the more game objects with tags are indexed, the slower it works. |
38:10 | Always assign World Camera for world space canvases. |
38:30 | Do not add Graphic Raycaster to non-interactive elements. |
39:30 | Layout components are painfully slow. |
43:50 | Each Layout Group multiplies its cost. Nested Layout Groups are particularly bad. |
44:10 | What marks a layout dirty: OnEnable, OnDisable, reparenting (twice: for the new and the old parent), OnDidApplyAnimatorProperties, OnRectTransformDimensionsChanged . |
45:10 | Use anchors instead of Layout Group for proportional UIs. For a truly dynamic UI write custom code to maintain its layout, you know better how your UI must work. Update UI on demand. |
46:00 | When using Object Pool for UI elements: On adding to the pool disable the game object first, only then reparent. When getting back from the pool reparent first, then update data, and only then enable. |
46:50 | Disable a Canvas component instead of disabling a game object. |
48:40 | Never use Animator for UI elements. Animator dirties elements every frame, even if nothing happens visually. |
Unite Berlin 2018 – Unity’s Evolving Best Practices
01:30 | Default tip: profile everything, trust no one. |
07:40 | String comparison is a pretty complicated topic, so if you use it really heavily make sure to choose the best method for your use case. |
11:40 | Use attribute [Il2CppSetOption(Option,NullChecks, false)] to remove null checks in the resulting C++ code. |
15:10 | Use Transform.SetPositionAndRotation() to batch transform updates. |
16:00 | Since Unity 5.4 transforms are stored in contiguous buffers. There is one TransformHierarchy structure for each root transform in a scene. |
18:50 | TransformChangedDispatch keeps a list of dirty TransformHierarchies pointers. Yes, it operates on a hierarchy level. |
19:30 | Set Transform.hierarchyCapacity if you know the size of the hierarchy. Use it, especially before mass reparenting operations.Set parent and position using GameObject.Instantiate(…) arguments. |
20:10 | Split your transform hierarchies. Smaller hierarchies are better. More root transforms are better since their checking is jobified. |
24:00 | Use Physics.SyncTransforms if you need to propagate transform changes to the PhysX system. Physics.AutoSyncTransforms = true , enables it by default, with performance hit of course. |
25:40 | Batch transform updates before using physics. Use flat hierarchies instead of deep ones. |
28:10 | AudioSource.mute doesn’t prevent computational work. Disable a muted Audio Source. |
28:30 | FMOD background thread CPU usage is not shown in the profiler. Scroll down to the Audio tab in Unity Profiler for evaluating CPU load by FMOD background audio threads. |
30:17 | Use Vorbis compression on iOS for audio clips. |
30:20 | Avoid having too many Audio Sources put on mute. |
30:55 | Use decompress on load option if you can afford memory overhead. E.g. for short frequent sounds Avoid playing lots of compressed clips, especially on mobile |
31:30 | FMOD background threads overload can cause framerate stutter since it has realtime priority and can interrupt the main thread. |
32:00 | Clamp the voice count to save CPU time via configuration = AudioSettings.GetConfiguration(); configuration.numRealVoices = Mathf.Clamp(configuration.numRealVoices, 1, configuration.numVirtualVoices); AudioSettings.Reset(configuration); |
35:40 | Animator can push work onto worker threads. Therefore it scales better. But has overhead. So always test it on the min target hardware. Consider a number of curves in animations. More cores – better for Animator. Animator outperforms Animation on a lower amount of animation curves, the more cores/power CPU has. Use Animation for simple animations. Use Animator for high curve count clips and complex scenarios. |
38:00 | In Animator active state on each layer is evaluated every frame, even if weight = 0. Zero-weight layers – waste of performance. |
38:50 | An additional layer in Animator increases cost by around 20%. |
39:30 | Use the right rig. The humanoid rig is slower than the generic rig. Because humanoid runs IK and retargeting calculations. |
40:30 | Pooling: Animator resets state when its GameObject is disabled. Fixed in 2018.1 by adding Animator.keepControllerStateOnDisable . Useful for pooling, but has a higher memory cost for disabled Animators. |
43:30 | Split dynamic and static transforms into separate hierarchies with postprocess scripts that do that split without affecting production flow. |
45:20 | No difference between X root transforms and X additive scenes in terms of how TransformChangedDispatch works. |
46:20 | Use scripts to animate simple UI. Or at least Animation as it has less overhead on start and stop. |
Post Scriptum
There is an enormous amount of top-quality talks from Unite. The ones listed here are my favorites. Share talks you personally like in the comments and leave your opinion on whether such type of posts is helpful or not. And if you like more tips and advice, give my telegram channel a follow: I post interesting stuff I find about game development every day.
Leave a Reply