Understand the Unity PlayerLoop
The Unity PlayerLoop contains functions for interacting with the core of the game engine. This structure includes a number of systems that handle initialization and per-frame updates. All of your scripts will rely on this PlayerLoop to create gameplay. When profiling, you’ll see your project’s user code under the PlayerLoop – with Editor components under the EditorLoop.
It’s important to understand the execution order of Unity’s FrameLoop. Every Unity script runs several event functions in a predetermined order. Learn the difference between Awake, Start, Update, and other functions that create the lifecycle of a script to strengthen performance.
Some examples include using FixedUpdate instead of Update when dealing with a Rigidbody or using Awake rather than Start to initialize variables or game state before the game starts. Use these to minimize the code that runs on each frame. Awake is called only once during the lifetime of the script instance and always before the Start functions. This means that you should use Start for dealing with objects you know can speak to other objects, or query them as they’ve been initialized.
See the Script lifecycle flowchart for the event functions’ specific order of execution.
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.