Using Lua's coroutines in your game

14 Dec 2012

Hoo boy, this turned out to be a long one. In short, I wanted a better way to implement cutscenes in Project Walnut, and with a bit of Lua I ended up with the ability to run a bunch of independent processes within the game and synchronize between them easily.

There’s a demo project showing it off here, but please, read on for details.

Project Walnut opens with a cutscene, and it was a pain in the ass to put together. It’s a jumbled up mess of conditional statements, flag variables, and convulted logic for 10 seconds of time where you aren’t even playing the darn game that took me hours to put together, and it’s a good chunk of why I took a month of from working on the game after slipping my deadline. I wasn’t terribly motivated to work on the game when every little scene was going to be that painful to put together.

Actually, I’ll go ahead and share what it looked like when I called it done:

- (void)updateOpeningSceneWithTimeDelta:(float)deltaTime
{
    [_tilemap updateWithDeltaTime:deltaTime];
    [_bees updateWithTimeDelta:deltaTime];
    [_jackie updateWithDeltaTime:deltaTime];
    [_jon updateWithDeltaTime:deltaTime];
    [_colin updateWithDeltaTime:deltaTime];
    [_kayla updateWithDeltaTime:deltaTime];
    [_kefka updateWithDeltaTime:deltaTime];
    [_dialogNode updateWithDeltaTime:deltaTime];

    if (!_hasKefkaAppeared && !_hasKefkaStolenFamily) {
        if (_kayla.followingPath == NO) {
            _kayla.facing = Direction_Down;
        }

        if (_colin.followingPath == NO) {
            _colin.facing = Direction_Down;
        }

        if (_jon.followingPath == NO) {
            _jon.facing = Direction_Up;
        }

        if (_jackie.followingPath == NO) {
            _jackie.facing = Direction_Up;
        }

        if (_jackie.followingPath == NO && _dialogNode.done) {
            // She reached her destination, bring in Kefka
            _hasKefkaAppeared = YES;
            _jackie.facing = Direction_Left;
            _jon.facing = Direction_Left;
            _kayla.facing = Direction_Left;
            _colin.facing = Direction_Left;
            [_jon stopWalking];
            [_kayla stopWalking];
            [_colin stopWalking];

            [_fadeNode fadeWithType:FadeType_Flash time:1.0f];

            [_dialogNode addLine:@"Wizard:\nMwuahahahah!"];
            [_dialogNode showNextLine];
        }
    } else if //...

The full setup and update methods are linked above. A good deal of the complexity is my fault: some sort of event system would help things, or I could have built it as a state machine, etc. The thing that complicates it the most though is that it has to be written as a function that gets called and returns, maintaining state somewhere else (hence _hasKefkaAppeared and _hasKefkaStolenFamily).

Last week, I came across the State-Based Scripting in Uncharted 2: Among Thieves slide deck. It’s an excellent read, but two parts really jumped out at me (starting at slide 64):

I immediately thought “Oh man, I want that.” The section on implementation (starting at slide 138) mentions how they pull it off: each track is implemented as a continuation. Essentially, a running function is stopped when it calls one of the wait-* functions, and restarted from that exact point later on. From the example in the slides:

(track ("player")
  [wait-move-to "player" "waypoint7"]
  [signal "player-at-waypoint"]
  [wait-for-signal "sully-at-waypoint"]
  [wait-animate "player" "shake-sullys-hand"])

(track ("sullivan")
  [wait-move-to "sullivan" "waypoint7"]
  [signal "sully-at-waypoint"]
  [wait-for-signal "player-at-waypoint"]
  [wait-animate "sullivan" "shake-drakes-hand"])

The two different tracks will block on each of the wait functions, and start at the next expression when the wait criteria is met.

Getting that in C or Objective-C (what I’ve been using to this point) is possible, but it’s hacky and I’m not sure how well it’d work in practice. Lua however, has coroutines built into the language, and they can be used to implement much the same thing. And here’s how to do it.

I’m going to work under the following assumptions:

Let’s take a look at a sample script:

print("Hello, world")
waitSeconds(2)
print("I'll print this out 2 seconds after I printed out Hello, world")

The first thing we’ll need to do is turn this into a coroutine, which means that the above script must be wrapped into a function:

function waitSecondsTest()
    print("Hello, world")
    waitSeconds(2)
    print("I'll print this out 2 seconds after I printed out Hello, world")
end

local co = coroutine.create(waitSecondsTest)
coroutine.resume(co) -- This runs the function above

That waitSeconds function needs to somehow register the running coroutine to be woken back up and then suspend. Then in 2 seconds, something needs to resume that coroutine. So we get this:

-- This table is indexed by coroutine and simply contains the time at which the coroutine
-- should be woken up.
local WAITING_ON_TIME = {}

-- Keep track of how long the game has been running.
local CURRENT_TIME = 0

function waitSeconds(seconds)
    -- Grab a reference to the current running coroutine.
    local co = coroutine.running()

    -- If co is nil, that means we're on the main process, which isn't a coroutine and can't yield
    assert(co ~= nil, "The main thread cannot wait!")

    -- Store the coroutine and its wakeup time in the WAITING_ON_TIME table
    local wakeupTime = CURRENT_TIME + seconds
    WAITING_ON_TIME[co] = wakeupTime

    -- And suspend the process
    return coroutine.yield(co)
end

function wakeUpWaitingThreads(deltaTime)
    -- This function should be called once per game logic update with the amount of time
    -- that has passed since it was last called
    CURRENT_TIME = CURRENT_TIME + deltaTime

    -- First, grab a list of the threads that need to be woken up. They'll need to be removed
    -- from the WAITING_ON_TIME table which we don't want to try and do while we're iterating
    -- through that table, hence the list.
    local threadsToWake = {}
    for co, wakeupTime in pairs(WAITING_ON_TIME) do
        if wakeupTime < CURRENT_TIME then
            table.insert(threadsToWake, co)
        end
    end

    -- Now wake them all up.
    for _, co in ipairs(threadsToWake) do
        WAITING_ON_TIME[co] = nil -- Setting a field to nil removes it from the table
        coroutine.resume(co)
    end
end

function runProcess(func)
    -- This function is just a quick wrapper to start a coroutine.
    local co = coroutine.create(func)
    return coroutine.resume(co)
end

-- And a function to demo it all:
runProcess(function ()
    print("Hello world. I will now astound you by waiting for 2 seconds.")
    waitSeconds(2)
    print("Haha! I did it!")
end)

And that’s it. Call wakeUpWaitingThreads from your game logic loop and you’ll be able to have a bunch of functions waking up after sleeping for some period of time.

Note: this might not scale to thousands of coroutines. You might need to store them in a priority queue or something at that point.

Time’s done, how about signals? Another little demo:

runProcess(function()
    print("1: I am the first function. The second function cannot speak until I say it can.")
    waitSeconds(2)
    print("1: In two more seconds, I will allow it to speak.")
    waitSeconds(2)
    signal("ok, you can talk")
    waitSignal("function 2 done talking")
    print("1: First function again. I'm done now too.")
end)

runProcess(function()
    waitSignal("ok, you can talk")
    print("2: Hey, I'm the second function. I like talking.")
    waitSeconds(2)
    print("2: I'd talk all the time, if that jerky first function would let me.")
    waitSeconds(2)
    print("2: I guess I'm done now though.")
    signal("function 2 done talking")
end)

To implement signals, we’ll use another table, this one indexed by signal instead of coroutine. There’s a pretty good chance you’ll want several coroutines waiting on the same signal, so we’ll store a list of them for each signal in the table.

local WAITING_ON_SIGNAL = {}

function waitSignal(signalName)
    -- Same check as in waitSeconds; the main thread cannot wait
    local co = coroutine.running()
    assert(co ~= nil, "The main thread cannot wait!")

    if WAITING_ON_SIGNAL[signalStr] == nil then
        -- If there wasn't already a list for this signal, start a new one.
        WAITING_ON_SIGNAL[signalName] = { co }
    else
        table.insert(WAITING_ON_SIGNAL[signalName], co)
    end

    return coroutine.yield()
end

function signal(signalName)
    local threads = WAITING_ON_SIGNAL[signalName]
    if threads == nil then return end

    WAITING_ON_SIGNAL[signalName] = nil
    for _, co in ipairs(threads) do
        coroutine.resume(co)
    end
end

Easy peasy. The waitSignal stuff doesn’t need to be called per-frame; the wakeups only happen when the signal function is called. You have to be a little careful with the order of operations when you’re using signals to synchronize between coroutines, or you might end up with one waiting for a signal that has already been sent.

On top of those two operations, I think you can implement whatever sort of waiting functions you like. For example, I have a walkToPointAndSignal function in my game now that’ll find a path from the character’s current location to whatever target point is given, move the character along that path, and send a signal when it’s done. In fact, the script I’m using to test this out in my game now looks like this:

function openingScene()
    -- Stops both the player from moving around and the camera from tracking the main character for abit
    disableControls()

    setCameraCenter(waypoint('forest.camera_start'))
    activate('jackie')
    activate('jon')
    activate('kayla')
    activate('colin')

    setPosition('jackie', waypoint('forest.jackie_start'))
    setPosition('jon', waypoint('forest.jon_start'))
    setPosition('kayla', waypoint('forest.kayla_start'))
    setPosition('colin', waypoint('forest.colin_start'))

    walkToPointAndSignal('jackie', 'jackie-arrived-at-waypoint', waypoint('forest.jackie_stop'))
    walkToPoint('jon', waypoint('forest.jon_stop'))
    walkToPoint('kayla', waypoint('forest.kayla_stop'))
    walkToPoint('colin', waypoint('forest.colin_stop'))

    wait_signal('jackie-arrived-at-waypoint')

    -- Fire off the processes that make the kids speak.
    coroutine.wrap(colinIdle)()
    coroutine.wrap(kaylaIdle)()

    -- Hand control back over to the player
    enableControls()
end

function runOpeningScene()
    local co = coroutine.create(openingScene)
    coroutine.resume(co)
end

local colinSays = { "I hungry.", "I want milk", "Can't get me!" }
local kaylaSays = { "I'm bored, can we go home?", "Look! A bee!", "What's that?", "Can I pick a flower?" }

function colinIdle()
    while true do
        wait(math.random(8))
        say('colin', colinSays[math.random(#colinSays)])
        wait(3)
        say('colin', '')
    end
end

function kaylaIdle()
    while true do
        wait(math.random(8))
        say('kayla', kaylaSays[math.random(#kaylaSays)])
        wait(3)
        say('kayla', '')
    end
end

I’ve put together a sample project to show this all working together; you can take a look at it on Bitbucket. It’s a little bit overkill in that it opens a full OpenGL window to draw some text, but I wanted it to at least have the semblance of a game. It should build on Windows (I used Visual Studio 2008), Mac OS X (Xcode 4.5), and Linux (umm, GNU make). You’ll need to have development headers/libraries for SDL and SDL_ttf installed to build it.

The important bits are in:

Enjoy!