====== 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 function 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 runs "on the side" within PICO-8, meaning it can pause execution and pass it back to whatever called ''coresume()'', allowing us to orchestrate coroutines to get parallel-ish computing. This gives us functions that last longer than a frame. If that coroutine calls a function that then yields, it pauses the ENTIRE coroutine environment, 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 unit of work. Each thing needs its own execution space to ''yield()'' from, if we want full flexibility. Ideally, one calls a function every frame, 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 updater, executed every frame └─> running coresume() on all coroutines in global list ├─> 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: function update_cors() if #corlist > 0 then for cor in all(corlist) do if costatus(cor) != 'dead' then coresume(cor) else del(corlist, cor) end end end end function _init() corlist = {} objlist = {} end function _update60() if #objlist > 0 then for o in all(objlist) do o:update() end end update_cors() 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 ''update_cors'' considerably: -- update_cors, version 2! function update_cors() local s local t 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 end The above version of ''update_cors'' 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 robust coroutine wrangler! We can still improve it one more time – with stacktrace capabilities – so we can debug our coroutines more easily. -- update_cors, version 3! function update_cors() local s local t 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 end At this point, ''update_cors'' 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 coroutine**. 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 dialog 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 say it's a simple table with ''x'', ''y'', and ''sid'' members: throbber = {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 function that can be 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), throbber}) 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. They also cost tokens and add complexity. Use only as needed. :) ===== Example ===== FIXME TODO: Produce a spartan coroutine-based cartridge showcasing how to use engine. ====== Credits ====== Thanks to @merwok for feedback on the [[https://www.lexaloffle.com/bbs/?tid=46571|Lexaloffle BBS thread]]