This is an old revision of the document!
Table of Contents
Coroutines
Coroutines are functions that can be paused, when run with the coresume()
function. They do so with the yield()
function, which returns control to whatever called coresume
. It's important to remember: when yield()
ing from a coroutine, it ends that 'thread' for the rest of the frame!
Why is that important? For starters, once you get a bunch of coroutines started in your cartridge, you may have trouble understanding the flow of the program.
Execution Model
Coroutines are simple in theory, but difficult to use in practice. The primary rule of coroutines is: try to yield in, at most, ONE place within your coroutine. Most coroutines are loops, so placing the yield()
at the end of the loop is a good practice. This way, you don't end up with effects or animations taking too long for inexplicable reasons. (Tip: It's usually yield()
ing too much when that happens. )
Each time a coroutine is executed (with coresume()
), it creates a sort of 'thread' within PICO-8, which can pause execution and pass it back to whatever called coresume()
, allowing us to orchestrate coroutines to get parallel-ish computing, as well as functions that last longer than a frame. If that coroutine calls a function that then yields, it pauses the ENTIRE thread, not just passing from the inner function to the outer coroutine. To do something like that reliably, the outer coroutine needs to either create a new coroutine and add it to a global list, or execute the coroutine itself, gaining the advantages of yield()
for the inner function.
There is a caveat for this, however: The inner coroutine runs will block the execution of the outer coroutine, so crafting nested coroutines is not suggested.
Consider each thing you want to get achieved as one thread. Each thing needs its own execution space to yield()
from, if we want full flexibility. Ideally, one uses a global coroutine, which then executes each coroutine in a globally-accessible list of coroutines. Then, adding animations or multi-frame logic at all is as easy!
Here's a text diagram that illustrates:
COROUTINE EXECUTION MODEL coroutine └─> loop running coresume() via list of coroutines ├─> coroutine1 │ └─> while condition do │ foo() │ yield() │ done -- a normally yielding coroutine ├─> coroutine2 │ ├─> foo() │ │ └─> yield() -- this pauses coroutine2, NOT foo │ ├─> bar() -- this won't happen until frame 2 │ └─> yield() │ ├─> coroutine3 │ ├─> func1() │ ├─> yield() -- this is the same result as above, just outside func1 │ ├─> func2() │ └─> yield() └─> coroutine4 └─> ...
Code Structure
In a PICO-8 cartridge, you'd use code like this to get started:
- coroutines.lua.p8
-- this gets turned into a coroutine function corhandler() while true do for cor in all(corlist) do if costatus(cor) != 'dead' then coresume(cor) else del(corlist, cor) end end yield() end end function _init() corlist = {} objlist = {} gcor = cocreate(corhandler) end function _update60() if #objlist > 0 then for o in all(objlist) do o:update() end end if #corlist > 0 then if costatus(gcor) != 'dead' then -- the magic happens here coresume(gcor) end end end function _draw() cls() map() if #objlist > 0 then for o in all(objlist) do o:draw() end end end
The above code doesn't account for coroutines that may accept arguments, which will need to differentiate between a coroutine entry in corlist
that is a table (i.e. it's accepting arguments) or a function (just a plain coroutine). This requires modifying corhandler
considerably:
-- corhandler version 2! function corhandler() local s local t while true do if #corlist > 0 then for c in all(corlist) do if type(c) == 'table' then s = costatus(c[1]) t = c[1] else s = costatus(c) t = c end if s != 'dead' then if type(c) == 'table' then coresume(t, unpack(c, 2)) else coresume(t) end else del(corlist, c) end end end yield() end end
The above version of corhandler
can deal with entries in corlist
that are either flat coroutines (the return value of cocreate()
or tables containing a coroutine as the first member:
corlist = { cocreate(foobar), {cocreate(barbaz), "some", "args", "to", "use"} } -- this will create a table with two entries: -- one containing a function, the other a table.
With this in place, we have a pretty powerful coroutine wrangler! As long as corhandler
is the global coroutine, it can handle any of the coroutines in the global corlist
! We can still improve it one more time, with stacktrace capabilities, so we can debug our coroutines more easily.
-- corhandler version 3! function corhandler() local s local t while true do if #corlist > 0 then for c in all(corlist) do if type(c) == 'table' then t = c[1] else t = c end s = costatus(t) if s != 'dead' then if type(c) == 'table' then active, exception = coresume(t, unpack(c, 2)) else active, exception = coresume(t) end if exception then printh(trace(t, exception)) stop(trace(t, exception)) end else del(corlist, c) end end end yield() end end
At this point, corhandler
is as good as it can be! Now you need to put some objects into the objlist
, write a few easy coroutines, and hook them up to the corlist
!
Writing Coroutines
As mentioned a little earlier on this page, yield()
is used to pause execution for the remainder of the frame, for that thread. We also typically only want to yield
in *one* place inside the function, where possible, so we don't end up with multiple-yield, which slows the coroutine down by spreading its work across more frames. Sometimes, this is what we want, with something like wait()
:
function wait(f) for _=1,f do yield() end end
wait
here simply calls yield()
for the number of frames you tell it to. It's an easy way to add a delay to an animation, and is often the first coroutine one learns to use.
Another easy coroutine to learn is toggle()
, which toggles some value back and forth. This is often combined with wait
to create a "throbber". Throbbers are interface elements like what you'd find at the end of a text box's output. It usually oscillates or toggles through graphics to convey that it's waiting for input.
We can do that pretty easily, provided we know a little more about how we're going to handle the graphical units on the screen. For a throbber, let's pretend it's a simple table with x
, y
, and sid
members:
my_obj = {x = 60, y = 60, sid = 1}
sid
is what we'll manipulate in the toggle
coroutine. Let's say it's meant to increase the sid
by one, then wait some time, and toggle it back. We'll use the wait
coroutine we wrote earlier, showing how to build and use coroutines together. Remember, a coroutine is just a thread of code that can paused with yield()
!
function toggle(o) while true do local os = o.sid wait(30) if o.sid == os then o.sid += 1 else o.sid -= 1 end end end
Okay, now we have a snazzy coroutine, but it needs an argument! How do we get it on the corlist
?
add(corlist, {cocreate(toggle), my_obj})
Note that we added a table to the corlist, containing the coroutine we're going to run (toggle
), and the arguments we want sent to it. That's it!
Or is it? This coroutine will continue, even if the object doesn't exist anymore or isn't on any lists used by the engine! Let's try that again:
-- we need this to test against tables function has(t, v) local found=false for a in all(t) do if a == v then found=true end end return found end function toggle(o) while true do if not has(objlist, o) then -- exit coroutine if source object is not being dealt with return end local os = o.sid wait(30) if o.sid == os then o.sid += 1 else o.sid -= 1 end end end
The code's getting longer and a little more complicated, but the result – and goal – is smart behavior in the background that you don't have to think about, so you can focus on the actual coroutines and other mechanics. In the above, I added a check against objlist
, the global table containing objects that need to be updated by the engine! Odds are, if the object is in objlist
, you want it to be accessible to coroutines and other things. If you wanted to be fancier, you could use a drawlist
and then only draw certain things. Lists are handy, but need maintenance!
Example
TODO: Produce a spartan coroutine-based cartridge showcasing how to use engine.