how can we benefit from coroutine

Started by
11 comments, last by Kylotan 17 years, 9 months ago
I learned from a book that coroutine is very useful in game script. But I can't imagine which game mode will it do good to? Otherwise coroutine only acts as if...then...else module. Who can give me a detailed explanation? Thanks! I use LUA.
Advertisement
co-routines (as I understand them) are effectively threads in the script environment that let you run concurrent processes in a co-operative threading environment (meaning that one has to yield before the other can take over). This is how GameMonkey Script uses them anyway...
Yep, that is totally spot on.

As for what they are usefull for, well the use which springs to mind is anything with state which loops.

So, you perform an action and then yield, when you resume you pick up where you left off. AI and animation systems would be a good candidate for this I'd guess, so name two things off the top of my head.
I don't understand so clear yet. Could you give some pseudo? I don't think the following two parts of codes are far different in efficiency.

part 1:
ai.lua
if true1 then
action1
end
if true2 then
action2
end
.....


part 2:
ai.lua
action1
yield
action2
yield

ai.cpp
if true1 resume
if true2 resume
.....
A perfect use would be for cutscene implementation where the scene has to wait for a character to reach a position or a user choice:

char.lua:
WalkTo( 20, 30 );
Say( "Hello World!" );
TurnLeft();
WalkTo( 10, 30 );


The WalkTo function would use a while loop with a yield function inside. Pseudo Code:

function WalkTo( X, Y )
NPC_Target( X, Y );
while ( not NPC_ReachedTarget( X, Y ) ) do
coroutine.yield();
end
end

So now your game code (C/C++) can update the position of the character and resume the script. The script will run inside the WalkTo function until the character reaches his target. The same with Say.

This allows for a cutscene script to be much clearer and not be littered with a lot of if then lines.

Fruny: Ftagn! Ia! Ia! std::time_put_byname! Mglui naflftagn std::codecvt eY'ha-nthlei!,char,mbstate_t>

Quote:Original post by Endurion
char.lua:
WalkTo( 20, 30 );
Say( "Hello World!" );
TurnLeft();
WalkTo( 10, 30 );

function WalkTo( X, Y )
NPC_Target( X, Y );
while ( not NPC_ReachedTarget( X, Y ) ) do
coroutine.yield();
end
end


Do you mean that using coroutine can make the code look clear and graceful? And where should I put the code "coroutine.resume()"?

Yes, in my case i think it makes for clearer code.

In that case i also use coroutine.resume in my C-code when i want to continue suspended scripts.

Whenever i start a lua "thread" script and it doesn't immediately finish i put the function into a list. So my main game completes the current update loop, runs the rendering and for the next update loop it resumes the suspended coroutines. It is a bit of working on the C side but it makes for much clearer lua scripts IMHO.

Fruny: Ftagn! Ia! Ia! std::time_put_byname! Mglui naflftagn std::codecvt eY'ha-nthlei!,char,mbstate_t>

A small tutorial about how to do this would be nice.
I recently started to learn lua and I can see that coroutines are a nice feature, but some code for resuming them on the C side would be a great help.

There is a lot of lua tutorials on the net, but I haven't found one that shows a clean and reasonable simple way to use Lua in a game. Most of the time I find myself writing a long script in lua that is full of if-else and ugly hacks.
Hi,

well I'm currently in the planning phase of adding support for coroutines in JewelScript (called cofunctions there) and would like to share my thoughts on this topic as well. I think a script language can very much benefit from coroutines, as they allow you to run different jobs "concurrently" in seperate threads. But they are also a very nifty alternative to programming state machines.

In case you want to write a state machine (for example AI, animation, path finding, ...) the "traditional" way of doing it would be to define some enum constants and make a switch statement that handles the various states of your state machine.

This is necessary, because such state machines usually are called on an event- or per-frame basis and must immediately return after processing one "step". Thus, you need to "remember" in which state your entity was when the function has been called last time.

Consider this simple AI function (pseudo code):
function AI( char )    switch char.state        case kIdle:     // nothing to do            if char.hunger > 75     // check if char needs to eat                char.state = kGetFood             break         case kGetFood:  // search for food an go there            pos = FindItem( kFood )             char.MoveTo( pos )             char.state = kEat             break         case kEat:      // check if we reached target and eat            if char.pos == char.target                if ItemAt(char.target) == kFood    // food still there?                    char.hunger = 0                     RemoveItem(char.target)                     char.state = kIdle                 else // no food here!!!                    char.state = kGetFood  // try again            break end

The above "AI" code just has 3 states, but already it looks rather complicated and its rather difficult to keep track of which state results in which next state, especially if your AI has more than a handful of states. But things can get really "ugly" when your state machine also needs to keep track of "state variables", meaning variables that change as your entity switches between states.

Imagine the above AI wanted to try 3 times to get food and after that stop trying that (probably in favor of having time to do other important things if there are more states, e.g. drink, build a house, etc...). In this case you need a variable that counts the failed attempts to eat, and a flag that tells the "Idle" state not to eat anymore. Plus, you need one more state "kGetFoodInit" that resets the state variables when eating is engaged for the first time:
function AI( char )    switch char.state        case kIdle:     // nothing to do            if char.hunger >= 75 && !char.stopEat   // check if char needs to eat                char.state = kGetFoodInit             break         case kGetFoodInit:  // start to look for food            char.eatAttempts = 0             char.stopEat = false             char.state = kGetFood             break         case kGetFood:  // search for food an go there            pos = FindItem( kFood )             char.MoveTo( pos )             char.state = kEat             break         case kEat:      // check if we reached target and eat            if char.pos == char.target                if ItemAt(char.target) == kFood     // food still there?                    char.hunger = 0                     RemoveItem(char.target)                     char.state = kIdle                 else // no food here!!!                    if char.eatAttempts == 3                        char.state = kIdle      // be idle to check if we can do other things                        char.stopEat = true     // stop eating                    else                        char.state = kGetFood   // try again                        char.eatAttempts++            break     endend

The state machine has already become quite "complex" even though what it does is so simple. Notice how I had to put the "eatAttempts" and "stopEat" state variables as members into the character, even though they not really "belong" there, because in principle they are variables of the state machine, not the char. Because we need to remember the complete state when we leave the state function, we would need to add lots of extra variables to our character as the state machine grows more complex. Using global variables would only work if there would be only one character.

And now lets see how the state machine would look when written using coroutines:
cofunction AI( char )    local eatAttempts, stopEat = false     while true                      // repeat forever        while( char.hunger < 75 || stopEat )   // remain idle until hungry            yield         for eatAttempts=0, eatAttempts < 3, eatAttempts++   // attempt to eat three times            pos = FindItem( kFood )             char.MoveTo( pos )             while char.pos != char.target   // wait until we reach the target position                yield             if ItemAt(char.target) == kFood     // food still there?                char.hunger = 0                 RemoveItem(char.target)                 break               // leave for loop        end        stopEat = (eatAttempts == 3)     endend

I think this is a good example how much coroutines can simplify programming of state machines. Even though the cofunction looks as if I forgot to implement something - it really does the same thing than the "switch" example. Notice, how the state variables have been moved into the cofunction and now are simple local variables. This is one of the biggest advantages of cofunctions IMHO, beeing able to use state variables local to the state function, which continue to exist even when you "leave" the function with "yield".

I hope this was helpful or of any interest for anyone. I found this article very helpful to understand what coroutines are and why they are useful:

Coroutines in C

Cheers,
Jewe

[Edited by - jewe_org on June 30, 2006 5:40:21 AM]
JewelScript - a free, open source programming language projecthttp://www.jewe.org
Does a threaded scripting system make coroutines obsolete?

For the example above, why do you need to call yield if it's threaded? Or better yet, could you call Thread.yeild instead? (but only because you are a nice programmer).

Edit: For this example I think it does, but you can use coroutines as generators which is different as I understand.

This topic is closed to new replies.

Advertisement