User Tools

Site Tools


programming:pico8:recipes:coroutine

Differences

This shows you the differences between two versions of the page.

Link to this comparison view

Next revision
Previous revision
programming:pico8:recipes:coroutine [2022-02-13 19:37] – created zlgprogramming:pico8:recipes:coroutine [2022-02-21 00:40] (current) – zut alor, I missed one! zlg
Line 1: Line 1:
 ====== Coroutines ====== ====== 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!**+**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. 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.
Line 9: Line 9:
 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. =)) 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.+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. 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 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!+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 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: Here's a text diagram that illustrates:
Line 20: Line 20:
 COROUTINE EXECUTION MODEL COROUTINE EXECUTION MODEL
  
-coroutine +coroutine updater, executed every frame 
-  └─> loop running coresume() via list of coroutines+  └─> running coresume() on all coroutines in global list
         ├─> coroutine1         ├─> coroutine1
         │     └─> while condition do         │     └─> while condition do
Line 47: Line 47:
  
 <code lua coroutines.lua.p8> <code lua coroutines.lua.p8>
--- this gets turned into a coroutine +function update_cors() 
-function corhandler() +  if #corlist > 0 then
-  while true do+
     for cor in all(corlist) do     for cor in all(corlist) do
       if costatus(cor) != 'dead' then       if costatus(cor) != 'dead' then
Line 57: Line 56:
       end       end
     end     end
-    yield() 
   end   end
 end end
Line 64: Line 62:
   corlist = {}   corlist = {}
   objlist = {}   objlist = {}
-  gcor = cocreate(corhandler) 
 end end
  
Line 73: Line 70:
     end     end
   end   end
-  if #corlist > 0 then +  update_cors()
-    if costatus(gcor!= 'dead' then +
-      -- the magic happens here +
-      coresume(gcor) +
-    end +
-  end+
 end end
  
Line 92: Line 84:
 </code> </code>
  
-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:+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:
  
 <code lua> <code lua>
--- corhandler version 2! +-- update_cors, version 2! 
-function corhandler()+function update_cors()
   local s   local s
   local t   local t
-  while true do +  if #corlist > 0 then 
-    if #corlist > 0 then +    for c in all(corlist) do 
-      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         if type(c) == 'table' then
-          s = costatus(c[1]) +          coresume(t, unpack(c, 2))
-          c[1]+
         else         else
-          s = costatus(c) +          coresume(t)
-          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
 +      else
 +        del(corlist, c)
       end       end
     end     end
-    yield() 
   end   end
 end end
 </code> </code>
  
-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:+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:
  
 <code lua> <code lua>
Line 133: Line 122:
 } }
 -- this will create a table with two entries: -- this will create a table with two entries:
---   one containing a function, the other a table.+-- one containing a function, the other a table.
 </code> </code>
  
-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 timewith stacktrace capabilitiesso we can debug our coroutines more easily. +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.
  
 <code lua> <code lua>
--- corhandler version 3! +-- update_cors, version 3! 
-function corhandler()+function update_cors()
   local s   local s
   local t   local t
-  while true do +  if #corlist > 0 then 
-    if #corlist > 0 then +    for c in all(corlist) do 
-      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         if type(c) == 'table' then
-          = c[1]+          active, exception coresume(t, unpack(c, 2))
         else         else
-          c+          active, exception coresume(t)
         end         end
-        s = costatus(t) +        if exception then 
-        if s != 'dead' then +          printh(trace(t, exception)) 
-          if type(c) == 'table' then +          stop(trace(t, exception))
-            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
 +      else
 +        del(corlist, c)
       end       end
     end     end
-    yield() 
   end   end
 end end
 </code> </code>
  
-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''!+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 ===== ===== 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()'':+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()'':
  
 <code lua> <code lua>
Line 189: Line 174:
 ''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. ''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.+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'pretend it's a simple table with ''x'', ''y'', and ''sid'' members:+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'say it's a simple table with ''x'', ''y'', and ''sid'' members:
  
 <code lua> <code lua>
-my_obj = {x = 60, y = 60, sid = 1}+throbber = {x = 60, y = 60, sid = 1}
 </code> </code>
  
-''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()''!+''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()''!
  
 <code lua> <code lua>
Line 216: Line 201:
  
 <code lua> <code lua>
-add(corlist, {cocreate(toggle), my_obj})+add(corlist, {cocreate(toggle), throbber})
 </code> </code>
  
 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! 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:+...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:
  
 <code lua> <code lua>
 -- we need this to test against tables -- we need this to test against tables
 function has(t, v) function has(t, v)
- local found=false +  local found=false 
- for a in all(t) do +  for a in all(t) do 
-  if a == v then +    if a == v then 
-   found=true+      found=true 
 +    end
   end   end
- end +  return found
- return found+
 end end
  
Line 252: Line 237:
 </code> </code>
  
-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!+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 ===== ===== Example =====
  
 FIXME TODO: Produce a spartan coroutine-based cartridge showcasing how to use engine. 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]]
programming/pico8/recipes/coroutine.1644781072.txt.gz · Last modified: 2022-02-13 19:37 by zlg