Back

Bonus: Coroutines

Optional — the most distinctively Lua thing in the language, and the one with no real equivalent in most others.

divider

The Idea

Every function you have written runs from top to bottom and then stops. A coroutine is a function that can pause in the middle, hand control back, and later carry on from exactly where it left off — with all its local variables still in place.

main.lua
local function sequence()
print("step 1")
coroutine.yield()
print("step 2")
coroutine.yield()
print("step 3")
end
local co = coroutine.create(sequence)
print("status:", coroutine.status(co))
coroutine.resume(co)
coroutine.resume(co)
coroutine.resume(co)
print("status:", coroutine.status(co))
Output
status: suspended
step 1
step 2
step 3
status: dead

Three functions do all the work:

  • coroutine.create wraps a function up. It does not run it — note that nothing printed until the first resume.
  • coroutine.resume starts it, or continues it from its last pause.
  • coroutine.yield pauses from the inside and returns control to whoever resumed.

This is not a thread. Nothing runs at the same time as anything else, there is no scheduler, and you never need a lock. Control moves between two places, and only one of them is ever running. That is why coroutines are easy to reason about and threads are not.

The shortcut: wrap

coroutine.wrap hands you a plain function instead of a coroutine object. Call the function to resume it, and whatever it yields comes straight back.

main.lua
local squares = coroutine.wrap(function()
for i = 1, 4 do
coroutine.yield(i * i)
end
end)
print(squares())
print(squares())
Output
1
4

Look at what that is. A function you can keep calling, which remembers where it got to and produces the next value. That is exactly the iterator contract from Activity 1.8 — so a wrapped coroutine can drive a for ... in loop directly, and when it finishes it returns nil and the loop stops on its own.

The difference from a closure iterator is what you can express. A closure has to reconstruct its position from stored variables every call; a coroutine just keeps standing where it was. For walking a nested structure, that is the difference between a bookkeeping exercise and a recursive function with a yield in it.

wrap has one cost: create plus resume reports errors by returning false, like pcall, whereas a wrapped one raises them at you. Use wrap for iterators and create when you want to check the status.

Why a game programmer cares

Here is the problem coroutines solve. A game runs one function per frame, sixty times a second. You want to write: open the door, wait two seconds, sound the alarm, wait one more, cut the lights.

Without coroutines that becomes a state machine — a phase variable, a timer, and a growing chain of elseif — and the sequence you wanted is scattered across it. With coroutines you write the sequence.

main.lua
local function wait(seconds)
local elapsed = 0
while elapsed < seconds do
elapsed = elapsed + coroutine.yield()
end
end
local cutscene = coroutine.create(function()
print("Door opens")
wait(2)
print("Alarm sounds")
wait(1)
print("Lights out")
end)
for frame = 1, 8 do
if coroutine.status(cutscene) ~= "dead" then
coroutine.resume(cutscene, 0.5)
end
end
Output
Door opens
Alarm sounds
Lights out

The loop stands in for a game's frames, each one worth half a second. The value passed to resume comes back out of yield inside the coroutine, which is how wait learns how much time went by. Values travel both directions.

In Unit 2 the frame loop is love.update(dt) and you would resume with the real dt. The cutscene function would not change at all.

Two things to remember. A dead coroutine cannot be resumed — check the status or you get a false and a "cannot resume dead coroutine" message. And you cannot yield from inside a normal function called by C code, which in practice means avoid yielding from inside a table.sort comparison or a metamethod.

divider

Build

Start a fresh main.lua.


Task 1: Pause and continue

  • Write a function that prints three steps with a coroutine.yield() between each.
  • Create a coroutine from it and print the status before resuming.
  • Resume three times, then print the status again.
  • Confirm nothing printed until the first resume. Creating is not running.
main.lua
-- main.lua - coroutines
local function sequence()
print("step 1")
coroutine.yield()
print("step 2")
coroutine.yield()
print("step 3")
end
local co = coroutine.create(sequence)
print("status:", coroutine.status(co))
coroutine.resume(co)
coroutine.resume(co)
coroutine.resume(co)
print("status:", coroutine.status(co))
Output
status: suspended
step 1
step 2
step 3
status: dead

Task 2: A generator, then a loop

  • Use coroutine.wrap to make something that yields square numbers, and call it twice.
  • Then make a second one and drive it with for n in gen do.
  • Note that you never wrote a stop condition. The coroutine finished, returned nil, and the loop ended.
main.lua
-- main.lua - coroutines
local function sequence()
print("step 1")
coroutine.yield()
print("step 2")
coroutine.yield()
print("step 3")
end
local co = coroutine.create(sequence)
print("status:", coroutine.status(co))
coroutine.resume(co)
coroutine.resume(co)
coroutine.resume(co)
print("status:", coroutine.status(co))
local squares = coroutine.wrap(function()
for i = 1, 4 do
coroutine.yield(i * i)
end
end)
print(squares())
print(squares())
local gen = coroutine.wrap(function()
for i = 1, 3 do coroutine.yield(i) end
end)
for n in gen do
print("got", n)
end
Output
status: suspended
step 1
step 2
step 3
status: dead
1
4
got 1
got 2
got 3

Task 3: A timed sequence

  • Write wait(seconds) that keeps yielding until enough time has been handed to it.
  • Write a cutscene coroutine that prints three lines with waits between them.
  • Drive it with a loop of eight frames, each worth 0.5, skipping the resume once it is dead.
  • Read the cutscene function on its own. It is a straight list of instructions with pauses in it — no state variable, no phase counter. That is the whole argument for coroutines.
main.lua
-- main.lua - coroutines
local function sequence()
print("step 1")
coroutine.yield()
print("step 2")
coroutine.yield()
print("step 3")
end
local co = coroutine.create(sequence)
print("status:", coroutine.status(co))
coroutine.resume(co)
coroutine.resume(co)
coroutine.resume(co)
print("status:", coroutine.status(co))
local squares = coroutine.wrap(function()
for i = 1, 4 do
coroutine.yield(i * i)
end
end)
print(squares())
print(squares())
local gen = coroutine.wrap(function()
for i = 1, 3 do coroutine.yield(i) end
end)
for n in gen do
print("got", n)
end
local function wait(seconds)
local elapsed = 0
while elapsed < seconds do
elapsed = elapsed + coroutine.yield()
end
end
local cutscene = coroutine.create(function()
print("Door opens")
wait(2)
print("Alarm sounds")
wait(1)
print("Lights out")
end)
for frame = 1, 8 do
if coroutine.status(cutscene) ~= "dead" then
coroutine.resume(cutscene, 0.5)
end
end

Challenge (Optional)

  • Change the frame size to 0.25 and confirm the cutscene still produces the same three lines, just over more frames. The sequence is written in seconds, not frames, which is the property you want.
  • Make wait return how much it overshot by, and have the caller print it.
  • Write a coroutine that walks a nested table and yields every value it finds at any depth, then drive it with for v in .... Recursion plus yield makes this about six lines; try it as a closure iterator afterwards and see how much harder it gets.
divider

Check Your Work

Output
status: suspended
step 1
step 2
step 3
status: dead
1
4
got 1
got 2
got 3
Door opens
Alarm sounds
Lights out

Three lines from a function that paused twice, across eight frames it knew nothing about.