Skip to content

Instantly share code, notes, and snippets.

@DanPuzey
Last active July 14, 2023 02:18
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save DanPuzey/54ec8f43a80ab5ff7d243336c588a374 to your computer and use it in GitHub Desktop.
Save DanPuzey/54ec8f43a80ab5ff7d243336c588a374 to your computer and use it in GitHub Desktop.
Unity3d: promises for Coroutines

This is a dead-noddy start to a promise-stye API for queuing up Coroutines (and optionally regular functions) to run in series.

Assume you have a Component with functions IEnumerator Foo() and void Bar() (that is, a coroutine and a regular function). You can then do this:

var go = new GameObject("CoroutineTests");
var tA = go.AddComponent<PromiseTestComponent>();
var tB = go.AddComponent<PromiseTestComponent>();

tA.Promise(tA.Foo()).Then(tB.Foo()).Then(tA.Bar).Then(tA.Foo()).Then(tB.Bar);```

Note that .Bar is passed without brackets (i.e. passing the function) but Coroutines are called. This does currently lead to the side effect that the first part of a coroutine (up to the first yield return) will be called immediately, but I'll work around that in a future version.

If you adopt this pattern across a project then you can expose the returned promise for easy use elsewhere. For example, if you have a component with code like this:

private IEnumerator doThingsSlowly() { .... }

public Promise StartDoingThingsSlowly()
{
    return this.Promise(doThingsSlowly());
}

... then you can extend that promise elsewhere, like so:

foo = <get component from somewhere>;
foo.StartDoingThingsSlowly().Then(this.bar); // add an extra job to the chain

Also worth remembering that you can create an inline anonymous function (e.g. to call a function with parameters), so this works for passing arguments to a function:

Promise(foo()).Then(() => Bar(x, y));

The returned Promise object can be interrupted by calling .Abort(), and you can check whether it's still running by examining .IsCompleted and .IsAborted.

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace Promises
{
/// <summary>
/// Contains extension methods that provide promise-like behaviour for coroutines.
/// </summary>
public static class PromiseExtensions
{
public static Promise Promise(this MonoBehaviour owner, IEnumerator coroutine)
{
var promise = new Promise(coroutine);
owner.StartCoroutine(promise);
return promise;
}
}
public class Promise : IEnumerator
{
private IEnumerator _currentCoroutine;
private Queue<object> _toProcess = new Queue<object>();
internal Promise(IEnumerator coroutine)
{
_currentCoroutine = coroutine;
}
public Promise Then(IEnumerator coroutine)
{
_toProcess.Enqueue(coroutine);
return this;
}
public Promise Then(Action action)
{
_toProcess.Enqueue(action);
return this;
}
/// <summary>
/// Stops the Promise from executing any further frames.
/// </summary>
public void Abort()
{
IsAborted = true;
}
public bool IsCompleted { get; private set; }
public bool IsAborted { get; private set; }
#region IEnumerator implementation
public object Current
{
get
{
if (IsCompleted || IsAborted) throw new InvalidOperationException("The enumeration is complete.");
return _currentCoroutine.Current;
}
}
public bool MoveNext()
{
if (IsAborted)
{
return false;
}
else if (_currentCoroutine.MoveNext())
{
return true;
}
else if (_toProcess.Count == 0)
{
return false;
}
else
{
IEnumerator coroutine;
do
{
var next = _toProcess.Dequeue();
var action = next as Action;
if (action != null)
{
action();
}
coroutine = next as IEnumerator;
} while (coroutine == null && _toProcess.Count > 0);
if (coroutine == null)
{
IsCompleted = true;
return false;
}
else
{
_currentCoroutine = coroutine;
// We use a recursion here in case the next coroutine we find happens to be empty
// If there's a coroutine that immediately returns false (but executes code in the process),
// we will skip through to the next item in the list without ending our enumeration
return MoveNext();
}
}
}
public void Reset()
{
// Part of IEnumerator but not actually used by Unity coroutines
throw new NotImplementedException();
}
#endregion
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment