I recently started working a a little coroutine library for cc65 very similar to the simple API for Lua coroutines. It currently comes in at ~200 bytes of code, and I think the yield/resume cost is reasonable from eyeballing the asm, but I haven't actually done any cycle counting.
https://gist.github.com/slembcke/ef7ae2 ... e9b2cbb948
For anyone that's not familiar, they are basically really lightweight threads that you explicitly switch between. They are really useful for things like animations or state machines where you want to a function that runs over several frames.
Basically, the game loop code for the block falling game I'm working on was getting a little confusing. A lot of code was spread out over several frames for animation or cost amortization reasons, and it was getting hard to see what the actual flow was. Something gross like this:
Gross and unwieldy, so I started refactoring it to use function pointers to separate the flow better than an else-if chain, but the code to switch between state functions was kinda dumb too. Either it had to be inlined into the previous state's function, where it didn't really seem to belong, or separated out into yet another function. Coroutines seemed like they would be fun to implement, so I did. Now the code looks more like this:
Using coroutines this way, basically every time coro_yield() is called, it waits for a frame and goes back to executing the "main thread" until it's resumed again. This makes it really easy to write code that happens over time, but is nicely contained in a single function without obscuring the control flow.
On my TODO list yet:
https://gist.github.com/slembcke/ef7ae2 ... e9b2cbb948
For anyone that's not familiar, they are basically really lightweight threads that you explicitly switch between. They are really useful for things like animations or state machines where you want to a function that runs over several frames.
Basically, the game loop code for the block falling game I'm working on was getting a little confusing. A lot of code was spread out over several frames for animation or cost amortization reasons, and it was getting hard to see what the actual flow was. Something gross like this:
Code:
void update(){
if(timer == 0){
// Make blocks fall
} else if(timer < grid_height){
// Blit updated tiles to the screen one row of blocks at a time
} else if(timer < wait_time){
// Apply matching logic, etc
// Possibly reset timer to grid_height to reset the wait, but not trigger logic above.
} else if(){
// ... more of the same
}
++timer;
}
if(timer == 0){
// Make blocks fall
} else if(timer < grid_height){
// Blit updated tiles to the screen one row of blocks at a time
} else if(timer < wait_time){
// Apply matching logic, etc
// Possibly reset timer to grid_height to reset the wait, but not trigger logic above.
} else if(){
// ... more of the same
}
++timer;
}
Gross and unwieldy, so I started refactoring it to use function pointers to separate the flow better than an else-if chain, but the code to switch between state functions was kinda dumb too. Either it had to be inlined into the previous state's function, where it didn't really seem to belong, or separated out into yet another function. Coroutines seemed like they would be fun to implement, so I did. Now the code looks more like this:
Code:
// ... Somewhere in the game init code.
// Sets the function to use as a coroutine.
coro_start(update_coro);
void update(){
// Resume executing the coroutine from where it last yielded.
coro_resume();
}
void update_coro(){
while(true){
// Make blocks fall.
// Yield jumps back to the main thread as if coro_resume() was returning.
coro_yield();
for(...){
// Blit row of blocks to the screen
coro_yield();
}
timer = 0
while(timer < wait_time){
// Apply matching logic and such.
// Possibly reset timer back to 0.
coro_yield();
}
// ... More events in the loop.
}
}
// Sets the function to use as a coroutine.
coro_start(update_coro);
void update(){
// Resume executing the coroutine from where it last yielded.
coro_resume();
}
void update_coro(){
while(true){
// Make blocks fall.
// Yield jumps back to the main thread as if coro_resume() was returning.
coro_yield();
for(...){
// Blit row of blocks to the screen
coro_yield();
}
timer = 0
while(timer < wait_time){
// Apply matching logic and such.
// Possibly reset timer back to 0.
coro_yield();
}
// ... More events in the loop.
}
}
Using coroutines this way, basically every time coro_yield() is called, it waits for a frame and goes back to executing the "main thread" until it's resumed again. This makes it really easy to write code that happens over time, but is nicely contained in a single function without obscuring the control flow.
On my TODO list yet:
- Allow the stack buffer to be placed anywhere in RAM and not hard coded in the .s file.
- Allow switching coroutines (push their state onto their stack buffers).
- Maybe remove the values passed in and out of yield/resume. Not as useful as in Lua without dynamic typing.