Easily Track Value Changes with Observable<T>
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!
#1 by Mirko on August 15th, 2016 ·
It would be great if you could do some articles using UniRX – reactiveX for Unity, it has a great feature and simplifies things greatly. This article could be an introduction
#2 by jackson on August 15th, 2016 ·
That looks like an interesting library. I’ll definitely check it out. Thanks for the tip!
#3 by Walker on August 15th, 2016 ·
I’m not sure how to do this without breaking encapsulation, but it seems like it would be really useful if the event handler also got a reference to the object that owned the Observable. Imagine that you want to listen for when something’s HP dropped to zero and then delete it. If you got a reference to the entity when the HP observable changed, then you could clean it up easily. With the current implementation, you’d have to generate a unique closure for each entity you wanted to listen to that captured a reference to that entity. It’s not a hard thing to write, but it does seem like passing a reference to the owning object would be convenient.
#4 by jackson on August 15th, 2016 ·
If you subscribe to the event with an instance function or a lambda that references the instance (i.e. this) in any way, the delegate that’s created for the event to hold will contain a reference to your instance. This is not the case with static functions though.
#5 by Walker on August 22nd, 2016 ·
Oh ya, totally. I’ve just found as a convenience that it’s nice to pass the instance into the handler, too. But you’re right that it’s not necessary. The downside with creating lots of lambdas is that you have to save references to them if you want to remove them from an event later.
#6 by jackson on August 22nd, 2016 ·
You can definitely pass the delegate as a parameter to the delegate if you prefer. For example:
Or you can save a local reference, which is really useful with events (as you mention):
It’s pretty awkward syntax, but since you call all the delegates in an event with the same parameters it’s hard to adapt the technique in the first block of code to events. You would need to switch to a list of delegates and handle all the thorny issues that events deal with, such as changes to the list while calling one of the delegates. Essentially you end up with the issues described in today’s article.
#7 by XANDER on March 14th, 2019 ·
A tiny improvement to the setter:
#8 by jackson on March 17th, 2019 ·
Using
?.
is arguably a bit cleaner thanif
and()
, but doesn’t support older versions of Unity so it’s a bit of a tradeoff. Likewise, usingEquals
prevents invokingOnChanged
when the value hasn’t changed but requiresEquals
to run and, far worse, boxing and dynamic type checking ifT
is a value type such as astruct
.#9 by XANDER on March 19th, 2019 ·
Agree, null-conditional operator is not so important, it’s a tradeoff. But preventing invoking OnChanged, when the value hasn’t changed, is really useful feature. Is it possible to get rid of boxing and dynamic type checking in this case?
#10 by jackson on March 20th, 2019 ·
Yes, the normal way being to add a
where
constraint that gives you access to a method other thanObject.Equals
. For example:Now the
Equals
call is using the overloadedbool Equals(T)
inIObservable<T>
instead of boxingthis
andvalue
toObject
instances and callingbool Equals(Object)
.The downside is that
T
must now implementIEquatable<T>
. If that’s acceptable, then this approach will probably work out quite nicely for you.#11 by XANDER on March 22nd, 2019 ·
Thanks a lot, now I understand generics and boxing a little bit better :)