What you will get from this page: Updated scripting performance and optimization tips from Ian Dundore that reflect the evolution of Unity’s architecture to support data-oriented design.
Unity’s evolving, and the old tricks might no longer be the best ways to squeeze performance out of the engine. In this article, you’ll get a rundown of a few Unity changes (from Unity 5.x to 2019.x) and how you can take advantage of them.
Generic vs humanoid rig
By default, Unity imports animated models with the Generic Rig, but developers often switch to the Humanoid Rig when animating a character. However, using the Humanoid Rig comes at a cost.
The Humanoid Rig brings two additional features to the Animator System: inverse kinematics (IK) and animation retargeting (which allows you to reuse animations across different avatars).
However, even if you don’t use IK or animation retargeting, the Animator of a Humanoid-rigged character computes IK and retargeting data for each frame. This consumes about 30–50% more CPU time than for the Generic Rig, where these calculations are not performed.
If you don’t need to take advantage of the specific features that the Humanoid Rig offers, you should use the Generic Rig.
Object pooling is a key strategy for avoiding performance spikes during gameplay. However, Animators have historically been difficult to use with object pooling. Whenever an Animator’s GameObject is enabled, it must build a list of pointers to the memory address of the properties the Animator is animating. This means querying the hierarchy for a component’s specific field, which can be expensive. This process is called an Animator Rebind, and it shows up in the Unity Profiler as Animator.Rebind.
The Animator Rebind is unavoidable at least once, for any scene. It involves recursively traversing all children of the GameObject the Animator is attached to, getting a hash of their name, and comparing these to the hash of each animation curve’s target path. Therefore, having children that a hierarchy is not animating adds extra cost to the binding process. Avoiding the Animator on a GameObject that has a huge number of children it’s not animating would help Rebind performance.
Rebind for MonoBehaviour is more expensive than that for built-in classes such as Transform. The Animator component would scan the fields of the MonoBehaviour to create a sorted list indexed by the hash of the fields’ name. Then, for each animation curve animating a field of the MonoBehaviour, a binary search is performed on that sorted list. Therefore, it may help reduce Rebind time if you keep the fields in the MonoBehaviour you are animating simple, and avoid large nested serializable structures.
After the inevitable initial Rebind, you should pool your GameObjects. Instead of enabling/disabling the whole GameObject, you can just enable/disable the Animator component to avoid rebinding.
Build a custom Update Manager
If your project has demanding performance requirements (e.g., an open-world game), consider creating a custom Update Manager using Update, LateUpdate, or FixedUpdate.
A common usage pattern for Update or LateUpdate is to run logic only when some condition is met. This can lead to a number of per-frame callbacks that effectively run no code except for checking this condition.
Whenever Unity calls a message method like Update or LateUpdate, it makes an interop call – meaning, a call from the C/C++ side to the managed C# side. For a small number of objects, this is not an issue. When you have thousands of objects, this overhead starts becoming significant.
Subscribe active objects to this Update Manager when they need callbacks, and unsubscribe when they don’t. This pattern can reduce many of the interop calls to your Monobehaviour objects.
Refer to the Game engine-specific optimization techniques for examples of implementation.
Minimize code that runs every frame
Consider whether code must run every frame. You can move unnecessary logic out of Update, LateUpdate, and FixedUpdate. These Unity event functions are convenient places to put code that must update every frame, but you can extract any logic that does not need to update with that frequency.
Only execute logic when things change. Remember to leverage techniques such as the observer pattern in the form of events to trigger a specific function signature.
If you need to use Update, you might run the code every n frames. This is one way to apply Time Slicing, a common technique of distributing a heavy workload across multiple frames.
In this example, we run the ExampleExpensiveFunction once every three frames.
The trick is to interleave this with other work that runs on the other frames. In this example, you could “schedule” other expensive functions when Time.frameCount % interval == 1 or Time.frameCount % interval == 2.
Alternatively, use a custom Update Manager class to update the subscribed objects every n frames.
Cache the results of expensive functions
In Unity versions prior to 2020.2, GameObject.Find, GameObject.GetComponent, and Camera.main can be expensive, so it’s best to avoid calling them in Update methods.
Additionally, try to avoid placing expensive methods in OnEnable and OnDisable if they are called often. Frequently calling these methods can contribute to CPU spikes.
Wherever possible, run expensive functions, such as MonoBehaviour.Awake and MonoBehaviour.Start during the initialization phase. Cache the needed references and reuse them later. Check out our earlier section on the Unity PlayerLoop for the script order execution in more detail.
Here’s an example that demonstrates inefficient use of a repeated GetComponent call:
Renderer myRenderer = GetComponent<Renderer>();
Instead, invoke GetComponent only once as the result of the function is cached. The cached result can be reused in Update without any further calls to GetComponent.
Read more about the Order of execution for event functions.
Avoid empty Unity events and debug log statements
Log statements (especially in Update, LateUpdate, or FixedUpdate) can bog down performance, so disable your log statements before making a build. To do this quickly, consider making a Conditional Attribute along with a preprocessing directive.
For example, you might want to create a custom class as shown below.
Generate your log message with your custom class. If you disable the ENABLE_LOG preprocessor in the Player Settings > Scripting Define Symbols, all of your log statements disappear in one fell swoop.
Handling strings and text is a common source of performance problems in Unity projects. That’s why removing log statements and their expensive string formatting can potentially be a big performance win.
Similarly, empty MonoBehaviours require resources, so you should remove blank Update or LateUpdate methods. Use preprocessor directives if you are employing these methods for testing:
Here, you can use the Update in-Editor for testing without unnecessary overhead slipping into your build.
Disable Stack Trace logging
Use the Stack Trace options in the Player Settings to control what type of log messages appear. If your application is logging errors or warning messages in your release build (e.g., to generate crash reports in the wild), disable Stack Traces to improve performance.
Learn more about Stack Trace logging.
Use hash values instead of string parameters
Unity does not use string names to address Animator, Material, or Shader properties internally. For speed, all property names are hashed into Property IDs, and these IDs are used to address the properties.
When using a Set or Get method on an Animator, Material, or Shader, leverage the integer-valued method instead of the string-valued methods. The string-valued methods perform string hashing and then forward the hashed ID to the integer-valued methods.
Related is the choice of data structure, which impacts performance as you iterate thousands of times per frame. Follow the MSDN guide to data structures in C# as a general guide for choosing the right structure.
Pool your objects
Instantiate and Destroy can generate garbage collection (GC) spikes. This is generally a slow process, so rather than regularly instantiating and destroying GameObjects (e.g., shooting bullets from a gun), use pools of preallocated objects that can be reused and recycled.
Create the reusable instances at a point in the game, like during a menu screen or a loading screen, when a CPU spike is less noticeable. Track this “pool” of objects with a collection. During gameplay, simply enable the next available instance when needed, and disable objects instead of destroying them, before returning them to the pool. This reduces the number of managed allocations in your project and can prevent GC problems.
Similarly, avoid adding components at runtime; Invoking AddComponent comes with some cost. Unity must check for duplicates or other required components whenever adding components at runtime. Instantiating a Prefab with the desired components already set up is more performant, so use this in combination with your Object Pool.
Related, when moving Transforms, use Transform.SetPositionAndRotation to update both the position and rotation at once. This avoids the overhead of modifying a Transform twice.
If you need to instantiate a GameObject at runtime, parent and reposition it for optimization, see below.
For more on Object.Instantiate, see the Scripting API.
Learn how to create a simple Object Pooling system in Unity here.
Harness the power of ScriptableObjects
Store unchanging values or settings in a ScriptableObject instead of a MonoBehaviour. The ScriptableObject is an asset that lives inside of the project. It only needs to be set up once, and cannot be directly attached to a GameObject.
Create fields in the ScriptableObject to store your values or settings, then reference the ScriptableObject in your MonoBehaviours. Using fields from the ScriptableObject can prevent the unnecessary duplication of data every time you instantiate an object with that MonoBehaviour.