Lifetime management in Unity with UniRx and IDisposables

Previously we discussed the Model-View-Controller pattern as it applies to games, and how C#’s events allow you to pass information between layers without creating dependencies.

In this post I’m going to expand upon the pain point I touched upon at the end of the previous post – managing lifetimes for event subscriptions so that you don’t unwittingly create memory leaks.

Enter UniRx, a Unity implementation of the Reactive Extensions library.

I’m not going to talk about the primary feature of the Reactive Extensions library (Rx for short), its powerful implementation of the observable/observer pattern, for the simple reason that I discovered it too late in Delugional’s development process to incorporate it.

I’m going to talk instead about a very handy set of tools in Rx for implementing and manipulating .NET’s ‘IDisposable’ interface, and how they make it easy to cleanly and correctly handle lifetimes.

At your disposal

The IDisposable interface is provided by .NET for whenever you’re dealing with resources that need to be explicitly released. The idea is that you get an IDisposable object whenever you request or create the resource, and call the Dispose() method on that object when you no longer need that resource.

The key thing to notice here is that IDisposable is essentially an abstraction over lifetime management. In the framework it’s mainly used for things like byte streams and database connections, but we can adapt it for anything we want – like, say, unsubscribing from an event. Rx gives us the tools to do just that.

We start with Rx’s Disposable.Create() helper method, which allows us to make an IDisposable from a delegate, like so:

	public class TerrainController : MonoBehaviour
	{
		private Terrain _terrain;
		private IDisposable _elevationChangedSubscription;

		public TerrainController(Terrain terrain)
		{
			_terrain = terrain;
			_terrain.ElevationChanged += OnElevationChanged;
			_elevationChangedSubscription =
Disposable.Create(() => _terrain.ElevationChanged -= OnElevationChanged);
		}

Now to unsubscribe from the event, we just have to dispose the IDisposable we created:

		private void OnDestroy() {
			// This will call _terrain.ElevationChanged -= OnElevationChanged
			_elevationChangedSubscription.Dispose();
		}

I see you bewildered. What has this gained us, exactly? So far not much; but now he have access to the composability and control flows offered by Rx’s custom disposable implementations.

CompositeDisposable

As the name hints, CompositeDisposable lets you group a whole bunch of IDisposables together, and dispose them all at once.

So for instance:

private readonly CompositeDisposable _subscriptions = new CompositeDisposable();

…

		_subscriptions.Add(Disposable.Create(() => _terrain.ElevationChanged -= OnElevationChanged);
		_subscriptions.Add(Disposable.Create(() => _terrain.ColorChanged -= OnColorChanged);
		_subscriptions.Add(Disposable.Create(() => _terrain.MonsterEntered -= OnMonsterEntered);

Here we’ve gained a bit more: now we can unsubscribe from any number of events by disposing one CompositeDisposable.

One nice feature is that this way we can define unsubscriptions from an event in the same place of code where we subscribe to the event. This may not seem like a big deal, but it makes it noticeably easier to do things the right way, and not forget to unsubscribe.

SerialDisposable – there can be only one

One more IDisposable, but this one is my favourite.

SerialDisposable is great wherever you only want at most one subscription to exist at a time. Its behaviour seems a little odd at first glance, but it turns out to be broadly useful.

SerialDisposable has a Disposable property of type IDisposable (surprise!). Whenever you set the Disposable property, the previous value, if any, is disposed. That’s it.

To illustrate:

var serialDisposable = new SerialDisposable();
serialDisposable.Disposable = Disposable.Create(() => Debug.Log(“Disposable 1”));

serialDisposable.Disposable = Disposable.Create(() => Debug.Log(“Disposable 2”)); //This assignation will cause “Disposable 1” to print

serialDisposable.Disposable = null; //This assignation will cause “Disposable 2” to print

How would you use this in practice? Well, consider our example. What if we wanted to change our Terrain object, for some reason? Maybe we want to recycle the controller.

Obviously we need to unsubscribe from events on the old Terrain object, or we’d get some weird behaviour.

Here’s how you can do it with SerialDisposable:

	public class TerrainController : MonoBehaviour
	{
		private Terrain _terrain;
		private readonly SerialDisposable _terrainSubscription = new SerialDisposable();

…

		public OnTerrainChanged(Terrain newTerrain)
		{
			_terrain = newTerrain;
			_terrainSubscription.Disposable =
Disposable.Create(() => newTerrain.ElevationChanged -= OnElevationChanged); //This disposes the previous subscription! Note that we’re closing over newTerrain, not _terrain
			_terrain.ElevationChanged += OnElevationChanged;
		}

Now SerialDisposable is enforcing exactly the behaviour we want: we can only ever be subscribed to one event at a time. Note, too, that if there were multiple IDisposables associated with Terrain, we could aggregate them in a CompositeDisposable and assign it to SerialDisposable.Disposable.

Building blocks

Those are the key building blocks for leveraging IDisposable. CompositeDisposable gives you the ability to handle multiple events (and other lifetime-limited subscriptions) gracefully; SerialDisposable enforces the invariant of only ever having one of something at a time. Those two keys unlock a cleaner coding style for handling events in your game.

Author: David Oliver

I’m a developer, ex-physicist, and occasional game designer. I’m interested in history, society, and the sciences of human behaviour, as well as technology and programming.