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.
- (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);
.
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.
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.
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.");
}