Using Lua's coroutines in your game
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):
- They have separate threads of execution within the scripts (they call them tracks)
- Those threads can block and wait on various things (time passing, events, signals, etc)
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:
- You want the ability to wait for a certain time, or for a signal/event to occur.
- You keep track of how much time is passing per frame in your game loop.
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:
- WaitSupport.lua has the waitSeconds, waitSignal, signal, etc. stuff described above
- Demo.lua uses those along with a couple functions defined in Main.cpp to give you a short conversation between George and Bob.
- Main.cpp opens up an SDL window and runs the main loop, which calls Lua’s wakeUpWaitingThreads function.
Enjoy!