← All patterns
Optimization

Dirty Flag

Remember that derived data is out of date and recompute it only when someone actually needs it.

What Is It?

Some values are expensive to derive from others: a world-space position built by walking up a hierarchy, a navmesh path, a combined mesh, a sorted leaderboard. The Dirty Flag pattern keeps a cached result plus a single bool isDirty that says "the inputs changed since I last computed this".

Writes are cheap: changing a local position just sets the flag (and, in a hierarchy, flags every descendant). Reads are lazy: the getter checks the flag, recomputes only if dirty, clears the flag and returns the cache. Ten writes in a frame cost one recompute instead of ten.

Unity itself does this inside Transform — moving a parent does not immediately recompute every child's localToWorldMatrix; it is marked and resolved when queried or rendered. Transform.hasChanged is the public face of the same idea.

When Is It Used?

Use it when a derived value is costly to compute, its inputs change in bursts, and it is read less often than it is written — or read many times between changes.

Classic fits are custom scene graphs, procedural mesh rebuilding, UI layout, pathfinding caches and stat systems where buffs stack into a final number.

Skip it when the computation is trivial (adding two numbers) or the value changes on every single read anyway; the flag bookkeeping then costs more than it saves.

Interactive Demo

Click the controls and watch the objects collaborate. The console mirrors what the C# code below would log.

Code

Assets / Scripts / Optimization/ DirtyFlag ›DirtyFlagTester.cs
using UnityEngine;

namespace Patterns.Optimization.DirtyFlag
{
    /// <summary>
    /// Builds a Ship → Turret → Barrel hierarchy, wiggles it and reports how
    /// many world positions were actually recomputed.
    /// </summary>
    public class DirtyFlagTester : MonoBehaviour
    {
        [SerializeField] private float wiggleSpeed = 2f;

        private SceneGraph graph;
        private SceneNode ship, turret, barrel;

        private void Awake()
        {
            graph = new SceneGraph();
            ship = graph.Create("Ship", null, Vector3.zero);
            turret = graph.Create("Turret", ship, new Vector3(0f, 1f, 0f));
            barrel = graph.Create("Barrel", turret, new Vector3(0f, 0f, 1.5f));
        }

        private void Update()
        {
            // Only the turret moves this frame; the ship stays clean.
            turret.LocalPosition = new Vector3(Mathf.Sin(Time.time * wiggleSpeed), 1f, 0f);

            // Reading the barrel recomputes Turret + Barrel, not Ship.
            Vector3 muzzle = barrel.WorldPosition;
            Debug.DrawRay(muzzle, Vector3.forward, Color.yellow);
        }

        private void LateUpdate()
        {
            if (Time.frameCount % 120 == 0)
                Debug.Log($"Recomputes so far: {SceneNode.RecomputeCount}");
        }
    }
}
3 files · namespace Patterns.Optimization.DirtyFlagC# · UTF-8 · LF

Advantages & Disadvantages

+ Advantages

  • Avoids redundant work: many writes collapse into one recompute.
  • Lazy evaluation means values nobody reads are never computed at all.
  • Simple to retrofit — a bool and a getter around existing code.

− Disadvantages

  • Forgetting to set the flag on one write path gives you stale data that is very hard to spot.
  • Recompute cost moves to the first read, which can cause an unexpected spike at a bad moment.
  • Hierarchies need careful propagation so children are marked whenever an ancestor changes.

Tips

  1. 01Funnel every write through a setter that calls SetDirty(); make the backing field private so nothing can bypass it.
  2. 02When propagating to children, stop early if a node is already dirty — its subtree must already be marked.
  3. 03If a spike on first read is a problem, resolve dirty values at a predictable point such as LateUpdate instead of inside the getter.
  4. 04Use Transform.hasChanged (and reset it yourself) to skip work in scripts that only care when an object actually moved.
  5. 05Expose a debug counter of recomputes while profiling; it makes the savings — or a missing SetDirty — obvious.