├── .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 | 
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 | }
--------------------------------------------------------------------------------