Last active
August 28, 2024 19:09
-
-
Save LotteMakesStuff/16b5f2fc108f9a0201950c797d53cfbf to your computer and use it in GitHub Desktop.
Run stuff in the editor with all the convenience of co-routines! Add a status UI super easily to see readouts what your long running code is actually doing! nice~ There is a short API demo at the top of the class. This is a prefect companion for my inspector buttons https://gist.github.com/LotteMakesStuff/dd785ff49b2a5048bb60333a6a125187
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using UnityEngine; | |
using UnityEditor; | |
using System; | |
using System.Collections; | |
using System.Collections.Generic; | |
using System.Reflection; | |
public class EditorCoroutineRunner | |
{ | |
[MenuItem("Window/Lotte's Coroutine Runner: Demo")] | |
public static void DemoEditorCoroutines() | |
{ | |
// adds a menu item to test the coroutine system. | |
if (!Application.isPlaying) | |
{ | |
// lets fire off the demo coroutine with a UI so we can see what its doing. We could also run it without a UI by using EditorCoroutineRunner.StartCoroutine(...) | |
EditorCoroutineRunner.StartCoroutineWithUI(DemoCoroutiune(), "Lotte's Coroutine Demo", true); | |
} | |
} | |
static IEnumerator DemoCoroutiune() | |
{ | |
// You can code editor coroutines exactly like you would a normal unity coroutine | |
Debug.Log("Step: 0"); | |
yield return null; | |
// all the normal return types that work with regular Unity coroutines should work here! for example lets wait for a second | |
Debug.Log("Step: 1"); | |
yield return new WaitForSeconds(1); | |
// We can also yeild any type that extends Unitys CustomYieldInstruction class. here we are going to use EditorStatusUpdate. this allows us to yield and update the | |
// editor coroutine UI at the same time! | |
yield return new EditorStatusUpdate("coroutine is running", 0.2f); | |
// We can also yield to nested coroutines | |
Debug.Log("Step: 2"); | |
yield return EditorCoroutineRunner.StartCoroutine(DemoTwo()); | |
EditorCoroutineRunner.UpdateUIProgressBar(0.35f); // we can use the UpdateUI helper methods to update the UI whenever, without yielding a EditorStatusUpdate | |
yield return DemoTwo(); // it shouldnt matter how we start the nested coroutine, the editor runner can hadle it | |
// we can even yield a WWW object if we want to grab data from the internets! | |
Debug.Log("Step: 3"); | |
// for example, lets as random.org to generate us a list of random numbers and shove it into the console | |
var www = new WWW("https://www.random.org/integers/?num=100&min=1&max=1000&col=1&base=10&col=5&format=plain&rnd=new"); | |
yield return www; | |
Debug.Log(www.text); | |
EditorCoroutineRunner.UpdateUI("Half way!", 0.5f); | |
yield return new WaitForSeconds(1); | |
// Finally lets do a long runnig task and split its updates over many frames to keep the editor responsive | |
Debug.Log("Step: 4"); | |
var test = 1000; | |
yield return new WaitUntil(() => { | |
test--; | |
EditorCoroutineRunner.UpdateUI("Crunching Numbers: " + test, 0.5f + (((1000 - test) / 1000f) * 0.5f)); | |
return (test <= 0); | |
}); | |
Debug.Log("Done!!"); | |
} | |
static IEnumerator DemoTwo() | |
{ | |
Debug.Log("TESTTWO: Starting second test coroutine"); | |
yield return new WaitForSeconds(1.2f); | |
Debug.Log("TESTTWO: finished second test coroutine"); | |
} | |
[MenuItem("Window/Lotte's Coroutine Runner: Force kill coroutines")] | |
public static void KillAllCoroutines() | |
{ | |
// force kills all running coroutines if something goes wrong. | |
EditorUtility.ClearProgressBar(); | |
uiCoroutineState = null; | |
coroutineStates.Clear(); | |
finishedThisUpdate.Clear(); | |
} | |
private static List<EditorCoroutineState> coroutineStates; | |
private static List<EditorCoroutineState> finishedThisUpdate; | |
private static EditorCoroutineState uiCoroutineState; | |
/// <summary> | |
/// Start a coroutine. equivilent of calling StartCoroutine on a mono behaviour | |
/// </summary> | |
public static EditorCoroutine StartCoroutine(IEnumerator coroutine) | |
{ | |
return StoreCoroutine(new EditorCoroutineState(coroutine)); | |
} | |
/// <summary> | |
/// Start a coroutine and display a progress UI. only one EditorCoroutine can display a UI at once. equivilent of calling StartCoroutine on a mono behaviour | |
/// </summary> | |
/// <param name="coroutine">coroutine to run</param> | |
/// <param name="title">Text to show in the UIs title bar</param> | |
/// <param name="isCancelable">Displays a cancel button if true</param> | |
public static EditorCoroutine StartCoroutineWithUI(IEnumerator coroutine, string title, bool isCancelable = false) | |
{ | |
if (uiCoroutineState != null) | |
{ | |
Debug.LogError("EditorCoroutineRunner only supports running one coroutine that draws a GUI! [" + title + "]"); | |
return null; | |
} | |
EditorCoroutineRunner.uiCoroutineState = new EditorCoroutineState(coroutine, title, isCancelable); | |
return StoreCoroutine(uiCoroutineState); | |
} | |
// Creates objects to manage the coroutines lifecycle and stores them away to be processed | |
private static EditorCoroutine StoreCoroutine(EditorCoroutineState state) | |
{ | |
if (coroutineStates == null) | |
{ | |
coroutineStates = new List<EditorCoroutineState>(); | |
finishedThisUpdate = new List<EditorCoroutineState>(); | |
} | |
if (coroutineStates.Count == 0) | |
EditorApplication.update += Runner; | |
coroutineStates.Add(state); | |
return state.editorCoroutineYieldInstruction; | |
} | |
/// <summary> | |
/// Updates the status label in the EditorCoroutine runner UI | |
/// </summary> | |
public static void UpdateUILabel(string label) | |
{ | |
if (uiCoroutineState != null && uiCoroutineState.showUI) | |
{ | |
uiCoroutineState.Label = label; | |
} | |
} | |
/// <summary> | |
/// Updates the progress bar in the EditorCoroutine runner UI | |
/// </summary> | |
public static void UpdateUIProgressBar(float percent) | |
{ | |
if (uiCoroutineState != null && uiCoroutineState.showUI) | |
{ | |
uiCoroutineState.PercentComplete = percent; | |
} | |
} | |
/// <summary> | |
/// Updates the status label and progress bar in the EditorCoroutine runner UI | |
/// </summary> | |
public static void UpdateUI(string label, float percent) | |
{ | |
if (uiCoroutineState != null && uiCoroutineState.showUI) | |
{ | |
uiCoroutineState.Label = label ; | |
uiCoroutineState.PercentComplete = percent; | |
} | |
} | |
// Manages running active coroutines! | |
private static void Runner() | |
{ | |
// Tick all the coroutines we have stored | |
for (int i = 0; i < coroutineStates.Count; i++) | |
{ | |
TickState(coroutineStates[i]); | |
} | |
// if a coroutine was finished whilst we were ticking, clear it out now | |
for (int i = 0; i < finishedThisUpdate.Count; i++) | |
{ | |
coroutineStates.Remove(finishedThisUpdate[i]); | |
if (uiCoroutineState == finishedThisUpdate[i]) | |
{ | |
uiCoroutineState = null; | |
EditorUtility.ClearProgressBar(); | |
} | |
} | |
finishedThisUpdate.Clear(); | |
// stop the runner if were done. | |
if (coroutineStates.Count == 0) | |
{ | |
EditorApplication.update -= Runner; | |
} | |
} | |
private static void TickState(EditorCoroutineState state) | |
{ | |
if (state.IsValid) | |
{ | |
// This coroutine is still valid, give it a chance to tick! | |
state.Tick(); | |
// if this coroutine is the active UI coroutine, give it a chance to update the UI | |
if (state.showUI && uiCoroutineState == state) | |
{ | |
uiCoroutineState.UpdateUI(); | |
} | |
} | |
else | |
{ | |
// We have finished running the coroutine, lets scrap it | |
finishedThisUpdate.Add(state); | |
} | |
} | |
// Special thanks to Thomas for donating the following two methods | |
// Github: @ThomasBousquet | |
// Twitter: @VimesTom | |
private static bool KillCoroutine(ref EditorCoroutine coroutine, ref List<EditorCoroutineState> states) | |
{ | |
foreach (EditorCoroutineState state in states) | |
{ | |
if (state.editorCoroutineYieldInstruction == coroutine) | |
{ | |
states.Remove (state); | |
coroutine = null; | |
return true; | |
} | |
} | |
return false; | |
} | |
public static void KillCoroutine(ref EditorCoroutine coroutine) | |
{ | |
if (uiCoroutineState.editorCoroutineYieldInstruction == coroutine) | |
{ | |
uiCoroutineState = null; | |
coroutine = null; | |
EditorUtility.ClearProgressBar (); | |
return; | |
} | |
if (KillCoroutine (ref coroutine, ref coroutineStates)) | |
return; | |
if (KillCoroutine (ref coroutine, ref finishedThisUpdate)) | |
return; | |
} | |
} | |
internal class EditorCoroutineState | |
{ | |
private IEnumerator coroutine; | |
public bool IsValid | |
{ | |
get { return coroutine != null; } | |
} | |
public EditorCoroutine editorCoroutineYieldInstruction; | |
// current state | |
private object current; | |
private Type currentType; | |
private float timer; // for WaitForSeconds support | |
private EditorCoroutine nestedCoroutine; // for tracking nested coroutines that are not started with EditorCoroutineRunner.StartCoroutine | |
private DateTime lastUpdateTime; | |
// UI | |
public bool showUI; | |
private bool cancelable; | |
private bool canceled; | |
private string title; | |
public string Label; | |
public float PercentComplete; | |
public EditorCoroutineState(IEnumerator coroutine) | |
{ | |
this.coroutine = coroutine; | |
editorCoroutineYieldInstruction = new EditorCoroutine(); | |
showUI = false; | |
lastUpdateTime = DateTime.Now; | |
} | |
public EditorCoroutineState(IEnumerator coroutine, string title, bool isCancelable) | |
{ | |
this.coroutine = coroutine; | |
editorCoroutineYieldInstruction = new EditorCoroutine(); | |
showUI = true; | |
cancelable = isCancelable; | |
this.title = title; | |
Label = "initializing...."; | |
PercentComplete = 0.0f; | |
lastUpdateTime = DateTime.Now; | |
} | |
public void Tick() | |
{ | |
if (coroutine != null) | |
{ | |
// First check if we have been canceled by the UI. If so, we need to stop before doing any wait processing | |
if (canceled) | |
{ | |
Stop(); | |
return; | |
} | |
// Did the last Yield want us to wait? | |
bool isWaiting = false; | |
var now = DateTime.Now; | |
if (current != null) | |
{ | |
if (currentType == typeof(WaitForSeconds)) | |
{ | |
// last yield was a WaitForSeconds. Lets update the timer. | |
var delta = now - lastUpdateTime; | |
timer -= (float)delta.TotalSeconds; | |
if (timer > 0.0f) | |
{ | |
isWaiting = true; | |
} | |
} | |
else if (currentType == typeof(WaitForEndOfFrame) || currentType == typeof(WaitForFixedUpdate)) | |
{ | |
// These dont make sense in editor, so we will treat them the same as a null return... | |
isWaiting = false; | |
} | |
else if (currentType == typeof(WWW)) | |
{ | |
// Web download request, lets see if its done! | |
var www = current as WWW; | |
if (!www.isDone) | |
{ | |
isWaiting = true; | |
} | |
} | |
else if (currentType.IsSubclassOf(typeof(CustomYieldInstruction))) | |
{ | |
// last yield was a custom yield type, lets check its keepWaiting property and react to that | |
var yieldInstruction = current as CustomYieldInstruction; | |
if (yieldInstruction.keepWaiting) | |
{ | |
isWaiting = true; | |
} | |
} | |
else if (currentType == typeof(EditorCoroutine)) | |
{ | |
// Were waiting on another coroutine to finish | |
var editorCoroutine = current as EditorCoroutine; | |
if (!editorCoroutine.HasFinished) | |
{ | |
isWaiting = true; | |
} | |
} | |
else if (typeof(IEnumerator).IsAssignableFrom(currentType)) | |
{ | |
// if were just seeing an enumerator lets assume that were seeing a nested coroutine that has been passed in without calling start.. were start it properly here if we need to | |
if (nestedCoroutine == null) | |
{ | |
nestedCoroutine = EditorCoroutineRunner.StartCoroutine(current as IEnumerator); | |
isWaiting = true; | |
} | |
else | |
{ | |
isWaiting = !nestedCoroutine.HasFinished; | |
} | |
} | |
else if (currentType == typeof(Coroutine)) | |
{ | |
// UNSUPPORTED | |
Debug.LogError("Nested Coroutines started by Unity's defaut StartCoroutine method are not supported in editor! please use EditorCoroutineRunner.Start instead. Canceling."); | |
canceled = true; | |
} | |
else | |
{ | |
// UNSUPPORTED | |
Debug.LogError("Unsupported yield (" + currentType + ") in editor coroutine!! Canceling."); | |
canceled = true; | |
} | |
} | |
lastUpdateTime = now; | |
// have we been canceled? | |
if (canceled) | |
{ | |
Stop(); | |
return; | |
} | |
if (!isWaiting) | |
{ | |
// nope were good! tick the coroutine! | |
bool update = coroutine.MoveNext(); | |
if (update) | |
{ | |
// yup the coroutine returned true so its been ticked... | |
// lets see what it actually yielded | |
current = coroutine.Current; | |
if (current != null) | |
{ | |
// is it a type we have to do extra processing on? | |
currentType = current.GetType(); | |
if (currentType == typeof(WaitForSeconds)) | |
{ | |
// its a WaitForSeconds... lets use reflection to pull out how long the actual wait is for so we can process the wait | |
var wait = current as WaitForSeconds; | |
FieldInfo m_Seconds = typeof(WaitForSeconds).GetField("m_Seconds", BindingFlags.NonPublic | BindingFlags.Instance); | |
if (m_Seconds != null) | |
{ | |
timer = (float)m_Seconds.GetValue(wait); | |
} | |
} | |
else if (currentType == typeof(EditorStatusUpdate)) | |
{ | |
// Special case yield that wants to update the UI! | |
var updateInfo = current as EditorStatusUpdate; | |
if (updateInfo.HasLabelUpdate) | |
{ | |
Label = updateInfo.Label; | |
} | |
if (updateInfo.HasPercentUpdate) | |
{ | |
PercentComplete = updateInfo.PercentComplete; | |
} | |
} | |
} | |
} | |
else | |
{ | |
// Coroutine returned false so its finally finished!! | |
Stop(); | |
} | |
} | |
} | |
} | |
private void Stop() | |
{ | |
// Coroutine has finished! do some cleanup... | |
coroutine = null; | |
editorCoroutineYieldInstruction.HasFinished = true; | |
} | |
public void UpdateUI() | |
{ | |
if (cancelable) | |
{ | |
canceled = EditorUtility.DisplayCancelableProgressBar(title, Label, PercentComplete); | |
if (canceled) | |
Debug.Log("CANCLED"); | |
} | |
else | |
{ | |
EditorUtility.DisplayProgressBar(title, Label, PercentComplete); | |
} | |
} | |
} | |
/// <summary> | |
/// Coroutine Yield instruction that allows an Editor Coroutine to update the Coroutine runner UI | |
/// </summary> | |
public class EditorStatusUpdate : CustomYieldInstruction | |
{ | |
public string Label; | |
public float PercentComplete; | |
public bool HasLabelUpdate; | |
public bool HasPercentUpdate; | |
public override bool keepWaiting | |
{ | |
get | |
{ | |
// always go to the next update | |
return false; | |
} | |
} | |
public EditorStatusUpdate(string label) | |
{ | |
HasPercentUpdate = false; | |
HasLabelUpdate = true; | |
Label = label; | |
} | |
public EditorStatusUpdate(float percent) | |
{ | |
HasPercentUpdate = true; | |
PercentComplete = percent; | |
HasLabelUpdate = false; | |
} | |
public EditorStatusUpdate(string label, float percent) | |
{ | |
HasPercentUpdate = true; | |
PercentComplete = percent; | |
HasLabelUpdate = true; | |
Label = label; | |
} | |
} | |
/// <summary> | |
/// Created when an Editor Coroutine is started, can be yielded to to allow another coroutine to finish first. | |
/// </summary> | |
public class EditorCoroutine : YieldInstruction | |
{ | |
public bool HasFinished; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment