CallLater: deferred callbacks for Unity

In many game projects, I've often found the need to execute some bit of code at a later time.  Often this relates to audiovisual flourishes.  For example, when the player does something scoreworthy, we may want to start a particle effect and sound right away, but then spawn a different effect after a short delay, and actually update the score a while after that.

You could certainly spread the code to do those things out into different classes or methods, triggered by events or other custom code.  I've certainly been known to do that; I love Unity Event, and most of my classes that do anything over time expose events for when they start and finish their work, making it easy to chain them together.

But sometimes, dividing up the logic that way makes the code less clear, not more.  There may be one place in the code that is handling everything related to this set of actions, and the only thing driving the code apart is that you want it to happen at different times.

CallLater was created to handle just this situation.  It lets you write some code, right there in the middle of a method, to actually be run later, after whatever delay you specify.

 

Usage

The prototype of the main entry point is:

CallLater.DoAfter(float secondsDelay, Callback callback, object argument=null);

where the first parameter is the delay, the second is a callback (a void function that takes one object argument), and the optional third parameter is a value to be passed to the callback.

Here's an example, taken from the splash screen of a game.  We want to fade in our logo, then fade it out, and finally play a movie clip.  And there is an additional startup delay added to account for the game launch time, which otherwise makes the fade-in happen too quickly.

    void Start() {
        float startup = 0.1f;
        CallLater.DoAfter(startup, x => {
            logo.color = Color.white;
            logo.CrossFadeAlpha(1, fadeInTime, true);
        });
        CallLater.DoAfter(startup + fadeInTime + dwellTime,
            x => logo.CrossFadeAlpha(0, fadeOutTime, true));
        CallLater.DoAfter(startup + fadeInTime + dwellTime + fadeOutTime,
            x => PlayMovie());
    }

So, I simply call CallLater three times, with three different delays.  I'm using lambda expressions here to specify the code to run for each one.  Note that lambda expressions bind the values of local variables and properties, so it's perfectly valid to use them within your CallLater code.

Here's another example.  In this case, the task is to highlight a series of random frames on the game board, one after another.  This was in a context where a coroutine was not convenient, so, CallLater to the rescue!

        float t = 0.5f;
        int row = 0, col = 0;
        for (int i=0; i<10; i++) {
            row = Random.Range(0, 8);
            col = Random.Range(0, 8);
            CallLater.DoAfter(t, arg => {
                Vector2 pos = (Vector2)arg;
                board.ClearAllHighlights();
                board.Highlight((int)pos.x, (int)pos.y, treasureHighlight);
                Audio.Play(Audio.Sound.Highlight, BoardToScreenPos(pos));
            }, new Vector2(col, row));
            t += 0.1f + 0.03f*i;
        }

In this example we're actually using the optional parameter, passing in the row/column to highlight.  Note that within the callback, this argument is of type 'object', so the first thing we do is cast it back to the type we know it to be (Vector2 in this case).  Then we set up the highlight and play the associated sound.  Our delay parameter, t, is incremented a little more each time, so the highlight starts out quick and then slows down towards the end.

The Code

Finally, here's the code, which should go in a file called CallLater.cs.

using UnityEngine;
using System.Collections.Generic;
using System.Linq;

public class CallLater : MonoBehaviour {
    #region Static Stuff

    public delegate void Callback(object arg);

    class PendingCallback {
        public Callback callback;
        public object argument;
        public float timeDue;
    }


    /// <summary>
    /// Do something (defined by arbitrary callback) after a delay.
    /// </summary>
    /// <param name="secondsDelay">Number of seconds to wait before invoking the callback.</param>
    /// <param name="callback">Callback to invoke.</param>
    /// <param name="callback">Optional argument to supply to the callback.</param>
    public static void DoAfter(float secondsDelay, Callback callback, object argument=null) {
        PendingCallback pd = new PendingCallback();
        pd.callback = callback;
        pd.argument = argument;
        pd.timeDue = Time.time + secondsDelay;
        GetInstance().pendingCallbacks.Add(pd);
    }

    static CallLater _instance;

    static CallLater GetInstance() {
        if (_instance == null) {
            GameObject obj = new GameObject();
            obj.name = "CallLater";
            _instance = obj.AddComponent<CallLater>();
        }
        return _instance;
    }

    // We keep our pending callbacks NOT static, but on the instance.
    List<PendingCallback> pendingCallbacks = new List<PendingCallback>();

    #endregion
    //--------------------------------------------------------------------------------
    #region MonoBehaviour Events

    void Awake() {
        _instance = this;
    }

    void Update() {
        if (pendingCallbacks.Count == 0) return;
        IEnumerable<PendingCallback> dueE = pendingCallbacks.Where(el => Time.time > el.timeDue);
        if (!dueE.Any()) return;
        List<PendingCallback> due = dueE.ToList<PendingCallback>();

        foreach (PendingCallback pd in due) {
            pd.callback(pd.argument);
            pendingCallbacks.Remove(pd);
        }
    }
    #endregion
}

Improvements to this code are certainly possible; it's using a simple unsorted list to store any pending callbacks, and using Linq to locate those which are due.  In practice, this hasn't been a problem, at least the way I use it, because the pending callbacks are always very few.  But if you find yourself using it heavily, an ordered queue would be more efficient.

Still, when you just need to execute a line or two of code at a later time, without spreading concerns across multiple methods or files, CallLater can be a real life-saver.  I hope you find CallLater as useful as I have!