On coroutines

One of my experiments for this pyweek was to make use of Python's new(ish) coroutine support to implement the behavior of the player, baddies and some other scripted things in the game.

In short this looks something like this:

class Player
    def update(self):
        while 1:
            dt = yield
            self.x += self.dx * dt
            self.y += self.dy * dt

player = Player()
update = player.update()
update.send(None)
...
update.send(dt)

OK, so this is neat in theory. You could have multiple loops in there with multiple entry points (yield statements) depending on the current "state" of the player, and so on. I've seen some nice implementations of byte-chomper network servers using this sort of approach.

Only it didn't seem to work. Eventually I was back at square one with a single yield statement and a really, really long update() code body.

I've just refactored it to use separate update() methods depending on the current state of the player:

class Player
    def update(self, dt):
        self.current_action(dt)

    def do_jump(self, dt):
        # do jump stuff
        if landed:
           self.current_action = self.do_land

player = Player()
player.current_action = player.stand_still

I'll note that the simpler coroutines I implemented for the baddies and other things are still coroutines - but I'll probably re-implement them as regular functions as well.

So, was I just doing it wrong, or are coroutines just not that useful for a time-based player update?

richard on 2009/09/07 05:51 of Easy


Comments: (log in to comment)

I think they may be more useful for enemy AI (that for example approaches the player first, then fires at the player until the clip lasts, and hides and reloads after that) — in generally it is useful when you have a state machine.
The trouble is probably that the player object's behaviour is too complicated to be represented easily using a linear sequence of steps. At any given moment it has to be able to respond to many different inputs by doing many different things. It's more of a state machine with rampant spaghetti connections between the states.

It may work better for things like enemies with fairly simple behaviour, e.g. a guard that follows a rigid patrol route. Even then it may need to be able to break out of the routine at unexpected places, e.g. when the player shoots it.

Maybe a hybrid approach would work best -- a number of states, each state represented by a coroutine that goes through a sequence. So the guard would spend most of its time running the patrol_route coroutine, then when he gets shot the whole coroutine would get replaced by shoot_back_at_player until the threat had passed, then patrol_route would resume.
Coroutines are primarily useful for managing asynchronous behaviour, the player's update method does not generally fall into that category. While the player may have some scripted behaviour the scripts should be the coroutine and the update method advances the script/scripts that are currently being executed. However this doesn't sound like what you were trying to accomplish.

If the player's behaviour is sufficiently state dependent then it may be best to encapsulate states in objects with methods called by the player object:

class State(object):
    can_jump = False
    def __init__(self, actor):
        self.actor = actor
    def update(self, dt):
        pass

class Jumping(State):
    def update(self, dt):
        self.actor.pos.z += 20 * dt

class Standing(State):
    can_jump = True
    def update(self, dt):
        pass

class Player(object):
    def __init__(self, ...):
        self.state = Standing(self)
        ...
    def update(self, dt):
        self.state.update(dt)
        if self.state.can_jump:
            ...

This has its flaws too, but I find when you ignore an implicit state machine and use a single method to do everything that it doesn't take long before you're completely baffled about all the transitions.
Of course, I tend to over-engineer... :-)
One use of coroutines would be to write enemy AI scripts that look like this:

def SneakyEnemy():
    while True:
move_to_ambush_point()
attack_player()
hide()
if (player_has_seen_me):
move_somewhere_else()

... which would allow you to present the higher-level logic of the enemy's behaviour in one script which runs in a coroutine and calls subroutines such as attack_player() which can update physics and so on and then yield out of the coroutine back to your main loop.

Unfortunately, Python's coroutines aren't quite powerful enough to support this: you can't explicitly invoke a function as a coroutine so that it can yield all the way back to the coroutine caller, you can only allow functions to yield to their immediate caller. You could work around this by scattering yield statements all over your scripts and moving any looping logic out of the innermost functions into the coroutine's caller (Robot Underground does this) but it makes them somewhat more unwieldy and removes most of the aesthetic appeal of doing this in the first place.
Thanks everyone for your enlightening words!

@adam, I'm still trying to get my head around how that form of yield would work/is useful. Could you please mock up a simple pseudocode of how it could work?
richard: Let's say you want a conversation system in your game, so you want to be able to run conversation code, with conditional blocks and loops and suchlike, but need to jump back to the main loop of your game every tick. It's nice to be able to write your conversations like this:

def talk_to_baker():
    say("Baker", "Hello!")
    say("Me", "Hi!")
    while give_choice("Baker", "What kind of bread would you like?", ["Ciabatta", "French stick"]) != "Ciabatta":

say("Baker", "Sorry, we're out of that kind.")
say("Me", "No problem!")
    say("Baker", "That'll be five pounds")
    if player.money < 5:
say("Me", "I don't have that, sorry")
    else:
say("Me", "Here you go")
give_me(bread)
    say("Baker", "Goodbye now!")
 
Obviously say and give_choice both need to leap out to the main loop, wait for player input, and then jump back in when the conversation should proceed. You can do this by scattering yields everywhere, but it's pretty ugly, especially if you want to call sub-conversations (other functions).
The examples can be made to work without any ugliness using Stackless Python. I definitely recommend giving it a try if these code examples look attractive to you! I guess greenlets can also be used in vanilla Python to make this stuff work.
cyhawk: I'd love to make more use of Stackless, but it does require anyone who wants to play your game to install a completely different implementation of Python, which loses some of the portability appeal of developing in Python in the first place.
richard: There are probably technical details I haven't considered, but the outline of it would look something like this:

(... in main loop ...)
    for enemy in enemies:
resume_coroutine(enemy.doThings)
    ... other stuff ...

(... in definition of a specific enemy class ...)
    def doThings(self):
while True:
self.moveToPlace(self.nest)

self.wait(50)
self.moveToPlace(somewhereNice())
self.killEverythingNearby()

(... in definition of enemy ...)
    def moveToPlace(self, place):
while not self.at(place):
self.pos = self.aBitCloserTo(place) # replace with real movement logic
coroutine_yield()

    def wait(self, time):
start = getCurrentTicks()
while getCurrentTicks() < start + time:
coroutine_yield()

(... and so on ...)

The attraction of this is that you can write the specific enemy behaviours in a very natural style without having to worry about the workings of the main loop. The coroutine resume/yield calls take care of moving you from the main loop to the inner levels of the scripting system (which you only need write once, on the common enemy superclass, or even your actor or entity class) without the doThings() function being any the wiser about what's really going on.
@adam ah, I understand now. To do that in current Python you'd have to:

    def doThings(self):
self.current_action()

    def moveToPlace(self, place):

while not self.at(place):
self.pos = self.aBitCloserTo(place)
coroutine_yield()
                self.current_action = self.wait(); self.wait.send(None)

or similar? Which is pretty much how I currently write my code now.

... the down-side of my implementation is that it's nowhere near as clear and doesn't allow for deeper calls.
Also, my implementation allows one enemy to have:

def doThings(self):
    self.moveToPlace(somewhere)
    self.attackPlayer()

and another to have ...

def doThings(self):
    self.moveToPlace(somewhere)
    self.huntForEasterEggs()

Unless I'm misunderstanding your implementation, your moveToPlace() has to explicitly set current_action to some specific next state, which makes it less useful as a building block for a bunch of different enemy behaviours.
Indeed!

In his presentation A Curious Course on Coroutines and Concurrency David Beazley looks into this stack issue and apparently proposes "trampolining" as a solution. I've not had a chance to actually look at the presentation in detail yet though.
you can't explicitly invoke a function as a coroutine so that it can yield all the way back to the coroutine caller, you can only allow functions to yield to their immediate caller.

This is why the Python world desperately needs the yield from statement (see PEP 380, Syntax for Delegating to a Subgenerator),

PEP 380 in my opinion feels like a small hack to cover for a lack of proper coroutines. It keeps the fundamental difference between coroutines and regular routines, which I don't think should really exist, and just uses syntactic sugar to hide the conceptual problem.