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()
.
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.
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
.
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
.
To improve code performance by caching component lookups, follow these steps:
unity4_assets_handyman_goodDirt
.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; } }
Sphere Prefab
, for script component SphereBuilder,
as shown in the following screenshot:SimpleMath
to the Main Camera:using UnityEngine; using System.Collections; public class SimpleMath : MonoBehaviour { public float Halve(float n){ return n / 2; } }
Follow these steps:
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); } }
TEST
. For whichever frame you select, you should see the percentage CPU load and milliseconds required for TESTING_method1.Follow these steps:
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;
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; } }
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();
TEST
. For whichever frame you select, you should see the percentage CPU load and milliseconds required for TESTING_method1 and TESTING_method2.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:
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; }
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();
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); }
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:
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; }
// 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); }
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();
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:
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>(); }
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); }
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();
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:
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; }
// 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); }
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();
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.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: 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:
3.15.29.119