Skip to content
Vadym Diachenko edited this page Sep 6, 2023 · 4 revisions

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:

  1. Iterators
    (looping over all contents of something without writing a bunch of boilerplate code every time)
  2. Timing
    ("come back here after N frames")
  3. 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.

yield

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;
		}
	}
}

Outputs

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

Outputs (continued)

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;
		}
	}
}

Inputs

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"

local and async resuming

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();

label and goto

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!");
}

"Flat" coroutines

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.

Use in GameMaker: Studio and GMS2.2.5

In older GameMaker versions, you'll need to import this extension that provides some scripts to make up for lack of accessor chaining.

Alternatives

  • 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.