| | 1 | | using System; |
| | 2 | | using System.Collections; |
| | 3 | | using System.Collections.Generic; |
| | 4 | | using UnityEngine; |
| | 5 | |
|
| | 6 | | namespace DCL |
| | 7 | | { |
| | 8 | | /// <summary> |
| | 9 | | /// Based on work from: http://JacksonDunstan.com/articles/3718 |
| | 10 | | /// </summary> |
| | 11 | | public static class DCLCoroutineRunner |
| | 12 | | { |
| | 13 | | public static Func<float> realtimeSinceStartup = () => Time.realtimeSinceStartup; |
| | 14 | |
|
| | 15 | | /// <summary> |
| | 16 | | /// Run an iterator function that might throw an exception. Call the callback with the exception |
| | 17 | | /// if it does or null if it finishes without throwing an exception. |
| | 18 | | /// </summary> |
| | 19 | | /// <param name="enumerator">Iterator function to run</param> |
| | 20 | | /// <param name="onException">Callback to call when the iterator has thrown an exception or finished. |
| | 21 | | /// The thrown exception or null is passed as the parameter.</param> |
| | 22 | | /// <param name="timeBudgetCounter">A func that takes elapsed time as parameter, and returns a bool |
| | 23 | | /// indicating if a frame should be skipped or not. Use this in combination with ThrottlingCounter.EvaluateTimeB |
| | 24 | | /// If this is passed as null, no time budget will be used.</param> |
| | 25 | | /// <returns>An enumerator that runs the given enumerator</returns> |
| | 26 | | public static IEnumerator Run( |
| | 27 | | IEnumerator enumerator, |
| | 28 | | Action<Exception> onException, |
| | 29 | | Func<double, bool> timeBudgetCounter = null |
| | 30 | | ) |
| | 31 | | { |
| | 32 | | float currentTime = realtimeSinceStartup(); |
| | 33 | | // The enumerator might yield return enumerators, in which case |
| | 34 | | // we need to enumerate those here rather than yield-returning |
| | 35 | | // them. Otherwise, any exceptions thrown by those "inner enumerators" |
| | 36 | | // would actually escape to an outer level of iteration, outside this |
| | 37 | | // code here, and not be passed to the done callback. |
| | 38 | | // So, this stack holds any inner enumerators. |
| | 39 | | var stack = new Stack<IEnumerator>(); |
| | 40 | | stack.Push(enumerator); |
| | 41 | |
|
| | 42 | | while (stack.Count > 0) |
| | 43 | | { |
| | 44 | | // any inner enumerator will be at the top of the stack |
| | 45 | | // otherwise the original one |
| | 46 | | var currentEnumerator = stack.Peek(); |
| | 47 | | // this is what get "yield returned" in the work enumerator |
| | 48 | | object currentYieldedObject; |
| | 49 | | // the contents of this try block run the work enumerator until |
| | 50 | | // it gets to a yield return statement |
| | 51 | | try |
| | 52 | | { |
| | 53 | | if (currentEnumerator.MoveNext() == false) |
| | 54 | | { |
| | 55 | | // in this case, the enumerator has finished |
| | 56 | | stack.Pop(); |
| | 57 | | // if the stack is empty, then everything has finished, |
| | 58 | | // and the while (stack.Count > 0) will pick it up |
| | 59 | | continue; |
| | 60 | | } |
| | 61 | |
|
| | 62 | | currentYieldedObject = currentEnumerator.Current; |
| | 63 | | } |
| | 64 | | catch (Exception ex) |
| | 65 | | { |
| | 66 | | // this part is the whole point of this method! |
| | 67 | | onException(ex); |
| | 68 | | yield break; |
| | 69 | | } |
| | 70 | |
|
| | 71 | | float newCurrentTime = realtimeSinceStartup(); |
| | 72 | | float elapsedTime = newCurrentTime - currentTime; |
| | 73 | | currentTime = newCurrentTime; |
| | 74 | |
|
| | 75 | | // NOTE: SkipFrameIfDepletedTimeBudget object type is used as a special token here and will not |
| | 76 | | // yield unless the time budget is exceeded for this frame. |
| | 77 | | // |
| | 78 | | // Handling the time budget frame skip for yield return null; calls was also considered. |
| | 79 | | // But this means that yield return null; will no longer skip a frame unless the time budget |
| | 80 | | // is exceeded. If the user wanted to skip the frame explicitly this detail would change |
| | 81 | | // the intended behaviour and introduce bugs. |
| | 82 | | bool handleTimeBudget = timeBudgetCounter != null |
| | 83 | | && currentYieldedObject != null |
| | 84 | | && currentYieldedObject is SkipFrameIfDepletedTimeBudget; |
| | 85 | |
|
| | 86 | | if ( handleTimeBudget ) |
| | 87 | | { |
| | 88 | | if ( timeBudgetCounter( elapsedTime ) ) |
| | 89 | | { |
| | 90 | | yield return null; |
| | 91 | | currentTime = realtimeSinceStartup(); |
| | 92 | | } |
| | 93 | |
|
| | 94 | | continue; |
| | 95 | | } |
| | 96 | |
|
| | 97 | | // in unity you can yield return whatever the hell you want, |
| | 98 | | // so this will pick up whether it's something to enumerate |
| | 99 | | // here, or pass through by yield returning it |
| | 100 | | if (currentYieldedObject is IEnumerator) |
| | 101 | | { |
| | 102 | | stack.Push(currentYieldedObject as IEnumerator); |
| | 103 | | } |
| | 104 | | else |
| | 105 | | { |
| | 106 | | yield return currentYieldedObject; |
| | 107 | | currentTime = realtimeSinceStartup(); |
| | 108 | |
|
| | 109 | | // Force reset of time budget if a frame is skipped on purpose |
| | 110 | | if ( timeBudgetCounter != null ) |
| | 111 | | timeBudgetCounter( double.MaxValue ); |
| | 112 | | } |
| | 113 | | } |
| | 114 | | } |
| | 115 | | } |
| | 116 | |
|
| | 117 | | /// <summary> |
| | 118 | | /// When a coroutine is started with throttling, yielding this object |
| | 119 | | /// will make the frame skip ONLY if the time budget is exceeded. |
| | 120 | | /// |
| | 121 | | /// If the time budget is not exceeded, no frames will be skipped by yielding this object. |
| | 122 | | /// </summary> |
| | 123 | | public class SkipFrameIfDepletedTimeBudget : CustomYieldInstruction |
| | 124 | | { |
| 0 | 125 | | public override bool keepWaiting => false; |
| | 126 | | } |
| | 127 | | } |