Description
A simple coroutine scheduler. This coroutine scheduler allows for completecontrol over the execution regime of a set of coroutines. Reading the codewill also help you understand how coroutines work behind the scenes.Understanding how coroutines are built on top of .Net generators will allowyou to add coroutine support to a non-Unity project.
Coroutines can yield until the next update "yield;", until a given number ofupdates have passed "yield anInt;", until a given amount of seconds have passed"yield aFloat;", or until another coroutine has finished"yield scheduler.StartCoroutine(Coroutine());".
Multiple scheduler instances are supported and can be very useful. Acoroutine running under one scheduler can yield (wait) for a coroutinerunning under a completely different scheduler instance.
Unity's YieldInstruction classes are not used because I cannotaccess their internal data needed for scheduling. Semantics are slightlydifferent from Unity's scheduler. For example, in Unity if you start acoroutine it will run up to its first yield immediately, while in thisscheduler it will not run until the next time UpdateAllCoroutines is called.This feature allows any code to start coroutines at any time, whilemaking sure the started coroutines only run at specific times.
You should not depend on update order between coroutines running on the sameupdate.
For a deeper understanding and to learn more about how coroutines are implemented as state machines by the compiler see my blog entry.
Usage
Creating and driving CoroutineSchedulers is easy and is best shown by an example.
var scheduler = new CoroutineScheduler(); function Update() { scheduler.UpdateAllCoroutines(Time.frameCount, Time.time); }
You normally would not use Unity's own Time.frameCount or Time.time as in that case you may as well use Unity's built-in scheduler, but it makes for an easy to understand example.
Starting coroutines and yielding execution is also best shown by an example.
scheduler.StartCoroutine(MyCoroutine()); function MyCoroutine() : IEnumerator { print("MyCoroutine: Begin"); yield; // wait for next update print("MyCoroutine: next update;"); yield 2; // wait for 2 updates, same as yield; yield; print("MyCoroutine: After yield 2;"); yield 3.5; // wait for 3.5 seconds print("MyCoroutine: After 3.5 seconds;"); // you can also yield for a coroutine running on a completely different scheduler instance yield scheduler.StartCoroutine(WaitForMe()); print("MyCoroutine: After WaitForMe() finished;"); } function WaitForMe() { yield 7.8; // wait for 7.8 seconds before finishing }
For details on features not used in this particular example please read the doc comments. If you have any questions feel free to contact me directly at fernando@cpudreams.com.
Code
CoroutineScheduler.js
#pragma strict /** * A simple coroutine scheduler. Coroutines can yield until the next update * "yield;", until a given number of updates "yield anInt", until a given * amount of seconds "yield aFloat;", or until another coroutine has finished * "yield scheduler.StartCoroutine(Coroutine())". * * Multiple scheduler instances are supported and can be very useful. A * coroutine running under one scheduler can yield (wait) for a coroutine * running under a completely different scheduler instance. * * Unity's YieldInstruction classes are not used because I cannot * access their internal data needed for scheduling. Semantics are slightly * different from Unity's scheduler. For example, in Unity if you start a * coroutine it will run up to its first yield immediately, while in this * scheduler it will not run until the next time UpdateAllCoroutines is called. * This feature allows any code to start coroutines at any time, while * making sure the started coroutines only run at specific times. * * You should not depend on update order between coroutines running on the same * update. For example, StartCoroutine(A), StartCoroutine(B), StartCoroutine(C) * where A, B, C => while(true) { print(A|B|C); yield; }, do not expect "ABC" or * "CBA" or any other specific ordering. */ class CoroutineScheduler { /** * The first node in list of the coroutines that are scheduled for execution. */ var first : CoroutineNode = null; var currentFrame : int; var currentTime : float; /** * Starts a coroutine, the coroutine does not run immediately but on the * next call to UpdateAllCoroutines. The execution of a coroutine can * be paused at any point using the yield statement. The yield return value * specifies when the coroutine is resumed. */ function StartCoroutine(fiber : IEnumerator) : CoroutineNode { // if function does not have a yield, fiber will be null and we no-op if (fiber == null) { return; } // create coroutine node and run until we reach first yield var coroutine = new CoroutineNode(fiber); AddCoroutine(coroutine); return coroutine; } /** * Stops all coroutines running on this behaviour. Use of this method is * discouraged, think of a natural way for your coroutines to finish * on their own instead of being forcefully stopped before they finish. * If you need finer control over stopping coroutines you can use multiple * schedulers. */ function StopAllCoroutines() { first = null; } /** * Returns true if this scheduler has any coroutines. You can use this to * check if all coroutines have finished or been stopped. */ function HasCoroutines() : boolean { return first != null; } /** * Runs all active coroutines until their next yield. Caller must provide * the current frame and time. This allows for schedulers to run under * frame and time regimes other than the Unity's main game loop. */ function UpdateAllCoroutines(frame : int, time : float) { currentFrame = frame; currentTime = time; var coroutine = first; while (coroutine != null) { // store listNext before coroutine finishes and is removed from the list var listNext = coroutine.listNext; if (coroutine.waitForFrame > 0 && frame >= coroutine.waitForFrame) { coroutine.waitForFrame = -1; UpdateCoroutine(coroutine); } else if (coroutine.waitForTime > 0.0 && time >= coroutine.waitForTime) { coroutine.waitForTime = -1.0; UpdateCoroutine(coroutine); } else if (coroutine.waitForCoroutine && coroutine.waitForCoroutine.finished) { coroutine.waitForCoroutine = null; UpdateCoroutine(coroutine); } else if (coroutine.waitForFrame == -1 && coroutine.waitForTime == -1.0 && coroutine.waitForCoroutine == null) { // initial update UpdateCoroutine(coroutine); } coroutine = listNext; } } /** * Executes coroutine until next yield. If coroutine has finished, flags * it as finished and removes it from scheduler list. */ private function UpdateCoroutine(coroutine : CoroutineNode) { var fiber = coroutine.fiber; if (coroutine.fiber.MoveNext()) { var yieldCommand : Object = (fiber.Current == null) ? 1 : fiber.Current; if (yieldCommand instanceof int) { coroutine.waitForFrame = yieldCommand; coroutine.waitForFrame += currentFrame; } else if (yieldCommand instanceof float) { coroutine.waitForTime = yieldCommand; coroutine.waitForTime += currentTime; } else if (yieldCommand instanceof CoroutineNode) { coroutine.waitForCoroutine = yieldCommand; } else { throw "Unexpected coroutine yield type: " + yieldCommand.GetType(); } } else { // coroutine finished coroutine.finished = true; RemoveCoroutine(coroutine); } } private function AddCoroutine(coroutine : CoroutineNode) { if (first != null) { coroutine.listNext = first; first.listPrevious = coroutine; } first = coroutine; } private function RemoveCoroutine(coroutine : CoroutineNode) { if (first == coroutine) { // remove first first = coroutine.listNext; } else { // not head of list if (coroutine.listNext != null) { // remove between coroutine.listPrevious.listNext = coroutine.listNext; coroutine.listNext.listPrevious = coroutine.listPrevious; } else if (coroutine.listPrevious != null) { // and listNext is null coroutine.listPrevious.listNext = null; // remove last } } coroutine.listPrevious = null; coroutine.listNext = null; } }
CoroutineNode.js
#pragma strict /** * Linked list node type used by coroutine scheduler to track scheduling of * coroutines. */ class CoroutineNode { var listPrevious : CoroutineNode = null; var listNext : CoroutineNode = null; var fiber : IEnumerator; var finished = false; var waitForFrame = -1; var waitForTime = -1.0; var waitForCoroutine : CoroutineNode = null; function CoroutineNode(fiber : IEnumerator) { this.fiber = fiber; } }
Ported to CSharp
CoroutineSchedulerTest.cs
using UnityEngine; using System.Collections; /// <summary> /// CoroutineSchedulerTest.cs /// /// Port of the Javascript version from /// http://www.unifycommunity.com/wiki/index.php?title=CoroutineScheduler /// /// Linked list node type used by coroutine scheduler to track scheduling of coroutines. /// /// BMBF Researchproject http://playfm.htw-berlin.de /// PlayFM - Serious Games für den IT-gestützten Wissenstransfer im Facility Management /// Gefördert durch das bmb+f - Programm Forschung an Fachhochschulen profUntFH /// /// <author>Frank.Otto@htw-berlin.de</author> /// /// </summary> public class CoroutineSchedulerTest : MonoBehaviour { CoroutineScheduler scheduler; // Use this for initialization void Start () { scheduler = new CoroutineScheduler (); scheduler.StartCoroutine (MyCoroutine ()); } IEnumerator MyCoroutine () { Debug.Log ("MyCoroutine: Begin"); yield return 0; // wait for next update Debug.Log ("MyCoroutine: next update;" + Time.time); yield return 2; // wait for 2 updates, same as yield; yield; Debug.Log ("MyCoroutine: After yield 2;" + Time.time); yield return 3.5f; // wait for 3.5 seconds Debug.Log ("MyCoroutine: After 3.5 seconds;" + Time.time); // you can also yield for a coroutine running on a completely different scheduler instance yield return scheduler.StartCoroutine (WaitForMe ()); Debug.Log ("MyCoroutine: After WaitForMe() finished;" + Time.time); } IEnumerator WaitForMe () { yield return 7.8f; // wait for 7.8 seconds before finishing } // Update is called once per void Update () { scheduler.UpdateAllCoroutines (Time.frameCount, Time.time); } }
CoroutineScheduler.cs
using System.Collections; using UnityEngine; /// <summary> /// CoroutineScheduler.cs /// /// Port of the Javascript version from /// http://www.unifycommunity.com/wiki/index.php?title=CoroutineScheduler /// /// Linked list node type used by coroutine scheduler to track scheduling of coroutines. /// /// /// BMBF Researchproject http://playfm.htw-berlin.de /// PlayFM - Serious Games für den IT-gestützten Wissenstransfer im Facility Management /// Gefördert durch das bmb+f - Programm Forschung an Fachhochschulen profUntFH /// /// <author>Frank.Otto@htw-berlin.de</author> /// /// /// A simple coroutine scheduler. Coroutines can yield until the next update /// "yield;", until a given number of updates "yield anInt", until a given /// amount of seconds "yield aFloat;", or until another coroutine has finished /// "yield scheduler.StartCoroutine(Coroutine())". /// /// Multiple scheduler instances are supported and can be very useful. A /// coroutine running under one scheduler can yield (wait) for a coroutine /// running under a completely different scheduler instance. /// /// Unity's YieldInstruction classes are not used because I cannot /// access their internal data needed for scheduling. Semantics are slightly /// different from Unity's scheduler. For example, in Unity if you start a /// coroutine it will run up to its first yield immediately, while in this /// scheduler it will not run until the next time UpdateAllCoroutines is called. /// This feature allows any code to start coroutines at any time, while /// making sure the started coroutines only run at specific times. /// /// You should not depend on update order between coroutines running on the same /// update. For example, StartCoroutine(A), StartCoroutine(B), StartCoroutine(C) /// where A, B, C => while(true) { print(A|B|C); yield; }, do not expect "ABC" or /// "CBA" or any other specific ordering. /// </summary> public class CoroutineScheduler : MonoBehaviour { CoroutineNode first = null; int currentFrame; float currentTime; /** * Starts a coroutine, the coroutine does not run immediately but on the * next call to UpdateAllCoroutines. The execution of a coroutine can * be paused at any point using the yield statement. The yield return value * specifies when the coroutine is resumed. */ public CoroutineNode StartCoroutine (IEnumerator fiber) { // if function does not have a yield, fiber will be null and we no-op if (fiber == null) { return null; } // create coroutine node and run until we reach first yield CoroutineNode coroutine = new CoroutineNode (fiber); AddCoroutine (coroutine); return coroutine; } /** * Stops all coroutines running on this behaviour. Use of this method is * discouraged, think of a natural way for your coroutines to finish * on their own instead of being forcefully stopped before they finish. * If you need finer control over stopping coroutines you can use multiple * schedulers. */ public void StopAllCoroutines () { first = null; } /** * Returns true if this scheduler has any coroutines. You can use this to * check if all coroutines have finished or been stopped. */ public bool HasCoroutines () { return first != null; } /** * Runs all active coroutines until their next yield. Caller must provide * the current frame and time. This allows for schedulers to run under * frame and time regimes other than the Unity's main game loop. */ public void UpdateAllCoroutines (int frame, float time) { currentFrame = frame; currentTime = time; CoroutineNode coroutine = this.first; while (coroutine != null) { // store listNext before coroutine finishes and is removed from the list CoroutineNode listNext = coroutine.listNext; if (coroutine.waitForFrame > 0 && frame >= coroutine.waitForFrame) { coroutine.waitForFrame = -1; UpdateCoroutine (coroutine); } else if (coroutine.waitForTime > 0.0f && time >= coroutine.waitForTime) { coroutine.waitForTime = -1.0f; UpdateCoroutine (coroutine); } else if (coroutine.waitForCoroutine != null && coroutine.waitForCoroutine.finished) { coroutine.waitForCoroutine = null; UpdateCoroutine (coroutine); } else if (coroutine.waitForFrame == -1 && coroutine.waitForTime == -1.0f && coroutine.waitForCoroutine == null) { // initial update UpdateCoroutine (coroutine); } coroutine = listNext; } } /** * Executes coroutine until next yield. If coroutine has finished, flags * it as finished and removes it from scheduler list. */ private void UpdateCoroutine (CoroutineNode coroutine) { IEnumerator fiber = coroutine.fiber; if (coroutine.fiber.MoveNext ()) { System.Object yieldCommand = fiber.Current == null ? (System.Object) 1 : fiber.Current; if (yieldCommand.GetType () == typeof(int)) { coroutine.waitForFrame = (int) yieldCommand; coroutine.waitForFrame += (int) currentFrame; } else if (yieldCommand.GetType () == typeof(float)) { coroutine.waitForTime = (float) yieldCommand; coroutine.waitForTime += (float) currentTime; } else if (yieldCommand.GetType () == typeof(CoroutineNode)) { coroutine.waitForCoroutine = (CoroutineNode) yieldCommand; } else { throw new System.ArgumentException ("CoroutineScheduler: Unexpected coroutine yield type: " + yieldCommand.GetType ()); } } else { // coroutine finished coroutine.finished = true; RemoveCoroutine (coroutine); } } private void AddCoroutine (CoroutineNode coroutine) { if (this.first != null) { coroutine.listNext = this.first; first.listPrevious = coroutine; } first = coroutine; } private void RemoveCoroutine (CoroutineNode coroutine) { if (this.first == coroutine) { // remove first this.first = coroutine.listNext; } else { // not head of list if (coroutine.listNext != null) { // remove between coroutine.listPrevious.listNext = coroutine.listNext; coroutine.listNext.listPrevious = coroutine.listPrevious; } else if (coroutine.listPrevious != null) { // and listNext is null coroutine.listPrevious.listNext = null; // remove last } } coroutine.listPrevious = null; coroutine.listNext = null; } }//class
CoroutineNode.cs
using System.Collections; using UnityEngine; /// <summary> /// CoroutineNode.cs /// /// Port of the Javascript version from /// http://www.unifycommunity.com/wiki/index.php?title=CoroutineScheduler /// /// Linked list node type used by coroutine scheduler to track scheduling of coroutines. /// /// BMBF Researchproject http://playfm.htw-berlin.de /// PlayFM - Serious Games für den IT-gestützten Wissenstransfer im Facility Management /// Gefördert durch das bmb+f - Programm Forschung an Fachhochschulen profUntFH /// /// <author>Frank.Otto@htw-berlin.de</author> /// /// </summary> public class CoroutineNode { public CoroutineNode listPrevious = null; public CoroutineNode listNext = null; public IEnumerator fiber; public bool finished = false; public int waitForFrame = -1; public float waitForTime = -1.0f; public CoroutineNode waitForCoroutine; public CoroutineNode (IEnumerator _fiber) { this.fiber = _fiber; } }
Additional Implementation C#
If you want to use CoroutineScheduler with Yield Commands from within the UnityEngine namespace here is an addition to the implementation, I found it useful for consuming api's in classes defined outside the UnityEngine namespace.
Sample Usage
m_scheduler = new CoroutineScheduler(); m_scheduler.StartCoroutine(testAPI()); IEnumerator testAPI() { // ...set up request var www = new UnityEngine.WWW(requestURL); yield return new UnityWWWYieldWrapper(www); // ...loading complete do some stuff }
Addition to CoroutineScheduler.cs
//line 110 public void UpdateAllCoroutines(int frame, float time) { currentFrame = frame; currentTime = time; CoroutineNode coroutine = this.first; while (coroutine != null) { // store listNext before coroutine finishes and is removed from the list CoroutineNode listNext = coroutine.listNext; if (coroutine.waitForFrame > 0 && frame >= coroutine.waitForFrame) { coroutine.waitForFrame = -1; UpdateCoroutine(coroutine); } else if (coroutine.waitForTime > 0.0f && time >= coroutine.waitForTime) { coroutine.waitForTime = -1.0f; UpdateCoroutine(coroutine); } else if (coroutine.waitForCoroutine != null && coroutine.waitForCoroutine.finished) { coroutine.waitForCoroutine = null; UpdateCoroutine(coroutine); } else if (coroutine.waitForUnityObject != null && coroutine.waitForUnityObject.finished)//lonewolfwilliams { coroutine.waitForUnityObject = null; UpdateCoroutine(coroutine); } else if (coroutine.waitForFrame == -1 && coroutine.waitForTime == -1.0f && coroutine.waitForCoroutine == null && coroutine.waitForUnityObject == null) { // initial update UpdateCoroutine(coroutine); } coroutine = listNext; } } //line 154 private void UpdateCoroutine(CoroutineNode coroutine) { IEnumerator fiber = coroutine.fiber; if (coroutine.fiber.MoveNext()) { System.Object yieldCommand = fiber.Current == null ? (System.Object)1 : fiber.Current; if (yieldCommand.GetType() == typeof(int)) { coroutine.waitForFrame = (int)yieldCommand; coroutine.waitForFrame += (int)currentFrame; } else if (yieldCommand.GetType() == typeof(float)) { coroutine.waitForTime = (float)yieldCommand; coroutine.waitForTime += (float)currentTime; } else if (yieldCommand.GetType() == typeof(CoroutineNode)) { coroutine.waitForCoroutine = (CoroutineNode)yieldCommand; } else if (yieldCommand is IYieldWrapper) //lonewolfwilliams { coroutine.waitForUnityObject = yieldCommand as IYieldWrapper; } else { throw new System.ArgumentException("CoroutineScheduler: Unexpected coroutine yield type: " + yieldCommand.GetType()); //this is an alternative if you don't have access to the function passed to the couroutineScheduler - maybe it's //precompiled in a dll for example - remember you will have to add a case every time you add a wrapper :/ /* var commandType = yieldCommand.GetType(); if(commandType == typeof(UnityEngine.WWW)) { coroutine.waitForUnityObject = new UnityWWWWrapper(yieldCommand as UnityEngine.WWW); } else if(commandType == typeof(UnityEngine.AsyncOperation)) { coroutine.waitForUnityObject = new UnityASyncOpWrapper(yieldCommand as UnityEngine.AsyncOperation); } else if(commandType == typeof(UnityEngine.AssetBundleRequest)) { coroutine.waitForUnityObject = new UnityAssetBundleRequestWrapper(yieldCommand as UnityEngine.AssetBundleRequest); } else { throw new System.ArgumentException("CoroutineScheduler: Unexpected coroutine yield type: " + yieldCommand.GetType()); } */ } } else { // coroutine finished coroutine.finished = true; RemoveCoroutine(coroutine); } }
Addition to CoroutineNode.cs
public class CoroutineNode { public CoroutineNode listPrevious = null; public CoroutineNode listNext = null; public IEnumerator fiber; public bool finished = false; public int waitForFrame = -1; public float waitForTime = -1.0f; public CoroutineNode waitForCoroutine; public IYieldWrapper waitForUnityObject; //lonewolfwilliams public CoroutineNode(IEnumerator _fiber) { this.fiber = _fiber; } }
IYieldWrapper.cs
you will need to write your own implementations for yield commands from within the UnityEngine namespace, this interface should help facilitate that.
/* * gareth williams * http://www.lonewolfwilliams.com */ public interface IYieldWrapper { bool finished { get; } }
Example Wrappers
Below are some examples of wrappers I have used, in fact they have almost identical signatures so a more generic implementation could probably be written ^_^
UnityASyncOpWrapper
using System; using System.Collections.Generic; using System.Linq; using System.Text; /* * Gareth Williams * http://www.lonewolfwilliams.com */ class UnityASyncOpWrapper : IYieldWrapper { private UnityEngine.AsyncOperation m_UnityObject; public bool finished { get { return m_UnityObject.isDone; } } public UnityASyncOpWrapper(UnityEngine.AsyncOperation wraps) { m_UnityObject = wraps; } }
UnityWWWYieldWrapper
using System; using System.Collections.Generic; using System.Linq; using System.Text; /* * Gareth Williams * http://www.lonewolfwilliams.com */ public class UnityWWWYieldWrapper : IYieldWrapper { private UnityEngine.WWW m_UnityObject; public bool finished { get { return m_UnityObject.isDone; } } public UnityWWWYieldWrapper(UnityEngine.WWW wraps) { m_UnityObject = wraps; } }