├── .github └── FUNDING.yml ├── .gitignore ├── Coroutine.sln ├── Coroutine ├── ActiveCoroutine.cs ├── Coroutine.csproj ├── CoroutineHandler.cs ├── CoroutineHandlerInstance.cs ├── Event.cs └── Wait.cs ├── Example ├── Example.cs └── Example.csproj ├── Jenkinsfile ├── LICENSE.md ├── Logo.png ├── README.md └── Tests ├── EventBasedCoroutineTests.cs ├── Tests.csproj └── TimeBasedCoroutineTests.cs /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: Ellpeck 2 | ko_fi: Ellpeck 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | bin 3 | obj 4 | packages 5 | *.user 6 | *.nupkg 7 | TestResults -------------------------------------------------------------------------------- /Coroutine.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Coroutine", "Coroutine\Coroutine.csproj", "{1657964D-2503-426A-8514-D020660BEE4D}" 4 | EndProject 5 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Example", "Example\Example.csproj", "{8BE6B559-927D-47A6-8253-D7D809D337AF}" 6 | EndProject 7 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests", "Tests\Tests.csproj", "{8E110BC2-38FD-404A-B5BD-02C771B0D1D5}" 8 | EndProject 9 | Global 10 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 11 | Debug|Any CPU = Debug|Any CPU 12 | Release|Any CPU = Release|Any CPU 13 | EndGlobalSection 14 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 15 | {1657964D-2503-426A-8514-D020660BEE4D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 16 | {1657964D-2503-426A-8514-D020660BEE4D}.Debug|Any CPU.Build.0 = Debug|Any CPU 17 | {1657964D-2503-426A-8514-D020660BEE4D}.Release|Any CPU.ActiveCfg = Release|Any CPU 18 | {1657964D-2503-426A-8514-D020660BEE4D}.Release|Any CPU.Build.0 = Release|Any CPU 19 | {8BE6B559-927D-47A6-8253-D7D809D337AF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 20 | {8BE6B559-927D-47A6-8253-D7D809D337AF}.Debug|Any CPU.Build.0 = Debug|Any CPU 21 | {8BE6B559-927D-47A6-8253-D7D809D337AF}.Release|Any CPU.ActiveCfg = Release|Any CPU 22 | {8BE6B559-927D-47A6-8253-D7D809D337AF}.Release|Any CPU.Build.0 = Release|Any CPU 23 | {8E110BC2-38FD-404A-B5BD-02C771B0D1D5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 24 | {8E110BC2-38FD-404A-B5BD-02C771B0D1D5}.Debug|Any CPU.Build.0 = Debug|Any CPU 25 | {8E110BC2-38FD-404A-B5BD-02C771B0D1D5}.Release|Any CPU.ActiveCfg = Release|Any CPU 26 | {8E110BC2-38FD-404A-B5BD-02C771B0D1D5}.Release|Any CPU.Build.0 = Release|Any CPU 27 | EndGlobalSection 28 | EndGlobal 29 | -------------------------------------------------------------------------------- /Coroutine/ActiveCoroutine.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | 5 | namespace Coroutine { 6 | /// 7 | /// A reference to a currently running coroutine. 8 | /// This is returned by . 9 | /// 10 | public class ActiveCoroutine : IComparable { 11 | 12 | private readonly IEnumerator enumerator; 13 | private readonly Stopwatch stopwatch; 14 | private Wait current; 15 | 16 | internal Event Event => this.current.Event; 17 | internal bool IsWaitingForEvent => this.Event != null; 18 | 19 | /// 20 | /// This property stores whether or not this active coroutine is finished. 21 | /// A coroutine is finished if all of its waits have passed, or if it . 22 | /// 23 | public bool IsFinished { get; private set; } 24 | /// 25 | /// This property stores whether or not this active coroutine was cancelled using . 26 | /// 27 | public bool WasCanceled { get; private set; } 28 | /// 29 | /// The total amount of time that took. 30 | /// This is the amount of time that this active coroutine took for the entirety of its "steps", or yield statements. 31 | /// 32 | public TimeSpan TotalMoveNextTime { get; private set; } 33 | /// 34 | /// The total amount of times that was invoked. 35 | /// This is the amount of "steps" in your coroutine, or the amount of yield statements. 36 | /// 37 | public int MoveNextCount { get; private set; } 38 | /// 39 | /// The amount of time that the last took. 40 | /// This is the amount of time that this active coroutine took for the last "step", or yield statement. 41 | /// 42 | public TimeSpan LastMoveNextTime { get; private set; } 43 | 44 | /// 45 | /// An event that gets fired when this active coroutine finishes or gets cancelled. 46 | /// When this event is called, is always true. 47 | /// 48 | public event FinishCallback OnFinished; 49 | /// 50 | /// The name of this coroutine. 51 | /// When not specified on startup of this coroutine, the name defaults to an empty string. 52 | /// 53 | public readonly string Name; 54 | /// 55 | /// The priority of this coroutine. The higher the priority, the earlier it is advanced compared to other coroutines that advance around the same time. 56 | /// When not specified at startup of this coroutine, the priority defaults to 0. 57 | /// 58 | public readonly int Priority; 59 | 60 | internal ActiveCoroutine(IEnumerator enumerator, string name, int priority, Stopwatch stopwatch) { 61 | this.enumerator = enumerator; 62 | this.Name = name; 63 | this.Priority = priority; 64 | this.stopwatch = stopwatch; 65 | } 66 | 67 | /// 68 | /// Cancels this coroutine, causing all subsequent s and any code in between to be skipped. 69 | /// 70 | /// Whether the cancellation was successful, or this coroutine was already cancelled or finished 71 | public bool Cancel() { 72 | if (this.IsFinished || this.WasCanceled) 73 | return false; 74 | this.WasCanceled = true; 75 | this.IsFinished = true; 76 | this.OnFinished?.Invoke(this); 77 | return true; 78 | } 79 | 80 | internal bool Tick(double deltaSeconds) { 81 | if (!this.WasCanceled && this.current.Tick(deltaSeconds)) 82 | this.MoveNext(); 83 | return this.IsFinished; 84 | } 85 | 86 | internal bool OnEvent(Event evt) { 87 | if (!this.WasCanceled && object.Equals(this.current.Event, evt)) 88 | this.MoveNext(); 89 | return this.IsFinished; 90 | } 91 | 92 | internal bool MoveNext() { 93 | this.stopwatch.Restart(); 94 | var result = this.enumerator.MoveNext(); 95 | this.stopwatch.Stop(); 96 | this.LastMoveNextTime = this.stopwatch.Elapsed; 97 | this.TotalMoveNextTime += this.stopwatch.Elapsed; 98 | this.MoveNextCount++; 99 | 100 | if (!result) { 101 | this.IsFinished = true; 102 | this.OnFinished?.Invoke(this); 103 | return false; 104 | } 105 | this.current = this.enumerator.Current; 106 | return true; 107 | } 108 | 109 | /// 110 | /// A delegate method used by . 111 | /// 112 | /// The coroutine that finished 113 | public delegate void FinishCallback(ActiveCoroutine coroutine); 114 | 115 | /// 116 | public int CompareTo(ActiveCoroutine other) { 117 | return other.Priority.CompareTo(this.Priority); 118 | } 119 | 120 | } 121 | } -------------------------------------------------------------------------------- /Coroutine/Coroutine.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | net45;netstandard2.0;net6.0 4 | true 5 | true 6 | 7 | 8 | 9 | Ellpeck 10 | A simple implementation of Unity's Coroutines to be used for any C# project 11 | coroutine utility unity 12 | https://github.com/Ellpeck/Coroutine 13 | https://github.com/Ellpeck/Coroutine 14 | MIT 15 | README.md 16 | Logo.png 17 | 2.1.5 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /Coroutine/CoroutineHandler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace Coroutine { 5 | /// 6 | /// This class can be used for static coroutine handling of any kind. 7 | /// Note that it uses an underlying object for management. 8 | /// 9 | public static class CoroutineHandler { 10 | 11 | private static readonly CoroutineHandlerInstance Instance = new CoroutineHandlerInstance(); 12 | 13 | /// 14 | public static int TickingCount => CoroutineHandler.Instance.TickingCount; 15 | /// 16 | public static int EventCount => CoroutineHandler.Instance.EventCount; 17 | 18 | /// 19 | public static ActiveCoroutine Start(IEnumerable coroutine, string name = "", int priority = 0) { 20 | return CoroutineHandler.Instance.Start(coroutine, name, priority); 21 | } 22 | 23 | /// 24 | public static ActiveCoroutine Start(IEnumerator coroutine, string name = "", int priority = 0) { 25 | return CoroutineHandler.Instance.Start(coroutine, name, priority); 26 | } 27 | 28 | /// 29 | public static ActiveCoroutine InvokeLater(Wait wait, Action action, string name = "", int priority = 0) { 30 | return CoroutineHandler.Instance.InvokeLater(wait, action, name, priority); 31 | } 32 | 33 | /// 34 | public static ActiveCoroutine InvokeLater(Event evt, Action action, string name = "", int priority = 0) { 35 | return CoroutineHandler.Instance.InvokeLater(evt, action, name, priority); 36 | } 37 | 38 | /// 39 | public static void Tick(double deltaSeconds) { 40 | CoroutineHandler.Instance.Tick(deltaSeconds); 41 | } 42 | 43 | /// 44 | public static void Tick(TimeSpan delta) { 45 | CoroutineHandler.Instance.Tick(delta); 46 | } 47 | 48 | /// 49 | public static void RaiseEvent(Event evt) { 50 | CoroutineHandler.Instance.RaiseEvent(evt); 51 | } 52 | 53 | /// 54 | public static IEnumerable GetActiveCoroutines() { 55 | return CoroutineHandler.Instance.GetActiveCoroutines(); 56 | } 57 | 58 | } 59 | } -------------------------------------------------------------------------------- /Coroutine/CoroutineHandlerInstance.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.Linq; 5 | 6 | namespace Coroutine { 7 | /// 8 | /// An object of this class can be used to start, tick and otherwise manage active s as well as their s. 9 | /// Note that a static implementation of this can be found in . 10 | /// 11 | public class CoroutineHandlerInstance { 12 | 13 | private readonly List tickingCoroutines = new List(); 14 | private readonly Dictionary> eventCoroutines = new Dictionary>(); 15 | private readonly HashSet<(Event, ActiveCoroutine)> eventCoroutinesToRemove = new HashSet<(Event, ActiveCoroutine)>(); 16 | private readonly HashSet outstandingEventCoroutines = new HashSet(); 17 | private readonly HashSet outstandingTickingCoroutines = new HashSet(); 18 | private readonly Stopwatch stopwatch = new Stopwatch(); 19 | private readonly object lockObject = new object(); 20 | 21 | /// 22 | /// The amount of instances that are currently waiting for a tick (waiting for time to pass) 23 | /// 24 | public int TickingCount { 25 | get { 26 | lock (this.lockObject) 27 | return this.tickingCoroutines.Count; 28 | } 29 | } 30 | /// 31 | /// The amount of instances that are currently waiting for an 32 | /// 33 | public int EventCount { 34 | get { 35 | lock (this.lockObject) 36 | return this.eventCoroutines.Sum(c => c.Value.Count); 37 | } 38 | } 39 | 40 | /// 41 | /// Starts the given coroutine, returning a object for management. 42 | /// Note that this calls to get the enumerator. 43 | /// 44 | /// The coroutine to start 45 | /// The that this coroutine should have. Defaults to an empty string. 46 | /// The that this coroutine should have. The higher the priority, the earlier it is advanced. Defaults to 0. 47 | /// An active coroutine object representing this coroutine 48 | public ActiveCoroutine Start(IEnumerable coroutine, string name = "", int priority = 0) { 49 | return this.Start(coroutine.GetEnumerator(), name, priority); 50 | } 51 | 52 | /// 53 | /// Starts the given coroutine, returning a object for management. 54 | /// 55 | /// The coroutine to start 56 | /// The that this coroutine should have. Defaults to an empty string. 57 | /// The that this coroutine should have. The higher the priority, the earlier it is advanced compared to other coroutines that advance around the same time. Defaults to 0. 58 | /// An active coroutine object representing this coroutine 59 | public ActiveCoroutine Start(IEnumerator coroutine, string name = "", int priority = 0) { 60 | var inst = new ActiveCoroutine(coroutine, name, priority, this.stopwatch); 61 | if (inst.MoveNext()) { 62 | lock (this.lockObject) 63 | this.GetOutstandingCoroutines(inst.IsWaitingForEvent).Add(inst); 64 | } 65 | return inst; 66 | } 67 | 68 | /// 69 | /// Causes the given action to be invoked after the given . 70 | /// This is equivalent to a coroutine that waits for the given wait and then executes the given . 71 | /// 72 | /// The wait to wait for 73 | /// The action to execute after waiting 74 | /// The that the underlying coroutine should have. Defaults to an empty string. 75 | /// The that the underlying coroutine should have. The higher the priority, the earlier it is advanced compared to other coroutines that advance around the same time. Defaults to 0. 76 | /// An active coroutine object representing this coroutine 77 | public ActiveCoroutine InvokeLater(Wait wait, Action action, string name = "", int priority = 0) { 78 | return this.Start(CoroutineHandlerInstance.InvokeLaterImpl(wait, action), name, priority); 79 | } 80 | 81 | /// 82 | /// Causes the given action to be invoked after the given . 83 | /// This is equivalent to a coroutine that waits for the given wait and then executes the given . 84 | /// 85 | /// The event to wait for 86 | /// The action to execute after waiting 87 | /// The that the underlying coroutine should have. Defaults to an empty string. 88 | /// The that the underlying coroutine should have. The higher the priority, the earlier it is advanced compared to other coroutines that advance around the same time. Defaults to 0. 89 | /// An active coroutine object representing this coroutine 90 | public ActiveCoroutine InvokeLater(Event evt, Action action, string name = "", int priority = 0) { 91 | return this.InvokeLater(new Wait(evt), action, name, priority); 92 | } 93 | 94 | /// 95 | /// Ticks this coroutine handler, causing all time-based s to be ticked. 96 | /// 97 | /// The amount of seconds that have passed since the last time this method was invoked 98 | public void Tick(double deltaSeconds) { 99 | lock (this.lockObject) { 100 | this.MoveOutstandingCoroutines(false); 101 | this.tickingCoroutines.RemoveAll(c => { 102 | if (c.Tick(deltaSeconds)) { 103 | return true; 104 | } else if (c.IsWaitingForEvent) { 105 | this.outstandingEventCoroutines.Add(c); 106 | return true; 107 | } 108 | return false; 109 | }); 110 | } 111 | } 112 | 113 | /// 114 | /// Ticks this coroutine handler, causing all time-based s to be ticked. 115 | /// This is a convenience method that calls , but accepts a instead of an amount of seconds. 116 | /// 117 | /// The time that has passed since the last time this method was invoked 118 | public void Tick(TimeSpan delta) { 119 | this.Tick(delta.TotalSeconds); 120 | } 121 | 122 | /// 123 | /// Raises the given event, causing all event-based s to be updated. 124 | /// 125 | /// The event to raise 126 | public void RaiseEvent(Event evt) { 127 | lock (this.lockObject) { 128 | this.MoveOutstandingCoroutines(true); 129 | var coroutines = this.GetEventCoroutines(evt, false); 130 | if (coroutines != null) { 131 | for (var i = 0; i < coroutines.Count; i++) { 132 | var c = coroutines[i]; 133 | var tup = (c.Event, c); 134 | if (this.eventCoroutinesToRemove.Contains(tup)) 135 | continue; 136 | if (c.OnEvent(evt)) { 137 | this.eventCoroutinesToRemove.Add(tup); 138 | } else if (!c.IsWaitingForEvent) { 139 | this.eventCoroutinesToRemove.Add(tup); 140 | this.outstandingTickingCoroutines.Add(c); 141 | } 142 | } 143 | } 144 | } 145 | } 146 | 147 | /// 148 | /// Returns a list of all currently active objects under this handler. 149 | /// 150 | /// All active coroutines 151 | public IEnumerable GetActiveCoroutines() { 152 | lock (this.lockObject) 153 | return this.tickingCoroutines.Concat(this.eventCoroutines.Values.SelectMany(c => c)); 154 | } 155 | 156 | private void MoveOutstandingCoroutines(bool evt) { 157 | // RemoveWhere is twice as fast as iterating and then clearing 158 | if (this.eventCoroutinesToRemove.Count > 0) { 159 | this.eventCoroutinesToRemove.RemoveWhere(c => { 160 | this.GetEventCoroutines(c.Item1, false).Remove(c.Item2); 161 | return true; 162 | }); 163 | } 164 | var coroutines = this.GetOutstandingCoroutines(evt); 165 | if (coroutines.Count > 0) { 166 | coroutines.RemoveWhere(c => { 167 | var list = evt ? this.GetEventCoroutines(c.Event, true) : this.tickingCoroutines; 168 | var position = list.BinarySearch(c); 169 | list.Insert(position < 0 ? ~position : position, c); 170 | return true; 171 | }); 172 | } 173 | } 174 | 175 | private HashSet GetOutstandingCoroutines(bool evt) { 176 | return evt ? this.outstandingEventCoroutines : this.outstandingTickingCoroutines; 177 | } 178 | 179 | private List GetEventCoroutines(Event evt, bool create) { 180 | if (!this.eventCoroutines.TryGetValue(evt, out var ret) && create) { 181 | ret = new List(); 182 | this.eventCoroutines.Add(evt, ret); 183 | } 184 | return ret; 185 | } 186 | 187 | private static IEnumerator InvokeLaterImpl(Wait wait, Action action) { 188 | yield return wait; 189 | action(); 190 | } 191 | 192 | } 193 | } -------------------------------------------------------------------------------- /Coroutine/Event.cs: -------------------------------------------------------------------------------- 1 | namespace Coroutine { 2 | /// 3 | /// An event is any kind of action that a can listen for. 4 | /// Note that, by default, events don't have a custom implementation. 5 | /// 6 | public class Event { 7 | 8 | } 9 | } -------------------------------------------------------------------------------- /Coroutine/Wait.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Coroutine { 4 | /// 5 | /// Represents either an amount of time, or an that is being waited for by an . 6 | /// 7 | public struct Wait { 8 | 9 | internal readonly Event Event; 10 | private double seconds; 11 | 12 | /// 13 | /// Creates a new wait that waits for the given . 14 | /// 15 | /// The event to wait for 16 | public Wait(Event evt) { 17 | this.Event = evt; 18 | this.seconds = 0; 19 | } 20 | 21 | /// 22 | /// Creates a new wait that waits for the given amount of seconds. 23 | /// 24 | /// The amount of seconds to wait for 25 | public Wait(double seconds) { 26 | this.seconds = seconds; 27 | this.Event = null; 28 | } 29 | 30 | /// 31 | /// Creates a new wait that waits for the given . 32 | /// Note that the exact value may be slightly different, since waits operate in rather than ticks. 33 | /// 34 | /// The time span to wait for 35 | public Wait(TimeSpan time) : this(time.TotalSeconds) { 36 | } 37 | 38 | internal bool Tick(double deltaSeconds) { 39 | this.seconds -= deltaSeconds; 40 | return this.seconds <= 0; 41 | } 42 | 43 | } 44 | } -------------------------------------------------------------------------------- /Example/Example.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading; 4 | using Coroutine; 5 | 6 | namespace Example { 7 | internal static class Example { 8 | 9 | private static readonly Event TestEvent = new Event(); 10 | 11 | public static void Main() { 12 | var seconds = CoroutineHandler.Start(Example.WaitSeconds(), "Awesome Waiting Coroutine"); 13 | CoroutineHandler.Start(Example.PrintEvery10Seconds(seconds)); 14 | 15 | CoroutineHandler.Start(Example.EmptyCoroutine()); 16 | 17 | CoroutineHandler.InvokeLater(new Wait(5), () => { 18 | Console.WriteLine("Raising test event"); 19 | CoroutineHandler.RaiseEvent(Example.TestEvent); 20 | }); 21 | CoroutineHandler.InvokeLater(new Wait(Example.TestEvent), () => Console.WriteLine("Example event received")); 22 | 23 | CoroutineHandler.InvokeLater(new Wait(Example.TestEvent), () => Console.WriteLine("I am invoked after 'Example event received'"), priority: -5); 24 | CoroutineHandler.InvokeLater(new Wait(Example.TestEvent), () => Console.WriteLine("I am invoked before 'Example event received'"), priority: 2); 25 | 26 | var lastTime = DateTime.Now; 27 | while (true) { 28 | var currTime = DateTime.Now; 29 | CoroutineHandler.Tick(currTime - lastTime); 30 | lastTime = currTime; 31 | Thread.Sleep(1); 32 | } 33 | } 34 | 35 | private static IEnumerator WaitSeconds() { 36 | Console.WriteLine("First thing " + DateTime.Now); 37 | yield return new Wait(1); 38 | Console.WriteLine("After 1 second " + DateTime.Now); 39 | yield return new Wait(9); 40 | Console.WriteLine("After 10 seconds " + DateTime.Now); 41 | CoroutineHandler.Start(Example.NestedCoroutine()); 42 | yield return new Wait(5); 43 | Console.WriteLine("After 5 more seconds " + DateTime.Now); 44 | yield return new Wait(10); 45 | Console.WriteLine("After 10 more seconds " + DateTime.Now); 46 | 47 | yield return new Wait(20); 48 | Console.WriteLine("First coroutine done"); 49 | } 50 | 51 | private static IEnumerator PrintEvery10Seconds(ActiveCoroutine first) { 52 | while (true) { 53 | yield return new Wait(10); 54 | Console.WriteLine("The time is " + DateTime.Now); 55 | if (first.IsFinished) { 56 | Console.WriteLine("By the way, the first coroutine has finished!"); 57 | Console.WriteLine($"{first.Name} data: {first.MoveNextCount} moves, " + 58 | $"{first.TotalMoveNextTime.TotalMilliseconds} total time, " + 59 | $"{first.LastMoveNextTime.TotalMilliseconds} last time"); 60 | Environment.Exit(0); 61 | } 62 | } 63 | } 64 | 65 | private static IEnumerator EmptyCoroutine() { 66 | yield break; 67 | } 68 | 69 | private static IEnumerable NestedCoroutine() { 70 | Console.WriteLine("I'm a coroutine that was started from another coroutine!"); 71 | yield return new Wait(5); 72 | Console.WriteLine("It's been 5 seconds since a nested coroutine was started, yay!"); 73 | } 74 | 75 | } 76 | } -------------------------------------------------------------------------------- /Example/Example.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | net6.0 4 | Exe 5 | false 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /Jenkinsfile: -------------------------------------------------------------------------------- 1 | pipeline { 2 | agent any 3 | stages { 4 | stage('Test') { 5 | steps { 6 | sh 'dotnet test --collect:"XPlat Code Coverage"' 7 | } 8 | } 9 | 10 | stage('Pack') { 11 | steps { 12 | sh 'find . -type f -name "*.nupkg" -delete' 13 | sh 'dotnet pack --version-suffix ${BUILD_NUMBER}' 14 | } 15 | } 16 | 17 | stage('Publish') { 18 | when { 19 | branch 'main' 20 | } 21 | steps { 22 | sh 'dotnet nuget push -s http://localhost:5000/v3/index.json **/*.nupkg -k $BAGET -n' 23 | } 24 | } 25 | } 26 | post { 27 | always { 28 | nunit testResultsPattern: '**/TestResults.xml' 29 | cobertura coberturaReportFile: '**/coverage.cobertura.xml' 30 | } 31 | } 32 | environment { 33 | BAGET = credentials('3db850d0-e6b5-43d5-b607-d180f4eab676') 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Ellpeck 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 | -------------------------------------------------------------------------------- /Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ellpeck/Coroutine/7501ffaf9c02e6dd74dbb9aa2f2197f166f4960a/Logo.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![The Coroutine logo](https://raw.githubusercontent.com/Ellpeck/Coroutine/main/Logo.png) 2 | 3 | **Coroutine** is a simple implementation of Unity's Coroutines to be used for any C# project 4 | 5 | # Features 6 | Coroutine adds the ability to run coroutines. Coroutines are methods that run in parallel to the rest of the application through the use of an `Enumerator`. This allows for the coroutine to pause execution using the `yield return` statement. 7 | 8 | There are two predefined ways to pause a coroutine: 9 | - Waiting for a certain amount of seconds to have passed 10 | - Waiting for a certain custom event to occur 11 | 12 | Additionally, Coroutine provides the following features: 13 | - Creation of custom events to wait for 14 | - No multi-threading, which allows for any kind of process to be executed in a coroutine, including rendering 15 | - Thread-safety, which allows for coroutines to be started from different threads 16 | 17 | # How to Use 18 | ## Setting up the CoroutineHandler 19 | The `CoroutineHandler` is the place where coroutines get executed. For this to occur, the `Tick` method needs to be called continuously. The `Tick` method takes a single parameter which represents the amount of time since the last time it was called. It can either be called in your application's existing update loop or as follows. 20 | ```cs 21 | var lastTime = DateTime.Now; 22 | while (true) { 23 | var currTime = DateTime.Now; 24 | CoroutineHandler.Tick(currTime - lastTime); 25 | lastTime = currTime; 26 | Thread.Sleep(1); 27 | } 28 | ``` 29 | 30 | ## Creating a Coroutine 31 | To create a coroutine, simply create a method with the return type `IEnumerator`. Then, you can use `yield return` to cause the coroutine to wait at any point: 32 | ```cs 33 | private static IEnumerator WaitSeconds() { 34 | Console.WriteLine("First thing " + DateTime.Now); 35 | yield return new Wait(1); 36 | Console.WriteLine("After 1 second " + DateTime.Now); 37 | yield return new Wait(5); 38 | Console.WriteLine("After 5 seconds " + DateTime.Now); 39 | yield return new Wait(10); 40 | Console.WriteLine("After 10 seconds " + DateTime.Now); 41 | } 42 | ``` 43 | 44 | ## Starting a Coroutine 45 | To start a coroutine, simply call `Start`: 46 | ```cs 47 | CoroutineHandler.Start(WaitSeconds()); 48 | ``` 49 | 50 | ## Using Events 51 | To use an event, an `Event` instance first needs to be created. When not overriding any equality operators, only a single instance of each event should be used. 52 | ```cs 53 | private static readonly Event TestEvent = new Event(); 54 | ``` 55 | 56 | Waiting for an event in a coroutine works as follows: 57 | ```cs 58 | private static IEnumerator WaitForTestEvent() { 59 | yield return new Wait(TestEvent); 60 | Console.WriteLine("Test event received"); 61 | } 62 | ``` 63 | Of course, having time-based waits and event-based waits in the same coroutine is also supported. 64 | 65 | To actually cause the event to be raised, causing all currently waiting coroutines to be continued, simply call `RaiseEvent`: 66 | ```cs 67 | CoroutineHandler.RaiseEvent(TestEvent); 68 | ``` 69 | 70 | ## Additional Examples 71 | For additional examples, take a look at the [Example class](https://github.com/Ellpeck/Coroutine/blob/main/Example/Example.cs). -------------------------------------------------------------------------------- /Tests/EventBasedCoroutineTests.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Coroutine; 3 | using NUnit.Framework; 4 | 5 | namespace Tests { 6 | public class EventBasedCoroutineTests { 7 | 8 | [Test] 9 | public void TestEventBasedCoroutine() { 10 | var counter = 0; 11 | var myEvent = new Event(); 12 | 13 | IEnumerator OnEventTriggered() { 14 | counter++; 15 | yield return new Wait(myEvent); 16 | counter++; 17 | } 18 | 19 | var cr = CoroutineHandler.Start(OnEventTriggered()); 20 | Assert.AreEqual(1, counter, "instruction before yield is not executed."); 21 | CoroutineHandler.Tick(1); 22 | CoroutineHandler.RaiseEvent(myEvent); 23 | Assert.AreEqual(2, counter, "instruction after yield is not executed."); 24 | CoroutineHandler.Tick(1); 25 | CoroutineHandler.RaiseEvent(myEvent); 26 | Assert.AreEqual(2, counter, "instruction after yield is not executed."); 27 | 28 | Assert.AreEqual(true, cr.IsFinished, "Incorrect IsFinished value."); 29 | Assert.AreEqual(false, cr.WasCanceled, "Incorrect IsCanceled value."); 30 | Assert.AreEqual(cr.MoveNextCount, 2, "Incorrect MoveNextCount value."); 31 | } 32 | 33 | [Test] 34 | public void TestInfiniteCoroutineNeverFinishesUnlessCanceled() { 35 | var myEvent = new Event(); 36 | var myOtherEvent = new Event(); 37 | var counter = 0; 38 | 39 | IEnumerator OnEventTriggeredInfinite() { 40 | while (true) { 41 | counter++; 42 | yield return new Wait(myEvent); 43 | } 44 | } 45 | 46 | void SetCounterToUnreachableValue(ActiveCoroutine coroutine) { 47 | counter = -100; 48 | } 49 | 50 | var cr = CoroutineHandler.Start(OnEventTriggeredInfinite()); 51 | CoroutineHandler.Tick(1); 52 | cr.OnFinished += SetCounterToUnreachableValue; 53 | for (var i = 0; i < 50; i++) 54 | CoroutineHandler.RaiseEvent(myOtherEvent); 55 | 56 | for (var i = 0; i < 50; i++) 57 | CoroutineHandler.RaiseEvent(myEvent); 58 | 59 | Assert.AreEqual(51, counter, "Incorrect counter value."); 60 | Assert.AreEqual(false, cr.IsFinished, "Incorrect IsFinished value."); 61 | Assert.AreEqual(false, cr.WasCanceled, "Incorrect IsCanceled value."); 62 | Assert.AreEqual(51, cr.MoveNextCount, "Incorrect MoveNextCount value."); 63 | 64 | cr.Cancel(); 65 | Assert.AreEqual(true, cr.WasCanceled, "Incorrect IsCanceled value after canceling."); 66 | Assert.AreEqual(-100, counter, "OnFinished event not triggered when canceled."); 67 | Assert.AreEqual(51, cr.MoveNextCount, "Incorrect MoveNextCount value."); 68 | Assert.AreEqual(true, cr.IsFinished, "Incorrect IsFinished value."); 69 | } 70 | 71 | [Test] 72 | public void TestOnFinishedEventExecuted() { 73 | var myEvent = new Event(); 74 | var counter = 0; 75 | 76 | IEnumerator OnEvent() { 77 | counter++; 78 | yield return new Wait(myEvent); 79 | } 80 | 81 | void SetCounterToUnreachableValue(ActiveCoroutine coroutine) { 82 | counter = -100; 83 | } 84 | 85 | var cr = CoroutineHandler.Start(OnEvent()); 86 | CoroutineHandler.Tick(1); 87 | cr.OnFinished += SetCounterToUnreachableValue; 88 | for (var i = 0; i < 10; i++) 89 | CoroutineHandler.RaiseEvent(myEvent); 90 | Assert.AreEqual(-100, counter, "Incorrect counter value."); 91 | } 92 | 93 | [Test] 94 | public void TestNestedCoroutine() { 95 | var onChildCreated = new Event(); 96 | var onParentCreated = new Event(); 97 | var myEvent = new Event(); 98 | var counterAlwaysRunning = 0; 99 | 100 | IEnumerator AlwaysRunning() { 101 | while (true) { 102 | yield return new Wait(myEvent); 103 | counterAlwaysRunning++; 104 | } 105 | } 106 | 107 | var counterChild = 0; 108 | 109 | IEnumerator Child() { 110 | yield return new Wait(myEvent); 111 | counterChild++; 112 | } 113 | 114 | var counterParent = 0; 115 | 116 | IEnumerator Parent() { 117 | yield return new Wait(myEvent); 118 | counterParent++; 119 | // OnFinish I will start child. 120 | } 121 | 122 | var counterGrandParent = 0; 123 | 124 | IEnumerator GrandParent() { 125 | yield return new Wait(myEvent); 126 | counterGrandParent++; 127 | // Nested corotuine starting. 128 | var p = CoroutineHandler.Start(Parent()); 129 | CoroutineHandler.RaiseEvent(onParentCreated); 130 | // Nested corotuine starting in OnFinished. 131 | p.OnFinished += _ => { 132 | CoroutineHandler.Start(Child()); 133 | CoroutineHandler.RaiseEvent(onChildCreated); 134 | }; 135 | } 136 | 137 | var always = CoroutineHandler.Start(AlwaysRunning()); 138 | CoroutineHandler.Start(GrandParent()); 139 | Assert.AreEqual(0, counterAlwaysRunning, "Always running counter is invalid at event 0."); 140 | Assert.AreEqual(0, counterGrandParent, "Grand Parent counter is invalid at event 0."); 141 | Assert.AreEqual(0, counterParent, "Parent counter is invalid at event 0."); 142 | Assert.AreEqual(0, counterChild, "Child counter is invalid at event 0."); 143 | CoroutineHandler.Tick(1); 144 | CoroutineHandler.RaiseEvent(myEvent); 145 | Assert.AreEqual(1, counterAlwaysRunning, "Always running counter is invalid at event 1."); 146 | Assert.AreEqual(1, counterGrandParent, "Grand Parent counter is invalid at event 1."); 147 | Assert.AreEqual(0, counterParent, "Parent counter is invalid at event 1."); 148 | Assert.AreEqual(0, counterChild, "Child counter is invalid at event 1."); 149 | CoroutineHandler.Tick(1); 150 | CoroutineHandler.RaiseEvent(myEvent); 151 | Assert.AreEqual(2, counterAlwaysRunning, "Always running counter is invalid at event 2."); 152 | Assert.AreEqual(1, counterGrandParent, "Grand Parent counter is invalid at event 2."); 153 | Assert.AreEqual(1, counterParent, "Parent counter is invalid at event 2."); 154 | Assert.AreEqual(0, counterChild, "Child counter is invalid at event 2."); 155 | CoroutineHandler.Tick(1); 156 | CoroutineHandler.RaiseEvent(myEvent); 157 | Assert.AreEqual(3, counterAlwaysRunning, "Always running counter is invalid at event 3."); 158 | Assert.AreEqual(1, counterGrandParent, "Grand Parent counter is invalid at event 3."); 159 | Assert.AreEqual(1, counterParent, "Parent counter is invalid at event 3."); 160 | Assert.AreEqual(1, counterChild, "Child counter is invalid at event 3."); 161 | CoroutineHandler.Tick(1); 162 | CoroutineHandler.RaiseEvent(myEvent); 163 | Assert.AreEqual(4, counterAlwaysRunning, "Always running counter is invalid at event 4."); 164 | Assert.AreEqual(1, counterGrandParent, "Grand Parent counter is invalid at event 4."); 165 | Assert.AreEqual(1, counterParent, "Parent counter is invalid at event 4."); 166 | Assert.AreEqual(1, counterChild, "Child counter is invalid at event 4."); 167 | always.Cancel(); 168 | } 169 | 170 | [Test] 171 | public void TestNestedRaiseEvent() { 172 | var event1 = new Event(); 173 | var event2 = new Event(); 174 | var event3 = new Event(); 175 | var coroutineCreated = new Event(); 176 | var counterCoroutineA = 0; 177 | var counter = 0; 178 | 179 | var infinite = CoroutineHandler.Start(OnCoroutineCreatedInfinite()); 180 | CoroutineHandler.Start(OnEvent1()); 181 | CoroutineHandler.Tick(1); 182 | CoroutineHandler.RaiseEvent(event1); 183 | CoroutineHandler.Tick(1); 184 | CoroutineHandler.RaiseEvent(event2); 185 | CoroutineHandler.Tick(1); 186 | CoroutineHandler.RaiseEvent(event3); 187 | Assert.AreEqual(3, counter); 188 | Assert.AreEqual(2, counterCoroutineA); 189 | infinite.Cancel(); 190 | 191 | IEnumerator OnCoroutineCreatedInfinite() { 192 | while (true) { 193 | yield return new Wait(coroutineCreated); 194 | counterCoroutineA++; 195 | } 196 | } 197 | 198 | IEnumerator OnEvent1() { 199 | yield return new Wait(event1); 200 | counter++; 201 | CoroutineHandler.Start(OnEvent2()); 202 | CoroutineHandler.RaiseEvent(coroutineCreated); 203 | } 204 | 205 | IEnumerator OnEvent2() { 206 | yield return new Wait(event2); 207 | counter++; 208 | CoroutineHandler.Start(OnEvent3()); 209 | CoroutineHandler.RaiseEvent(coroutineCreated); 210 | } 211 | 212 | IEnumerator OnEvent3() { 213 | yield return new Wait(event3); 214 | counter++; 215 | } 216 | } 217 | 218 | [Test] 219 | public void TestPriority() { 220 | var myEvent = new Event(); 221 | var counterShouldExecuteBefore0 = 0; 222 | 223 | IEnumerator ShouldExecuteBefore0() { 224 | while (true) { 225 | yield return new Wait(myEvent); 226 | counterShouldExecuteBefore0++; 227 | } 228 | } 229 | 230 | var counterShouldExecuteBefore1 = 0; 231 | 232 | IEnumerator ShouldExecuteBefore1() { 233 | while (true) { 234 | yield return new Wait(myEvent); 235 | counterShouldExecuteBefore1++; 236 | } 237 | } 238 | 239 | var counterShouldExecuteAfter = 0; 240 | 241 | IEnumerator ShouldExecuteAfter() { 242 | while (true) { 243 | yield return new Wait(myEvent); 244 | if (counterShouldExecuteBefore0 == 1 && 245 | counterShouldExecuteBefore1 == 1) { 246 | counterShouldExecuteAfter++; 247 | } 248 | } 249 | } 250 | 251 | var counterShouldExecuteFinally = 0; 252 | 253 | IEnumerator ShouldExecuteFinally() { 254 | while (true) { 255 | yield return new Wait(myEvent); 256 | if (counterShouldExecuteAfter > 0) { 257 | counterShouldExecuteFinally++; 258 | } 259 | } 260 | } 261 | 262 | const int highPriority = int.MaxValue; 263 | var before1 = CoroutineHandler.Start(ShouldExecuteBefore1(), priority: highPriority); 264 | var after = CoroutineHandler.Start(ShouldExecuteAfter()); 265 | var before0 = CoroutineHandler.Start(ShouldExecuteBefore0(), priority: highPriority); 266 | var @finally = CoroutineHandler.Start(ShouldExecuteFinally(), priority: -1); 267 | CoroutineHandler.Tick(1); 268 | CoroutineHandler.RaiseEvent(myEvent); 269 | Assert.AreEqual(1, counterShouldExecuteAfter, $"ShouldExecuteAfter counter {counterShouldExecuteAfter} is invalid."); 270 | Assert.AreEqual(1, counterShouldExecuteFinally, $"ShouldExecuteFinally counter {counterShouldExecuteFinally} is invalid."); 271 | 272 | before1.Cancel(); 273 | after.Cancel(); 274 | before0.Cancel(); 275 | @finally.Cancel(); 276 | } 277 | 278 | [Test] 279 | public void InvokeLaterAndNameTest() { 280 | var myEvent = new Event(); 281 | var counter = 0; 282 | var cr = CoroutineHandler.InvokeLater(new Wait(myEvent), () => { 283 | counter++; 284 | }, "Bird"); 285 | 286 | CoroutineHandler.InvokeLater(new Wait(myEvent), () => { 287 | counter++; 288 | }); 289 | 290 | CoroutineHandler.InvokeLater(new Wait(myEvent), () => { 291 | counter++; 292 | }); 293 | 294 | Assert.AreEqual(0, counter, "Incorrect counter value after 5 seconds."); 295 | CoroutineHandler.Tick(1); 296 | CoroutineHandler.RaiseEvent(myEvent); 297 | Assert.AreEqual(3, counter, "Incorrect counter value after 10 seconds."); 298 | Assert.AreEqual(true, cr.IsFinished, "Incorrect IsFinished value."); 299 | Assert.AreEqual(false, cr.WasCanceled, "Incorrect IsCanceled value."); 300 | Assert.AreEqual(cr.MoveNextCount, 2, "Incorrect MoveNextCount value."); 301 | Assert.AreEqual(cr.Name, "Bird", "Incorrect name of the coroutine."); 302 | } 303 | 304 | [Test] 305 | public void MovingCoroutineTest() { 306 | var evt = new Event(); 307 | 308 | IEnumerator MovingCoroutine() { 309 | while (true) { 310 | yield return new Wait(evt); 311 | yield return new Wait(0d); 312 | } 313 | } 314 | 315 | var moving = CoroutineHandler.Start(MovingCoroutine(), "MovingCoroutine"); 316 | CoroutineHandler.RaiseEvent(evt); 317 | CoroutineHandler.RaiseEvent(evt); 318 | CoroutineHandler.RaiseEvent(evt); 319 | CoroutineHandler.RaiseEvent(evt); 320 | 321 | CoroutineHandler.Tick(1d); 322 | CoroutineHandler.Tick(1d); 323 | CoroutineHandler.Tick(1d); 324 | CoroutineHandler.Tick(1d); 325 | 326 | CoroutineHandler.RaiseEvent(evt); 327 | CoroutineHandler.Tick(1d); 328 | CoroutineHandler.RaiseEvent(evt); 329 | CoroutineHandler.Tick(1d); 330 | CoroutineHandler.RaiseEvent(evt); 331 | CoroutineHandler.Tick(1d); 332 | 333 | CoroutineHandler.Tick(1d); 334 | CoroutineHandler.RaiseEvent(evt); 335 | CoroutineHandler.Tick(1d); 336 | CoroutineHandler.RaiseEvent(evt); 337 | CoroutineHandler.Tick(1d); 338 | CoroutineHandler.RaiseEvent(evt); 339 | 340 | moving.Cancel(); 341 | } 342 | 343 | } 344 | } -------------------------------------------------------------------------------- /Tests/Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net6.0 4 | false 5 | nunit 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /Tests/TimeBasedCoroutineTests.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading; 3 | using Coroutine; 4 | using NUnit.Framework; 5 | 6 | namespace Tests { 7 | public class TimeBasedCoroutineTests { 8 | 9 | [Test] 10 | public void TestTimerBasedCoroutine() { 11 | var counter = 0; 12 | 13 | IEnumerator OnTimeTickCodeExecuted() { 14 | counter++; 15 | yield return new Wait(0.1d); 16 | counter++; 17 | } 18 | 19 | var cr = CoroutineHandler.Start(OnTimeTickCodeExecuted()); 20 | Assert.AreEqual(1, counter, "instruction before yield is not executed."); 21 | Assert.AreEqual(string.Empty, cr.Name, "Incorrect default name found"); 22 | Assert.AreEqual(0, cr.Priority, "Default priority is not minimum"); 23 | for (var i = 0; i < 5; i++) 24 | CoroutineHandler.Tick(1); 25 | Assert.AreEqual(2, counter, "instruction after yield is not executed."); 26 | Assert.AreEqual(true, cr.IsFinished, "Incorrect IsFinished value."); 27 | Assert.AreEqual(false, cr.WasCanceled, "Incorrect IsCanceled value."); 28 | Assert.AreEqual(cr.MoveNextCount, 2, "Incorrect MoveNextCount value."); 29 | } 30 | 31 | [Test] 32 | public void TestCoroutineReturningWeirdYields() { 33 | var counter = 0; 34 | 35 | IEnumerator OnTimeTickNeverReturnYield() { 36 | counter++; // 1 37 | // condition that's expected to be false 38 | if (counter == 100) 39 | yield return new Wait(0.1d); 40 | counter++; // 2 41 | } 42 | 43 | IEnumerator OnTimeTickYieldBreak() { 44 | counter++; // 3 45 | yield break; 46 | } 47 | 48 | var cr = new ActiveCoroutine[2]; 49 | cr[0] = CoroutineHandler.Start(OnTimeTickNeverReturnYield()); 50 | cr[1] = CoroutineHandler.Start(OnTimeTickYieldBreak()); 51 | for (var i = 0; i < 5; i++) 52 | CoroutineHandler.Tick(1); 53 | 54 | Assert.AreEqual(3, counter, "Incorrect counter value."); 55 | for (var i = 0; i < cr.Length; i++) { 56 | Assert.AreEqual(true, cr[i].IsFinished, $"Incorrect IsFinished value on index {i}."); 57 | Assert.AreEqual(false, cr[i].WasCanceled, $"Incorrect IsCanceled value on index {i}"); 58 | Assert.AreEqual(1, cr[i].MoveNextCount, $"Incorrect MoveNextCount value on index {i}"); 59 | } 60 | } 61 | 62 | [Test] 63 | public void TestCoroutineReturningDefaultYield() { 64 | var counter = 0; 65 | 66 | IEnumerator OnTimeTickYieldDefault() { 67 | counter++; // 1 68 | yield return default; 69 | counter++; // 2 70 | } 71 | 72 | var cr = CoroutineHandler.Start(OnTimeTickYieldDefault()); 73 | for (var i = 0; i < 5; i++) 74 | CoroutineHandler.Tick(1); 75 | 76 | Assert.AreEqual(2, counter, "Incorrect counter value."); 77 | Assert.AreEqual(true, cr.IsFinished, "Incorrect IsFinished value."); 78 | Assert.AreEqual(false, cr.WasCanceled, "Incorrect IsCanceled value."); 79 | Assert.AreEqual(2, cr.MoveNextCount, "Incorrect MoveNextCount value."); 80 | } 81 | 82 | [Test] 83 | public void TestInfiniteCoroutineNeverFinishesUnlessCanceled() { 84 | var counter = 0; 85 | 86 | IEnumerator OnTimerTickInfinite() { 87 | while (true) { 88 | counter++; 89 | yield return new Wait(1); 90 | } 91 | } 92 | 93 | void SetCounterToUnreachableValue(ActiveCoroutine coroutine) { 94 | counter = -100; 95 | } 96 | 97 | var cr = CoroutineHandler.Start(OnTimerTickInfinite()); 98 | cr.OnFinished += SetCounterToUnreachableValue; 99 | for (var i = 0; i < 50; i++) 100 | CoroutineHandler.Tick(1); 101 | 102 | Assert.AreEqual(51, counter, "Incorrect counter value."); 103 | Assert.AreEqual(false, cr.IsFinished, "Incorrect IsFinished value."); 104 | Assert.AreEqual(false, cr.WasCanceled, "Incorrect IsCanceled value."); 105 | Assert.AreEqual(51, cr.MoveNextCount, "Incorrect MoveNextCount value."); 106 | 107 | cr.Cancel(); 108 | Assert.AreEqual(true, cr.WasCanceled, "Incorrect IsCanceled value after canceling."); 109 | Assert.AreEqual(-100, counter, "OnFinished event not triggered when canceled."); 110 | Assert.AreEqual(51, cr.MoveNextCount, "Incorrect MoveNextCount value."); 111 | Assert.AreEqual(true, cr.IsFinished, "Incorrect IsFinished value."); 112 | } 113 | 114 | [Test] 115 | public void TestOnFinishedEventExecuted() { 116 | var counter = 0; 117 | 118 | IEnumerator OnTimeTick() { 119 | counter++; 120 | yield return new Wait(0.1d); 121 | } 122 | 123 | void SetCounterToUnreachableValue(ActiveCoroutine coroutine) { 124 | counter = -100; 125 | } 126 | 127 | var cr = CoroutineHandler.Start(OnTimeTick()); 128 | cr.OnFinished += SetCounterToUnreachableValue; 129 | CoroutineHandler.Tick(50); 130 | Assert.AreEqual(-100, counter, "Incorrect counter value."); 131 | } 132 | 133 | [Test] 134 | public void TestNestedCoroutine() { 135 | var counterAlwaysRunning = 0; 136 | 137 | IEnumerator AlwaysRunning() { 138 | while (true) { 139 | yield return new Wait(1); 140 | counterAlwaysRunning++; 141 | } 142 | } 143 | 144 | var counterChild = 0; 145 | 146 | IEnumerator Child() { 147 | yield return new Wait(1); 148 | counterChild++; 149 | } 150 | 151 | var counterParent = 0; 152 | 153 | IEnumerator Parent() { 154 | yield return new Wait(1); 155 | counterParent++; 156 | // OnFinish I will start child. 157 | } 158 | 159 | var counterGrandParent = 0; 160 | 161 | IEnumerator GrandParent() { 162 | yield return new Wait(1); 163 | counterGrandParent++; 164 | // Nested corotuine starting. 165 | var p = CoroutineHandler.Start(Parent()); 166 | // Nested corotuine starting in OnFinished. 167 | p.OnFinished += _ => CoroutineHandler.Start(Child()); 168 | } 169 | 170 | var always = CoroutineHandler.Start(AlwaysRunning()); 171 | CoroutineHandler.Start(GrandParent()); 172 | Assert.AreEqual(0, counterAlwaysRunning, "Always running counter is invalid at time 0."); 173 | Assert.AreEqual(0, counterGrandParent, "Grand Parent counter is invalid at time 0."); 174 | Assert.AreEqual(0, counterParent, "Parent counter is invalid at time 0."); 175 | Assert.AreEqual(0, counterChild, "Child counter is invalid at time 0."); 176 | CoroutineHandler.Tick(1); 177 | Assert.AreEqual(1, counterAlwaysRunning, "Always running counter is invalid at time 1."); 178 | Assert.AreEqual(1, counterGrandParent, "Grand Parent counter is invalid at time 1."); 179 | Assert.AreEqual(0, counterParent, "Parent counter is invalid at time 1."); 180 | Assert.AreEqual(0, counterChild, "Child counter is invalid at time 1."); 181 | CoroutineHandler.Tick(1); 182 | Assert.AreEqual(2, counterAlwaysRunning, "Always running counter is invalid at time 2."); 183 | Assert.AreEqual(1, counterGrandParent, "Grand Parent counter is invalid at time 2."); 184 | Assert.AreEqual(1, counterParent, "Parent counter is invalid at time 2."); 185 | Assert.AreEqual(0, counterChild, "Child counter is invalid at time 2."); 186 | CoroutineHandler.Tick(1); 187 | Assert.AreEqual(3, counterAlwaysRunning, "Always running counter is invalid at time 3."); 188 | Assert.AreEqual(1, counterGrandParent, "Grand Parent counter is invalid at time 3."); 189 | Assert.AreEqual(1, counterParent, "Parent counter is invalid at time 3."); 190 | Assert.AreEqual(1, counterChild, "Child counter is invalid at time 3."); 191 | CoroutineHandler.Tick(1); 192 | Assert.AreEqual(4, counterAlwaysRunning, "Always running counter is invalid at time 4."); 193 | Assert.AreEqual(1, counterGrandParent, "Grand Parent counter is invalid at time 4."); 194 | Assert.AreEqual(1, counterParent, "Parent counter is invalid at time 4."); 195 | Assert.AreEqual(1, counterChild, "Child counter is invalid at time 4."); 196 | always.Cancel(); 197 | } 198 | 199 | [Test] 200 | public void TestPriority() { 201 | var counterShouldExecuteBefore0 = 0; 202 | 203 | IEnumerator ShouldExecuteBefore0() { 204 | while (true) { 205 | yield return new Wait(1); 206 | counterShouldExecuteBefore0++; 207 | } 208 | } 209 | 210 | var counterShouldExecuteBefore1 = 0; 211 | 212 | IEnumerator ShouldExecuteBefore1() { 213 | while (true) { 214 | yield return new Wait(1); 215 | counterShouldExecuteBefore1++; 216 | } 217 | } 218 | 219 | var counterShouldExecuteAfter = 0; 220 | 221 | IEnumerator ShouldExecuteAfter() { 222 | while (true) { 223 | yield return new Wait(1); 224 | if (counterShouldExecuteBefore0 == 1 && 225 | counterShouldExecuteBefore1 == 1) { 226 | counterShouldExecuteAfter++; 227 | } 228 | } 229 | } 230 | 231 | var counterShouldExecuteFinally = 0; 232 | 233 | IEnumerator ShouldExecuteFinally() { 234 | while (true) { 235 | yield return new Wait(1); 236 | if (counterShouldExecuteAfter > 0) { 237 | counterShouldExecuteFinally++; 238 | } 239 | } 240 | } 241 | 242 | const int highPriority = int.MaxValue; 243 | var before1 = CoroutineHandler.Start(ShouldExecuteBefore1(), priority: highPriority); 244 | var after = CoroutineHandler.Start(ShouldExecuteAfter()); 245 | var before0 = CoroutineHandler.Start(ShouldExecuteBefore0(), priority: highPriority); 246 | var @finally = CoroutineHandler.Start(ShouldExecuteFinally(), priority: -1); 247 | CoroutineHandler.Tick(10); 248 | Assert.AreEqual(1, counterShouldExecuteAfter, $"ShouldExecuteAfter counter {counterShouldExecuteAfter} is invalid."); 249 | Assert.AreEqual(1, counterShouldExecuteFinally, $"ShouldExecuteFinally counter {counterShouldExecuteFinally} is invalid."); 250 | 251 | before1.Cancel(); 252 | after.Cancel(); 253 | before0.Cancel(); 254 | @finally.Cancel(); 255 | } 256 | 257 | [Test] 258 | public void TestTimeBasedCoroutineIsAccurate() { 259 | var counter0 = 0; 260 | 261 | IEnumerator IncrementCounter0Ever10Seconds() { 262 | while (true) { 263 | yield return new Wait(10); 264 | counter0++; 265 | } 266 | } 267 | 268 | var counter1 = 0; 269 | 270 | IEnumerator IncrementCounter1Every5Seconds() { 271 | while (true) { 272 | yield return new Wait(5); 273 | counter1++; 274 | } 275 | } 276 | 277 | var incCounter0 = CoroutineHandler.Start(IncrementCounter0Ever10Seconds()); 278 | var incCounter1 = CoroutineHandler.Start(IncrementCounter1Every5Seconds()); 279 | CoroutineHandler.Tick(3); 280 | Assert.AreEqual(0, counter0, "Incorrect counter0 value after 3 seconds."); 281 | Assert.AreEqual(0, counter1, "Incorrect counter1 value after 3 seconds."); 282 | CoroutineHandler.Tick(3); 283 | Assert.AreEqual(0, counter0, "Incorrect counter0 value after 6 seconds."); 284 | Assert.AreEqual(1, counter1, "Incorrect counter1 value after 6 seconds."); 285 | 286 | // it's 5 over here because IncrementCounter1Every5Seconds 287 | // increments 5 seconds after last yield. not 5 seconds since start. 288 | // So the when we send 3 seconds in the last SimulateTime, 289 | // the 3rd second was technically ignored. 290 | CoroutineHandler.Tick(5); 291 | Assert.AreEqual(1, counter0, "Incorrect counter0 value after 10 seconds."); 292 | Assert.AreEqual(2, counter1, "Incorrect counter1 value after next 5 seconds."); 293 | 294 | incCounter0.Cancel(); 295 | incCounter1.Cancel(); 296 | } 297 | 298 | [Test] 299 | public void InvokeLaterAndNameTest() { 300 | var counter = 0; 301 | var cr = CoroutineHandler.InvokeLater(new Wait(10), () => { 302 | counter++; 303 | }, "Bird"); 304 | 305 | CoroutineHandler.Tick(5); 306 | Assert.AreEqual(0, counter, "Incorrect counter value after 5 seconds."); 307 | CoroutineHandler.Tick(5); 308 | Assert.AreEqual(1, counter, "Incorrect counter value after 10 seconds."); 309 | Assert.AreEqual(true, cr.IsFinished, "Incorrect IsFinished value."); 310 | Assert.AreEqual(false, cr.WasCanceled, "Incorrect IsCanceled value."); 311 | Assert.AreEqual(cr.MoveNextCount, 2, "Incorrect MoveNextCount value."); 312 | Assert.AreEqual(cr.Name, "Bird", "Incorrect name of the coroutine."); 313 | } 314 | 315 | [Test] 316 | public void CoroutineStatsAreUpdated() { 317 | static IEnumerator CoroutineTakesMax500Ms() { 318 | Thread.Sleep(200); 319 | yield return new Wait(10); 320 | Thread.Sleep(500); 321 | } 322 | 323 | var cr = CoroutineHandler.Start(CoroutineTakesMax500Ms()); 324 | for (var i = 0; i < 5; i++) 325 | CoroutineHandler.Tick(50); 326 | 327 | Assert.IsTrue(cr.TotalMoveNextTime.TotalMilliseconds >= 700); 328 | Assert.IsTrue(cr.LastMoveNextTime.TotalMilliseconds >= 500); 329 | Assert.IsTrue(cr.MoveNextCount == 2); 330 | } 331 | 332 | [Test] 333 | public void TestTickWithNestedAddAndRaiseEvent() { 334 | var coroutineCreated = new Event(); 335 | var counterCoroutineA = 0; 336 | var counter = 0; 337 | 338 | var infinite = CoroutineHandler.Start(OnCoroutineCreatedInfinite()); 339 | CoroutineHandler.Start(OnEvent1()); 340 | CoroutineHandler.Tick(1); 341 | CoroutineHandler.Tick(1); 342 | CoroutineHandler.Tick(1); 343 | Assert.AreEqual(3, counter); 344 | Assert.AreEqual(2, counterCoroutineA); 345 | infinite.Cancel(); 346 | 347 | IEnumerator OnCoroutineCreatedInfinite() { 348 | while (true) { 349 | yield return new Wait(coroutineCreated); 350 | counterCoroutineA++; 351 | } 352 | } 353 | 354 | IEnumerator OnEvent1() { 355 | yield return new Wait(1); 356 | counter++; 357 | CoroutineHandler.Start(OnEvent2()); 358 | CoroutineHandler.RaiseEvent(coroutineCreated); 359 | } 360 | 361 | IEnumerator OnEvent2() { 362 | yield return new Wait(1); 363 | counter++; 364 | CoroutineHandler.Start(OnEvent3()); 365 | CoroutineHandler.RaiseEvent(coroutineCreated); 366 | } 367 | 368 | IEnumerator OnEvent3() { 369 | yield return new Wait(1); 370 | counter++; 371 | } 372 | } 373 | 374 | [Test] 375 | public void TestTickWithNestedAddAndRaiseEventOnFinish() { 376 | var onChildCreated = new Event(); 377 | var onParentCreated = new Event(); 378 | var counterAlwaysRunning = 0; 379 | 380 | IEnumerator AlwaysRunning() { 381 | while (true) { 382 | yield return new Wait(1); 383 | counterAlwaysRunning++; 384 | } 385 | } 386 | 387 | var counterChild = 0; 388 | 389 | IEnumerator Child() { 390 | yield return new Wait(1); 391 | counterChild++; 392 | } 393 | 394 | var counterParent = 0; 395 | 396 | IEnumerator Parent() { 397 | yield return new Wait(1); 398 | counterParent++; 399 | // OnFinish I will start child. 400 | } 401 | 402 | var counterGrandParent = 0; 403 | 404 | IEnumerator GrandParent() { 405 | yield return new Wait(1); 406 | counterGrandParent++; 407 | // Nested corotuine starting. 408 | var p = CoroutineHandler.Start(Parent()); 409 | CoroutineHandler.RaiseEvent(onParentCreated); 410 | // Nested corotuine starting in OnFinished. 411 | p.OnFinished += _ => { 412 | CoroutineHandler.Start(Child()); 413 | CoroutineHandler.RaiseEvent(onChildCreated); 414 | }; 415 | } 416 | 417 | var always = CoroutineHandler.Start(AlwaysRunning()); 418 | CoroutineHandler.Start(GrandParent()); 419 | Assert.AreEqual(0, counterAlwaysRunning, "Always running counter is invalid at event 0."); 420 | Assert.AreEqual(0, counterGrandParent, "Grand Parent counter is invalid at event 0."); 421 | Assert.AreEqual(0, counterParent, "Parent counter is invalid at event 0."); 422 | Assert.AreEqual(0, counterChild, "Child counter is invalid at event 0."); 423 | CoroutineHandler.Tick(1); 424 | Assert.AreEqual(1, counterAlwaysRunning, "Always running counter is invalid at event 1."); 425 | Assert.AreEqual(1, counterGrandParent, "Grand Parent counter is invalid at event 1."); 426 | Assert.AreEqual(0, counterParent, "Parent counter is invalid at event 1."); 427 | Assert.AreEqual(0, counterChild, "Child counter is invalid at event 1."); 428 | CoroutineHandler.Tick(1); 429 | Assert.AreEqual(2, counterAlwaysRunning, "Always running counter is invalid at event 2."); 430 | Assert.AreEqual(1, counterGrandParent, "Grand Parent counter is invalid at event 2."); 431 | Assert.AreEqual(1, counterParent, "Parent counter is invalid at event 2."); 432 | Assert.AreEqual(0, counterChild, "Child counter is invalid at event 2."); 433 | CoroutineHandler.Tick(1); 434 | Assert.AreEqual(3, counterAlwaysRunning, "Always running counter is invalid at event 3."); 435 | Assert.AreEqual(1, counterGrandParent, "Grand Parent counter is invalid at event 3."); 436 | Assert.AreEqual(1, counterParent, "Parent counter is invalid at event 3."); 437 | Assert.AreEqual(1, counterChild, "Child counter is invalid at event 3."); 438 | CoroutineHandler.Tick(1); 439 | Assert.AreEqual(4, counterAlwaysRunning, "Always running counter is invalid at event 4."); 440 | Assert.AreEqual(1, counterGrandParent, "Grand Parent counter is invalid at event 4."); 441 | Assert.AreEqual(1, counterParent, "Parent counter is invalid at event 4."); 442 | Assert.AreEqual(1, counterChild, "Child counter is invalid at event 4."); 443 | always.Cancel(); 444 | } 445 | 446 | } 447 | } --------------------------------------------------------------------------------