├── .gitignore ├── Editor.meta ├── Editor ├── EditorRoutineManager.cs └── EditorRoutineManager.cs.meta ├── IRoutineContext.cs ├── IRoutineContext.cs.meta ├── LICENSE ├── LICENSE.meta ├── README.md ├── README.md.meta ├── Resumable.cs ├── Resumable.cs.meta ├── Routine.cs ├── Routine.cs.meta ├── RoutineContext.cs ├── RoutineContext.cs.meta ├── RoutineManager.cs └── RoutineManager.cs.meta /.gitignore: -------------------------------------------------------------------------------- 1 | # vim 2 | *.swp 3 | *.swo 4 | -------------------------------------------------------------------------------- /Editor.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 60daa7745b060194cb032bb0c956b98b 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Editor/EditorRoutineManager.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | using UnityEngine; 4 | using UnityEngine.Assertions; 5 | using UnityEditor; 6 | 7 | namespace Routines 8 | { 9 | public class EditorRoutineManager : IRoutineContext 10 | { 11 | private static EditorRoutineManager instance; 12 | private static object _lock = new object(); 13 | 14 | private RoutineContext context; 15 | 16 | private EditorRoutineManager() 17 | { 18 | this.context = new RoutineContext(); 19 | } 20 | 21 | public static EditorRoutineManager Instance 22 | { 23 | get 24 | { 25 | lock(_lock) 26 | { 27 | if (instance == null) 28 | { 29 | instance = new EditorRoutineManager(); 30 | instance.Start(); 31 | } 32 | 33 | return instance; 34 | } 35 | } 36 | } 37 | 38 | public Routine.Handle RunRoutine(IEnumerator enumerator, System.Action onStop = null) 39 | { 40 | return context.RunRoutine(enumerator, null, onStop); 41 | } 42 | 43 | public IEnumerator WaitForNextFrame() 44 | { 45 | return context.WaitForNextFrame(); 46 | } 47 | 48 | public IEnumerator WaitForSeconds(float seconds) 49 | { 50 | return context.WaitForSeconds(seconds); 51 | } 52 | 53 | public IEnumerator WaitUntil(System.Func condition) 54 | { 55 | return context.WaitUntil(condition); 56 | } 57 | 58 | public IEnumerator WaitForAsyncOperation(AsyncOperation asyncOperation, System.Action onProgress = null) 59 | { 60 | return context.WaitForAsyncOperation(asyncOperation, onProgress); 61 | } 62 | 63 | public IEnumerator WaitForCustomYieldInstruction(CustomYieldInstruction yieldInstruction) 64 | { 65 | return context.WaitForCustomYieldInstruction(yieldInstruction); 66 | } 67 | 68 | private void Start() 69 | { 70 | Assert.IsNotNull(context); 71 | 72 | EditorApplication.update += Step; 73 | EditorApplication.playModeStateChanged += OnPlayModeStateChange; 74 | AssemblyReloadEvents.beforeAssemblyReload += OnBeforeAssemblyReload; 75 | } 76 | 77 | private void OnBeforeAssemblyReload() 78 | { 79 | Stop(); 80 | } 81 | 82 | private void OnPlayModeStateChange(PlayModeStateChange change) 83 | { 84 | if (change == PlayModeStateChange.ExitingPlayMode || change == PlayModeStateChange.ExitingEditMode) 85 | { 86 | Stop(); 87 | } 88 | } 89 | 90 | private void Stop() 91 | { 92 | if (context == null) 93 | { 94 | return; 95 | } 96 | 97 | EditorApplication.update -= Step; 98 | EditorApplication.playModeStateChanged -= OnPlayModeStateChange; 99 | AssemblyReloadEvents.beforeAssemblyReload -= OnBeforeAssemblyReload; 100 | 101 | context.OnDestroy(); 102 | 103 | lock (_lock) 104 | { 105 | if (instance == this) 106 | { 107 | instance = null; 108 | } 109 | } 110 | 111 | context = null; 112 | } 113 | 114 | private void Step() 115 | { 116 | Assert.IsNotNull(context); 117 | 118 | context.Update(); 119 | context.LateUpdate(); 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /Editor/EditorRoutineManager.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 68c0a28cfd91f15448bb34d5c82e4ded 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /IRoutineContext.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | using UnityEngine; 4 | 5 | namespace Routines 6 | { 7 | public partial interface IRoutineContext 8 | { 9 | Routine.Handle RunRoutine(IEnumerator routine, System.Action onStop = null); 10 | IEnumerator WaitForNextFrame(); 11 | IEnumerator WaitForSeconds(float seconds); 12 | IEnumerator WaitUntil(System.Func condition); 13 | IEnumerator WaitForAsyncOperation(AsyncOperation asyncOperation, System.Action onProgress = null); 14 | IEnumerator WaitForCustomYieldInstruction(CustomYieldInstruction yieldInstruction); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /IRoutineContext.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 8b413170626738d4ca301f0ec323ff2c 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Tom 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /LICENSE.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 78c7a54bef7576b48946f7a24174b86c 3 | DefaultImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Routines for Unity 2 | 3 | _If you're using Unity 2018.3 or newer, check out [Unity AsyncRoutines](https://github.com/tomblind/unity-async-routines) which accomplishes the same thing, but uses C# 7's async/await for cleaner and more type-safe syntax._ 4 | 5 | ## What Is It? 6 | Routines is a replacement for Unity's coroutines that provides hierarchical support. This means that a running routine can yield on one or more child routines and resume when they complete. Routines also utilize pooling under the hood to reduce garbage generation as much as possible so that they can used extensively without dropping frames. 7 | 8 | ## Basic Usage 9 | Normally, you want to use a RoutineManager component to manage routines on a GameObject. This will handle all of the details of a routine's lifetime, including stopping it if the object is destroyed. 10 | 11 | ### Simple Example 12 | ```cs 13 | public class MyBehavior : MonoBehaviour 14 | { 15 | public RoutineManager routineMgr; 16 | 17 | public void Start() 18 | { 19 | routineMgr = gameObject.AddComponent(); 20 | routineMgr.RunRoutine(MainRoutine()); 21 | } 22 | 23 | public IEnumerator MainRoutine() 24 | { 25 | for (var i = 0; i < 5; ++i) 26 | { 27 | yield return ChildRoutine(i); 28 | var result = Routine.GetResult(); 29 | Debug.Log(result); 30 | } 31 | } 32 | 33 | public IEnumerator ChildRoutine(int i) 34 | { 35 | yield return routineMgr.WaitForSeconds(1); 36 | Routine.SetResult(i * 2); 37 | } 38 | } 39 | ``` 40 | 41 | Here we add a RoutineManager and run a simple routine that loops from 0 to 4. It then calls a child which waits for a second and returns the number multiplied by 2, which is received by the parent and sent to the console. 42 | 43 | *Protip: You can also directly derive your behavior from RoutineManager to get routine support built in to your component.* 44 | 45 | ### Mutliple Children 46 | Here you can see multiple routines being yielded at once. 47 | 48 | ```cs 49 | public IEnumerator DoFirstThing() 50 | { 51 | yield return routineMgr.WaitForSeconds(1); 52 | Routine.SetResult(1); 53 | } 54 | 55 | public IEnumerator DoSecondThing() 56 | { 57 | yield return routineMgr.WaitForSeconds(2); 58 | Routine.SetResult(2); 59 | } 60 | 61 | public IEnumerator DoAllTheThingsInOrder() 62 | { 63 | yield return new IEnumerator[] {DoFirstThing(), DoSecondThing()}; 64 | Debug.Log(Routine.GetResult()); // 2 65 | } 66 | 67 | public IEnumerator DoAllTheThingsAtOnce() 68 | { 69 | yield return Routine.All(DoFirstThing(), DoSecondThing()); 70 | Debug.Log(Routine.GetAllResults()); // List {1, 2} 71 | } 72 | 73 | public IEnumerator DoAnyOfTheThings() 74 | { 75 | yield return Routine.Any(DoFirstThing(), DoSecondThing()); 76 | Debug.Log(Routine.GetResult()); // 1 77 | } 78 | ``` 79 | DoAllTheThingsInOrder() yields an array (it could be any IEnumerable) which causes the system to execute them in sequence - waiting for each child to finish before starting the next. The result will be whatever the last child set as a result. 80 | 81 | DoAllTheThingsAtOnce() uses Routine.All() to specify a set of children which should be executed in parallel. The parent routine will resume once all children have finished and the result will be a List containing the results of each child; 82 | 83 | DoAnyOfTheThings() uses Routine.Any(). This is similar to Routine.All() except that it will resume the parent as soon as any of the children finish (stopping the rest). Its result will be from whichever child finished. Determining which one that was is an exercise left to the user. 84 | 85 | ### Helpers 86 | RoutineManager also provides a number of useful WaitFor...() methods that can be yielded. These are replacements for the built-in Unity YieldInstructions like WaitForSeconds. 87 | - WaitForNextFrame() 88 | - WaitForSeconds() 89 | - WaitUntil() 90 | - WaitForAsyncOperation() 91 | - WaitForCustomYieldInstruction() //Can be used to yield on Unity CustomYieldInstructions, including WWW 92 | 93 | ## Advanced Usage 94 | ### Stopping Routines 95 | RunRoutine() returns a handle to the routine which has a Stop() method. This can be used to stop a routine before it has completed. RoutineManager also has a StopAllRoutines() which stops all routines currently managed by the component. 96 | 97 | ### Managing Your Own Routines 98 | In some circumstances, it might be ideal to manage the lifecycle of a routine youself. 99 | ```cs 100 | public class MyBehavior : MonoBehaviour 101 | { 102 | public Routine r = null; 103 | 104 | public void Start() 105 | { 106 | r = Routine.Create(); 107 | r.Start(MyRoutine()); 108 | } 109 | 110 | public void Update() 111 | { 112 | if (r != null && r.IsDone) 113 | { 114 | Routine.Release(ref r); //Sets r to null 115 | } 116 | } 117 | 118 | public void ForceStop() 119 | { 120 | if (r != null) 121 | { 122 | r.Stop(); 123 | Routine.Release(ref r); 124 | } 125 | } 126 | 127 | public IEnumerator MyRoutine() 128 | { 129 | ... 130 | } 131 | } 132 | ``` 133 | Routines are pooled and cannot be constructed directly, so use Create() and be sure to call Release() when done with them. 134 | 135 | ### Error Handling 136 | By default, routines catch exceptions thrown and report them using Debug.LogException(). The routine will be stopped, but not interfere with execution of the rest of the game. You can supply a custom exception handling callback to Start() if you desire other behavior. 137 | 138 | ### Custom Resumables 139 | In addition to IEnumerator methods, objects implementing IResumable can be yielded as well. These objects should have a Run() method which is called as soon as it is yielded and receives an IResumer. This can be used to resume the routine that yielded the object. This is useful for when a routine needs to wait for a callback (such as a Unity animation event). 140 | ```cs 141 | public class MyBehavior : MonoBehaviour 142 | { 143 | public Animator animator; 144 | public SimpleResumable resumable = new SimpleResumable(); 145 | 146 | public IEnumerator TriggerAnimationAndWait(string trigger) 147 | { 148 | animator.SetTrigger(trigger); 149 | yield return resumable; 150 | Debug.Log("Animation finished!"); 151 | } 152 | 153 | public void OnAnimationEvent() 154 | { 155 | resumable.Resume(); 156 | } 157 | } 158 | ``` 159 | This example uses SimpleResumable, which catches the IResumer and calls it with Resume(). TriggerAnimationAndWait() sends a trigger to an animator and waits for OnAnimationEvent() to be called. In Unity, an event is set to call that method when the triggered animation finishes. 160 | 161 | ## Gotchas 162 | - You cannot yield null to wait for the next frame. Use WaitForNextFrame() on RoutineManager. 163 | - Routines have a hard limit on the number of times they can be stepped without yielding something that will wait. This is to prevent Unity from locking up if an inifinite loop is encountered. If you need to iterate more than that limit, you'll have to change maxIterations in Routine.cs and may the powers that be have mercy upon your soul. 164 | - You should always call GetResult() immediately after the yield statement that ran the routine you want the result from. Internally it's stored in a static variable, so it could be replaced if some code runs a new routine before you call it. 165 | - It is safe to yield on methods from other objects than the one with the RoutineManager, but you should make sure those objects aren't destroyed before the routine ends! 166 | - Routine.All() and Routine.Any() cannot be nested. If you need to do this, wrap them in their own IEnumerator functions: 167 | ```cs 168 | public IEnumerator DoTheThings() 169 | { 170 | yield return Routine.All(...); 171 | } 172 | 173 | public IEnumerator DoTheOtherThings() 174 | { 175 | yield return Routine.All(...); 176 | } 177 | 178 | public IEnumerator DoAllTheThings() 179 | { 180 | yield return Routine.All(DoTheThings(), DoTheOtherThings()); 181 | } 182 | ``` 183 | -------------------------------------------------------------------------------- /README.md.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 1e8f9be845ba9b248b15e96ecf49c1fb 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /Resumable.cs: -------------------------------------------------------------------------------- 1 | //MIT License 2 | // 3 | //Copyright (c) 2018 Tom Blind 4 | // 5 | //Permission is hereby granted, free of charge, to any person obtaining a copy 6 | //of this software and associated documentation files (the "Software"), to deal 7 | //in the Software without restriction, including without limitation the rights 8 | //to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | //copies of the Software, and to permit persons to whom the Software is 10 | //furnished to do so, subject to the following conditions: 11 | // 12 | //The above copyright notice and this permission notice shall be included in all 13 | //copies or substantial portions of the Software. 14 | // 15 | //THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | //IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | //FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | //AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | //LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | //OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | //SOFTWARE. 22 | using System.Collections; 23 | using UnityEngine.Assertions; 24 | 25 | namespace Routines 26 | { 27 | //Passed to IResumables to allow control of the routine 28 | public interface IResumer 29 | { 30 | //Check if the routine is still alive. If not, Release() should be called. 31 | bool IsAlive { get; } 32 | 33 | //Resume the routine and optionally pass a result value back to it. 34 | void Resume(object result = null); 35 | 36 | //Stop routine and propagate error 37 | void Throw(System.Exception error); 38 | 39 | //Call to release the IResumer object. Only needs to be used if IsAlive is false. 40 | void Release(); 41 | } 42 | 43 | //Objects that can be yielded instead of IEnumerators: 44 | //When yielded, Run() will be called with an object that can 45 | //be used to resume the coroutine and set a result value. 46 | public interface IResumable : IEnumerator 47 | { 48 | void Run(IResumer resumer); 49 | } 50 | 51 | //Resumables are 'fake' IEnumerators (they won't work with foreach), simply so they can 52 | //be passed around easily. If c# had sum-types/unions, this wouldn't be necessary. 53 | public abstract class Resumable : IResumable 54 | { 55 | public abstract void Run(IResumer resumer); 56 | 57 | public virtual object Current { get { throw new System.NotSupportedException(); } } 58 | public virtual bool MoveNext() { throw new System.NotSupportedException(); } 59 | public virtual void Reset() { throw new System.NotSupportedException(); } 60 | } 61 | 62 | //A simple, general purpose resumer 63 | public class SimpleResumable : Resumable 64 | { 65 | public IResumer resumer = null; 66 | 67 | public override void Run(IResumer resumer) 68 | { 69 | Assert.IsNull(this.resumer); 70 | this.resumer = resumer; 71 | } 72 | 73 | public void Resume(object result = null) 74 | { 75 | Assert.IsNotNull(resumer); 76 | resumer.Resume(result); 77 | resumer = null; 78 | } 79 | 80 | public void Throw(System.Exception error) 81 | { 82 | Assert.IsNotNull(resumer); 83 | resumer.Throw(error); 84 | resumer = null; 85 | } 86 | 87 | public override void Reset() 88 | { 89 | resumer = null; 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /Resumable.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: e7d9b2b2388530c4c99802c75dc85217 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Routine.cs: -------------------------------------------------------------------------------- 1 | //MIT License 2 | // 3 | //Copyright (c) 2018 Tom Blind 4 | // 5 | //Permission is hereby granted, free of charge, to any person obtaining a copy 6 | //of this software and associated documentation files (the "Software"), to deal 7 | //in the Software without restriction, including without limitation the rights 8 | //to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | //copies of the Software, and to permit persons to whom the Software is 10 | //furnished to do so, subject to the following conditions: 11 | // 12 | //The above copyright notice and this permission notice shall be included in all 13 | //copies or substantial portions of the Software. 14 | // 15 | //THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | //IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | //FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | //AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | //LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | //OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | //SOFTWARE. 22 | using System.Collections; 23 | using System.Collections.Generic; 24 | using UnityEngine; 25 | using UnityEngine.Assertions; 26 | 27 | namespace Routines 28 | { 29 | public class Routine 30 | { 31 | public delegate void ExceptionHandlerDelegate(System.Exception e, Object context = null); 32 | 33 | public interface IWrappedArray {} 34 | 35 | //Use to safely perform actions on a routine 36 | public struct Handle 37 | { 38 | private Routine routine; 39 | private ulong id; 40 | 41 | public bool IsDone 42 | { 43 | get 44 | { 45 | return (routine == null || id != routine.id || routine.IsDone); 46 | } 47 | } 48 | 49 | public Routine Get() 50 | { 51 | return IsDone ? null : routine; 52 | } 53 | 54 | public Handle(Routine routine, ulong id) 55 | { 56 | this.routine = routine; 57 | this.id = id; 58 | } 59 | 60 | public void Stop() 61 | { 62 | if (routine != null && id == routine.id) 63 | { 64 | routine.Stop(); 65 | } 66 | } 67 | } 68 | 69 | private class WrappedArray : IWrappedArray 70 | { 71 | public IEnumerable yieldables; 72 | public bool isAny; 73 | } 74 | 75 | private class RoutineException: System.Exception 76 | { 77 | public RoutineException() {} 78 | public RoutineException(string message) : base(message) {} 79 | public RoutineException(string message, System.Exception inner) : base(message, inner) {} 80 | } 81 | 82 | private class Resumer : IResumer 83 | { 84 | public ulong steppingId; 85 | public Routine routine; 86 | public bool called; 87 | 88 | public bool IsAlive 89 | { 90 | get 91 | { 92 | return (steppingId != noId && steppingId == routine.id); 93 | } 94 | } 95 | 96 | public void Resume(object result) 97 | { 98 | Assert.IsFalse(called); 99 | called = true; 100 | if (steppingId == routine.id) 101 | { 102 | routine.returnValue = result; 103 | routine.Finish(null); 104 | } 105 | Release(); 106 | } 107 | 108 | public void Throw(System.Exception error) 109 | { 110 | Assert.IsFalse(called); 111 | called = true; 112 | if (steppingId == routine.id) 113 | { 114 | routine.Finish(routine.CreateException(error.Message, error)); 115 | } 116 | Release(); 117 | } 118 | 119 | public void Release() 120 | { 121 | resumerPool.Push(this); 122 | } 123 | 124 | public void Reset() 125 | { 126 | steppingId = noId; 127 | routine = null; 128 | called = false; 129 | } 130 | } 131 | 132 | private class ImmediateResumable : Resumable 133 | { 134 | public override void Run(IResumer resumer) 135 | { 136 | resumer.Resume(); 137 | } 138 | } 139 | 140 | private const ulong noId = ulong.MaxValue; 141 | private const int maxIterations = 999999; //Maximum times a routine will step without yielding (to prevent lockup from infinite loops) 142 | 143 | private ulong id = noId; 144 | private IEnumerator enumerator = null; 145 | private Routine parent = null; 146 | private Object context = null; 147 | private ExceptionHandlerDelegate exceptionHandler = null; 148 | private List children = new List(); 149 | private List yieldedArray = new List(); 150 | private bool arrayWasYielded = false; 151 | private bool finishOnAny = false; 152 | private bool isStepping = false; 153 | private object returnValue = null; 154 | private RoutineException error = null; 155 | private System.Action onStop; 156 | 157 | private static Stack pool = new Stack(); 158 | 159 | private static IResumable resumeImmediately = new ImmediateResumable(); 160 | private static ulong nextId = 0; 161 | private static Stack resumerPool = new Stack(); 162 | private static Stack wrappedArrayPool = new Stack(); 163 | private static Stack steppingStack = new Stack(); 164 | private static object currentReturnValue = null; 165 | private static List currentAllReturnValues = new List(); 166 | 167 | public bool IsDone { get { return id == noId; } } 168 | public System.Exception Error { get { return error; } } 169 | 170 | public void Start(IEnumerator enumerator, Object context = null, ExceptionHandlerDelegate exceptionHandler = null, System.Action onStop = null) 171 | { 172 | Stop(); 173 | Setup(enumerator, null, context, exceptionHandler, onStop); 174 | Step(); 175 | } 176 | 177 | public void Start(IEnumerable enumerable, Object context = null, ExceptionHandlerDelegate exceptionHandler = null, System.Action onStop = null) 178 | { 179 | Stop(); 180 | Setup(enumerable.GetEnumerator(), null, context, exceptionHandler, onStop); 181 | Step(); 182 | } 183 | 184 | public void Stop() 185 | { 186 | ClearChildren(); 187 | yieldedArray.Clear(); 188 | arrayWasYielded = false; 189 | finishOnAny = false; 190 | isStepping = false; 191 | id = noId; 192 | 193 | var oldOnStop = onStop; 194 | onStop = null; 195 | 196 | if (oldOnStop != null) 197 | { 198 | oldOnStop(); 199 | } 200 | } 201 | 202 | public void Reset() 203 | { 204 | Stop(); 205 | enumerator = null; 206 | parent = null; 207 | returnValue = null; 208 | error = null; 209 | context = null; 210 | exceptionHandler = null; 211 | } 212 | 213 | public Handle GetHandle() 214 | { 215 | return new Handle(this, id); 216 | } 217 | 218 | //Get new routine from pool 219 | public static Routine Create() 220 | { 221 | return (pool.Count > 0) ? pool.Pop() : new Routine(); 222 | } 223 | 224 | //Release routine back to pool 225 | public static void Release(ref Routine routine) 226 | { 227 | routine.Reset(); 228 | pool.Push(routine); 229 | routine = null; 230 | } 231 | 232 | //Retrieve the result of the last finished routine 233 | public static T GetResult() 234 | { 235 | return (T)currentReturnValue; 236 | } 237 | 238 | public static object GetResult() 239 | { 240 | return currentReturnValue; 241 | } 242 | 243 | public static List GetAllResults() 244 | { 245 | return currentAllReturnValues; 246 | } 247 | 248 | //Set the result of the currently stepping routine 249 | public static void SetResult(object value) 250 | { 251 | Assert.IsTrue(steppingStack.Count > 0); 252 | steppingStack.Peek().returnValue = value; 253 | } 254 | 255 | //Wrap multiple enumerators to be yielded. 256 | //Routine won't resume until all have finished. 257 | //Result will be a List continaing results from all routines. DO NOT alter that list. 258 | public static IWrappedArray All(params IEnumerator[] yieldables) 259 | { 260 | var allArray = (wrappedArrayPool.Count > 0) ? wrappedArrayPool.Pop() : new WrappedArray(); 261 | allArray.yieldables = yieldables; 262 | allArray.isAny = false; 263 | return allArray; 264 | } 265 | 266 | public static IWrappedArray All(IEnumerable yieldables) 267 | { 268 | var allArray = (wrappedArrayPool.Count > 0) ? wrappedArrayPool.Pop() : new WrappedArray(); 269 | allArray.yieldables = yieldables; 270 | allArray.isAny = false; 271 | return allArray; 272 | } 273 | 274 | //Wrap multiple enumerators to be yielded. 275 | //Routine will resume when first one finishes. 276 | //Result will be value from that first finished routine. 277 | public static IWrappedArray Any(params IEnumerator[] yieldables) 278 | { 279 | var anyArray = (wrappedArrayPool.Count > 0) ? wrappedArrayPool.Pop() : new WrappedArray(); 280 | anyArray.yieldables = yieldables; 281 | anyArray.isAny = true; 282 | return anyArray; 283 | } 284 | 285 | public static IWrappedArray Any(IEnumerable yieldables) 286 | { 287 | var anyArray = (wrappedArrayPool.Count > 0) ? wrappedArrayPool.Pop() : new WrappedArray(); 288 | anyArray.yieldables = yieldables; 289 | anyArray.isAny = true; 290 | return anyArray; 291 | } 292 | 293 | //Dummy resumer that resumes immediately upon yielding 294 | public static IEnumerator ResumeImmediately() 295 | { 296 | return resumeImmediately; 297 | } 298 | 299 | private Routine() {} //Use Routine.Create() 300 | 301 | private void Setup(IEnumerator enumerator, Routine parent, Object context, ExceptionHandlerDelegate exceptionHandler, System.Action onStop = null) 302 | { 303 | id = nextId++; 304 | Assert.IsTrue(nextId != ulong.MaxValue); 305 | returnValue = null; 306 | if (parent != null) 307 | { 308 | this.parent = parent; 309 | } 310 | this.enumerator = enumerator; 311 | this.context = context; 312 | this.exceptionHandler = exceptionHandler ?? Debug.LogException; 313 | this.onStop = onStop; 314 | } 315 | 316 | private void ClearChildren() 317 | { 318 | foreach (var child in children) 319 | { 320 | child.Reset(); 321 | pool.Push(child); 322 | } 323 | children.Clear(); 324 | } 325 | 326 | private void Finish(RoutineException error) 327 | { 328 | this.error = error; 329 | Stop(); 330 | if (parent != null && !parent.isStepping) 331 | { 332 | parent.Step(); 333 | } 334 | } 335 | 336 | private void Step() 337 | { 338 | Assert.IsTrue(!isStepping); 339 | isStepping = true; 340 | 341 | //Used to detect that routine was killed during step 342 | var steppingId = id; 343 | 344 | //Running IResumable 345 | if (enumerator is IResumable) 346 | { 347 | var resumer = (resumerPool.Count > 0) ? resumerPool.Pop() : new Resumer(); 348 | resumer.called = false; 349 | resumer.routine = this; 350 | resumer.steppingId = steppingId; 351 | 352 | var resumable = enumerator as IResumable; 353 | resumable.Run(resumer); 354 | 355 | //In case of suicide or immediate finish 356 | if (steppingId != id) 357 | { 358 | return; 359 | } 360 | } 361 | 362 | //Running enumerator 363 | else 364 | { 365 | int itCount; 366 | for (itCount = 0; itCount < Routine.maxIterations; ++itCount) 367 | { 368 | currentReturnValue = null; 369 | currentAllReturnValues.Clear(); 370 | 371 | //Bail out if any children are yielding: 372 | 373 | //Any array: stop if all children are yielding 374 | if (finishOnAny) 375 | { 376 | var childIsDone = true; 377 | for (int i = 0, l = children.Count; i < l; ++i) 378 | { 379 | var child = children[i]; 380 | 381 | if (child.error != null) 382 | { 383 | Finish(child.error); 384 | return; 385 | } 386 | 387 | childIsDone = child.IsDone; 388 | if (childIsDone) 389 | { 390 | currentReturnValue = child.returnValue; 391 | break; 392 | } 393 | } 394 | if (!childIsDone) 395 | { 396 | break; 397 | } 398 | } 399 | 400 | //Single or all-array: stop if any children are yielding 401 | else 402 | { 403 | var childIsYielding = false; 404 | for (int i = 0, l = children.Count; i < l; ++i) 405 | { 406 | var child = children[i]; 407 | 408 | if (child.error != null) 409 | { 410 | Finish(child.error); 411 | return; 412 | } 413 | 414 | childIsYielding = !child.IsDone; 415 | if (childIsYielding) 416 | { 417 | break; 418 | } 419 | } 420 | if (childIsYielding) 421 | { 422 | break; 423 | } 424 | 425 | //Collect return values from children 426 | if (arrayWasYielded) 427 | { 428 | for (int i = 0, l = children.Count; i < l; ++i) 429 | { 430 | currentAllReturnValues.Add(children[i].returnValue); 431 | } 432 | currentReturnValue = currentAllReturnValues; 433 | } 434 | else if (children.Count > 0) 435 | { 436 | Assert.IsTrue(children.Count == 1); 437 | currentReturnValue = children[0].returnValue; 438 | } 439 | } 440 | 441 | //Stop and clear children 442 | ClearChildren(); 443 | 444 | //Step this routine 445 | steppingStack.Push(this); 446 | bool done; 447 | RoutineException stepError = null; 448 | try 449 | { 450 | done = !enumerator.MoveNext(); 451 | } 452 | catch (System.Exception e) 453 | { 454 | stepError = CreateException(e.Message, e); 455 | exceptionHandler(stepError, context); 456 | done = true; 457 | } 458 | steppingStack.Pop(); 459 | 460 | //Check for suicide 461 | if (steppingId != id) 462 | { 463 | return; 464 | } 465 | 466 | //Prevent GetResult() from giving back something when it shouldn't 467 | currentReturnValue = null; 468 | currentAllReturnValues.Clear(); 469 | 470 | //Routine finished? 471 | if (done) 472 | { 473 | Finish(stepError); 474 | return; 475 | } 476 | 477 | arrayWasYielded = false; 478 | finishOnAny = false; 479 | 480 | //Check for yielded array 481 | var result = enumerator.Current; 482 | if (result is WrappedArray) 483 | { 484 | var wrappedArray = result as WrappedArray; 485 | var arr = wrappedArray.yieldables; 486 | finishOnAny = wrappedArray.isAny; 487 | wrappedArrayPool.Push(wrappedArray); 488 | 489 | if (arr == null) 490 | { 491 | Finish(CreateException("yieldables not set in WrappedArray")); 492 | return; 493 | } 494 | 495 | arrayWasYielded = true; 496 | 497 | //Copy array contents in case one of contained routines messes with it when stepped 498 | Assert.IsTrue(yieldedArray.Count == 0); 499 | foreach (var element in arr) 500 | { 501 | yieldedArray.Add(element); 502 | } 503 | 504 | for (int i = 0, l = yieldedArray.Count; i < l; ++i) 505 | { 506 | var yieldedValue = yieldedArray[i]; 507 | if (yieldedValue is IEnumerable) 508 | { 509 | yieldedValue = (yieldedValue as IEnumerable).GetEnumerator(); 510 | } 511 | else if (!(yieldedValue is IEnumerator)) 512 | { 513 | Finish(CreateException(string.Format("yielded value [{0}] is not an IEnumerator: {1}", i, yieldedValue))); 514 | return; 515 | } 516 | 517 | var child = CreateChild(yieldedValue as IEnumerator); 518 | 519 | //Check for parenticide 520 | if (steppingId != id) 521 | { 522 | return; 523 | } 524 | 525 | //Exit if any child completes in any-array mode 526 | if (finishOnAny && child.IsDone) 527 | { 528 | break; 529 | } 530 | } 531 | 532 | yieldedArray.Clear(); 533 | } 534 | 535 | //Single runable yielded: create child routine 536 | else if (result is IEnumerable) //Check for IEnumerable before IEnumerator, as the object could be both (Linq) and we want to treat it as the former if so 537 | { 538 | CreateChild((result as IEnumerable).GetEnumerator()); 539 | 540 | //Check for parenticide 541 | if (steppingId != id) 542 | { 543 | return; 544 | } 545 | } 546 | else if (result is IEnumerator) 547 | { 548 | CreateChild(result as IEnumerator); 549 | 550 | //Check for parenticide 551 | if (steppingId != id) 552 | { 553 | return; 554 | } 555 | } 556 | 557 | //Something not-runable was returned 558 | else 559 | { 560 | throw CreateException(string.Format("yielded value is not an IEnumerator or IEnumerable: {0}", result)); 561 | } 562 | } 563 | if (itCount == maxIterations) 564 | { 565 | throw CreateException("Infinite loop in routine!"); 566 | } 567 | } 568 | 569 | isStepping = false; 570 | } 571 | 572 | private Routine CreateChild(IEnumerator enumerator) 573 | { 574 | var child = Create(); 575 | child.Setup(enumerator, this, context, exceptionHandler); 576 | children.Add(child); 577 | child.Step(); 578 | return child; 579 | } 580 | 581 | private RoutineException CreateException(string message, System.Exception inner = null) 582 | { 583 | var stack = new List(); 584 | stack.Add(message); 585 | stack.Add("Routine stack:"); 586 | var routine = this; 587 | do 588 | { 589 | stack.Add(string.Format("{0}] {1}", stack.Count - 2, routine.enumerator.ToString())); 590 | routine = routine.parent; 591 | } 592 | while (routine != null); 593 | message = string.Join("\n", stack.ToArray()); 594 | return (inner != null) ? new RoutineException(message, inner) : new RoutineException(message); 595 | } 596 | } 597 | } 598 | -------------------------------------------------------------------------------- /Routine.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: adf9af5d1858a5e4eb582f202b980285 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /RoutineContext.cs: -------------------------------------------------------------------------------- 1 | //MIT License 2 | // 3 | //Copyright (c) 2018 Tom Blind 4 | // 5 | //Permission is hereby granted, free of charge, to any person obtaining a copy 6 | //of this software and associated documentation files (the "Software"), to deal 7 | //in the Software without restriction, including without limitation the rights 8 | //to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | //copies of the Software, and to permit persons to whom the Software is 10 | //furnished to do so, subject to the following conditions: 11 | // 12 | //The above copyright notice and this permission notice shall be included in all 13 | //copies or substantial portions of the Software. 14 | // 15 | //THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | //IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | //FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | //AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | //LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | //OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | //SOFTWARE. 22 | using System.Collections; 23 | using System.Collections.Generic; 24 | using UnityEngine; 25 | 26 | namespace Routines 27 | { 28 | public class RoutineContext : IRoutineContext 29 | { 30 | private class NextFrameResumable : Resumable 31 | { 32 | public List newResumers = new List(); 33 | public List resumers = new List(); 34 | 35 | public override void Run(IResumer resumer) 36 | { 37 | newResumers.Add(resumer); 38 | } 39 | 40 | public void Resume() 41 | { 42 | foreach (var resumer in resumers) 43 | { 44 | resumer.Resume(); 45 | } 46 | resumers.Clear(); 47 | } 48 | 49 | public void Flush() 50 | { 51 | resumers.AddRange(newResumers); 52 | newResumers.Clear(); 53 | } 54 | } 55 | 56 | private NextFrameResumable nextFrameResumable = new NextFrameResumable(); 57 | private List activeRoutines = new List(); 58 | 59 | public void Update() 60 | { 61 | for (var i = 0; i < activeRoutines.Count;) 62 | { 63 | var routine = activeRoutines[i]; 64 | if (routine.IsDone) 65 | { 66 | Routine.Release(ref routine); 67 | activeRoutines.RemoveAt(i); 68 | } 69 | else 70 | { 71 | ++i; 72 | } 73 | } 74 | 75 | nextFrameResumable.Resume(); 76 | } 77 | 78 | public void LateUpdate() 79 | { 80 | nextFrameResumable.Flush(); 81 | } 82 | 83 | public void OnDestroy() 84 | { 85 | StopAllRoutines(); 86 | } 87 | 88 | public Routine.Handle RunRoutine(IEnumerator enumerator, System.Action onStop = null) 89 | { 90 | return RunRoutine(enumerator, null); 91 | } 92 | 93 | public Routine.Handle RunRoutine(IEnumerator enumerator, Object context, System.Action onStop = null) 94 | { 95 | var routine = Routine.Create(); 96 | routine.Start(enumerator, context, null, onStop); 97 | activeRoutines.Add(routine); 98 | return routine.GetHandle(); 99 | } 100 | 101 | public void StopAllRoutines() 102 | { 103 | for (var i = 0; i < activeRoutines.Count; ++i) 104 | { 105 | var routine = activeRoutines[i]; 106 | Routine.Release(ref routine); 107 | } 108 | activeRoutines.Clear(); 109 | } 110 | 111 | public IEnumerator WaitForNextFrame() 112 | { 113 | return nextFrameResumable; 114 | } 115 | 116 | public IEnumerator WaitForSeconds(float seconds) 117 | { 118 | while (seconds > 0.0f) 119 | { 120 | yield return nextFrameResumable; 121 | seconds -= Time.deltaTime; 122 | } 123 | } 124 | 125 | public IEnumerator WaitUntil(System.Func condition) 126 | { 127 | while (!condition()) 128 | { 129 | yield return nextFrameResumable; 130 | } 131 | } 132 | 133 | public IEnumerator WaitForAsyncOperation(AsyncOperation asyncOperation, System.Action onProgress = null) 134 | { 135 | var lastProgress = (onProgress != null) ? asyncOperation.progress : float.MaxValue; 136 | while (!asyncOperation.isDone) 137 | { 138 | yield return nextFrameResumable; 139 | 140 | if (asyncOperation.progress > lastProgress) 141 | { 142 | lastProgress = asyncOperation.progress; 143 | onProgress(lastProgress); 144 | } 145 | } 146 | } 147 | 148 | public IEnumerator WaitForCustomYieldInstruction(CustomYieldInstruction yieldInstruction) 149 | { 150 | while (yieldInstruction.keepWaiting) 151 | { 152 | yield return nextFrameResumable; 153 | } 154 | } 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /RoutineContext.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: c202f941a372d324aa831a48509a8159 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /RoutineManager.cs: -------------------------------------------------------------------------------- 1 | //MIT License 2 | // 3 | //Copyright (c) 2018 Tom Blind 4 | // 5 | //Permission is hereby granted, free of charge, to any person obtaining a copy 6 | //of this software and associated documentation files (the "Software"), to deal 7 | //in the Software without restriction, including without limitation the rights 8 | //to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | //copies of the Software, and to permit persons to whom the Software is 10 | //furnished to do so, subject to the following conditions: 11 | // 12 | //The above copyright notice and this permission notice shall be included in all 13 | //copies or substantial portions of the Software. 14 | // 15 | //THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | //IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | //FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | //AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | //LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | //OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | //SOFTWARE. 22 | using System.Collections; 23 | using System.Collections.Generic; 24 | using UnityEngine; 25 | 26 | namespace Routines 27 | { 28 | public class RoutineManager : MonoBehaviour, IRoutineContext 29 | { 30 | private RoutineContext context = new RoutineContext(); 31 | 32 | protected void Update() 33 | { 34 | context.Update(); 35 | } 36 | 37 | protected void LateUpdate() 38 | { 39 | context.LateUpdate(); 40 | } 41 | 42 | public void OnDestroy() 43 | { 44 | context.StopAllRoutines(); 45 | } 46 | 47 | public Routine.Handle RunRoutine(IEnumerator enumerator, System.Action onStop = null) 48 | { 49 | return context.RunRoutine(enumerator, this, onStop); 50 | } 51 | 52 | public void StopAllRoutines() 53 | { 54 | context.StopAllRoutines(); 55 | } 56 | 57 | public IEnumerator WaitForNextFrame() 58 | { 59 | return context.WaitForNextFrame(); 60 | } 61 | 62 | public IEnumerator WaitForSeconds(float seconds) 63 | { 64 | return context.WaitForSeconds(seconds); 65 | } 66 | 67 | public IEnumerator WaitUntil(System.Func condition) 68 | { 69 | return context.WaitUntil(condition); 70 | } 71 | 72 | public IEnumerator WaitForAsyncOperation(AsyncOperation asyncOperation, System.Action onProgress = null) 73 | { 74 | return context.WaitForAsyncOperation(asyncOperation, onProgress); 75 | } 76 | 77 | public IEnumerator WaitForCustomYieldInstruction(CustomYieldInstruction yieldInstruction) 78 | { 79 | return context.WaitForCustomYieldInstruction(yieldInstruction); 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /RoutineManager.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 59e49ec2474795842b4c6f8d283b2a19 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: -100 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | --------------------------------------------------------------------------------