Cache GameObject and component references to avoid expensive lookups

Optimization principal 2: Minimize actions requiring Unity to perform "reflection" over objects and searching of all current scene objects.

Reflection is when, at run time, Unity has to analyze objects to see whether they contain a method corresponding to a "message" that the object has received - an example would be SendMessage(). An example of making Unity perform a search over all active objects in a scene would be the simple and useful, but slow, FindObjectsByTag(). Another action that slows Unity down is each time we make it look up an object's component using GetComponent().

Cache GameObject and component references to avoid expensive lookups

In the olden days for many components, Unity offered quick component property getters such as .audio to reference the AudioSource component of a script's parent GameObject, rigidbody to reference the RigidBody component, and so on. However, this wasn't a consistent rule, and in other cases, you had to use GetComponent(). With Unity 5, all these quick component property getters have been removed (with the exception of .transform, which is automatically cached, so has no performance cost to use). To help game developers update their scripts to work with Unity 5, they introduced Automatic Script Updating, whereby (after a suitable warning to have backed up files before going ahead!) Unity will go through scripts replacing quick component property getters code with the standardized GetComponent<ComponentTyle>() code pattern, such as GetComponent<Rigidbody>() and GetComponent<AudioSource>(). However, while script updating makes things consistent, and also makes explicit all these GetComponent() reflection statements, each GetComponent() execution eats up valuable processing resources.

Note

You can read more about Unity's reasons for this (and the alternative Extension Methods approach they rejected; a shame—I think we'll see them appear in a later version of Unity since it's an elegant way to solve this coding situation) in this June 2014 blog post and manual page at:

In this recipe, we'll incrementally refactor a method, making it more efficient at each step by removing reflection and component lookup actions. The method we'll improve is to find half the distance from the GameObject in the scene tagged Player (a 3rd Person Controller) and 1,000 other GameObjects in the scene tagged Respawn.

Getting ready

For this recipe, we have prepared a package named unity4_assets_handyman_goodDirt containing the 3rdPersonController handyman and Terrain material goodDirt. The package is in the folder 1362_11_15.

How to do it...

To improve code performance by caching component lookups, follow these steps:

  1. Create a new 3D project, importing the provided Unity package unity4_assets_handyman_goodDirt.
  2. Create a new Terrain (size 200 x 200, located at -100, 0, -100) and texture-paint it with GoodDirt.
  3. Add a 3rdPersonController at the center of the terrain (that is, 0, 1, 0). Note that this will already be tagged Player.
  4. Create a new Sphere and give it the tag Respawn.
  5. In the Project panel, create a new empty prefab named prefab_sphere and drag the Sphere from the Hierarchy panel into your prefab in the Project panel.
  6. Now, delete the Sphere from the Hierarchy panel (since all its properties have been copied into our prefab).
  7. Add the following C# script class SphereBuilder to the Main Camera:
    using UnityEngine;
    using System.Collections;
    
    public class SphereBuilder : MonoBehaviour
    {
      public const int NUM_SPHERES = 1000;
      public GameObject spherePrefab;
    
      void Awake(){
        List<Vector3> randomPositions = BuildVector3Collection(NUM_SPHERES);
        for(int i=0; i < NUM_SPHERES; i++){
          Vector3 pos = randomPositions[i];
          Instantiate(spherePrefab, pos, Quaternion.identity);
        }
      }
    
      public List<Vector3> BuildVector3Collection(int numPositions){
        List<Vector3> positionArrayList = new List<Vector3>();
        for(int i=0; i < numPositions; i++) {
          float x = Random.Range(-100, 100);
          float y = Random.Range(1, 100);
          float z = Random.Range(-100, 100);
          Vector3 pos = new Vector3(x,y,z);
          positionArrayList.Add (pos);
        }
    
        return positionArrayList;
      }
    }
  8. With the Main Camera selected in the Hierarchy, drag prefab_sphere from the Project panel in Inspector public variable Sphere Prefab, for script component SphereBuilder, as shown in the following screenshot:
    How to do it...
  9. Add the following C# script class SimpleMath to the Main Camera:
    using UnityEngine;
    using System.Collections;
    
    public class SimpleMath : MonoBehaviour {
      public float Halve(float n){
        return n / 2;
      }
    }

Method 1 – AverageDistance calculation

Follow these steps:

  1. Add the following C# script class AverageDistance to the Main Camera:
    using UnityEngine;
    using System.Collections;
    using System;
    
    public class AverageDistance : MonoBehaviour
    {
      void Update(){
        // method1 - basic
        Profiler.BeginSample("TESTING_method1");
        GameObject[] sphereArray = GameObject.FindGameObjectsWithTag("Respawn");
        for (int i=0; i < SphereBuilder.NUM_SPHERES; i++){
          HalfDistanceBasic(sphereArray[i].transform);
        }
        Profiler.EndSample();
      }
    
      // basic
      private void HalfDistanceBasic(Transform sphereGOTransform){
        Transform playerTransform = GameObject.FindGameObjectWithTag("Player").transform;
        Vector3 pos1 = playerTransform.position;
        Vector3 pos2 = sphereGOTransform.position;
    
        float distance = Vector3.Distance(pos1, pos2);
    
        SimpleMath mathObject = GetComponent<SimpleMath>();
        float halfDistance = mathObject.Halve(distance);
      }
    }
  2. Open the Profiler panel and ensure that record is selected and and that the script processing load is being recorded.
  3. Run the game for 10 to 20 seconds.
  4. In the Profiler panel, restrict the listed results to only samples starting with TEST. For whichever frame you select, you should see the percentage CPU load and milliseconds required for TESTING_method1.

Method 2 – Cache array of Respawn object transforms

Follow these steps:

  1. FindGameObjectWithTag() is slow, so let's fix that for the search for objects tagged Respawn. First, in C# script class AverageDistance, add a private Transform array variable named sphereTransformArrayCache:
    private Transform[] sphereTransformArrayCache;
  2. Now, add the Start() method, the statement that stores in this array references to the Transform component of all our Respawn tagged objects:
    private void Start(){
      GameObject[] sphereGOArray = GameObject.FindGameObjectsWithTag("Respawn");
      sphereTransformArrayCache = new Transform[SphereBuilder.NUM_SPHERES];
      for (int i=0; i < SphereBuilder.NUM_SPHERES; i++){
        sphereTransformArrayCache[i] = sphereGOArray[i].transform;
      }
    }
  3. Now, in the Update()method, start a new Profiler sample named TESTING_method2, which uses our cached array of games objects tagged with Respawn:
    // method2 - use cached sphere ('Respawn' array)
    Profiler.BeginSample("TESTING_method2");
    for (int i=0; i < SphereBuilder.NUM_SPHERES; i++){
      HalfDistanceBasic(sphereTransformArrayCache[i]);
    }
    Profiler.EndSample();
  4. Once again, run the game for 10 to 20 seconds and set the Profiler panel to restrict the listed results to only samples starting with TEST. For whichever frame you select, you should see the percentage CPU load and milliseconds required for TESTING_method1 and TESTING_method2.

Method 3 – Cache reference to Player transform

That should run faster. But wait! Let's improve things some more. Let's make use of a cached reference to Cube-Player component's transform, avoiding the slow object-tag reflection lookup altogether. Follow these steps:

  1. First, add a new private variable and a statement in the Start()method to assign the Player object's transform in this variable playerTransformCache:
    private Transform playerTransformCache;
    private Transform[] sphereTransformArrayCache;
    
    private void Start(){
      GameObject[] sphereGOArray = GameObject.FindGameObjectsWithTag("Respawn");
      sphereTransformArrayCache = new Transform[SphereBuilder.NUM_SPHERES];
      for (int i=0; i < SphereBuilder.NUM_SPHERES; i++){
        sphereTransformArrayCache[i] = sphereGOArray[i].transform;
      }
    
      playerTransformCache = GameObject.FindGameObjectWithTag("Player").transform;
    }
  2. Now, in Update(), add the following code to start a new Profiler sample named TESTING_method3:
    // method3 - use cached playerTransform
    Profiler.BeginSample("TESTING_method3");
    for (int i=0; i < SphereBuilder.NUM_SPHERES; i++){
    HalfDistanceCachePlayerTransform(sphereTransformArrayCache[i]);
    }
    Profiler.EndSample();
  3. Finally, we need to write a new method that calculates the half distance making use of the cached player transform variable we have set up. So, add this new method, HalfDistanceCachePlayerTransform( sphereTransformArrayCache[i] ):
    // playerTransform cached
    private void HalfDistanceCachePlayerTransform(Transform sphereGOTransform){
      Vector3 pos1 = playerTransformCache.position;
      Vector3 pos2 = sphereGOTransform.position;
      float distance = Vector3.Distance(pos1, pos2);
      SimpleMath mathObject = GetComponent<SimpleMath>();
      float halfDistance = mathObject.Halve(distance);
    }

Method 4 – Cache Player's Vector3 position

Let's improve things some more. If, for our particular game, we can make the assumption that the player character does not move, we have an opportunity to cache the player's position once, rather than retrieving it for each frame.

Follow these steps:

  1. At the moment, to find pos1, we are making Unity find the position Vector3 value inside playerTransform every time the Update() method is called. Let's cache this Vector3 position with a variable and statement in Start(), as follows:
    private Vector3 pos1Cache;
    
    private void Start(){
    ...
    pos1Cache = playerTransformCache.position;
    }
  2. Now, write a new half-distance method that makes use of this cached position:
    // player position cached
    private void HalfDistanceCachePlayer1Position(Transform sphereGOTransform){
      Vector3 pos1 = pos1Cache;
      Vector3 pos2 = sphereGOTransform.position;
      float distance = Vector3.Distance(pos1, pos2);
      SimpleMath mathObject = GetComponent<SimpleMath>();
      float halfDistance = mathObject.Halve(distance);
    }
  3. Now, in the Update() method, add the following code so that we create a new sample for our method 4, and call our new half-distance method:
    // method4 - use cached playerTransform.position
    Profiler.BeginSample("TESTING_method4");
    for (int i=0; i < SphereBuilder.NUM_SPHERES; i++){
      HalfDistanceCachePlayer1Position(sphereTransformArrayCache[i]);
    }
    Profiler.EndSample();

Method 5 – Cache reference to SimpleMath component

That should improve things again. But we can still improve things—you'll notice in our latest half-distance method that we have an explicit GetComponent() call to get a reference to our mathObject; this will be executed every time the method is called. Follow these steps:

  1. Let's cache this scripted component reference as well to save a GetComponent() reflection for each iteration. We'll declare a variable mathObjectCache, and in Awake(), we will set it to refer to our SimpleMath scripted component:
    private SimpleMath mathObjectCache;
    
    private void Awake(){
      mathObjectCache = GetComponent<SimpleMath>();
    }
  2. Let's write a new half-distance method that uses this cached reference to the math component HalfDistanceCacheMathComponent(i):
    // math Component cache
    private void HalfDistanceCacheMathComponent(Transform sphereGOTransform){
      Vector3 pos1 = pos1Cache;
      Vector3 pos2 = sphereGOTransform.position;
      float distance = Vector3.Distance(pos1, pos2);
      SimpleMath mathObject = mathObjectCache;
      float halfDistance = mathObject.Halve(distance);
    }
  3. Now, in the Update() method, add the following code so that we create a new sample for our method5 and call our new half-distance method:
    // method5 - use cached math component
    Profiler.BeginSample("TESTING_method5");
    for (int i=0; i < SphereBuilder.NUM_SPHERES; i++){
      HalfDistanceCacheMathComponent(sphereTransformArrayCache[i]);
    }
    Profiler.EndSample();

Method 6 – Cache array of sphere Vector3 positions

We've improved things quite a bit, but there is still a glaring opportunity to use caching to improve our code (if we can assume that the spheres do not move, which seems reasonable in this example). At present, for every frame and every sphere in our half-distance calculation method, we are asking Unity to retrieve the value of the Vector3 position property in the transform of the current sphere (this is our variable pos2), and this position is used to calculate the distance of the current sphere from Player. Let's create an array of all those Vector3 positions so that we can pass the current one to our half-distance calculation method and save the work of retrieving it so many times.

Follow these steps:

  1. First, add a new private variable and a statement inside our existing loop in the Start() method to assign each sphere's Vector3 transform position in the array spherePositionArrayCache:
    private Vector3[] spherePositionArrayCache = new Vector3[SphereBuilder.NUM_SPHERES];
    
    private void Start(){
      GameObject[] sphereGOArray = GameObject.FindGameObjectsWithTag("Respawn");
      sphereTransformArrayCache = new Transform[SphereBuilder.NUM_SPHERES];
      for (int i=0; i < SphereBuilder.NUM_SPHERES; i++){
        sphereTransformArrayCache[i] = sphereGOArray[i].transform;
        spherePositionArrayCache[i] = sphereGOArray[i].transform.position;
      }
    
      playerTransformCache = GameObject.FindGameObjectWithTag("Player").transform;
      pos1Cache = playerTransformCache.position;
    }
  2. Let's write a new half-distance method that uses this array of cached positions:
    // sphere position cache
    private void HalfDistanceCacheSpherePositions(Transform sphereGOTransform, Vector3 pos2){
      Vector3 pos1 = pos1Cache;
      float distance = Vector3.Distance(pos1, pos2);
      SimpleMath mathObject = mathObjectCache;
      float halfDistance = mathObject.Halve(distance);
    }
  3. Now, in the Update()method, add the following code so that we create a new sample for our method6 and call our new half-distance method:
    // method6 - use cached array of sphere positions
    Profiler.BeginSample("TESTING_method6");
    for (int i=0; i < SphereBuilder.NUM_SPHERES; i++){
    HalfDistanceCacheSpherePositions(sphereTransformArrayCache[i], spherePositionArrayCache[i]);
    }
    Profiler.EndSample();
  4. Open the Profiler panel and ensure that record is selected and script processing load is being recorded.
  5. Run the game for 10 to 20 seconds.
  6. In the Profiler panel, restrict the listed results to only samples starting with TEST. For whichever frame you select, you should see the percentage CPU load and milliseconds required for each method (lower is better for both these values!). For almost every frame, you should see how/if each method refined by caching has reduced the CPU load.
    Method 6 – Cache array of sphere Vector3 positions

How it works...

This recipe illustrates how we try to cache references once, before any iteration, for variables whose value will not change, such as references to GameObjects and their components, and, in this example, the Transform components and Vector3 positions of objects tagged Player and Respawn. Of course, as with everything, there is a "cost" associated with caching, and that cost is the memory requirements to store all those references. This is known as the Space-Time Tradeoff. You can learn more about this classic computer science speed versus memory tradeoff at https://en.wikipedia.org/wiki/Space%E2%80%93time_tradeoff.

In methods that need to be performed many times, this removing of implicit and explicit component and object lookups may offer a measurable performance improvement.

Note

Note: Two good places to learn more about Unity performance optimization techniques are from the Performance Optimization web page in the Unity script reference and from Unity's Jonas Echterhoff and Kim Steen Riber Unite2012 presentation Performance Optimization Tips and Tricks for Unity. Many recipes in this chapter had their origins from suggestions in the following sources:

See also

Refer to the following recipes in this chapter for more information:

  • Improving efficiency with delegates and events and avoiding SendMessage!
  • Identifying performance bottlenecks with the Unity performance Profiler
  • Identifying performance bottlenecks with Do-It-Yourself performance profiling
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.15.29.119