A lot of times we want to take some action when a value changes. If the player’s level increases I want to play a “ding” sound effect. If a player loses health points I want the screen to flash red. Today we introduce Observable<T>: an easy way to track these value changes. Read on for the source code and examples!

Let’s look straight away at how to use Observable<T>. We’ll do this by way of a very short program demonstrating its capabilities:

using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
 
using UnityEngine;
 
public class TestScript : MonoBehaviour
{
	private static string log = string.Empty;
 
	private static void Log(string msg)
	{
		log += msg + "\n";
	}
 
	void Start()
	{
		// Create an observable with the default value for the type. For int this is 0.
		var defaultObs = new Observable<int>();
		Log("default value: " + defaultObs.Value); // Get the observable's value with Value
 
		// Create an observable with a particular value
		var obs = new Observable<int>(123);
		Log("initialized value: " + obs.Value);
 
		// Listen for the value to change by subscribing to the OnChanged event
		obs.OnChanged += (o, oldVal, newVal) => Log("changed from " + oldVal + " to " + newVal);
 
		// Change the value and dispatch the OnChanged event
		obs.Value = 456;
 
		// Convert a plain int to an Observable<int>. No need for a cast!
		Observable<int> obsFromPlain = 789;
		Log("observable from plain type: " + obsFromPlain);
 
		// Convert an Observable<int> to a plain int. You need to cast here.
		int plainFromObs = (int)obs;
		Log("plain type from observable: " + plainFromObs);
 
		// Convert an Observable<int> to a string. You can do this with string concatenation or by
		// explicitly calling ToString().
		Log("observable as string: " + obs);
 
		// Observable is [Serializable] so you can easily write it to a stream (e.g. a file) and
		// read it back from that stream
		var stream = new MemoryStream();
		var formatter = new BinaryFormatter();
		formatter.Serialize(stream, obs);
		stream.Position = 0;
		var deserializedObs = (Observable<int>)formatter.Deserialize(stream);
		Log("deserialized observable from stream: " + deserializedObs);
 
		// You can compare two observables with Equals and it'll compare their values
		Log(
			"123 equals 123? "
			+ new Observable<int>(123).Equals(new Observable<int>(123))
		);
		Log(
			"123 equals 456? "
			+ new Observable<int>(123).Equals(new Observable<int>(456))
		);
 
		// You can compare an observable with any object, but it has to be the right type and have
		// the same value in order to be true.
		Log(
			"123 equals \"hello\"? "
			+ new Observable<int>(123).Equals("hello")
		);
		Log(
			"123 equals 123 object? "
			+ new Observable<int>(123).Equals((object)new Observable<int>(123))
		);
		Log(
			"123 equals 456 object? "
			+ new Observable<int>(123).Equals((object)new Observable<int>(456))
		);
 
		Debug.Log(log);
	}
}

This little demo script actually runs and prints this:

default value: 0
initialized value: 123
changed from 123 to 456
observable from plain type: 789
plain type from observable: 456
observable as string: 456
deserialized observable from stream: 456
123 equals 123? True
123 equals 456? False
123 equals "hello"? False
123 equals 123 object? True
123 equals 456 object? False

So how does this work? It’s actually pretty simple. Here’s the main part of the Observable class:

public class Observable<T>
{
	private T value;
 
	public Action<Observable<T>, T, T> OnChanged;
 
	public T Value
	{
		get { return value; }
		set
		{
			var oldValue = this.value;
			this.value = value;
			if (OnChanged != null)
			{
				OnChanged(this, oldValue, value);
			}
		}
	}
}

The key here is overloading the set part of the Value property. We can write whatever code we want in this, just like a function. In this case we set the value and also dispatch the OnChanged event. Since value is private, no one else can change it. This means if you subscribe to the OnChanged event you’ll always know when the value changes.

The rest of the class is mostly boilerplate to implement type conversions (i.e. plain int to/from Observable<T>), Equals, and the [Serializable] attribute. Here’s the full source, which is MIT licensed for easy inclusion into most projects:

using System;
 
/// <summary>
/// Wraps a value in order to allow observing its value change
/// </summary>
/// <example>>
///   var obs = new Observable<int>(123);
///   obs.OnChanged += (o, oldVal, newVal) => Log("changed from " + oldVal + " to " + newVal);
///   obs.Value = 456; // dispatches OnChanged
/// </example>
/// <author>Jackson Dunstan, http://JacksonDunstan.com/articles/3547</author>
/// <license>MIT</license>
[Serializable]
public class Observable<T> : IEquatable<Observable<T>>
{
	private T value;
 
	public Observable()
	{
	}
 
	public Observable(T value)
	{
		this.value = value;
	}
 
	public Action<Observable<T>, T, T> OnChanged;
 
	public T Value
	{
		get { return value; }
		set
		{
			var oldValue = this.value;
			this.value = value;
			if (OnChanged != null)
			{
				OnChanged(this, oldValue, value);
			}
		}
	}
 
	public static implicit operator Observable<T>(T observable)
	{
		return new Observable<T>(observable);
	}
 
	public static explicit operator T(Observable<T> observable)
	{
		return observable.value;
	}
 
	public override string ToString()
	{
		return value.ToString();
	}
 
	public bool Equals(Observable<T> other)
	{
		return other.value.Equals(value);
	}
 
	public override bool Equals(object other)
	{
		return other != null
			&& other is Observable<T>
			&& ((Observable<T>)other).value.Equals(value);
	}
 
	public override int GetHashCode()
	{
		return value.GetHashCode();
	}
}

And that’s it for Observable<T>. Let me know in the comments if you find it—or anything similar—useful!