Sign in to follow this  
soarsky

how can we benefit from coroutine

Recommended Posts

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.

Share this post


Link to post
Share on other sites
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...

Share this post


Link to post
Share on other sites
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.

Share this post


Link to post
Share on other sites
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
.....

Share this post


Link to post
Share on other sites
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.

Share this post


Link to post
Share on other sites
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()"?

Share this post


Link to post
Share on other sites
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.

Share this post


Link to post
Share on other sites
Guest Anonymous Poster
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.

Share this post


Link to post
Share on other sites
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
end
end

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)
end
end

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]

Share this post


Link to post
Share on other sites
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.

Share this post


Link to post
Share on other sites
With coroutines, you have well-defined points where the script can pause, making script-level synchronisation potentially easier and low-level synchronisation unnecessary. You often have more control over how and when to resume them too, making them potentially more scalable.

Share this post


Link to post
Share on other sites
Guest Anonymous Poster
Quote:
Original post by jewe_org
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
end
end

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)
end
end

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



Unfortunately state machines are often not linear sequences and have more complex patterns of transitions. You will wind up using the switch/case statement method for many/most.

If the big issue is cleaner code, then the use of C macros can go a long way to simplify the amount of code for the common state machine constructs. See the early Game Programming Gems book volumes for articles on macros for Finite State Machines.

You would think with the common use of state machines that someone would have written the constructs into their script language.




Share this post


Link to post
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

Sign in to follow this