├── Coroutines.cs
├── Example
├── .gitignore
├── Example.csproj
├── Example.sln
├── Program.cs
└── Properties
│ └── AssemblyInfo.cs
├── LICENSE
└── README.md
/Coroutines.cs:
--------------------------------------------------------------------------------
1 | /*
2 |
3 | MIT License
4 |
5 | Copyright (c) 2017 Chevy Ray Johnston
6 |
7 | Permission is hereby granted, free of charge, to any person obtaining a copy
8 | of this software and associated documentation files (the "Software"), to deal
9 | in the Software without restriction, including without limitation the rights
10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | copies of the Software, and to permit persons to whom the Software is
12 | furnished to do so, subject to the following conditions:
13 |
14 | The above copyright notice and this permission notice shall be included in all
15 | copies or substantial portions of the Software.
16 |
17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23 | SOFTWARE.
24 |
25 | */
26 |
27 | using System.Collections;
28 | using System.Collections.Generic;
29 |
30 | namespace Coroutines
31 | {
32 | ///
33 | /// A container for running multiple routines in parallel. Coroutines can be nested.
34 | ///
35 | public class CoroutineRunner
36 | {
37 | List running = new List();
38 | List delays = new List();
39 |
40 | ///
41 | /// Run a coroutine.
42 | ///
43 | /// A handle to the new coroutine.
44 | /// How many seconds to delay before starting.
45 | /// The routine to run.
46 | public CoroutineHandle Run(float delay, IEnumerator routine)
47 | {
48 | running.Add(routine);
49 | delays.Add(delay);
50 | return new CoroutineHandle(this, routine);
51 | }
52 |
53 | ///
54 | /// Run a coroutine.
55 | ///
56 | /// A handle to the new coroutine.
57 | /// The routine to run.
58 | public CoroutineHandle Run(IEnumerator routine)
59 | {
60 | return Run(0f, routine);
61 | }
62 |
63 | ///
64 | /// Stop the specified routine.
65 | ///
66 | /// True if the routine was actually stopped.
67 | /// The routine to stop.
68 | public bool Stop(IEnumerator routine)
69 | {
70 | int i = running.IndexOf(routine);
71 | if (i < 0)
72 | return false;
73 | running[i] = null;
74 | delays[i] = 0f;
75 | return true;
76 | }
77 |
78 | ///
79 | /// Stop the specified routine.
80 | ///
81 | /// True if the routine was actually stopped.
82 | /// The routine to stop.
83 | public bool Stop(CoroutineHandle routine)
84 | {
85 | return routine.Stop();
86 | }
87 |
88 | ///
89 | /// Stop all running routines.
90 | ///
91 | public void StopAll()
92 | {
93 | running.Clear();
94 | delays.Clear();
95 | }
96 |
97 | ///
98 | /// Check if the routine is currently running.
99 | ///
100 | /// True if the routine is running.
101 | /// The routine to check.
102 | public bool IsRunning(IEnumerator routine)
103 | {
104 | return running.Contains(routine);
105 | }
106 |
107 | ///
108 | /// Check if the routine is currently running.
109 | ///
110 | /// True if the routine is running.
111 | /// The routine to check.
112 | public bool IsRunning(CoroutineHandle routine)
113 | {
114 | return routine.IsRunning;
115 | }
116 |
117 | ///
118 | /// Update all running coroutines.
119 | ///
120 | /// True if any routines were updated.
121 | /// How many seconds have passed sinced the last update.
122 | public bool Update(float deltaTime)
123 | {
124 | if (running.Count > 0)
125 | {
126 | for (int i = 0; i < running.Count; i++)
127 | {
128 | if (delays[i] > 0f)
129 | delays[i] -= deltaTime;
130 | else if (running[i] == null || !MoveNext(running[i], i))
131 | {
132 | running.RemoveAt(i);
133 | delays.RemoveAt(i--);
134 | }
135 | }
136 | return true;
137 | }
138 | return false;
139 | }
140 |
141 | bool MoveNext(IEnumerator routine, int index)
142 | {
143 | if (routine.Current is IEnumerator)
144 | {
145 | if (MoveNext((IEnumerator)routine.Current, index))
146 | return true;
147 |
148 | delays[index] = 0f;
149 | }
150 |
151 | bool result = routine.MoveNext();
152 |
153 | if (routine.Current is float)
154 | delays[index] = (float)routine.Current;
155 |
156 | return result;
157 | }
158 |
159 | ///
160 | /// How many coroutines are currently running.
161 | ///
162 | public int Count
163 | {
164 | get { return running.Count; }
165 | }
166 | }
167 |
168 | ///
169 | /// A handle to a (potentially running) coroutine.
170 | ///
171 | public struct CoroutineHandle
172 | {
173 | ///
174 | /// Reference to the routine's runner.
175 | ///
176 | public CoroutineRunner Runner;
177 |
178 | ///
179 | /// Reference to the routine's enumerator.
180 | ///
181 | public IEnumerator Enumerator;
182 |
183 | ///
184 | /// Construct a coroutine. Never call this manually, only use return values from Coroutines.Run().
185 | ///
186 | /// The routine's runner.
187 | /// The routine's enumerator.
188 | public CoroutineHandle(CoroutineRunner runner, IEnumerator enumerator)
189 | {
190 | Runner = runner;
191 | Enumerator = enumerator;
192 | }
193 |
194 | ///
195 | /// Stop this coroutine if it is running.
196 | ///
197 | /// True if the coroutine was stopped.
198 | public bool Stop()
199 | {
200 | return IsRunning && Runner.Stop(Enumerator);
201 | }
202 |
203 | ///
204 | /// A routine to wait until this coroutine has finished running.
205 | ///
206 | /// The wait enumerator.
207 | public IEnumerator Wait()
208 | {
209 | if (Enumerator != null)
210 | while (Runner.IsRunning(Enumerator))
211 | yield return null;
212 | }
213 |
214 | ///
215 | /// True if the enumerator is currently running.
216 | ///
217 | public bool IsRunning
218 | {
219 | get { return Enumerator != null && Runner.IsRunning(Enumerator); }
220 | }
221 | }
222 | }
--------------------------------------------------------------------------------
/Example/.gitignore:
--------------------------------------------------------------------------------
1 | # Autosave files
2 | *~
3 |
4 | # build
5 | [Oo]bj/
6 | [Bb]in/
7 | packages/
8 | TestResults/
9 |
10 | # globs
11 | Makefile.in
12 | *.DS_Store
13 | *.sln.cache
14 | *.suo
15 | *.cache
16 | *.pidb
17 | *.userprefs
18 | *.usertasks
19 | config.log
20 | config.make
21 | config.status
22 | aclocal.m4
23 | install-sh
24 | autom4te.cache/
25 | *.user
26 | *.tar.gz
27 | tarballs/
28 | test-results/
29 | Thumbs.db
30 |
31 | # Mac bundle stuff
32 | *.dmg
33 | *.app
34 |
35 | # resharper
36 | *_Resharper.*
37 | *.Resharper
38 |
39 | # dotCover
40 | *.dotCover
41 |
--------------------------------------------------------------------------------
/Example/Example.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Debug
5 | x86
6 | {C51DDF3D-88A1-495E-AB07-5B20DF02DE40}
7 | Exe
8 | Example
9 | Example
10 | v4.6.1
11 |
12 |
13 | true
14 | full
15 | false
16 | bin\Debug
17 | DEBUG;
18 | prompt
19 | 4
20 | true
21 | x86
22 |
23 |
24 | true
25 | bin\Release
26 | prompt
27 | 4
28 | true
29 | x86
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 | Coroutines.cs
39 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/Example/Example.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio 2012
4 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Example", "Example.csproj", "{C51DDF3D-88A1-495E-AB07-5B20DF02DE40}"
5 | EndProject
6 | Global
7 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
8 | Debug|x86 = Debug|x86
9 | Release|x86 = Release|x86
10 | EndGlobalSection
11 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
12 | {C51DDF3D-88A1-495E-AB07-5B20DF02DE40}.Debug|x86.ActiveCfg = Debug|x86
13 | {C51DDF3D-88A1-495E-AB07-5B20DF02DE40}.Debug|x86.Build.0 = Debug|x86
14 | {C51DDF3D-88A1-495E-AB07-5B20DF02DE40}.Release|x86.ActiveCfg = Release|x86
15 | {C51DDF3D-88A1-495E-AB07-5B20DF02DE40}.Release|x86.Build.0 = Release|x86
16 | EndGlobalSection
17 | EndGlobal
18 |
--------------------------------------------------------------------------------
/Example/Program.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections;
3 | using System.Diagnostics;
4 | using Coroutines;
5 |
6 | namespace Example
7 | {
8 | // Here's a silly example animating a little roguelike-style character moving.
9 | class Program
10 | {
11 | public static void Main(string[] args)
12 | {
13 | //Timer variables to run the update loop at 30 fps
14 | var watch = Stopwatch.StartNew();
15 | const float updateRate = 1f / 30f;
16 | float prevTime = watch.ElapsedMilliseconds / 1000f;
17 | float accumulator = 0f;
18 |
19 | //The little @ character's position
20 | int px = 0;
21 | int py = 0;
22 |
23 | //Routine to move horizontally
24 | IEnumerator MoveX(int amount, float stepTime)
25 | {
26 | int dir = amount > 0 ? 1 : -1;
27 | while (amount != 0)
28 | {
29 | yield return stepTime;
30 | px += dir;
31 | amount -= dir;
32 | }
33 | }
34 |
35 | //Routine to move vertically
36 | IEnumerator MoveY(int amount, float stepTime)
37 | {
38 | int dir = amount > 0 ? 1 : -1;
39 | while (amount != 0)
40 | {
41 | yield return stepTime;
42 | py += dir;
43 | amount -= dir;
44 | }
45 | }
46 |
47 | //Walk the little @ character on a path
48 | IEnumerator Movement()
49 | {
50 | //Walk normally
51 | yield return MoveX(5, 0.25f);
52 | yield return MoveY(5, 0.25f);
53 |
54 | //Walk slowly
55 | yield return MoveX(2, 0.5f);
56 | yield return MoveY(2, 0.5f);
57 | yield return MoveX(-2, 0.5f);
58 | yield return MoveY(-2, 0.5f);
59 |
60 | //Run fast
61 | yield return MoveX(5, 0.1f);
62 | yield return MoveY(5, 0.1f);
63 | }
64 |
65 | //Render a little map with the @ character in the console
66 | void DrawMap()
67 | {
68 | Console.Clear();
69 | for (int y = 0; y < 16; ++y)
70 | {
71 | for (int x = 0; x < 16; ++x)
72 | {
73 | if (x == px && y == py)
74 | Console.Write('@');
75 | else
76 | Console.Write('.');
77 | }
78 | Console.WriteLine();
79 | }
80 | }
81 |
82 | //Run the coroutine
83 | var runner = new CoroutineRunner();
84 | var moving = runner.Run(Movement());
85 |
86 | //Run the update loop until we've finished moving
87 | while (moving.IsRunning)
88 | {
89 | //Track time
90 | float currTime = watch.ElapsedMilliseconds / 1000f;
91 | accumulator += currTime - prevTime;
92 | prevTime = currTime;
93 |
94 | //Update at our requested rate (30 fps)
95 | if (accumulator > updateRate)
96 | {
97 | accumulator -= updateRate;
98 | runner.Update(updateRate);
99 | DrawMap();
100 | }
101 | }
102 | }
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/Example/Properties/AssemblyInfo.cs:
--------------------------------------------------------------------------------
1 | using System.Reflection;
2 | using System.Runtime.CompilerServices;
3 |
4 | // Information about this assembly is defined by the following attributes.
5 | // Change them to the values specific to your project.
6 |
7 | [assembly: AssemblyTitle("Example")]
8 | [assembly: AssemblyDescription("")]
9 | [assembly: AssemblyConfiguration("")]
10 | [assembly: AssemblyCompany("")]
11 | [assembly: AssemblyProduct("")]
12 | [assembly: AssemblyCopyright("${AuthorCopyright}")]
13 | [assembly: AssemblyTrademark("")]
14 | [assembly: AssemblyCulture("")]
15 |
16 | // The assembly version has the format "{Major}.{Minor}.{Build}.{Revision}".
17 | // The form "{Major}.{Minor}.*" will automatically update the build and revision,
18 | // and "{Major}.{Minor}.{Build}.*" will update just the revision.
19 |
20 | [assembly: AssemblyVersion("1.0.*")]
21 |
22 | // The following attributes are used to specify the signing key for the assembly,
23 | // if desired. See the Mono documentation for more information about signing.
24 |
25 | //[assembly: AssemblyDelaySign(false)]
26 | //[assembly: AssemblyKeyFile("")]
27 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Chevy Ray Johnston
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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Coroutines
2 | A simple system for running nested coroutines in C#. Just drop `Coroutines.cs` into your project and you're ready to go.
3 |
4 | ## What is a coroutine?
5 | C# has a feature called "enumerators" which are functions that can be *suspended* during their execution. They do this by using `yield return`, rather than regular `return`. When you use `yield`, you effectively *pause* the function, and at any time you can *resume* it and it will continue after the most recent `yield`.
6 |
7 | In this context, a "coroutine" is basically one of these functions, but wrapped up in a nice little package and a few extra features:
8 |
9 | - A container that will automatically update several coroutines for you
10 | - A simple syntax for nesting coroutines so they can yield to others
11 | - The ability to yield a floating point number, which will pause the routine for a desired amount of time
12 | - Handles to routines are provided to make tracking them easier
13 |
14 | ## Example usage
15 | To use the system, you need to create a `CoroutineRunner` and update it at regular intervals (eg. in a game loop). You also need to tell it a time interval.
16 |
17 | ```csharp
18 | CoroutineRunner runner = new CoroutineRunner();
19 |
20 | void UpdateGame(float deltaTime)
21 | {
22 | runner.Update(deltaTime);
23 | }
24 | ```
25 |
26 | Now you can run coroutines by calling `Run()`. Here's a simple coroutine that counts:
27 |
28 | ```csharp
29 | IEnumerator CountTo(int num, float delay)
30 | {
31 | for (int i = 1; i <= num; ++i)
32 | {
33 | yield return delay;
34 | Console.WriteLine(i);
35 | }
36 | }
37 | void StartGame()
38 | {
39 | //Count to 10, pausing 1 second between each number
40 | runner.Run(CountTo(10, 1.0f));
41 | }
42 | ```
43 |
44 | When you yield a floating-point number, it will pause the coroutine for that many seconds.
45 |
46 | You can also nest coroutines by yielding to them. Here we will have a parent routine that will run several sub-routines:
47 |
48 | ```csharp
49 | IEnumerator DoSomeCounting()
50 | {
51 | Console.WriteLine("Counting to 3 slowly...");
52 | yield return CountTo(3, 2.0f);
53 | Console.WriteLine("Done!");
54 |
55 | Console.WriteLine("Counting to 5 normally...");
56 | yield return CountTo(5, 1.0f);
57 | Console.WriteLine("Done!");
58 |
59 | Console.WriteLine("Counting to 99 quickly...");
60 | yield return CountTo(99, 0.1f);
61 | Console.WriteLine("Done!");
62 | }
63 |
64 | void StartGame()
65 | {
66 | runner.Run(DoSomeCounting());
67 | }
68 | ```
69 |
70 | You can also stop any running routines:
71 |
72 | ```csharp
73 | //Stop all running routines
74 | runner.StopAll();
75 |
76 | //Start a routine and store a handle to it
77 | CoroutineHandle myRoutine = runner.Run(SomeRoutine());
78 |
79 | //Stop a specific routine
80 | myRoutine.Stop();
81 | ```
82 |
83 | ## Other tips and tricks
84 | A coroutine can run infinitely as well by using a loop. You can also tell the routine to "wait for the next update" by yielding `null`:
85 |
86 | ```csharp
87 | IEnumerator RunThisForever()
88 | {
89 | while (true)
90 | {
91 | yield return null;
92 | }
93 | }
94 | ```
95 |
96 | Coroutines are very handy for games, especially for sequenced behavior and animations, acting sort of like *behavior trees*. For example, a simple enemy's AI routine might look like this:
97 |
98 | ```csharp
99 | IEnumerator EnemyBehavior()
100 | {
101 | while (enemyIsAlive)
102 | {
103 | yield return PatrolForPlayer();
104 | yield return Speak("I found you!");
105 | yield return ChaseAfterPlayer();
106 | yield return Speak("Wait... where did you go!?");
107 | yield return ReturnToPatrol();
108 | }
109 | }
110 | ```
111 |
112 | Sometimes you might want to run multiple routines in parallel, and have a parent routine wait for them both to finish. For this you can use the return handle from `Run()`:
113 |
114 | ```csharp
115 | IEnumerator GatherNPCs(Vector gatheringPoint)
116 | {
117 | //Make three NPCs walk to the gathering point at the same time
118 | var move1 = runner.Run(npc1.WalkTo(gatheringPoint));
119 | var move2 = runner.Run(npc2.WalkTo(gatheringPoint));
120 | var move3 = runner.Run(npc3.WalkTo(gatheringPoint));
121 |
122 | //We don't know how long they'll take, so just wait until all three have finished
123 | while (move1.IsPlaying || move2.IsPlaying || move3.IsPlaying)
124 | yield return null;
125 |
126 | //Now they've all gathered!
127 | }
128 | ```
129 |
130 | Here is a more complicated example where I show how you can use coroutines in conjunction with asynchronous functions (in this case, to download a batch of files and wait until they've finished):
131 |
132 | ```csharp
133 | IEnumerator DownloadFile(string url, string toFile)
134 | {
135 | //I actually don't know how to download files in C# so I just guessed this, but you get the point
136 | bool done = false;
137 | var client = new WebClient();
138 | client.DownloadFileCompleted += (e, b, o) => done = true;
139 | client.DownloadFileAsync(new Uri(url), toFile);
140 | while (!done)
141 | yield return null;
142 | }
143 |
144 | //Download the files one-by-one in sync
145 | IEnumerator DownloadOneAtATime()
146 | {
147 | yield return DownloadFile("http://site.com/file1.png", "file1.png");
148 | yield return DownloadFile("http://site.com/file2.png", "file2.png");
149 | yield return DownloadFile("http://site.com/file3.png", "file3.png");
150 | yield return DownloadFile("http://site.com/file4.png", "file4.png");
151 | yield return DownloadFile("http://site.com/file5.png", "file5.png");
152 | }
153 |
154 | //Download the files all at once asynchronously
155 | IEnumerator DownloadAllAtOnce()
156 | {
157 | //Start multiple async downloads and store their handles
158 | var downloads = new List();
159 | downloads.Add(runner.Run(DownloadFile("http://site.com/file1.png", "file1.png")));
160 | downloads.Add(runner.Run(DownloadFile("http://site.com/file2.png", "file2.png")));
161 | downloads.Add(runner.Run(DownloadFile("http://site.com/file3.png", "file3.png")));
162 | downloads.Add(runner.Run(DownloadFile("http://site.com/file4.png", "file4.png")));
163 | downloads.Add(runner.Run(DownloadFile("http://site.com/file5.png", "file5.png")));
164 |
165 | //Wait until all downloads are done
166 | while (downloads.Count > 0)
167 | {
168 | yield return null;
169 | for (int i = 0; i < downloads.Count; ++i)
170 | if (!downloads[i].IsRunning)
171 | downloads.RemoveAt(i--);
172 | }
173 | }
174 | ```
175 |
176 | ## Why coroutines?
177 |
178 | I use coroutines a lot in my games, as I find them great for organizing actor behavior and animations. As opposed to an async callback-based system, coroutines allow you to write your behaviors line-by-line, like how you would naturally write code, and result in very clean and easy to understand sequences.
179 |
180 | There are good and bad times to use them, and you will get better at distinguishing this as you use them more. For many of my games, coroutines have been completely priceless, and have helped me organize and maintain very large and complicated systems that behave exactly in the order I wish them to.
181 |
182 | **NOTE:** Not all languages have built-in support for coroutine systems like this. If you plan on porting your code to other languages, it may not be worth the pain of porting if your target language does not have a reliable means of implementing coroutines.
183 |
--------------------------------------------------------------------------------