Welcome, Guest. Please login or register.

Login with username, password and session length

 
Advanced search

1411281 Posts in 69324 Topics- by 58380 Members - Latest Member: bob1029

March 28, 2024, 10:50:34 PM

Need hosting? Check out Digital Ocean
(more details in this thread)
TIGSource ForumsDeveloperTechnical (Moderator: ThemsAllTook)Custom Coroutines in C#
Pages: [1]
Print
Author Topic: Custom Coroutines in C#  (Read 9069 times)
ChevyRay
Level 2
**



View Profile
« on: March 13, 2013, 09:00:47 PM »

Mirrored from my blog.

I’m currently developing a game in C# using MonoGame. It’s been great so far, I really love this language and the features it has.

One thing I’ve found myself missing from my home grown C# engine though is a simple Coroutine system, one like Unity has.

Read the pages linked above to learn a bit more about how Coroutines (can) work if you haven’t used them before. But basically, they provide an awesome tool for controlling logic flow in your game, and are wonderful for managing things like scripted/sequential events, behavior trees, and motion tweening.

So to gain this power back, I wrote my own Coroutine system in C#, and I’ve written it into a nice little standalone class that anyone is welcome to use in their own projects if they like!

Coroutines.cs

Code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Collections;

namespace CoroutineTest
{
    public class Coroutines
    {
        List<IEnumerator> routines = new List<IEnumerator>();

        public Coroutines()
        {

        }

        public void Start(IEnumerator routine)
        {
            routines.Add(routine);
        }

        public void StopAll()
        {
            routines.Clear();
        }

        public void Update()
        {
            for (int i = 0; i < routines.Count; i++)
            {
                if (routines[i].Current is IEnumerator)
                    if (MoveNext((IEnumerator)routines[i].Current))
                        continue;
                if (!routines[i].MoveNext())
                    routines.RemoveAt(i--);
            }
        }

        bool MoveNext(IEnumerator routine)
        {
            if (routine.Current is IEnumerator)
                if (MoveNext((IEnumerator)routine.Current))
                    return true;
            return routine.MoveNext();
        }

        public int Count
        {
            get { return routines.Count; }
        }

        public bool Running
        {
            get { return routines.Count > 0; }
        }
    }
}

So if you're writing a game in SFML.Net, OpenTK, MonoGame, XNA, or whatever, you should be able to use this.

Here's a simple test file if you want to try it out. Just start a new Console Application solution, include the Coroutines.cs file above, and make this the main program file:

Code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Collections;
using System.Diagnostics;

namespace CoroutineTest
{
    class Program
    {
        //"Death" by John Donne
        const string poem = "\"Death\" by John Donne\n\n" +
                            "Death be not proud, though some have called thee\n" +
                            "Mighty and dreadfull, for, thou art not so,\n" +
                            "For, those, whom thou think'st, thou dost overthrow,\n" +
                            "Die not, poore death, nor yet canst thou kill me.\n" +
                            "From rest and sleepe, which but thy pictures bee,\n" +
                            "Much pleasure, then from thee, much more must flow,\n" +
                            "And soonest our best men with thee doe goe,\n" +
                            "Rest of their bones, and soules deliverie.\n" +
                            "Thou art slave to Fate, Chance, kings, and desperate men,\n" +
                            "And dost with poyson, warre, and sicknesse dwell,\n" +
                            "And poppie, or charmes can make us sleepe as well,\n" +
                            "And better then thy stroake; why swell'st thou then;\n" +
                            "One short sleepe past, wee wake eternally,\n" +
                            "And death shall be no more; death, thou shalt die.";

        static void Main(string[] args)
        {
            var coroutines = new Coroutines();
            coroutines.Start(ReadPoem(poem));
            while (coroutines.Running)
                coroutines.Update();
        }

        static IEnumerator ReadPoem(string poem)
        {
            //Read the poem letter by letter
            foreach (var letter in poem)
            {
                Console.Write(letter);
                switch (letter)
                {
                    //Pause for punctuation
                    case ',':
                    case ';':
                        yield return Pause(0.5f);
                        break;

                    //Long pause for full-stop
                    case '.':
                        yield return Pause(1);
                        break;

                    //Short pause for anything else
                    default:
                        yield return Pause(0.05f);
                        break;
                }
            }

            //Wait for user input to close
            Console.WriteLine("\nPress any key to exit");
            Console.ReadLine();
        }

        static IEnumerator Pause(float time)
        {
            var watch = Stopwatch.StartNew();
            while (watch.Elapsed.TotalSeconds < time)
                yield return 0;
        }
    }
}

Hope somebody finds this useful Smiley
Logged
JoshuaSmyth
Level 0
***


View Profile WWW
« Reply #1 on: May 05, 2015, 04:42:38 PM »

This is really awesome. I didn't realise how simple a custom co-routine library would be to create.
Now I can refactor all those annoying little fire-and-forget timers out of my codebase.
Logged

Layl
Level 3
***

professional jerkface


View Profile WWW
« Reply #2 on: May 05, 2015, 06:48:40 PM »

Nice trick on the recursive enumerators to get the syntax working out nicely!
Logged
Endurion
Level 2
**



View Profile WWW
« Reply #3 on: May 06, 2015, 09:34:56 AM »

Very nice. yield return is pure awesomeness.
Logged
Orz
Level 1
*


"When nothing is done, nothing is left undone."


View Profile WWW
« Reply #4 on: May 19, 2015, 12:43:18 PM »

This is uncanny.  I have been working on my own game/engine in C# that borrows a lot from Flashpunk, and I just browsed over here looking for a better way to write events and event chains, wondering "how would Chevy Ray do it?"
Logged
oahda
Level 10
*****



View Profile
« Reply #5 on: May 19, 2015, 01:09:07 PM »

I'm unfamiliar with what IEnumerator does or how you're seemingly trying to replicate some of its functionality here by using the same names for some of your functions.

I'm not entirely sure what all of your code does since I don't know what IEnumerator does or how it works.

Would you care to explain?
Logged

Layl
Level 3
***

professional jerkface


View Profile WWW
« Reply #6 on: May 19, 2015, 01:44:00 PM »

IEnumerator predates C#'s coroutines. It's a generic interface that says "you can iterate over this". This simply means that the code can request the next value. What C#'s yield return does is a bunch of compiler magic to only run when a value is requested. Every time .MoveNext is called on the enumerator, the function continues to the next yield return statement, where it stops and the value ends up in the IEnumerator.
Logged
BorisTheBrave
Level 10
*****


View Profile WWW
« Reply #7 on: May 19, 2015, 01:48:05 PM »

Would you care to explain?
It's not really possible to explain yield properly, without a solid understanding of enumerators, which itself is probably better not done in a post. In any case, using them as they done here is very unidiomatic, so you wouldn't find it that enlightening.

I think you're better off googling about "coroutines", which is the general concept being implemented here. Essentially, ChevyRay's code, in a remarkably short number of lines, allows you to write code that suspends and resumes itself according to useful conditions. E.g. "yield return Pause(0.5f);" tells the script to suspend for 0.5 seconds, then resume. You can probably imagine how this would be useful for scripting purposes.

It's a popular technique in C# game dev - unity includes a system for it: http://docs.unity3d.com/Manual/Coroutines.html

C# already supports the suspension part via the "yield" keyword. The coroutine system is just responsible for resuming the code at the appropriate point, hence why this is short and sweet.
Logged
oahda
Level 10
*****



View Profile
« Reply #8 on: May 19, 2015, 11:34:10 PM »

Yeah, the way it looks to me, who's unfamiliar, is like Update() and MoveNext() aren't really actually doing anything and that the same effect would be achieved simply by calling ReadPoem() and ignoring the entire Coroutines class. Tongue

Unity is the only environment where I use C#, so I guess I might not ever have the necessity of learning about things like this, tho I suppose I could read up on it out of interest.

Thanks anyway!
Logged

Boreal
Level 6
*


Reinventing the wheel


View Profile
« Reply #9 on: May 20, 2015, 07:50:48 AM »

Under the hood, C# coroutines (and async) are implemented as continuations.  Keywords like "yield" (and "await") automatically chop up your straightforward imperative code into continuations, which are then called when necessary by MoveNext().  For asynchronous operations this is when the operation is finished, and for coroutines this is dependent on the primitives (such as Pause) that you return.

This technique is (probably) lifted from Haskell, which is able to do a very similar transformation from pseudo-imperative code to continuations with monads and "do" blocks.
Logged

"In software, the only numbers of significance are 0, 1, and N." - Josh Barczak

magma - Reconstructed Mantle API
ChevyRay
Level 2
**



View Profile
« Reply #10 on: May 20, 2015, 10:50:46 AM »

Hah, this post is so old. Hilarious that nobody read it until 2 years after I wrote it. I guess with Unity getting more popular, more devs are picking up C# these days.

Unity is the only environment where I use C#, so I guess I might not ever have the necessity of learning about things like this, tho I suppose I could read up on it out of interest.

Yeah don't feel like you *need* to know any of this stuff in order to code games in C# (or any language). I just really enjoy playing with new languages and the idioms and tools they provide, because I spend so much time staring at boring-ass code.

First post on TIGSource in awhile.
Logged
BorisTheBrave
Level 10
*****


View Profile WWW
« Reply #11 on: May 20, 2015, 11:06:40 AM »

Under the hood....
No, continuations are a different, more powerful feature. Continuations can be resumed multiple times and even one-shot continuations can capture multiple stack frames. Haskell Monads are also a different kettle of fish - aside from being more general than generators, the transformation used doesn't really resemble that used by the C# compiler, due to the huge differences in how the languages work.

I'm sorry to be pedantic. I love C# as much as the next man, but let's not pretend it has all the cool features.

Logged
Boreal
Level 6
*


Reinventing the wheel


View Profile
« Reply #12 on: May 20, 2015, 12:10:45 PM »

They are still continuations - just not exposed in full to the programmer.  And yes, when I drew parallels to monads I was being very lenient, but it is helpful to see how they are both essentially transforming imperative style to CPS.
Logged

"In software, the only numbers of significance are 0, 1, and N." - Josh Barczak

magma - Reconstructed Mantle API
BorisTheBrave
Level 10
*****


View Profile WWW
« Reply #13 on: May 20, 2015, 01:23:35 PM »

No really, the C# compiler is not converting anythign to CPS, unless you understand something quite wildly different to me by that phrase.

The generator transformation is to make each function a state machine, where the state is the location most recently suspended yield, and the locals become additional data of the machine. Each time you resume the machine it runs for a bit, marks the new state, then returns.

This is nothing at all like CPS, where you pass in additional function representing the continuation of the current program, and in lieu of returning, you tail call that function.

And, monads are not CPS either, monads are cleanly expressible just using higher order functions, of which they are sugar.

All these things are related (they have varying ability to suspend and resume amongst other things), but they are imho more different than they are similar.
Logged
oahda
Level 10
*****



View Profile
« Reply #14 on: May 21, 2015, 12:47:53 AM »

Yeah don't feel like you *need* to know any of this stuff in order to code games in C# (or any language). I just really enjoy playing with new languages and the idioms and tools they provide, because I spend so much time staring at boring-ass code.
Yeah, I checked that link for the Unity implementation out and it might actually be kinda useful. But I mean that I won't have to look into IEnumerator's core itself.
Logged

creatify
TIGBaby
*


View Profile
« Reply #15 on: June 25, 2015, 11:27:57 AM »

This looks very promising, thank you! However, I'm not successfully getting this to work with WWW where I'm batch-loading a number of images. Can you assist? BTW, I'm a bit new to C#.

Code:
// ...
coroutine = new Coroutines();
coroutine.Start(BeginWWWLoading(loadCount));
while (coroutine.Running) {
   coroutine.Update();
}
// ...
private IEnumerator BeginWWWLoading(int cnt)
{
// assetURIs is a list of image locations
foreach(string uri in assetURIs) {

WWW www = new WWW(uri);
yield return www;

if (www.error != null) {
Debug.LogError("WWW download had an error: " + www.error+" when attempting to load "+uri+".\n");
}

EvtOnAssetLoaded(uri.Replace (prependURI, "")); // send event that asset loaded

loadCount++;

if(loadCount == totalAssets) {
EvtOnAllLoaded(); // send event when all have loaded
}
}
}
Logged
Pages: [1]
Print
Jump to:  

Theme orange-lt created by panic