Version: Unity 6.0 (6000.0)
Language : English
Code optimization
Asynchronous programming with the Awaitable class

Unity programming best practices

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 myGameObject == null can evaluate true (and conversely myGameObject != null can evaluate false) even if myGameObject technically holds a valid C# object reference. For more information on the specifics of this behavior, refer to Custom equality operators.

Unity’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 if (obj == null) and not ReferenceEquals for Unity objects.
  • In circumstances where you want to check for actual C# null references, use ReferenceEquals or cast to System.Object first.
  • When comparing two Unity objects for equality, be aware that obj1 == obj2 may return true even if both references are different C# objects, if one or both have been destroyed and recreated, for example through 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 sceneA Scene contains the environments and menus of your game. Think of each unique Scene file as a unique level. In each Scene, you place your environments, obstacles, and decorations, essentially designing and building your game in pieces. More info
    See in Glossary
    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, Object.DestroyImmediate is Editor-only so use Object.Destroy at runtime and let Unity schedule destruction.

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 WaitForSeconds and other yield instructions when using coroutines.
  • Where possible, use non-allocating versions of methods, and perform expensive operations like GameObject.GetComponent in Awake and cache references to returned objects, rather than calling repeatedly from Update.
  • Avoid use of LINQ in runtime code, and especially in the context of the per-frame Update or FixedUpdate and other hot paths. Methods from the System.Linq namespace can create unnecessary allocations and involve boxing and closures.
  • 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 MonoBehaviour.Update, MonoBehaviour.FixedUpdate, and MonoBehaviour.LateUpdate that typically run many times per second.

This 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 Update functions incurs a small overhead from Unity’s internal management and interaction with the native layer. When you have many such MonoBehaviour scriptsA piece of code that allows you to create your own Components, trigger game events, modify Component properties over time and respond to user input in any way you like. More info
    See in Glossary
    , the cumulative overhead can have significant performance impact.
  • 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 Update functions that run unnecessarily most of the time, or that are unnecessarily memory-intensive when they do run.

To mitigate these risks, consider the following options:

Thread safety

While Unity has multithreaded capabilities, the core runtime is single-threaded and most APIs in the UnityEngine and UnityEditor namespaces can only be called from the main thread. Don’t reference GameObjectsThe fundamental object in Unity scenes, which can represent characters, props, scenery, cameras, waypoints, and more. A GameObject’s functionality is defined by the Components attached to it. More info
See in Glossary
, Transforms, Components, or asset APIs from background threads. Never await with a Task.Result or Task.Wait on the main thread as this leads to deadlocks.

When dealing with inherently asynchronous and long-running operations, Unity provides the Awaitable class as a Unity-specific alternative to .NET Task. Awaitable uses object pooling to reduce allocations and is aware of Unity-specific concepts like Update and FixedUpdate, which allows you to await tasks and schedule them to resume at specific points in the Player loop. For more information refer to Asynchronous programming with the Awaitable class.

For shorter-lived but more compuationally-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 #if 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 #if UNITY_EDITOR directives 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.
  • 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 ProfilerA window that helps you to optimize your game. It shows how much time is spent in the various areas of your game. For example, it can report the percentage of time spent rendering, animating, or in your game logic. More info
See in Glossary
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](roslyn-analyzers.

For more information on Unity’s suite of analysis tools, refer to Optimization.

Additional resources

Code optimization
Asynchronous programming with the Awaitable class