-
Notifications
You must be signed in to change notification settings - Fork 48
Using #gmcr magic
A coroutine is a kind of a function that you can pause mid-execution and later resume - while maintaining position, local variables, and other context.
As far as videogames go, the common uses for coroutines include:
- Iterators
(looping over all contents of something without writing a bunch of boilerplate code every time) - Timing
("come back here after N frames") - Asynchronous code
("come back here when there's a reply")
GMEdit includes a coroutine generator that creates state machines for scripts using yield
and marked with a #gmcr
tag.
A yield
keyword is exclusive to coroutines and will pause the coroutine's execution.
You might do the following
#gmcr
function test() {
show_debug_message("hello")
yield 1;
show_debug_message("world")
}
And then use your coroutine like so:
var co = test();
co.next(); // prints "hello"
co.next(); // prints "world"
Generated code
It's just a constructor with a "next" method that has a switch-block inside.
function test() {
return new test_coroutine();
}
function test_coroutine() constructor {
__label__ = 0;
result = undefined;
static next = function() {
while (true) switch (__label__) {
case 0/* [L2,c17] begin */:
show_debug_message("hello");
result = 1; __label__ = 1; return true;
case 1/* [L4,c2] post yield */:
show_debug_message("world");
default/* [L6,c1] end */:
result = undefined; __label__ = -1; return false;
}
}
}
So what about that 1
, you might ask? When a coroutine yields, it can return a value,
which will be stored in the result
variable and the next()
call will return true
.
When the coroutine reaches its end or is terminated using return
or exit
,
the final result (if any) is similarly stored in the result
variable
and the next()
call will return false
.
This means that a coroutine can be "iterated" with a simple while-loop.
Coroutine:
#gmcr
function test() {
yield "A";
yield "B";
yield "C";
}
Use:
var co = test();
while (co.next()) {
show_debug_message(co.result);
}
Output:
A
B
C
But let's get to things that are not easy to do by hand, like loops.
Coroutine:
#gmcr
function array_foreach_co(arr) {
var n = array_length(arr);
for (var i = 0; i < n; i++) {
yield arr[i];
}
}
Use:
var co = array_foreach_co([1, 2, 3]);
while (co.next()) {
show_debug_message(co.result);
}
Output:
1
2
3
Generated code
Since we're yielding mid-loop, it has to be broken down into smaller parts.
function array_foreach_co(arr) {
var l_argc = argument_count;
var l_args = array_create(l_argc);
while (--l_argc >= 0) l_args[l_argc] = argument[l_argc];
return new array_foreach_co_coroutine(l_args);
}
function array_foreach_co_coroutine(_args) constructor {
__label__ = 0;
result = undefined;
__argument__ = _args;
static next = function() {
while (true) switch (__label__) {
case 0/* [L2,c28] begin */:
n = array_length(__argument__[0]);
i = 0;
case 1/* [L4,c2] check for */:
if (i >= n) { __label__ = 4; continue; }
result = __argument__[0][i]; __label__ = 2; return true;
case 2/* [L5,c3] post yield */:
case 3/* [L4,c2] post for */:
i++;
__label__ = 1; continue;
case 4/* [L4,c2] end for */:
default/* [L21,c0] end */:
result = undefined; __label__ = -1; return false;
}
}
}
You can also take the values back into the coroutine when resuming:
Coroutine:
#gmcr
function test() {
show_debug_message("hello");
var who = yield 1;
show_debug_message(who);
}
Use:
var co = test();
co.next(); // prints "hello"
co.next("world"); // prints "world"
In a coroutine, local
keyword references the current coroutine struct,
not unlike how self
references the current instance/structure in normal GML code.
You can use this to pass a coroutine to a function called from it - for example, have functions resume the coroutine after it calls them and yields:
#gmcr
function test_wait() {
show_debug_message("This shows instantly!")
yield coro_wait(local, 5);
show_debug_message("This shows after 5 seconds!")
}
Helper:
function coro_wait(_coro, _time_in_s) {
var fn = method({ coro: _coro }, function() {
coro.next();
});
return call_later(_time_in_s, time_source_units_seconds, fn);
}
Use:
test_wait().next();
Since I already did this work anyway, you can also use labels and goto statements in code ran through coroutine generator.
The syntax is as following:
label your_label_name: // ":" is optional
goto your_label_name
Example:
#gmcr
function scr_goto_test() {
show_debug_message("one!");
goto myLabel;
show_debug_message("this won't be executed");
myLabel:
show_debug_message("two!");
}
GMEdit's coroutine generator also supports "flat"/"linear" coroutines. These primarily exist for GameMaker versions that did not have structs and constructors, but you may also opt to use them in modern GameMaker versions by changing your tag line to
#gmcr linear
"Flat" coroutines will store their state in an array instead of a struct, which takes up less memory, but changes how you would use your coroutines afterwards - so, instead of
var co = test(arr);
while (co.next()) {
show_debug_message(co.result);
}
You would have to write
var co = test(0, arr);
while (test(co)) {
show_debug_message(co[0]);
}
The way this works is that the coroutine function takes a prefix argument being the coroutine state - if that's an array, it'll do an iteration (like .next()
would for standard coroutines). Otherwise it'll create a new coroutine and return it.
In older GameMaker versions, you'll need to import this extension that provides some scripts to make up for lack of accessor chaining.
- Juju's Coroutines library implements coroutines through macro trickery involving function literals. This approach makes it possible to work with coroutines in GameMaker IDE relatively comfortably, but has a higher memory cost than GMEdit's pre-generated coroutines.
- Promises can be used in place of coroutines for asynchronous code, though generally not code with conditions and loops.
- Smart auto-completion
- Types
- JSDoc tags (incl. additional ones)
- @hint tag (mostly 2.3)
- `vals: $v1 $v2` (template strings)
- #args (pre-2.3 named arguments)
- ??= (for pre-GM2022 optional arguments)
- ?? ?. ?[ (pre-GM2022 null-conditional operators)
- #lambda (pre-2.3 function literals)
- => (2.3+ function shorthands)
- #import (namespaces and aliases)
- v:Type (local variable types)
- #mfunc (macros with arguments)
- #gmcr (coroutines)