Most of the unity-developers know that you should not abuse expensive operations for productivity, such as, for example, obtaining components. For this it is worth using
caching . But for such a simple optimization one can find several different approaches.
This article will look at various caching options, their unobvious features and performance.

It is worth noting that we will mainly talk about “internal” caching, that is, getting those components that are on the current object for its internal needs. To begin with, we abandon the direct assignment of dependencies in the inspector - this is inconvenient to use, litters the script settings and can lead to broken links when the editor is launched. Therefore, we will use GetComponent ().
')
Basic knowledge of components and a simple example of cachingIn Unity3D, every object on the game scene is a container (GameObject) for various components (Component), which can be either embedded in the engine (Transform, AudioSource, etc.), or custom scripts (MonoBehaviour).
The component can be assigned directly in the editor, and the GetComponent () method is used to get the component from the container in the script.
If the component needs to be used more than once, the traditional approach is to declare a variable in the script, where it will be used, to take the necessary component once and then use the resulting value. Example:
public class Example : MonoBehaviour { Rigidbody _rigidbody; void Start () { _rigidbody = GetComponent<Rigidbody>(); } void Update () { _rigidbody.AddForce(Vector3.up * Time.deltaTime); } }
Initialization caching is also relevant for the properties provided by the GameObject by default, such as .transform, .render, and others. To access them, explicit caching will still be faster (and most of them are marked as deprecated in Unity 5, so it’s good practice to refuse to use them).
It is worth noting that the most obvious caching method (direct acquisition of components) is initially the most efficient in general and other options only use it and provide easier access to components, eliminating the need to write the same type of code each time or receive components on demand.
A little bit about the fact that caching is not importantAlso note that there are much more expensive operations (such as creating and deleting objects on the scene) and caching components, without paying attention to them, will be a waste of time. For example, in your game there is a machine gun that shoots bullets, each of which is a separate object (which in itself is wrong, but this is a spherical example). You create an object, cache a Collider, ParticleSystem and a whole bunch of it, but the bullet flies into the sky and is killed in 3 seconds, and these components are not used at all.
In order to avoid this, use a pool of objects, there are articles on Habré (
1 ,
2 ) about this and there are
ready-made solutions . In this case, you will not constantly create and delete objects and cache them again and again, they will be reused, and caching will happen only once.
The performance of all considered caching options will be displayed in a pivot chart.
The basics
The GetComponent method has two uses: the template GetComponent () and the regular GetComponent (type), which requires an additional cast (comp as T). In the performance summary chart, both of these options will be considered, but it is worth considering that the template method is easier to use. There is also an option to get a list of components GetComponents with similar options, they will also be checked. In the diagrams, the execution time of GetComponent on each platform is taken as 100% for leveling equipment features, and there are also interactive versions for greater convenience.
Use of properties
For caching, you can use properties. The advantage of this method is that caching will occur only when we access the property, and it will not be when that property is used. The downside is that in this case we write more of the same type of code.
The easiest option:
Transform _transform = null; public Transform CachedTransform { get { if( !_transform ) { _transform = GetComponent<Transform>(); } return _transform; } }
This option, due to checking for a missing component, has performance issues.
! component, what is it?Here you need to take into account that a custom comparison operator is used in Unity3D, so when we safely check if a component is cached (if (! Component)), the engine actually turns into native code, which is resource intensive, you can read more in this
article .
There are two options for solving this problem:
Use an additional flag indicating whether caching was performed:
Transform _transform = null; bool _transformCached = false; public Transform CachedTransform { get { if( !_transformCached ) { _transformCached = true; _transform = GetComponent<Transform>(); } return _transform; } }
Explicitly cast a component to an object:
Transform _transform = null; public Transform CachedTransform { get { if( (object)_transform == null ) { _transform = GetComponent<Transform>(); } return _transform; } }
But keep in mind that this option is safe only when the components of the object are not deleted (which usually happens infrequently).
Why in Unity you can refer to the destroyed object?A short excerpt from the article at the link above:
When you get the ac # object of type “GameObject”, it contains almost nothing. Unity is a C / C ++ engine. This is a GameObject (its name, it’s a list of components, it’s HideFlags, etc.) lives in the c ++ side. It is a pointer to the native object. We call these c # objects “wrapper objects”. The life of these c ++ objects like GameObject and everything else that derives from UnityEngine.Object is explicitly managed. These objects get destroyed when you load a new scene. Or when you call Object.Destroy (myObject); on them. Lifetime of c # garment collector. Ac # wrapper object that still exists, it has been destroyed. If you’re not sure, you’ll not want to make it true.
The problem here is that coercion to object allows us to bypass the expensive call of the native code, but at the same time it deprives us of the custom operator to check the existence of the object. Its C # wrapper can still exist when in fact the object is already destroyed.
Inheritance
To simplify the task, you can inherit your classes from the component that caches the most used properties, but this option is not universal (requires creating and modifying all the necessary properties) and does not allow inheriting from other components if necessary (in C # there is no multiple inheritance).
The first problem can be solved using patterns:
public class InnerCache : MonoBehaviour { Dictionary<Type, Component> cache = new Dictionary<Type, Component>(); public T Get<T>() where T : Component { var type = typeof(T); Component item = null; if (!cache.TryGetValue(type, out item)) { item = GetComponent<T>(); cache.Add(type, item); } return item as T; } }
You can get around the second problem by creating a separate component for caching and using a link to it in your scripts.
Static caching
There is a variant of using such a feature of C # as an extension. It allows you to add your methods to existing classes without modifying them and inheriting them. This is done as follows:
public static class ExternalCache { static Dictionary<GameObject, TestComponent> test = new Dictionary<GameObject, TestComponent>(); public static TestComponent GetCachedTestComponent(this GameObject owner) { TestComponent item = null; if (!test.TryGetValue(owner, out item)) { item = owner.GetComponent<TestComponent>(); test.Add(owner, item); } return item; } }
After that, in any script you can get this component:
gameObject.GetCachedTestComponent();
But this option again requires setting all necessary components in advance. You can solve this with the help of templates:
public static class ExternalCache { static Dictionary<GameObject, Dictionary<Type, Component>> cache = new Dictionary<GameObject, Dictionary<Type, Component>>(); public static T GetCachedComponent<T>(this GameObject owner) where T : Component { var type = typeof(T); Dictionary<Type, Component> container = null; if (!cache.TryGetValue(owner, out container)) { container = new Dictionary<Type, Component>(); cache.Add(owner, container); } Component item = null; if (!container.TryGetValue(type, out item)) { item = owner.GetComponent<T>(); container.Add(type, item); } return item as T; } }
The disadvantage of these options is to keep track of dead links. If you do not clear the cache (for example, when loading a scene), then its volume will only grow and clog the memory with references to objects that have already been deleted.
Performance comparison
Interactive optionAs we see, a silver bullet was not found and the most optimal caching option is to receive components directly during initialization. Workarounds are not optimal, except for properties that require writing additional code.
Using Attributes
Attributes allow you to add meta-information for code elements, such as, for example, class members. Attributes themselves are not fulfilled, they must be used with the help of reflection, which is quite an expensive operation.
We can declare our own attribute for caching:
[AttributeUsage(AttributeTargets.Field)] public class CachedAttribute : Attribute { }
And use it for the fields of your classes:
[Cached] public TestComponent Test;
But so far this will not give us anything, this information is not used in any way.
Inheritance
We can create our own class, which will receive class members with this attribute and explicitly receive them during initialization:
public class AttributeCacheInherit : MonoBehaviour { protected virtual void Awake () { CacheAll(); } void CacheAll() { var type = GetType(); CacheFields(GetFieldsToCache(type)); } List<FieldInfo> GetFieldsToCache(Type type) { var fields = new List<FieldInfo>(); foreach (var field in type.GetFields()) { foreach (var a in field.GetCustomAttributes(false)) { if (a is CachedAttribute) { fields.Add(field); } } } return fields; } void CacheFields(List<FieldInfo> fields) { var iter = fields.GetEnumerator(); while (iter.MoveNext()) { var type = iter.Current.FieldType; iter.Current.SetValue(this, GetComponent(type)); } } }
If we create an inheritor of this component, then we can mark its members with the [Cached] attribute, thereby not caring for their explicit caching.
But the problem with performance and the need for inheritance levels the convenience of this method.
Static type cache
The list of class members does not change when the code is executed, so we can get it once, save it for this type and use it in the future, almost without resorting to expensive reflection. To do this, we need a static class that stores the results of the type analysis.
Type Caching public static class CacheHelper { static Dictionary<Type, List<FieldInfo>> cachedTypes = new Dictionary<Type, List<FieldInfo>>(); public static void CacheAll(MonoBehaviour instance, bool internalCache = true) { var type = instance.GetType(); if ( internalCache ) { List<FieldInfo> fields = null; if ( !cachedTypes.TryGetValue(type, out fields) ) { fields = GetFieldsToCache(type); cachedTypes[type] = fields; } CacheFields(instance, fields); } else { CacheFields(instance, GetFieldsToCache(type)); } } static List<FieldInfo> GetFieldsToCache(Type type) { var fields = new List<FieldInfo>(); foreach ( var field in type.GetFields() ) { foreach ( var a in field.GetCustomAttributes(false) ) { if ( a is CachedAttribute ) { fields.Add(field); } } } return fields; } static void CacheFields(MonoBehaviour instance, List<FieldInfo> fields) { var iter = fields.GetEnumerator(); while(iter.MoveNext()) { var type = iter.Current.FieldType; iter.Current.SetValue(instance, instance.GetComponent(type)); } } }
And now for caching in any script we use the appeal to it:
void Awake() { CacheHelper.CacheAll(this); }
After that, all class members marked [Cached] will be retrieved using GetComponent.
Efficiency of caching using attributes
Compare performance for variants with 1 or 5 cached components:
Interactive option
Interactive optionAs you can see, this method is inferior in performance to the direct production of components (the gap decreases slightly with an increase in their number), but has several features:
- A critical performance degradation occurs only when initializing the first instance of a class.
- Initialization of subsequent instances of this class is much faster, but not as fast as direct caching.
- The performance of getting components after initialization is identical to getting a member of a class and higher than that of GetComponent and various options with properties
- But at the same time all members of the class are initialized, regardless of whether they will be used in the future
Step back or use editor
Already when I finished this article, I was prompted by one interesting solution for caching. Is it necessary in our case to keep the components in the running state of the application? Not at all, we only do this once for each instance, respectively, functionally, it is no different from assigning them in the editor before launching the application. And everything that can be done in the editor can be automated.
So the idea was to cache the script dependencies using a separate option in the menu, which prepares instances on the scene for further use.
The last for today sheet code using UnityEngine; using UnityEditor; using System; using System.Reflection; using System.Collections; using System.Collections.Generic; namespace UnityCache { public static class PreCacheEditor { public static bool WriteToLog = true; [MenuItem("UnityCache/PreCache")] public static void PreCache() { var items = GameObject.FindObjectsOfType<MonoBehaviour>(); foreach(var item in items) { if(PreCacheAll(item)) { EditorUtility.SetDirty(item); if(WriteToLog) { Debug.LogFormat("PreCached: {0} [{1}]", item.name, item.GetType()); } } } } static bool PreCacheAll(MonoBehaviour instance) { var type = instance.GetType(); return CacheFields(instance, GetFieldsToCache(type)); } static List<FieldInfo> GetFieldsToCache(Type type) { var fields = new List<FieldInfo>(); foreach (var field in type.GetFields()) { foreach (var a in field.GetCustomAttributes(false)) { if (a is PreCachedAttribute) { fields.Add(field); } } } return fields; } static bool CacheFields(MonoBehaviour instance, List<FieldInfo> fields) { bool cached = false; UnityEditor.SerializedObject serObj = null; var iter = fields.GetEnumerator(); while (iter.MoveNext()) { if(serObj == null) { serObj = new UnityEditor.SerializedObject(instance); cached = true; } var type = iter.Current.FieldType; var name = iter.Current.Name; var property = serObj.FindProperty(name); property.objectReferenceValue = instance.GetComponent(type); Debug.Log(property.objectReferenceValue); } if(cached) { serObj.ApplyModifiedProperties(); } return cached; } } }
This method has its own characteristics:
- It does not require resources for explicit initialization.
- Objects are prepared explicitly (recompiling the code is not enough)
- Objects during preparation must be on stage
- Preparation does not affect the prefabs in the project (unless you save them explicitly from the scene) and objects in other scenes.
It is possible that the current restrictions can be removed in the future.
Bonus for readingFeatures of getting the missing components
An interesting feature was that trying to get the missing component takes more time than getting an existing one. At the same time, there is a noticeable anomaly in the editor, which made me think of testing this behavior. So never rely on profiling results in the editor.
Interactive option Conclusion
In this article, you saw the evaluation of various component caching methods, and also learned about one of the useful uses of attributes. Methods based on reflection, in principle, can be used when creating projects on Unity3D, given its features. One of them allows you to write less of the same type of code, but a little less productive than the solution "in the forehead". The second at the moment requires a little more attention, but does not affect the final performance.
The project with the source scripts for the test and the proof-of-concept cache using attributes is available on
GitHub (a separate package with the final version is
here ). You may have suggestions for improvement.
Thank you for your attention, I hope for useful comments. Surely this question was considered by many and you have something to say about this.
UPDATEIn the latest available version (0.32) 2 new features have been added:
- Separate class for caching property ()
- When using the "in the editor" mode, before assembling the scene, the necessary components will be cached and a warning will be displayed if something has not been cached in advance using the menu item (unfortunately, you cannot save the scene in OnPostProcessScene).