Developing Game in Unity? Check Template Projects, Tools, VFX & many more! (close)

Unity 3D How to Use Profiler to Optimize Your Code

420

Performance is a key aspect of any game, and no surprise. No matter how good the game is, if it runs poorly on user's machine, it will not feel as enjoyable.

And since not everyone has a high-end PC / Device, it's important to pay special attention to this aspect.

Now, there are multiple reasons why the game could run slowly, I will highlight them below:

  • Due to Rendering (Too many complex meshes, advanced shaders or image effects)
  • Due to Audio (Mostly caused by incorrect import settings, you can check this article to learn more about it)
  • Due to Poorly Written Code (Scripts are the core of any game and it's really easy to write the code that does its job but contains performance-demanding functions in the wrong places)

In this post I will be showing how to optimize your code with a help of Unity Profiler.

Unity version used in this tutorial: Unity 2019.3.4f1 (64-bit)

Intro

Historically, debugging performance in Unity was a tedious task, but since then, a new feature has been added, called Profiler.

Unity 3D Profiler Chart

Profiler lets you quickly pinpoint the bottlenecks in your game which simplify the optimization process.

Example Use

Performance degradation can happen at any time: Let's say you're working on enemy instance and when you place it in the scene, it works fine without any issues, however as you spawn more enemies you may notice fps (frames per second) begin to fall.

Check the example below:

In my Scene I have a Cube with a script attached to it, which moves the Cube from side to side and displays the Object name:

SC_ShowName.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class SC_ShowName : MonoBehaviour
{
    bool moveLeft = true;
    float movedDistance = 0;

    // Start is called before the first frame update
    void Start()
    {
        moveLeft = Random.Range(0, 10) > 5;
    }

    // Update is called once per frame
    void Update()
    {
        //Move left and right in ping-pong fashion
        if (moveLeft)
        {
            if(movedDistance > -2)
            {
                movedDistance -= Time.deltaTime;
                Vector3 currentPosition = transform.position;
                currentPosition.x -= Time.deltaTime;
                transform.position = currentPosition;
            }
            else
            {
                moveLeft = false;
            }
        }
        else
        {
            if (movedDistance < 2)
            {
                movedDistance += Time.deltaTime;
                Vector3 currentPosition = transform.position;
                currentPosition.x += Time.deltaTime;
                transform.position = currentPosition;
            }
            else
            {
                moveLeft = true;
            }
        }
    }

    void OnGUI()
    {
        //Show object name on screen
        Camera mainCamera = Camera.main;
        Vector2 screenPos = mainCamera.WorldToScreenPoint(transform.position + new Vector3(0, 1, 0));
        GUI.color = Color.green;
        GUI.Label(new Rect(screenPos.x - 150/2, Screen.height - screenPos.y, 150, 25), gameObject.name);
    }
}

Looking at the stats, we can see that the game runs at a good 800+ fps, so it barely have any impact on performance.

Now let's see what happen when we duplicate the Cube to 100 instances:

Fps dropped by more than 700 points!

NOTE: All tests are done with Vsync disabled

Generally, it's a good idea to start optimizing when the game begins to exhibit stuttering, freezing or the fps drops below 120.

We will be using Profiler to find which part of the code is slowing the game.

  • Start your game by pressing Play
  • Go to Window -> Analysis -> Profiler (or press Ctrl + 7)

New Window will appear that look something like this:

Unity 3D Profiler Window

It might look intimidating at first (with all those charts etc.), but it's not the part where we will be looking at.

  • Click on the Timeline tab and change it to Hierarchy:

  • You'll notice 3 sections (EditorLoop, PlayerLoop and Profiler.CollectEditorStats):

  • Expand the PlayerLoop to see all the parts where the computation power is being spent (NOTE: If the PlayerLoop values are not updating, click on "Clear" button at the top of the Profiler window).

For the best results, direct your game character to the situation (or place) where the game lags the most and wait for couple of seconds.

  • After waiting a bit, Stop the game and observe PlayerLoop list

Specifically you need to look at GC Alloc value, which stand for Garbage Collection Allocation. This is a type of memory that has been allocated by the component but is no longer needed and is waiting to be freed by the Garbage Collection mechanism. Ideally the code should not generate garbage (or be as close to 0 as possible).

Time ms is also an important value, it shows how long the code took to run in milliseconds, so ideally you should aim to reduce this value as well (by caching values, avoiding calling performance demanding functions each Update etc.).

To locate the troublesome parts faster, click on GC Alloc column 2 times to sort the values from higher to lower)

  • In the CPU Usage chart click anywhere to skip to that frame. Specifically we need to look at a peaks, where the fps was the lowest:

Unity CPU Usage Chart

Here is what the Profiler revealed in my case:

GUI.Repaint is allocating a whopping 45.4KB which is quite alot. Expanding it revealed even more info:

  • Conveniently it shows that most of the allocations are coming from GUIUtility.BeginGUI() and OnGUI() method in SC_ShowName script. Knowing that we can begin optimizing it.

GUIUtility.BeginGUI() represents an empty OnGUI() method (Yes, even empty OnGUI() method allocates quite a lot of memory).

TIP: If you see a name in the profiler which you don't recognize, simply search it in Google or other search engine and chances are, someone has already explained it.

And here is the OnGUI() part of the code:

    void OnGUI()
    {
        //Show object name on screen
        Camera mainCamera = Camera.main;
        Vector2 screenPos = mainCamera.WorldToScreenPoint(transform.position + new Vector3(0, 1, 0));
        GUI.color = Color.green;
        GUI.Label(new Rect(screenPos.x - 150/2, Screen.height - screenPos.y, 150, 25), gameObject.name);
    }

Optimization

Each SC_ShowName script is calling its own OnGUI() method, which is not good considering we have 100 instances. So what can be done about it? The answer is, to have a single script with OnGUI() method that call GUI method for each Cube.

  • First, I replaced void OnGUI() in SC_ShowName.cs with public void GUIMethod() which will be called from another script:
    public void GUIMethod()
    {
        //Show object name on screen
        Camera mainCamera = Camera.main;
        Vector2 screenPos = mainCamera.WorldToScreenPoint(transform.position + new Vector3(0, 1, 0));
        GUI.color = Color.green;
        GUI.Label(new Rect(screenPos.x - 150/2, Screen.height - screenPos.y, 150, 25), gameObject.name);
    }
  • Then I created a new script and called it SC_GUIMethod:

SC_GUIMethod.cs

using UnityEngine;

public class SC_GUIMethod : MonoBehaviour
{
    SC_ShowName[] instances; //All instances where GUI method will be called

    void Start()
    {
        //Find all instances
        instances = FindObjectsOfType<SC_ShowName>();
    }

    void OnGUI()
    {
        for(int i = 0; i < instances.Length; i++)
        {
            instances[i].GUIMethod();
        }
    }
}

SC_GUIMethod.cs is going to be attached to any Object in the scene, then at the start it will search for all the instances and call their GUI methods.

  • We went from having 100 OnGUI() methods to having just one, let's run the Profiler again and see the result:

  • GUIUtility.BeginGUI() is now only allocating 368B instead of 36.7KB, big reduction!

However, SC_GUIMethod's OnGUI() method is still allocating memory, but since we know it's only calling GUIMethod() from SC_ShowName.cs, we are going straight to debugging that method.

But Profiler is only showing the memory allocated by a method, how do we see what exactly is allocating memory inside that method?

Good question, for this, Unity has a handy API called Profiler.BeginSample

Profiler.BeginSample allows you to capture specific section of the method, showing how long it takes to complete and how much memory it allocates.

  • Before being able to use Profiler class in code, we need to import the UnityEngine.Profiling namespace at the beginning of the script:
using UnityEngine.Profiling;
  • The Profiler sample is captured by adding Profiler.BeginSample("SOME_NAME"); at the start of capture and adding Profiler.EndSample(); at the end of capture, like this:
        Profiler.BeginSample("SOME_CODE");
        //...your code goes here
        Profiler.EndSample();

Since I don't know what part of the GUIMethod() is causing memory allocations, I enclosed each line in Profiler.BeginSample and Profiler.EndSample (But if your method has a lot of lines, you definitely don't need to enclose each line, just split it into even chunks and then work from there).

Here is a final method with Profiler Samples implemented:

    public void GUIMethod()
    {
        //Show object name on screen
        Profiler.BeginSample("sc_show_name part 1");
        Camera mainCamera = Camera.main;
        Profiler.EndSample();

        Profiler.BeginSample("sc_show_name part 2");
        Vector2 screenPos = mainCamera.WorldToScreenPoint(transform.position + new Vector3(0, 1, 0));
        Profiler.EndSample();

        Profiler.BeginSample("sc_show_name part 3");
        GUI.color = Color.green;
        GUI.Label(new Rect(screenPos.x - 150/2, Screen.height - screenPos.y, 150, 25), gameObject.name);
        Profiler.EndSample();
    }
  • Now I press Play and see what it shows in the Profiler:
  • For convenience, I searched for "sc_show_" in the Profiler to show the samples that start with that name.

  • Interesting... A lot of memory is being allocated in sc_show_names part 3, which corresponds to this part of code:
        GUI.color = Color.green;
        GUI.Label(new Rect(screenPos.x - 150/2, Screen.height - screenPos.y, 150, 25), gameObject.name);

As you gain experience in coding, you would be able to filter some parts right away, for example Color.green and new Rect() are unlikely to allocate that much memory, so the only part that's left is gameObject.name.

Of course after some Googling I discovered that getting Object's name allocates quite a lot of memory. The solution for this is to assign Object's name to a string variable in the void Start(). That way it will only be called once.

Here is the more optimized code:

SC_ShowName.cs

using UnityEngine;
using UnityEngine.Profiling;

public class SC_ShowName : MonoBehaviour
{
    bool moveLeft = true;
    float movedDistance = 0;

    string objectName = "";

    // Start is called before the first frame update
    void Start()
    {
        moveLeft = Random.Range(0, 10) > 5;
        objectName = gameObject.name; //Store Object name to a variable
    }

    // Update is called once per frame
    void Update()
    {
        //Move left and right in ping-pong fashion
        if (moveLeft)
        {
            if(movedDistance > -2)
            {
                movedDistance -= Time.deltaTime;
                Vector3 currentPosition = transform.position;
                currentPosition.x -= Time.deltaTime;
                transform.position = currentPosition;
            }
            else
            {
                moveLeft = false;
            }
        }
        else
        {
            if (movedDistance < 2)
            {
                movedDistance += Time.deltaTime;
                Vector3 currentPosition = transform.position;
                currentPosition.x += Time.deltaTime;
                transform.position = currentPosition;
            }
            else
            {
                moveLeft = true;
            }
        }
    }

    public void GUIMethod()
    {
        //Show object name on screen
        Profiler.BeginSample("sc_show_name part 1");
        Camera mainCamera = Camera.main;
        Profiler.EndSample();

        Profiler.BeginSample("sc_show_name part 2");
        Vector2 screenPos = mainCamera.WorldToScreenPoint(transform.position + new Vector3(0, 1, 0));
        Profiler.EndSample();

        Profiler.BeginSample("sc_show_name part 3");
        GUI.color = Color.green;
        GUI.Label(new Rect(screenPos.x - 150/2, Screen.height - screenPos.y, 150, 25), objectName);
        Profiler.EndSample();
    }
}
  • Let's see what the Profiler is showing:

Voila! No more memory is being allocated.

Of course that code can be optimized even more, further reducing Time ms (For example caching Camera.main to a variable etc.)