-
Notifications
You must be signed in to change notification settings - Fork 4
How does coroutine execution work?
This document explains the process by which coroutines are built using function calls, and how coroutines are executed. If you're looking for an explanation of the inner workings of the GML syntax extension, please see How do we extend GML?
Coroutine execution in this library centres around the "coroutine root struct", a data container that both manages the execution of the coroutine as well as stores coroutine state (the variables that you read and write whilst the coroutine is executing). When a coroutine is created, it is registered in a global list of coroutines that are processed every frame. When a coroutine completes it is removed from that global list meaning the memory allocated to it can be freed as well, provided that your code isn't holding a reference to it.
When a coroutine is defined, the root struct is given a sequence of instructions it must carry out. Instructions can simply be "run this block of GML", or the can be flow control (loops and branching), or they can be behaviours that require the root struct to wait for further input. Instructions are added to the root struct using function calls that look like this:
__CoroutineFunction(function() //Instruction 1
{
i = 0;
});
__CoroutineWhile(function() //Instruction 2
{
return (i < 6);
});
__CoroutineFunction(function() //Instruction 3
{
show_debug_message("Six messages!");
show_debug_message("(i=" + string(i) + ")");
});
__CoroutineEndLoop(); //Instruction 4
__CoroutineFunction(function() //Instruction 5
{
show_debug_message("Done!");
});
We call a sequence of function calls that adds instructions to a coroutine a "generator function". Note how the coroutine generator function requires a step to start the while-loop __CoroutineWhile()
and another instruction to end the loop __CoroutineEndLoop()
. This generator function is equivalent to the following standard GML:
var i = 0;
while (i < 6)
{
show_debug_message("Six messages!");
show_debug_message("(i=" + string(i) + ")");
}
show_debug_message("Done!");
It's obvious by comparing the generator function and its standard GML equivalent that the standard GML is much more compact! We solve this problem by extending what syntax can be used in GML - please see How do we extend GML? for more information.
At any rate, coroutines are built using these generator function calls. All these functions are doing is pushing data into an instruction array, or changing which instruction array the data is being pushed into. We start off by writing data into the coroutine's root struct, but when we enter into the while-loop we instead start pushing data into the while-loop. After we end the while-loop we return to pushing instructions into the root struct's array again.
We can visualize this by writing this out as nested arrays (I'm abbreviating function calls here for simplicity):
root : [
i = 0; Instruction 1
while (i < 6) : [ Instruction 2
"Six messages!" Instruction 3
"(i=" + string(i) + ")"
] Instruction 4
"Done!" Instruction 5
]
@jujuadams 2021