I've recently finished writing a page on my personal wiki about coroutines, and quick examples on how to make the most of them. I would appreciate any feedback people have, including misconceptions I have regarding the execution model. Most of my PICO-8 work as of late has been figuring out how to create easy animation engines that are also expressive and flexible. I think I have the functionality down; now comes the hard part of nailing coroutine and data structure design for complex animations!
I'd love to read what you guys think of it.
https://wiki.zlg.space/programming/pico8/recipes/coroutine
Thanks for reading!
some notes:
-
I would not bring the word «thread» in a discussion of pico8 coroutines, as it could bring previous knowledge of OS threads into the reader’s mind and create confusion (even if your text does say they are not running in parallel)
- curious about why the corhandler function is a coroutine itself; it could be a normal function (update_coroutines) that resumes all active coroutines. I even wonder if it should not be a regular function, given that yielding from a nested coresume call will propagate all the way to the top of the stack of coroutine functions.
@merwok: Thanks for the feedback! I was aware coroutines and true OS threads are different, but not quite sure how else to relate them to the reader. The end result is similar to parellel execution, even though it isn't.
In a cart I'm working on, I removed the infinite loop from corhandler
and replaced the gcor
stuff in _update()
with a plain call to corhandler; it seems to work just fine! Perhaps it doesn't need to be a coroutine. My first brush with the structure was in @ddaann's Cutscenes and Coroutines. Perhaps he put it in a coroutine for cutscene-related reasons that don't apply with my engine. Great question though, and the code's a bit simpler. Win!
I've updated the page. The changes can be seen via the page's history, but I more or less did s/corhandler/update_cors/
, then cleaned up the prose to match, and reword places where 'thread' was used.
> but not quite sure how else to relate them to the reader
why not the straight way: coroutines are function calls that can be paused and resumed, keeping their local state.
another piece of feedback: you update_cors function seem to pass parameters for each call (when cor is a table), but it’s only the first call to coresume after cocreate that can pass parameters.
Without some way of determining when a coroutine has been created, but not started, I'm not sure how to pass arguments only the first time. costatus()
only detects between "suspended" and "dead", and the Fandom wiki indicates this on coresume()
:
> coresume() may pass extra arguments to the coroutine. These arguments will be mapped to the coroutine's function arguments on the first call, and will be used as return values for yield() on subsequent calls.
(See: https://pico-8.fandom.com/wiki/Coresume)
The return values part has to do extra arguments being passed back for functions that don't yield, or something like that. The Lua documentation offers a little insight: http://www.lua.org/pil/9.1.html
A workaround is to add a state variable to each entry in the corlist, which adds unnecessary complexity and RAM use, for a variable whose useful lifetime is only the first frame of operation.
Since update_cors only updates them, it can't know how many frames a coroutine has been in the list, or that it wasn't there last frame. I think the limitation of Lua coroutines being unable to express an 'unstarted' or similar state prevents this from being easy or economical to put together. If that were possible, it'd be a third case that update_cors could check for.
If you know of a trick for this, I'd be happy to read about it!
Apologies if I'm misunderstanding the problem, but if you want to pass parameters to a co-routine function, you can wrap it in an inline function.
I.e. instead of
c=cocreate(myfunction) ... function myfunction() -- no params :-( end |
You would write
c=cocreate(function()myfunction(1,2,3)end) ... function myfunction(a,b,c) -- i have params :-) end |
Mot, that is not necessary: the first call to coresume passes the parameters that the function wants
coro = cocreate(myfunction) coresume(coro, 1, 2, 3) -- pass a,b,c to myfunction and proceed to first yield |
zlg: good points! I had forgotten about passing params to be returned by yield inside the coroutine, I had a test cart for this and it is indeed something we can do. Now it’s a shame that we can’t return values to the caller of coresume! In my usage I pass a table to the coroutine function (for example an actor with x/y coords or visible boolean for blinking) or I change globals (I have very few, like camera x/y).
I haven’t had issue with the lack of distinction between created and started because I haven’t yet written a cart fully based on a coroutine engine: I only create some for transition effects and AI in _update, so I don’t mind calling cocreate+coresume to go to the first yield.
I had also forgotten that the lua type for a coroutine (object, not function) is 'thread'! So that’s a good reason for using that word in write-ups. I would state very clearly that these are pausable functions, not OS threads, and don’t run in parallel or concurrently at all.
[Please log in to post a comment]