The new class in Extensions, ObservedValue: what is it for and how to use it

If you have written a lot of game code, then you probably wrote something like this before:

class MyBehaviour : MonoBehaviour
{
   int count;
   
   public void Start()
   {
       count = CalculateCurrentCount();
   }

   public void Update()
   {
       var currentCount = CalculateCurrentCount();
       
       if(currentCount != count)
       {
           count = currentCount;
           DoSeomtingExpensive();
       }
   }
}

The idea is, you check or calculate some value very often (every frame, for example), but only perform some action when it changes. In this example it is not too bad, but when you have a few of these in a file, it can become messy. It’s also a bit error prone, especially if you update the value in several places and want to do checks right after you changed them instead of in the Update method.

ObservedValue (a new class in our free Extensions library) is meant to package this code neatly; it takes care of the boiler plate part, and at the same time makes it easy to move around in your code without having to worry about performing the actions when the value changes. Here is how the code above looks rewritten with ObservedValue:

class MyBehaviour : MonoBehaviour
{
   ObservedValue<int> count;

   public void Start()
   {
      count = new ObservedValue(CalculateCurrentCount());
      count.OnValueChanged += DoSeomtingExpensive;
   }

   public void Update()
   {
      count.Value = CalculateCurrentCount();
   }
}

This does exactly the same as first version, but it is much cleaner.

One important use case is when using the immediate GUI mode (typically, in editor code), where you want to change something when the user changes a value. In fact, we wrote this class when a level editor we developed for a client had about 20 variables that could influence what should be drawn on the screen, and started to run into many bugs as we made improvements to the tool.

Another useful strategy is to combine it with a state machine to prevent multiple calculations per frame (this is normally done with a dirty flag). Here is how it looks without a state machine:

class MyBehaviour : MonoBehaviour
{
   bool isDirty;
   ObservedValue<int> count1;
   ObservedValue<int> count2;

   public void Start()
   {
      isDirty = false;
      count1 = new ObservedValue(CalculateCurrentCount1());
      count1 += () { isDirty = true; };

      count2 = new ObservedValue(CalculateCurrentCount2());
      count2 += () { isDirty = true; };
   }
   
   public void Update()
   {
       count1.Value = CalculateCurrentCount1();
       count2.Value = CalculateCurrentCount2();

       if(isDirty)
       {
           DoSomethingExpensive();
           isDirty = false;
       }
   }
}

Again, in this simple case it is not too bad. The code can become more complicated when spread across multiple classes, or if you have a few things that can become dirty independently, or if you have different “levels” of dirt.

Here is how the example above looks with a state machine

class MyBehaviour : MonoBehaviour
{
   public enum DirtyState {Dirty, Clean};

   StateMachine dirtyManager;
   ObservedValue<int> count1;
   ObservedValue<int> count2;

   public void Start()
   {
      dirtyManager = new StateMachine();

      dirtyManager.AddState(DirtyState.Clean);
      dirtyManager.AddState(
          DirtyState.Dirty, 
          null, 
          () => 
          {
             DoSomethingExpensive();
             dirtyManager.SetState(DirtyState.Clean);
          }); 

      count1 = new ObservedValue(CalculateCurrentCount1());
      count1 += () { dirtyManager.SetState(DirtyState.Dirty); };

      count2 = new ObservedValue(CalculateCurrentCount2());
      count2 += () { isDirty = true; };

   }
   
   public void Update()
   {
       count1.Value = CalculateCurrentCount1();
       count2.Value = CalculateCurrentCount2();
       
       dirtyManager.Update();
   }
}

In this simple example there is not a benefit from using the state machine; benefits come only once the situation becomes a bit more complex.

Of course, the code above has many boilerplate aspects, so you may want a DirtyManager class to take care of it (and this is a good candidate for a future addition to our Extensions library).

Here is a draft of such a class:

public class DirtyManager
{
   private enum DirtyState {Dirty, Clean};

   private StateMachine dirtyManager;
   public event OnShouldCleanDirt; //needs a better name!

   public DirtyManager()
   {
      dirtyManager = new StateMachine();

      dirtyManager.AddState(DirtyState.Clean);
      dirtyManager.AddState(
          DirtyState.Dirty, 
          null, 
          () => 
          {
             if(OnShouldCleanDirt != null) 
             {
                 OnShouldCleanDirt();
             }

             dirtyManager.SetState(DirtyState.Clean);
          }); 
   }

   public void Update()
   {
       dirtyManager.Update();
   }

   public void Observe(ObservedValue observedValue)
   {
       observedValue.OnValueChanged += SetDirty;
   }
   
   private void SetDirty()
   {
       dirtyManager.SetState(DirtyState.Dirty);
   }
}

Now the example can become this:

class MyBehaviour : MonoBehaviour
{
   DirtyManager dirtyManager;
   ObservedValue<int> count1;
   ObservedValue<int> count2;

   public void Start()
   {
      count1 = new ObservedValue(CalculateCurrentCount1());
      count2 = new ObservedValue(CalculateCurrentCount2());

      dirtyManager = new DirtyManager();

      dirtyManager.Observe(count1);
      dirtyManager.Observe(count2);

      dirtyManager.OnShouldCleanDirt += DoSomethingExpensive; 
   }
   
   public void Update()
   {
       count1.Value = CalculateCurrentCount1();
       count2.Value = CalculateCurrentCount2();
       
       dirtyManager.Update();
   }
}

The code is much cleaner now, and much more robust against additional complexity.

One question is, do we really need the state machine? We don’t; we could use a bool just as easily to keep track. But in more complex cases, a state machine can offer value. For example, to deal with levels of dirt, or to deal with special circumstances. (For example, when we want to delay the calculation until something else happens.)

1 thought on “The new class in Extensions, ObservedValue: what is it for and how to use it”

  1. line 27 in the state machine example says
    `count2 += () { isDirty = true; };`
    I think it should be
    `count2 += () { dirtyManager.SetState(DirtyState.Dirty); };`

    right ?

Comments are closed.

Scroll to Top