Menu Close

Easy performance improvements

One of the many challenges when developing a game is that everything comes at a performance cost – even something as simple as handling a string. There are many things that are easily overlooked while developing code or creating a scene that can end up with serious implications on the games performance. All of them have in common that the only way to fix them is to use the profiler and debug the application, until you find what’s creating the bottleneck, and then find a more performant way to achieve the same result.

This short article is not so much about finding a specific issue, but instead there are a few things you can do to generally increase the performance of your game. The only drawback is that you’ll need to test your game in order to make sure they don’t have any side effects.

Please keep in mind that the optimisations are based on PC releases. Behaviour might be different when developing for other platforms.

Scripting Backend

You can either use Mono or IL2CPP as scripting backend. Both of these have advantages and disadvantages. The implications on performance are, simply put: Mono compiles code at runtime (Just In Time compilation), whereas IL2CPP compiles the entire game before it’s run (when you build it).

The default (as of Unity 2019) is Mono, whereas Unity recommends using IL2CPP.

When using IL2CPP, you can choose between 3 different configurations:
– Debug: Your game compiles faster, but your application runs slower
– Release: Your game takes a bit longer to compile, but your game runs a bit faster
– Master: Your game takes even longer to compile, but your game runs even faster

You can change the Scripting Backend in your Project Settings -> Player -> Other Settings

If you build your game for testing a small change by yourself on your fast rig, feel free to use Debug or Release or even go back to Mono, so you don’t have to wait too long for the project to build. When building for a longer testing sequence or before shipping it to your testers or even releasing it, go for IL2CPP with the Master configuration, to make sure the game is performing as well as possible.

Incremental Garbage Collector

Garbage Collection is the process of removing discarded data from memory. Everything you instance, from a tiny string to your Enemy class (that contains way too much functionality and you know it should be split up but you’ll do that when you have the time) needs to be cleaned from memory once it is no longer used.

Garbage Collection is an automatic process (by default), but depending on all the junk floating around in your memory it can take a little time. Garbage collections happen every now and then, it can be as frequent as every few seconds. So, if there is a lot of garbage to collect, then every few seconds rendering a frame can take substantially longer. This doesn’t necessarily tank your FPS, but a single frame that takes 3x as long as the other frames can be felt by a player and it’s very annoying.

Incremental Garbage Collection is a new feature in Unity 2019. It doesn’t change the amount of garbage collected, or the time it takes, but it spaces the work out over a couple of frames. This makes the process a lot smoother, there are no spikes anymore. The feature is active by default in all Unity versions that support it. But if you upgraded from an earlier version, or if you ever messed around with your settings, it might be deactivated.

You absolutely need to make sure incremental garbage collection is enabled.

You can activate the Incremental Garbage Collector in your Project Settings -> Player -> Other Settings

Other player settings

The following settings can improve performance, but they can also come with side effects. They are all found at the very bottom of the player settings.

Prebake Collision Meshes:
Bakes the collision data into the meshes at build time.

Keep Loaded Shaders Alive:
Results in more memory consumption but better performance.

Optimize Mesh Data:
Can increase performance, but can also have side effects (e.g. with post processing).

Disable Occlusion Culling

Occlusion Culling (and other similar methods) try to evaluate which GameObjects are currently invisible to the player (in other words: outside the field of view of all active cameras) and simply not rendering them. This sounds like a great idea for performance, so why disable it? Evaluating which GameObjects are not visible takes CPU performance. So you put more load on the CPU, in order to take some load off the GPU, which now doesn’t have to render as many objects.

Many Unity games are CPU bound. Many functionalities in Unity are single threaded and therefore only use one CPU core. Also, almost all of your code runs on the main thread of your game. While all the code in the Unity engine is optimized and tried and tested in thousands of projects (which doesn’t mean it’s perfect), your code is probably less than ideal. While you should absolutely try to make your code as efficient as possible (more on that below), the less competition it has for time on the main CPU core, the better.

Run your benchmarks with and without Occlusion Culling enabled, and see if there’s a difference.

Occlusion Culling can be enabled/disabled directly on the camera object

Check your cameras

Rendering an image takes time. Rendering the same image twice takes twice as much time. If you work with different cameras, you need to make sure that only your current camera is active. If you activate camera B without deactivating camera A, then camera B will render its image on top of camera A and everything looks great. But camera A is still active and rendering its image as well. If you work with multiple cameras a lot, I’d recommend creating a manager that makes sure only one camera is active at a time.

RaycastTarget on UI elements

UI elements (like images) have a RaycastTarget property. This needs to be enabled for mouse input to work, for example for clicking a button. For any kind of UI element that is not interacted with, for example a background image, this should be disabled.

Don’t run your code every single frame

A tip you read a lot is “don’t perform any expensive actions in Update(), because they will be called every single frame”. Well, duh. But then where do you check if your current target is still alive? Where do you check if a piece of burning debris or some ragdoll body parts have lived for long enough and should be despawned? You can use coroutines for some scenarios, but what if not? Back to Update(), I guess.

Actions performed in Update() are expensive, because Update() is called every single frame. Ideally, that’s 60x a second, or more. Many actions that you want to perform periodically don’t need to be called within 16ms (1/60 seconds). It’s good enough to check if your target is still alive 10x a second, and checking if something should despawn can probably be done only once or twice a second.

I have created a very simple class for this, which I use instead of directly inheriting from MonoBehaviour when I have code I want to run less frequently than once per frame. It’s nothing special, in fact I can already see a few things that could be improved. But it it gives me the option to run code 1/2/4/5/10x a second and passes me an accurate deltaTime that I can use, so it does the job.

Warning: If you end up using this class, any Code called in Update?xPerSecond can no longer use Time.deltaTime. Time.deltaTime returns the time since the last frame. You need to use the deltaTime that was provided as a parameter, which returns the time since this particular method was last called. Also, Update10xPerSecond does NOT get called exactly 10x per second. It gets called UP TO 10x per second (it’s probably closer to 8x per second). There are ways to implement this a lot more accurately, but the method that I have used is extremely simple and works good enough for me.

I would strongly recommend using something similar to this class – something that allows you to call code periodically, but not every frame. Make it a habit: Every time you’re about to add code to an Update() function, ask yourself: does it really have to run every frame? How many times a second does it have to run in order to do its job?

Usage example 1:

public class AmbientSound3D : ExtendedMonoBehaviour
{
    ...

    public override void Update4xPerSecond(float deltaTime)
    {
        SetVolumeBasedOnDistance();
    }

    ...
 }

Usage example 2:

    public class AnalogClock : ExtendedMonoBehaviour
    {
        public Transform HourHand;
        public Transform MinuteHand;
        public float HourHandOffset = 0f;
        public float MinuteHandOffset = 0f;
        public int DebugHours = -1;
        public int DebugMinutes = -1;

        public override void Update1xPerSecond(float deltaTime)
        {
            SetHands();
        }

        private void SetHands()
        {
            float hours = System.DateTime.Now.Hour;
            float minutes = System.DateTime.Now.Minute;

            if (DebugHours > -1) hours = DebugHours;
            if (DebugMinutes > -1) minutes = DebugMinutes;

            if (hours > 12) hours -= 12;

            hours += minutes / 60f;
            minutes += System.DateTime.Now.Second / 60f;

            HourHand.localRotation = Quaternion.Euler(0f, 0f, hours * 30f + HourHandOffset);
            MinuteHand.localRotation = Quaternion.Euler(0f, 0f, minutes * 6f + MinuteHandOffset);
        }
    }