Skip to content

Instantly share code, notes, and snippets.

@michaelbartnett
Last active August 29, 2015 14:03
Show Gist options
  • Save michaelbartnett/72ab563d19d417db7687 to your computer and use it in GitHub Desktop.
Save michaelbartnett/72ab563d19d417db7687 to your computer and use it in GitHub Desktop.
using System;
using UnityEngine;
using IEnumerator=System.Collections.IEnumerator;
public enum CoroStatus
{
Running,
Suspended,
Stopped,
}
public sealed class Coro
{
public CoroStatus Status { get; private set; }
public Exception Error { get; private set; }
public bool Deactivated { get; private set; }
private IEnumerator enumerator;
private IEnumerator runEnumerator;
private MonoBehaviour script;
private Coroutine coroutine;
private Coro child = null;
private int depth = 0;
#region Start Variants
public static Coro Start(MonoBehaviour script, Func<IEnumerator> coroFunc)
{
return new Coro(script, coroFunc());
}
public static Coro Start<T1>(MonoBehaviour script, Func<T1, IEnumerator> coroFunc, T1 arg1)
{
return new Coro(script, coroFunc(arg1));
}
public static Coro Start<T1, T2>(MonoBehaviour script, Func<T1, T2, IEnumerator> coroFunc, T1 arg1, T2 arg2)
{
return new Coro(script, coroFunc(arg1, arg2));
}
public static Coro Start<T1, T2, T3>(MonoBehaviour script, Func<T1, T2, T3, IEnumerator> coroFunc, T1 arg1, T2 arg2, T3 arg3)
{
return new Coro(script, coroFunc(arg1, arg2, arg3));
}
public static Coro Start<T1, T2, T3, T4>(MonoBehaviour script, Func<T1, T2, T3, T4, IEnumerator> coroFunc, T1 arg1, T2 arg2, T3 arg3, T4 arg4)
{
return new Coro(script, coroFunc(arg1, arg2, arg3, arg4));
}
public static Coro Start<T1, T2, T3, T4>(MonoBehaviour script, Func<T1, T2, T3, T4, IEnumerator> coroFunc, T1 arg1, T2 arg2, T3 arg3, T4 arg4)
{
return new Coro(script, coroFunc(arg1, arg2, arg3, arg4));
}
#endregion
public void Resume()
{
if (this.Status == CoroStatus.Stopped) {
throw new InvalidOperationException("can't resume a stopped Coro");
} else if (this.Status == CoroStatus.Running) {
Debug.LogError("trying to resume an already-running Coro");
return;
}
if (this.child != null) {
child.Resume();
}
this.Status = CoroStatus.Running;
}
public void Suspend()
{
if (this.Status == CoroStatus.Stopped) {
throw new InvalidOperationException("can't suspend a stopped Coro");
} else if (this.Status == CoroStatus.Suspended) {
Debug.LogError("trying to suspend an already-suspended Coro");
return;
}
if (this.child != null) {
child.Suspend();
}
this.Status = CoroStatus.Suspended;
}
public void Stop()
{
if (this.Status == CoroStatus.Stopped) {
Debug.LogError("trying to stop an already-stopped Coro");
return;
}
if (this.child != null) {
child.Stop();
}
Debug.Log("STOPPING COROUTINE AT DEPTH " + depth);
StopRun();
this.Status = CoroStatus.Stopped;
}
// Only valid when Status is Coro.Running. Suspends the Coro and marks it
// as needing a restart. Useful for when your game object deactivates and
// your script needs to resume a Coro that was previously running.
public void Deactivate()
{
if (this.Status == CoroStatus.Stopped) {
throw new InvalidOperationException("can't deactivate a stopped Coro");
return;
}
if (this.child != null) {
child.Deactivate();
}
Debug.Log("STOPPING COROUTINE AT DEPTH " + depth);
StopRun();
this.Deactivated = true;
}
// Only valid after calling Deactivate on this Coro. Causes the Coro to be
// resumed with a fresh StartCoroutine call, which is useful for when your
// game object deactivates and your script needs to resume a Coro that
// was previously running.
public void Reactivate()
{
if (this.Status == CoroStatus.Stopped) {
// Like Deactivate, you can't reactivate a stopped Coro. But I left the error
// here in this case, because you're trying to reactivate a stopped Coor, you
// probably want to know if there's some reason why it isn't activating.
throw new InvalidOperationException("can't reactivate a stopped Coro");
return;
}
if (!this.Deactivated) {
Debug.LogError("trying to reactivate a Coro that was not deactivated.");
}
if (this.child != null) {
this.child.Reactivate();
}
// this.Status = CoroStatus.Running;
StartRun();
this.Deactivated = false;
}
private Coro(MonoBehaviour script, IEnumerator enumerator)
{
// Private constructor so we can guarantee that we aren't calling
// StartCoroutine on the same IEnumerator instance multiple times.
this.script = script;
this.enumerator = enumerator;
StartRun();
}
private void StartRun()
{
runEnumerator = Run();
this.coroutine = script.StartCoroutine(runEnumerator);
}
private void StopRun()
{
if (runEnumerator != null) {
this.script.StopCoroutine(runEnumerator);
runEnumerator = null;
}
}
private IEnumerator Run()
{
while (true) {
switch (this.Status) {
case CoroStatus.Running:
// Must first let child coroutine finish executing if present
if (this.child != null) {
yield return child.coroutine;
// The child has set an error on itself, an exception was thrown
// so we need to update the parent coroutine's status to Stopped
// and set containing error on parent as well.
if (child.Error != null) {
this.Status = CoroStatus.Stopped;
this.Error = new Exception("Child Coroutine at depth " + depth + " threw an exception", child.Error);
child = null;
yield break;
} else {
child = null;
}
// Break, because we may need to re-eval Status
break;
}
try {
// Advance coroutine
if (!enumerator.MoveNext()) {
this.Status = CoroStatus.Stopped;
yield break;
}
} catch (Exception e) {
// On error, log it, save it, and break so caller can see error
this.Error = e;
this.Status = CoroStatus.Stopped;
Debug.LogError(e.ToString());
yield break;
}
// Grab the yielded value from the generator func
var yielded = enumerator.Current;
if (yielded is IFuture) {
var future = yielded as IFuture;
while (!future.IsComplete) {
yield return null;
}
} else {
var yieldedType = yielded == null ? null : yielded.GetType();
if (yieldedType == typeof (Coro)) {
// If child coro was yielded, increment depth and keep
// reference to child so we know to yield to it on next iteration
var yieldedCoro = yielded as Coro;
child = yieldedCoro;
child.depth = this.depth + 1;
} else {
yield return yielded;
}
}
break;
case CoroStatus.Suspended:
// Do nothing if suspended
yield return null;
break;
case CoroStatus.Stopped:
// If status is Stopped, someone has called Stop and we can exit the loop
yield break;
}
}
}
}
using System;
public abstract class FutureException : InvalidOperationException
{
public FutureException(string message)
: base(message) { }
}
public class FutureValueNotSetException : FutureException
{
public FutureValueNotSetException()
: base( "This future hasn't been set yet.") { }
}
public class FutureValueAlreadySetException : FutureException
{
public FutureValueAlreadySetException()
: base( "This future has already been set.") { }
}
public class FutureErrorAlreadySetException : FutureException
{
public FutureErrorAlreadySetException()
: base("This future's Error has already been set.") { }
}
public interface IFuture
{
object Value { get; }
bool HasValue { get; }
Exception Error { get; }
bool IsComplete { get; }
}
public interface IFuture<T> : IFuture
{
T Value { get; }
}
public class Future<T> : IFuture<T>
{
private T value;
public T Value {
get {
if (this.HasValue) {
return this.value;
} else if (this.Error != null) {
throw Error;
}
throw new FutureValueNotSetException();
}
}
object IFuture.Value { get { return this.Value; } }
public Exception Error { get; private set; }
public bool HasValue { get; private set; }
public bool IsComplete { get { return HasValue || Error != null; } }
public void SetValue(T value)
{
if (this.Error != null) {
throw new FutureErrorAlreadySetException();
} else if (this.HasValue) {
throw new FutureValueAlreadySetException();
}
this.HasValue = true;
this.value = value;
}
public void SetException(Exception e)
{
if (this.HasValue) {
throw new FutureValueAlreadySetException();
} else if (this.Error != null) {
throw new FutureErrorAlreadySetException();
}
this.Error = e;
}
}
Copyright (C) 2014 Michael Bartnett
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>

Our game needs to grab a bunch of crap exquisitely modeled player data from a web server. Coroutines are a good fit for managing this, especially if your logic requires multiple sequential (i.e. each dependent on the result of the previous) fetches in some cases.

My problem was that I had a player list that would need to update asynchronously because there was a chance that in order to display some dynamic UI elements, a web request would be required (e.g. fetch avatar if new player appears in list).

Problem was, this UI switched state by activating and deactivating parts of its hierarchy. When a GameObject is deactivated in Unity, all running coroutines on the GameObject are stopped. This makes sense. I mean, except for the fact that when you disable a MonoBehaviour that doesn't stop or suspend its coroutines. And that Update() calls keeping getting sent like nothing happened to a GameObject.

Whatever.

So I added a Deactivate/Reactivate mechanism for coroutines. It uses the method Twisted Oak demoed at Unite 2013, removes the future-ish return value system, and adds susport for suspend/resume and deactivating/reactivating a coroutine (useful for when your GameObject is set to inactive, then active again).

It works by ensuring StopCoroutine() and StartCoroutine() are called on IEnumerator references at the right time with the right MonoBehaviour instance and being reasonably sure that no one else has a reference to said IEnumerator.

Method pairs

  • (static) Start / (instance) Stop
  • Suspend / Resume
  • Deactivate / Reactivate

Can't call any on the right without calling its buddy on the left. Start is static (and Coro's constructor is private) for defensive programming reasons, and because I like the way yield return Coro.Start(this, func); looks more than yield return new Coro(this, func);.

Bringing Coroutines Back from the Dead

You need to manually call Deactivate and Reactivate, because there's no way to know it should happen otherwise. Any MonoBehaviour that uses this feature needs the following:

void Enable()
{
    if (coroRef != null && coroRef.Deactivated) {
        coroRef.Reactivate();
    }
}

void Disable()
{
    if (!this.gameObject.activeInHierarchy && coroRef != null) {
        coroRef.Deactivate();
    }
}

There's no unintrusive way to automate this that I know of (making a BaseMonoBehaviour is intrusive in my book). All that's needed is to call StartCoroutine on an IEnumerator reference to start things back up again. So Coro keep tracks track of the IEnumerator and sets appropriate state properties on Coros and their children.

Calling Interface

Intsead of the standard Unity syntax, it's more like javascript/python. Coro.Start is a static method that takes a Func<IEnumerator> and has overloads for 1-4 parameters. It's obvious how to add more in case you're a masochist.

void Awake()
{
    Coor.Start(this, MyRootCoro);
}

private IEnumerator MyRootCoro()
{
    yield return new Coro.Start(this, MyChild, time);
}

private IEnumerator MyChildCoro(float time)
{
    yield return new WaitForSeconds(time);
    Debug.Log("Yay!");
}

Using the delegates means that Coro can guarantee that it has control over the IEnumerator references created by your generator functions.

Absence of return values

To address why I didn't include return values, the reason is that I like to stick async return values in an IFuture object. Makes sense to me that Coro could implement IFuture, but I didn't feel like working out the semantics of that yet, and the fact that the Twisted Oak guy had to hijack IEnumerator.Current when it was type T as a return value didn't sit right with me. What if you want to write a coroutine that returns coroutines? :P So for now the way to get a return value from a Coro is to pass it a Future reference.

var futureThing = new Future<AThing>();
var coro = Coro.Start(GoGetAThing(futureThing));

yield return coro;

if (futureThing.HasValue) {
    Debug.Log("Got a thing: " + futureThing.Value);
} else {
    Debug.Log("Did not get a thing.");
}

The IFuture.cs file has a basic little set of Future helper classes. Coro also knows how to identify an IFuture and spin-yield-wait for it, so you could also do this if it suited you:

var futureThing = new Future<AThing>();
var coro = Coro.Start(GoGetAThing(futureThing));

yield return futureThing; // OMG, Magic from before the dawn of time!

if (futureThing.HasValue) {
    Debug.Log("Got a thing: " + futureThing.Value);
} else {
    Debug.Log("Did not get a thing.");
}
using System;
using UnityEngine;
using IEnumerator=System.Collections.IEnumerator;
using System.Collections.Generic;
public class TestCoroOps : MonoBehaviour
{
public string myName = "TestCoro";
public bool startNewCoro = false;
public bool resumeCoro = false;
public bool suspendCoro = false;
public bool stopCoro = false;
public bool deactivateCoro = false;
public bool reactivateCoro = false;
public bool printCoroStatus = false;
public bool throwNestedCoroException = false;
int nestedCounter = 0;
int counter = 0;
int nestDepth = 0;
Coro coro = null;
string GetStatusString()
{
return string.Format(
"({4}): gameObject == null ? {3}; gameObject.activeSelf={0}; gameObject.activeInHierarchy={1}; this.enabled={2}",
gameObject.activeSelf, gameObject.activeInHierarchy, this.enabled, gameObject == null, myName);
}
void LogCoroStatus()
{
Debug.Log("CORO STATUS = " + (coro == null ? "NULL" : coro.Status.ToString() + ", deactivated = " + coro.Deactivated));
}
void OnEnable()
{
if (coro != null) {
if (coro.Deactivated) {
Debug.Log("Deactivating game object, deactivating coro");
coro.Reactivate();
} else {
Debug.LogWarning("Um, probably should have deactivated ths coro");
}
}
}
void OnDisable()
{
if (!gameObject.activeInHierarchy) {
if (coro != null) {
Debug.Log("Deactivating game object, deactivating coro");
coro.Deactivate();
}
}
}
void Update()
{
if (startNewCoro) {
startNewCoro = false;
if (coro != null && coro.Status != CoroStatus.Stopped) {
coro.Stop();
}
coro = Coro.Start(this, IntCoro);
LogCoroStatus();
}
if (resumeCoro) {
resumeCoro = false;
if (coro != null) {
coro.Resume();
}
LogCoroStatus();
}
if (suspendCoro) {
suspendCoro = false;
if (coro != null) {
coro.Suspend();
}
LogCoroStatus();
}
if (stopCoro) {
stopCoro = false;
if (coro != null) {
coro.Stop();
}
LogCoroStatus();
}
if (deactivateCoro) {
deactivateCoro = false;
if (coro != null) {
coro.Deactivate();
}
}
if (reactivateCoro) {
reactivateCoro = false;
if (coro != null) {
coro.Reactivate();
}
}
if (printCoroStatus) {
printCoroStatus = false;
LogCoroStatus();
}
}
IEnumerator IntCoro()
{
int counterAtStart = counter;
while (true) {
++counter;
Debug.Log("IntCoro: counter is " + counter + " started from " + counterAtStart + " fc: " + Time.frameCount);
yield return new WaitForSeconds(2);
Debug.Log("IntCoro yield-waiting for NestIntCoro" + " fc: " + Time.frameCount);
yield return Coro.Start(this, NestIntCoro, 2);
Debug.Log("IntCoro: NestIntCoro finished" + " fc: " + Time.frameCount);
};
}
IEnumerator NestIntCoro(int amt)
{
nestDepth++;
Debug.Log("NestIntCoro: Started, depth @ " + nestDepth);
int nestedCounterAtStart = nestedCounter;
for (int i = 0; i < amt; ++i) {
++nestedCounter;
Debug.Log("NestIntCoro: nestedCounter is " + nestedCounter + " started from " + nestedCounterAtStart + " fc: " + Time.frameCount);
yield return new WaitForSeconds(2);
Debug.Log("NestIntCoro: Yield one more frame for good measure ");
if (throwNestedCoroException) {
throwNestedCoroException = false;
throw new Exception("ARGH EXCEPTION OMG");
}
yield return Coro.Start(this, NestIntCoro, amt - 1);
yield return new WaitForSeconds(2);
}
Debug.Log("NestIntCoro: Finished depth @ " + nestDepth);
nestDepth--;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment