Unity programming best practices
Key issues to be aware of when writing code for Unity applications, along with best practices to help you avoid common pitfalls.
Read time 7 minutesLast updated 8 days ago
There are some unique features of Unity's programming environment that require extra consideration when writing code compared to standard C#/.NET projects. The following is a summary of key issues to be aware of when writing code for Unity applications, along with best practices to help you avoid common pitfalls.
Unity Object lifecycle and references
When writing C# in Unity, be careful when comparing objects for equality with other objects or with null. For types that inherit from UnityEngine.Object, Unity uses a custom version of the C# equality and inequality operators. This means the null check can evaluate (and conversely can evaluate ) even if technically holds a valid C# object reference. For more information on the specifics of this behavior, refer to Custom equality operators.
myGameObject == nulltruemyGameObject != nullfalsemyGameObjectUnity's custom equality behavior and object lifecycle have a few implications for your code:
- In circumstances where you want to be sure of excluding destroyed objects in your check, make sure to use and not
if (obj == null)for Unity objects.ReferenceEquals - In circumstances where you want to check for actual C# null references, use or cast to
ReferenceEqualsfirst.System.Object - When comparing two Unity objects for equality, be aware that may return
obj1 == obj2even if both references are different C# objects, if one or both have been destroyed and recreated, for example throughtrue.Undo - The custom equality operator is slower than the standard C# one. This is usually not a problem, but be mindful of this at scale and in hot paths.
- Don't cache components across scene unloads without guarding because they can be destroyed but remain as C# wrapper objects with no unmanaged counterpart.
- Don't hold strong references to large assets in static fields because they persist across scenes and prevent unloading.
- When destroying objects, is Editor-only so use
Object.DestroyImmediateat runtime and let Unity schedule destruction.Object.Destroy
Avoid C# finalizers
Don't use C# finalizers in runtime code, for the following reasons:
- They run on separate finalizer threads and Unity APIs usually require the main thread.
- They run non-determinisitically, leading to unpredictable behavior.
- They might not run at all, unless the application garbage collects and waits.
- Exceptions thrown from finalizers can cause an application halt unless specially handled.
- By their very existence they increase garbage collector overhead.
Garbage collector overhead and allocations
One of the most important performance risks to be mindful of in Unity applications is runtime code that allocates memory and increases garbage collector overhead, especially in hot paths.
To avoid this, apply the following coding practices:
- Avoid per-frame allocations by caching and reusing lists and using non-allocating versions of methods where available. This includes caching and other yield instructions when using coroutines.
WaitForSeconds - Where possible, use non-allocating versions of methods, and perform expensive operations like in
GameObject.GetComponentand cache references to returned objects, rather than calling repeatedly fromAwake.Update - Avoid use of LINQ in runtime code, and especially in the context of the per-frame or
Updateand other hot paths. Methods from theFixedUpdatenamespace can create unnecessary allocations and involve boxing and closures.System.Linq - Avoid repeated string operations like concatenation.
- Identify and avoid instances of reflection. For more information, refer to Avoid C# reflection overhead.
For more detailed guidance and examples of these issues, refer to Optimizing your code for managed memory.
For information on tracking and reducing garbage collector overhead, refer to Managed memory.
MonoBehaviour Update loop optimization
The traditional pattern for many Unity projects involves using MonoBehaviour script components to regularly update the game state through built-in callbacks such as , , and that typically run many times per second.
MonoBehaviour.UpdateMonoBehaviour.FixedUpdateMonoBehaviour.LateUpdateThis is a simple model that can still work well when used appropriately, but it has some key performance risks that commonly catch inexperienced developers out:
- The default implementation of Unity's per-frame or other regular event functions can scale poorly. Each of these functions incurs a small overhead from Unity's internal management and interaction with the native layer. When you have many such MonoBehaviour scripts, the cumulative overhead can have significant performance impact.
Update - The fact that built-in updates run very often makes them a hot code path and magnifies the effect of any inefficient, memory-intensive operations you place in them. A common bad pattern from inexperienced users is to have many MonoBehaviour scripts with functions that run unnecessarily most of the time, or that are unnecessarily memory-intensive when they do run.
Update
To mitigate these risks, consider the following options:
-
Consider converting your project to a data-oriented architecture using Unity's Entity Component System (ECS) for better scalability with many entities.
-
If you use a MonoBehaviour-based architecture:
- To ensure you're minimizing managed memory impact in your hot paths, refer to Optimizing your code for managed memory.
- Minimize the number of active functions by using a centralized update manager or customizing the Player loop. For more information, refer to Using a custom update manager and Customizing the Player loop.
Update - Bear in mind that even in a MonoBehaviour-based project, you can often use specific features of Unity's data-oriented systems. You can use Jobs, Burst-compile sections of your code, use more efficient data structures like in performance-critical sections of your code, and choose unmanaged alternatives to managed APIs such as those for transform operations.
NativeArray
Thread safety
While Unity has multithreaded capabilities, the core runtime is single-threaded and most APIs in the and namespaces can only be called from the main thread. Don't reference GameObjects, Transforms, Components, or asset APIs from background threads. Never with a or on the main thread as this leads to deadlocks.
UnityEngineUnityEditorawaitTask.ResultTask.WaitWhen dealing with inherently asynchronous and long-running operations, Unity provides the class as a Unity-specific alternative to .NET . uses object pooling to reduce allocations and is aware of Unity-specific concepts like and , which allows you to tasks and schedule them to resume at specific points in the Player loop. For more information refer to Asynchronous programming with the Awaitable class.
AwaitableTaskAwaitableUpdateFixedUpdateawaitFor shorter-lived but more computationally-intensive parallelized work, Unity provides the job system, which can be Burst compiled. For more information, refer to Write multithreaded code with the job system.
Compilation considerations
For optimal performance it's important to think not just about how you write code but how it's compiled. Compiling code naively rather than actively defining which contexts certain source files or regions of your code are relevant for imposes the following costs:
- Increased build size if unnecessary code is included.
- Time spent compiling and recompiling to apply changes. This can especially affect your iteration time in the Editor.
- Potential runtime errors if inappropriate code is included for a given platform or context.
Unity provides several mechanisms to help you control which parts of your code are compiled for different platforms and contexts:
- You can use Assembly Definitions to group source files into assemblies that can be compiled separately. This allows you to isolate Editor-only code from runtime code, and platform-specific code from cross-platform code. For more information, refer to Organizing scripts into assemblies.
- You can use directives with scripting symbols to exclude specific regions of code from compilation based on the target platform or context. For example, guard Editor-only code behind
#ifdirectives to exclude it from runtime builds. For more information on the various methods Unity offers for conditionally including or excluding code, refer to Conditional compilation.#if UNITY_EDITOR - A key Unity-specific concept is domain reload in the Editor on entering and exiting Play mode or recompiling scripts. Domain reloads are time consuming and can affect iteration times when writing and testing in the Editor. You can disable domain reloads on entering Play mode to improve iteration times, but you must then reset static state manually. For more information, refer to Code and scene reload on entering Play mode.
Unity's analysis tools
Unity provides a variety of tools to help you identify bottlenecks and write more performant code. The Project Auditor tool can analyze your project code to identify common performance issues and suggest fixes. The Profiler can help you identify runtime performance bottlenecks in your code by providing detailed information about CPU and GPU usage, memory allocation, and more. You can also create Roslyn analyzers to enforce coding standards and identify performance issues specific to your project.
For more information on creating custom Roslyn analyzers and source generators, refer to Roslyn analyzers and source generators.
For more information on Unity's suite of analysis tools, refer to Optimization.