Whether you are running on PC, Mobile or Console, there is going to be a point where you want to optimize your game. In this post, I have a couple of tips, tricks, dos and do-nots to help your code run a bit more performantly.
Firstly, we can stop the game creating unnecessary garbage (and thus stop the garbage collector being called as frequently) by caching. Let’s look at the following code:
We will talk about the drawbacks of FindObjectsOfType later, but specifically, in this case, we are calling what is already a slow function and creating a new array every time OnTriggerEnter is called. An easy optimization is to cache the array.
LINQ is a useful component within the .NET framework and it is great when you need to search through a chunk of data once every so often. However, although it can be very clean and easy to use, it generally
requires more computation time and creates more garbage because of the boxing that goes on behind the scenes.
For context, dotnetperls did a loop vs LINQ test over some data and found that LINQ was almost 10X slower.
Overall, writing LINQ-equivalent code is often faster and produces less garbage. When performance is a factor, there is no real reason not to use a normal for loop to iterate over the code when doing the equivalent of LINQ methods including, but not limited to, Where(), Select(), Sum(), Count(), etc. Jackson Dunstan, has this article and this article on this and shows some serious performance differences by creating the same logic by hand.
Therefore when it comes to actual gameplay or anywhere in your game that a potential noticeable hitch, LINQ should be avoided.
Common Unity APIs
There are a couple of Unity APIs that although useful can hit performance pretty hard. This includes:
- GameObject.Find / Object.Find
- GameObject.FindWithTag / Object.FindWithTag
- GameObject.FindObjectOfType / Object.FindObjectOfType
Most of the above operations involve going through your whole scene graph looking for some matching list of GameObjects.
SendMessage and BroadcastMessage, in particular, can be up to 1000x slower than calling the function directly. In my opinion, these two functions are also bad programming and should be eliminated from a codebase altogether.
Although the Find methods can be bad for performance, they do have their place. Although they are slow, using them once every so often in non-performant reliant code is not the end of the world, however using them in places where the player is going to notice is a big no-no.
GetComponentsInChildren also has the potential to be very slow, especially with large game objects that have a large hierarchy of objects. It is a similar scenario to find, except instead of looping through the whole scene, you are doing it over your game objects. If you absolutely need to use GetComponentsInChildren consider using the version of the API that accepts a pre-allocated list.
However, in the majority of cases, this is function is used for caching during initialization. In this case, it is much better to serialize an array of the objects in the editor.
Instantiating a new object in the scene can create performance hic-ups and hitches the player will see when playing. If you need to Instantiate a lot of Objects, consider using a Pooling Manager that is used to create objects in a pool ahead of time. If it is not possible to use a pooling manager, consider using a nested prefab, and enabling it and initializing it the way you need rather than Instantiating it. Depending on your game, on modern-day hardware with a lot of memory, there is an argument for not Instantiating anything during the games run-time and instead just use the pooling manager paradigm.
Limit Allocations in Frequently called Functions
It is safe to say Update and LateUpdate are two functions that are called frequently. If we are allocating heap memory in these functions, then we are probably going to have a bad time. The amount of garbage is going to build up and inevitably the garbage collector is going to kick in more often adding noticeable hiccups to the game.
In this example, we literally have a VeryExpensiveGarbageGeneratingFunction. In this case, let’s say it is a move and do some stuff that is expensive. Due to the nature of our game, we have to call this function as if we don’t, the game does not work as intended by design
Right now, this will be called every frame. With a simple check like so:
The function is now called less frequently and thus fewer allocations happen. better, not perfect, but it will help with our total garbage count.
As a follow on from above, say we are dealing with collections like lists that we are manipulating every frame. When we create a new collection, it causes a new heap allocation. If we are newing up a list every time we are calling update, we are going to cause a lot of allocations and therefore a lot of garbage. There is a really simple way to solve this. Instead of creating a new list in the frame, do something like the following:
Overall, in this case, it is going to be cheaper and faster to clear an already created list than creating a new one each time.
Caching with Coroutines
Unity’s Coroutine system is great, however, it can be the cause of garbage creation. You can decalre coroutines outside of functions like the following:
Threads vs Coroutines
Contrary to popular belief, threads can be used within Unity. There are of course some caveats.
Firstly, coroutines have nothing to do with Threads.
Although Coroutines can be executed piece by piece over time, the processing inside a coroutine is still done on the main thread.
This means that if you are running something expensive in a coroutine, it will still potentially lock up the main thread and a freeze in the game will occur.
On the other hand, Threads run in parallel and therefore you can offload expensive operations to them. The operations may still take a bit of time, but you are less likely to see the game actively freeze.
One way to use threads in gameplay is by combining them with coroutines and wait for the thread to finish. Although the game will wait for the period of time to crunch through the operation you are doing, the game is less likely to visually freeze.
Creating threads yourself can be expensive, so it is recommended that ThreadPool.QueueUserWorkItem is used.
Also, the Unity API is not thread safe. This means that anything in the Unity API should still be called on the main thread.
Further reading can be found here:
Resources.Load is slow. On some platforms, it is the slowest way possible of grabbing an asset from your game. There are a variety of ways to avoid Resources.Load, but mainly, you should aim to use a serialized reference to an item you need in the project. Again, there is a time and a place for it, maybe behind a loading screen, however, you should avoid using Resources.Load in performance-critical points of your code.
Although foreach loops are not as bad as they used to be, for hotspots in the code, there is still an argument to use standard for loops over foreach. Unity’s IL2CPP can also further complicate the issue.
Overall, the safest and fastest possible loops we can do over Arrays and lists is a for loop with the length/count cached.
Although Properties are good for reading, there is an argument to bin a property and make it a public variable. Although a CS lecturer somewhere may be shouting at you about this, there are cases where pure public variables are completely fine.
For example, there is no real reason for:
and when it comes to performance hot spots, the latter will be faster.
If possible use readonly public variables instead of properties as they will give the best read performance. Ideally, in performance-critical places where you have to use properties, Getter properties should not be doing work other than returning a value or creating an instance to keep their cost down.
Unity Accessors and Special Functions
Some Unity accessors that seem quite innocent can actually generate garbage. One of these is GameObject.tag. I like tags. Tags can be a really good way to check if a gameobject is a thing or not, however, it can be misused.
Say you want to check a tag and you use the == operator to do it. By calling GameObject.tag you actually end up returning a new string and thus generate garbage. In this case, there is a special function that Unity has made that you can use.
This is a special Unity function that does not create garbage and it is not the only one.
There are a few others we could use including Physics.SphereCastNonAlloc(). Instead of allocating the array with the results of the query, this method stores the results into the user-provided array. It will only compute as many hits as fit into the buffer, and store them in no particular order. It’s not guaranteed that it will store only the closest hits. Most importantly this method generates no garbage.
There are a few of the above functions and if you are finding a particular hotspot on one a Unity API call, you may be able to find a faster version of the function or alternative method that produces no garbage.
Structs containing references
We have previously talked about the Garbage Collector and how some code is bad because it can create garbage via heap allocations. However, there are also ways that code can have an impact on garbage collection just by the way it is structured.
In the case of structs, by including a reference type, you end up making the garbage collector examining things it should not have to examine and thus causing it to take longer.
In other words, Structs are value-typed variables. If we don’t include a reference type in the struct then the garbage collector does not have to examine the whole thing. However if we include something like a string which is a reference-type the garbage collector has to examine the whole struct. On top of that, if there is a large array of these structs, the garbage collector will take even longer examining the array. In this case, a char is a value-type, so you could replace your string with a char array.
Again, you can have structs that contain references, but if you need them to be performance-critical, try and keep them to value-types.
Structs are value types. When you pass a struct directly to a function it will copy the contents into a newly created instance of that struct. If you have a large struct, and you pass it to the function there will be a chunk of memory allocated on the stack and CPU cost passing it. Also, with both large and small structs, passing a copy every frame in somewhere like an Update loop is not going to be very performant.
In hotspots, you should look to pass small structs
Boxing in C# can be a performance hit, even if you are using a pure C# application rather than Unity. For those of you that don’t know, Boxing is what happens when a value type is used in place of a reference type and usually occurs with functions with object parameters.
String.Format is a prime example of this as it allows a number of Objects as parameters and you can pass int, floats and other value types.
When the value type is passed, a temporary System.Object is created on the heap to wrap the value-type variable. This temporary Object is later disposed of and thus creates garbage.
In performance-critical parts of your code, it is a good idea to avoid calling methods that use boxing as it can cause unnecessary heap allocations and garbage.
Empty Update, Awake, OnEnable, etc Functions
It is very easy to leave Update and Awake functions in your scripts, even if you don’t have anything in them as when you create a MonoBehaviour, they are auto-generated. Although it may seem fine to leave these in your scripts, it is not.
Unity operates back and forth between un-managed Unity Engine Code and managed game code. When functions like Update are called, this happens. This context switching over this bridge can be expensive, even if there is no actual code to call. It is wasted time if there is nothing to execute. Although there may not be any major impact initially, if there are 100s of a gameobject with a script with empty Update loops, you are likely to see a performance hit.
it is good practice in both an Optimization and Codebase health respects to get rid of empty functions if possible.