Caching is important

I have to disagree that ‘avoid caching where possible’ should be applied. Caching is extremely important, especially when doing things such as GetComponent.

The nice thing about Components is that Unity overrides the getter so that it returns null when it is destroyed in the engine, so that is already a good thing.

Secondly, if you write methods, make it so you always have a setter for variables, and those setters can refresh the cache. If the DoubleWidthAndHeight() method would just call SetWidth() and SetHeight(), nothing would be the problem because those setters should respectfully update the cache.

What I do think is that a cache should be simplified and abstracted. If you keep track of booleans for isInAir and isOnGround… then consider the fact that one can never be true when the other is true. So, in other words, simplify the state. One cache can already answer the other. (consider using a state machine (enum or class based) when going into boolean hell)

My conclusion:

Write unoptimized code first, then optimize where you can when you do your second coding pass (don’t overdo it, but don’t call GetComponent in the update loop etc). And keep it as simply and confined as possible. Don’t set variables directly of the global scope, always have a method which sets them so that cache data can be updated.

2 Likes

Caching is good in the right circumstances.

It also causes more headaches than it’s worth in the wrong circumstances. Much like off by one errors, they can be exceptionally painful to debug.

Absolutely. I always recommend caching component references in Awake(), and avoiding using these methods within an Update() loop when the reference should remain fixed.

While this works internally, the problems can occur when you cache these values in a different component… Consider the following:

public class World: MonoBehaviour
{
    [SerializeField] private int width;
    [SerializeField] private int height;

    public int Width => width;
    public int Height => height;

    public void SetWidth(int newWidth)
    {
         width = newWidth;
     }
     public void SetHeight(int newHeight)
     {
          height = newHeight;
     }

     void Update()
     {
           SetHeight(height+1);
           SetHeight(width+1);
      }
}

public class WorldUI : MonoBehaviour
{
   
     int width;
     int height;
     void Awake()
     {
          width = GetComponent<World>().Width;
          height = GetComponent<World>().Height; //we have now cached external state in this class
     }
     void Update()
     {
          Debug.Log($"WorldUI:  width = {width}, height = {height}");
     }
}

In this (purely contrived for the example) example, the initial width and height are set in the inspector. Our worldUI caches these values on Awake().
The values in world are changing every frame, but the worldUI will never increase because it is dependent on cached information. So while internally, it can generally be save to cache state, caching external state can lead to stale information being acted upon.
This can be overcome through the observer pattern, by throwing an event in SetHeight and SetWidth, and then updating the cache based on the new information. Since Unity is a singlethreaded application, you can rely on the events to keep this information up to date.

I always thought that caching was important to save memory and other resources? I’m very surprised it’s suggested to calculate or update certain caches later. Isn’t object pooling a good example of why caching is useful? Though I suppose the biggest difference is state after reading about it further online. Heavy calculations though like raycasting or something surely would be better with a cache though?

Resources yes, but memory… it tends to use more memory as you’re storing more in memory than you usually would.

Computers are so powerful these days, it makes little sense to cache certain things that give you no performance benefit, but can easily be the source of bugs.

I never really considered object pooling caching, but yes it is. Part of the reason object pooling is important as when there’s a lot of memory on the heap being allocated and destroyed, it can cause a pause in a game due to the garbage collector doing its thing.

I wouldn’t expect so, but it does depend on your use case. A raycast is usually to determine where something is in relation to another, so I would expect caching these to cause way more pain than the benefits you stand to gain.

I’m used to caching Components as public values and setting them in editor. It’s usefull for me when I need to get components in children/parent.
Doesn’t getting Components in Awake() create situations that some components wouldn’t be … wake? Therefore not cached?

If the component exists within the inspector (even if it’s Awake() has not run), it can be found by GetComponent. Now accessing that reference in Awake(), that’s a different matter.
Examples:

Health health;
void Awake()
{
    //This is fine
    health = GetComponent<Health>();
    //This is NOT fine and could result in a race condition
    Debug.Log($"Health points is {health.currentHealth}");
    //This is fine
    health.OnDie.AddListener(HandleDeath);
    //Also fine
    health.TakeDamage += HandleDamage;
    //Not fine
    if(health.IsDead()) Debug.Log("I'm Dead");
}
1 Like

Privacy & Terms