Application processor optimizations

This section of the guide describes a few application processor optimizations that can improve the performance of your Unity programs.

 

Use coroutines

A coroutine is a function of type IEnumerator that can return control to Unity with a special yield return statement. You can call the function again later, and it resumes where it left off.

Use coroutines instead of Invoke()

The Monobehaviour.Invoke() method is a fast and convenient way to call a method in a class with a time delay. However, the method has the following limitations:

  • Monobehaviour.Invoke() uses reflection in C# to find the method to call, which can be slower than calling the method directly.
  • There are no compile-time checks on the method signature.
  • You cannot supply more parameters.

The following code shows the Invoke() function:

public void Function()
{ 
        [...]
}
Invoke("Function", 3.0f);

 

An alternative method is to use coroutines. The following code shows calling coroutines through the MonoBehaviour.StartCoroutine() method:

public IEnumerator Function(float delay)
{
        yield return new WaitForSeconds(delay);
        [...]
}
StartCoroutine(Function(3.0f));

Changing from the Monobehaviour.Invoke() method to using coroutines provides more flexibility over the parameters that are passed to the functions dealing with animation states.

Use coroutines for relaxed updates

If your game requires a repeated action at a specific time interval, you can try launching a coroutine in the MonoBehaviour.Start() callback. Launching a coroutine is an alternative to performing an action in every frame through the MonoBehaviour.Update() callback. The following code shows an example of a coroutine:

void Update()
{
        // Perform an action every frame 
}
                                        
IEnumerator Start() 
{
        while(true)
        {
                // Do something every quarter of second 
                yield return new WaitForSeconds(0.25f);
        }
}

Note: Another use of coroutines is to spawn enemies at irregular rather than regular intervals. You can use an infinite loop inside the coroutine that spawns an enemy and generates a random number. Then pass the random number to the WaitForSeconds() function.

 

Avoid hard-coded strings for tags

Avoid hard-coded values for tags. This is because they restrict the scalability and robustness of your game. For example, with tag names, if you refer to the names directly by strings you cannot easily modify them. Therefore, you are potentially exposed to spelling errors. A hard-coded value for a tag is shown in the following code:

if(gameObject.CompareTag("Player"))
{
        [...]
}                               

You can improve the preceding code by implementing a special class for tags that exposes public constant strings. For example:

public class Tags
{
        public const string Player = "Player";
        [...]
}
if(gameObject.CompareTag(Tags.Player))
{
        [...]
}                               

 

Reduce the number of physics calculations

Most physics calculations take place at a fixed time step. You can increase or decrease the length of this step to reduce computation load. Increasing the time step decreases the load on the application processor, but reduces the accuracy of physics calculations.

To access the time manager from the main menu, select Edit > Project Settings > Time.

The following image shows the time manager:

Fixed timestep settings

 

Remove empty callbacks

If your code includes empty definitions for functions, like Awake(), Start(), or Update(), remove them. There is an overhead associated with the empty functions. This is because the engine attempts to access them even though they are empty. For example:

// Remove the following empty definition

void Awake()
{

}      

 

Avoid using GameObject.Find() in every frame

GameObject.Find() is a function that iterates through every object in the scene. This function can cause a significant increase in the main thread size if it is used in an incorrect part of your code. For example:

void Update()
{ 
        GameObject playerGO = GameObject.Find("Player");
        playerGO.transform.Translate(Vector3.forward * Time.deltaTime);
}

A better technique is to call GameObject.Find() on startup and cache the result, for example, in the Start() or Awake() function. The following code uses the Start() function:

private GameObject _playerGO = null;
                                
void Start()
{
        _playerGO = GameObject.Find("Player");
}
                                
void Update()
{
        _playerGO.transform.Translate(Vector3.forward * Time.deltaTime);
}

The function GameObject.FindWithTag() is a faster alternative to GameObject.Find(). The following code shows GameObject.FindWithTag():

void Update()
{
        GameObject playerGO = GameObject.FindWithTag("Player");
        playerGO.transform.Translate(Vector3.forward * Time.deltaTime);
}

Note: Use a dedicated class called LocatorManager that performs all the object retrievals immediately when the scene finishes loading. This allows other classes to use LocatorManager as a service, so that objects are not retrieved multiple times.

 

Use the StringBuilder class to concatenate strings

When concatenating complex strings, use the System.Text.StringBuilder class. This class is faster than the string.Format() method and uses less memory than concatenation with the plus operator. This code shows the plus operator concatenation and string.Format() methods:

// Concatenation with the plus operator
string str = "foo" + "bar";

// String.Format() method 
string str = string.Format("{1}{2}", "foo", "bar");

To make the code faster, use the System.Text.StringBuilder class:

// StringBuilder class 
using System.Text;

StringBuilder strBld = new StringBuilder(); 
strBld.Append("foo"); 
strBld.Append("bar"); 
string str = strBld.toString(); 

The following screenshot shows the difference in performance between using the string.Format() method, the concatenation method and the StringBuilder class:

Performance for the three methods

 

Use the CompareTag() method

Use the GameObject.CompareTag() method instead of the GameObject.tag property. This is because the CompareTag() method is faster and does not allocate extra memory. These methods are shown in the following code:

GameObject mainCamera = GameObject.Find("Main Camera");

// Gameobject.tag property
if(mainCamera.tag == "MainCamera")
{
        // Perform an action
}

// Gameobject.CompareTag() method
if(mainCamera.CompareTag("MainCamera"))
{
        // Perform an action
}

The following image compares the use of CompareTag() and GameObject:

Compare tag and game object comparison

 

Use object pools

If your game has many objects of the same kind that are created and destroyed at runtime, you can use the design pattern object pool. This design pattern avoids the performance penalty of allocating and freeing many objects dynamically. Using object pools for enemies and bombs restricts the allocation of those objects to the loading phase of the game.

If you know the total number of objects that you require, you can create them all immediately and disable the objects that are not immediately required. When a new object is required, search the pool for the first unused one and enable it.

When an object is not required anymore, you can return it to the pool by disabling it and resetting it to a default starting state.

You can use this technique with objects like enemies, projectiles, and particles. If you do not know the exact number of objects that you require, test to find out how many are used. Create a pool that is slightly bigger than the number that you find, to have a safety margin. Without a safety margin, the player’s experience can be affected by an object disappearing as the game creates a new one. The game might even crash if it fails to create needed objects.

 

Cache component retrievals

Cache the component instance that GameObject.GetComponent() returns. The function call that is involved is quite expensive.

Properties like GameObject.camera, GameObject.renderer, or GameObject.transform are shortcuts to the corresponding GameObject.GetComponent(), GameObject.GetComponent(), and GameObject.GetComponent () . The following code shows the correct usage:

private Transform _transform = null;

void Start()
{ 
        _transform = GameObject.GetComponent();
}

void Update() 
{
        _transform.Translate(Vector3.forward * Time.deltaTime); 
}

Consider caching the return value of Transform.position. Even if the function is a C# getter property, there is overhead associated with an iteration over the transform hierarchy to calculate the global position.

Note: In Unity 5 and newer, the transform component is automatically cached.

 

Use OnBecameVisible() and OnBecameInvisible() callbacks

Callbacks like MonoBehaviour.OnBecameVisible() and MonoBehaviour.OnBecameInvisible() notify your scripts if their associated game objects become visible or invisible on screen.

These calls enable you to, for example, disable computationally heavy code routines or effects when a game object is not rendered on screen.

 

Use sqrMagnitude for comparing vector magnitudes

If your application requires the comparison of vector magnitudes, use Vector3.sqrMagnitude instead of Vector3.Distance() or Vector3.magnitude.

Vector3.sqrMagnitude sums the squared components without calculating the root, but this sum is useful for comparisons. The other calls use a computationally expensive square root.

The following code shows the three different techniques that are used to compare two positions in space:

// Vector3.sqrMagnitude property 
if ((_transform.position - targetPos).sqrMagnitude < maxDistance * maxDistance) 
{
        // Perform an action 
}

// Vector3.Distance() method
if (Vector3.Distance(transform.position, targetPos) < maxDistance)
{ 
        // Perform an action
} 
                                        
// Vector3.magnitude property 
if ((_transform.position - targetPos).magnitude < maxDistance) 
{ 
        // Perform an action
}

 

Use built-in arrays

If you know the size of an array in advance, use the built-in arrays.

The ArrayList and List classes have more flexibility than built-in arrays because they grow when you insert more elements. However, they are slower than the built-in arrays.

 

Use planes as collision targets

If your scene only requires particle collisions with planar objects like floors or walls, change the particle system collision mode to Planes to reduce the required computations. In this mode, you can provide Unity with a list of empty GameObjects to act as the collider planes.

The following image shows collision settings for Planes mode:

Collision settings for Planes mode

 

Use compound primitive colliders

Mesh colliders are based on the real geometry of an object. Mesh colliders are accurate for collision detection but are computationally expensive.

You can combine shapes like boxes, capsules, or spheres into a compound collider that mimics the shape of the original mesh. Combining shapes provides similar results to mesh colliders, with a much lower computational overhead.

Previous Next