Updater: An Easy Way to Get Updates Without Inheriting MonoBehaviour
At first glance an Updater class seems unnecessary in Unity. All you have to do is inherit from MonoBehaviour and add an Update function. But what if you don’t want to inherit from MonoBehaviour? Enter Updater, an easy way to still get an update event and cut your dependency on MonoBehaviour. This works great for code in DLLs, “pure code” projects, or just projects that don’t want to put everything into MonoBehaviours. Read on for the source code and how to use it!
Let’s start with the interface: IUpdater. This defines a type that has no dependency on MonoBehaviour or any other Unity class. It’s therefore ideal for DLLs or code you want to share with other C# environments like a Photon server or command-line tool.
using System; /// <summary> /// Periodically dispatches an event /// </summary> /// <author>Jackson Dunstan, http://JacksonDunstan.com/articles/3382 /// <license>MIT</license> public interface IUpdater { /// <summary> /// Dispatched periodically /// </summary> event Action OnUpdate; /// <summary> /// Start dispatching OnUpdate events /// </summary> void Start(); /// <summary> /// Stop dispatching OnUpdate events /// </summary> void Stop(); }
Using IUpdater is easy- just subscribe to the event!
class DamageOverTime { IPlayer player; DamageOverTime(IUpdater updater, IPlayer player) { this.player = player; // Subscribe to be called every frame updater.OnUpdate += HandleUpdate; } // Called every frame void HandleUpdate() { player.Health -= 5; if (player.Health <= 0) { // Unsubscribe to stop being called every frame updater.OnUpdate -= HandleUpdate; } } }
You can also explicitly start and stop the updater so that OnUpdate gets called every frame or stops getting called every frame.
class HUD { void HandlePauseButton() { updater.Stop(); } void HandleResumeButton() { updater.Start(); } }
Now that we’ve seen how to use an IUpdater, let’s see how one is implemented. There are many ways to do this, but for the purposes of this article we’ll use a MonoBehaviour with a coroutine. Here’s the full CoroutineUpdater class:
using System; using System.Collections; using UnityEngine; /// <summary> /// Dispatches an event every frame when a MonoBehaviour's coroutines are resumed /// </summary> /// <author>Jackson Dunstan, http://JacksonDunstan.com/articles/3382 /// <license>MIT</license> public class CoroutineUpdater : IUpdater { private MonoBehaviour monoBehaviour; private Coroutine coroutine; /// <summary> /// Dispatched every frame /// </summary> public event Action OnUpdate; /// <summary> /// The MonoBehaviour to run the coroutine with. /// Setting this stops any previous coroutine. /// </summary> public MonoBehaviour MonoBehaviour { get { return monoBehaviour; } set { Stop(); monoBehaviour = value; Start(); } } /// <summary> /// Start dispatching OnUpdate every frame /// </summary> public void Start() { if (coroutine == null && monoBehaviour) { coroutine = monoBehaviour.StartCoroutine(DispatchOnUpdate()); } } /// <summary> /// Stop dispatching OnUpdate every frame /// </summary> public void Stop() { if (coroutine != null && monoBehaviour) { monoBehaviour.StopCoroutine(coroutine); } coroutine = null; } private IEnumerator DispatchOnUpdate() { while (true) { if (OnUpdate != null) { OnUpdate(); } yield return null; } } }
At it’s core, the Start function calls MonoBehaviour.StartCoroutine to have Unity call DispatchOnUpdate every frame. Likewise, the Stop function stops it from being called. Both of them check to make sure they don’t start or stop if already started or stopped.
The MonoBehaviour setter allows users to set the MonoBehaviour they want the coroutine to run on. It automatically stops the previous coroutine and starts a new one on the new MonoBehaviour.
Now let’s see how to use CoroutineUpdater.
class MyScript : MonoBehaviour { IUpdater updater; IPlayer player; DamageOverTime dot; void Awake() { // Make the updater updater = new CoroutineUpdater(); // Give the updater a MonoBehaviour. This starts it. updater.MonoBehaviour = this; // Give a non-MonoBehaviour the updater dot = new DamageOverTime(updater, player); // Give the updater a new MonoBehaviour, perhaps because this one is // being destroyed due to a scene being loaded updater.MonoBehaviour = someOtherMonoBehaviour; } }
There’s really not much to it, but in the end we’ve created an abstract IUpdater interface that allows us to decouple some of our code from MonoBehaviour and even Unity.
Spot a bug? Have a question or suggestion? Post a comment!
#1 by Walker on February 29th, 2016 ·
Thanks for sharing, I use this pattern a lot. I often find it useful to pass the time delta with the update event. That way all my listening code don’t have to be bound to Unity’s Time class or implement dt math themselves. It also gives me a central place to scale time, making it faster or slower, if I need to. This tweak doesn’t make sense for everyone, though.