-
Notifications
You must be signed in to change notification settings - Fork 4
Coroutine Syntax
The following are valid command macros used to define coroutines. Please remember that the code that goes in between each coroutine command macro is in separate GML functions. These functions are executed in the same scope (the coroutine root struct) but otherwise they cannot share local variables ("var" variables) due to being in separate functions.
- Basics
- Returning Values
- Loops
- Branching
- Helpers
- Async Events
coroutineRootStruct = CO_BEGIN
show_debug_message("This is an example coroutine.");
CO_END
CO_BEGIN
and CO_END
are required to bracket all coroutine code. Coroutine commands must be placed within these two commands to be valid (and you will likely experience fatal compile errors otherwise). CO_BEGIN
returns a coroutine root struct for the created coroutine instance. If you'd like to read values from the coroutine or control its execution using methods then you will need to keep a reference to the coroutine root struct.
Creating a coroutine will automatically add it to a global list of coroutines to be executed every frame. Once the coroutine has completed it will be removed from global execution and will be available for memory collection. Of course, if you're maintaining a reference to the coroutine beyond its completion then it will not be garbage collected until the reference you hold has also been discarded.
coroutineRootStruct = CO_BEGIN
show_debug_message("This is an example parent coroutine.");
CO_BEGIN
show_debug_message("This is an example child coroutine.");
CO_END
CO_END
Coroutine definitions can be nested inside each other so that one coroutine parent can create additional coroutine children. Child coroutines created inside a parent coroutine will continue executing regardless of whether the parent coroutine was paused, cancelled, or otherwise interacted with. Each child coroutine exists in its own scope such that variables inside each child coroutine are unique to that coroutine. Parent and child coroutines do not share variables, nor do child coroutines share variables with each other.
Child coroutines, if any are created, do not block execution of the parent coroutine - if you'd like child coroutines to block their parent coroutine's execution, please use the RACE
or SYNC
commands (or create your own functionality using AWAIT
).
coroutineRootStruct = CO_BEGIN
show_debug_message("This will");
THEN
show_debug_message("be displayed");
THEN
show_debug_message("in the");
THEN
show_debug_message("same frame");
CO_END
coroutineRootStruct = CO_BEGIN
REPEAT 5 THEN
show_debug_message("Five messages!");
END
CO_END
THEN
by itself has no particular meaning and if used without context will simply append code blocks onto the end of the preceding code block. However, THEN
is required syntax in multiple places and should be used as directed for those commands.
function ShowPopUpMessageTwice(_message)
{
CO_PARAMS.message = _message;
return CO_BEGIN
show_message(message);
DELAY 1000 THEN
show_message(message);
CO_END
}
CO_PARAMS
allows you to set variables in a coroutine before defining it. This is very helpful for passing in arguments if your coroutine is located inside a function that starts new coroutines.
coroutineRootStruct = CO_BEGIN
list = ds_list_create();
CO_ON_COMPLETE
//Clean up the list to avoid a memory leak
ds_list_destroy(list);
CO_END
CO_ON_COMPLETE
adds additional, final code to be executed by the coroutine when it completes. CO_ON_COMPLETE
will also be executed when the .Restart()
method is called.
Please note that the contents of CO_ON_COMPLETE
code must be simple GML. This means you cannot use coroutine commands within the code block.
This is an advanced feature provided for convenience and should not be used without due care and attention.
////Create Event of an object
//Set the scope of the next coroutine to ourselves (CO_SCOPE is reset by CO_END)
CO_SCOPE = self;
//Start
CO_BEGIN
WHILE true THEN //Repeat forever!
//Randomize our position and angle
image_angle = random(360);
x = xprevious + random_range(-5, 5);
y = yprevious + random_Range(-5, 5);
//Wait 120ms before doing this again
DELAY 120 THEN
END
CO_END
////Draw Event
draw_self();
It is useful, from time to time, to have a coroutine interact directly with an instance's (or struct's) state directly. CO_PARAMS
and .GetCreator()
are provided to help smooth interaction between a coroutine and other data containers in your game, but calling code in a specific scope can be advantageous.
When a coroutine is generated, all code between each command is collected together in a native GameMaker function. The scope of this function, by default, is forced to be the root coroutine struct. This ensures instance variables are always created and modified in an isolated environment. Whilst this is substantially safer than alternatives, it can also be inconvenient. CO_SCOPE
overrides the default behaviour (scoping to the coroutine struct) such that functions are scoped to an instance or struct of your choosing.
The coroutine struct is still generated, however, and will exist as an endpoint to call coroutine methods. All coroutine methods are still accessible by directly referencing the coroutine struct returned by CO_BEGIN
.
CO_SCOPE
applies to the next coroutine definition, and the next definition only. When CO_END
is called, CO_SCOPE
will be reset to the default behaviour (scoping to the root coroutine struct). In this regard, CO_SCOPE
is similar to CO_PARAMS
.
Please note that by using CO_SCOPE
it is very easy to create race conditions where two coroutines fight to set the same value for an instance. This can lead to unpleasant and tricky bugs to fix. You use this feature at your own risk.
CO_PARAMS.cells_to_travel = 10;
CO_SCOPE = self;
CO_BEGIN
CO_LOCAL.i = 0; //Use a coroutine variable to count how many times we've moved
WHILE CO_LOCAL.i < CO_LOCAL.cells_to_travel THEN
//Move down the grid, 32px at a time
y += 32;
//Change our sprite
sprite_index = sprMoveDown;
//Wait 90ms before doing this again
DELAY 90 THEN
END
CO_END
CO_LOCAL
contains a reference to the coroutine that is currently being processed. By default, CO_LOCAL
will be the self
scope inside coroutine code blocks. This changes if you're using CO_SCOPE
(see above) because the coroutine code blocks are now running in the scope of some other instance/struct. In order to be able to reference sandboxed variables held by the coroutine root struct, CO_LOCAL
is required.
coroutineRootStruct = CO_BEGIN
show_debug_message("This will");
YIELD THEN
show_debug_message("be displayed");
YIELD THEN
show_debug_message("over several");
YIELD THEN
show_debug_message("different frames");
CO_END
coroutineRootStruct = CO_BEGIN
i = 1;
REPEAT 5 THEN
YIELD i THEN //Yield the values 1, 2, 4, 8, 16 over 5 frames
i *= 2
END
CO_END
YIELD
instructs the coroutine to temporarily stop execution of the coroutine and return a value. Unlike PAUSE...THEN
or RETURN
, execution will resume in the next frame without any other action required. The value emitted by YIELD
can be read from the coroutine using the .Get()
method. If no value is specified between YIELD
and THEN
then undefined
will be returned by .Get()
.
Please note that a YIELD
command must be followed by a THEN
command. If you forget a THEN
command then code will mysteriously fail to run and will appear to be "skipped over".
coroutineRootStruct = CO_BEGIN
show_debug_message("Look left");
PAUSE "left" THEN
show_debug_message("Look right");
PAUSE "right" THEN
show_debug_message("Look left again");
PAUSE "left" THEN
show_debug_message("Then cross the road");
CO_END
PAUSE
instructs the coroutine to immediately pause execution and return a value. This behaves similarly to YIELD
but, unlike YIELD
, a paused coroutine will not resume execution on the next frame. You will instead need to call the .Resume()
method to resume execution of a paused coroutine. The value emitted by PAUSE
can be read from the coroutine using the .Get()
method. If no value is specified between PAUSE
and THEN
then undefined
will be returned by .Get()
.
Please note that a PAUSE
command must be followed by a THEN
command. If you forget a THEN
command then code will mysteriously fail to run and will appear to be "skipped over".
coroutineRootStruct = CO_BEGIN
IF oPlayer.x > 55 THEN
RETURN "Too far right"
ELSE
CutsceneFindMyFroggy();
RETURN "Playing cutscene"
END_IF
CO_END
RETURN
instructs the coroutine to immediately complete execution and return the given value. Unlike YIELD
or PAUSE
, the coroutine's execution is stopped entirely (though the coroutine may be restarted with the .Restart()
method). The value emitted by RETURN
can be read from the coroutine using the .Get()
method. If no value is specified after RETURN
then undefined
will be returned by .Get()
.
Please note that a RETURN
command does not need to be followed by a THEN
command. Anything written after the RETURN
command will, of course, never be executed, much like GML's native return
command.
coroutineRootStruct = CO_BEGIN
CreateSmokeParticle(oChimney.x, oChimney.y);
DELAY random_range(300, 350) THEN
RESTART
CO_END
RESTART
instructs the coroutine to yield and then, on the next coroutine frame, restart execution. A coroutine that has been restarted will return undefined
if the .Get()
method is called on the coroutine struct. By placing RESTART
at the end of a coroutine you can get a coroutine to loop endlessly until otherwise cancelled. CO_ON_COMPLETE
will be called when restarting a coroutine.
Please note that variables in the coroutine will not be reset.
Please note that a RESTART
command does not need to be followed by a THEN
command. Anything written after the RESTART
command will never be executed, much like GML's native return
command.
coroutineRootStruct = CO_BEGIN
REPEAT 5 THEN
show_debug_message("Five messages!");
END
CO_END
Has no utility on its own. END
is, however, necessary to terminate a REPEAT
, WHILE
, or FOREACH
loop. It should also be used to terminate a RACE
or SYNC
block. It must not be used in other contexts.
coroutineRootStruct = CO_BEGIN
REPEAT 5 THEN
show_debug_message("Five messages!");
END
CO_END
Analogous to GameMaker's own repeat()
loop. It is not necessary to use this macro in all cases to replace standard repeat()
loops. The use of a REPEAT...END
loop is required only when the repeat()
loop would otherwise contain a coroutine command.
coroutineRootStruct = CO_BEGIN
fireball = instance_create_depth(oPlayer.x, oPlayer.y, 0, oFireball);
//Wait until the fireball has risen above the player by 30 pixels
WHILE fireball.y <= fireball.ystart - 30 THEN
fireball.y -= 5;
YIELD THEN
END
//Then shoot the fireball at the nearest enemy!
nearest = instance_nearest(fireball.x, fireball.ystart, oEnemy);
fireball.direction = point_direction(fireball.x, fireball.y, nearest.x, nearest.y);
fireball.speed = 11;
CO_END
WHILE
is analogous to GameMaker's own while()
loop. It is not necessary to use this macro in all cases to replace standard while()
loops. The use of a WHILE...END
loop is required only when the while()
loop would otherwise contain a coroutine command.
coroutineRootStruct = CO_BEGIN
highestHP = 0;
highestInstance = noone;
//Find the enemy from our array with the highest HP
FOREACH instance IN global.arrayOfEnemies THEN
if (instance.hp > highestHP)
{
highestHP = instance.hp;
highestInstance = instance;
}
END
//Bash them!
if (instance_exists(lowestInstance)) hp -= 100;
CO_END
FOREACH...THEN
loops are a convience feature that iterates over either
- an array,
- a struct,
- instances of an object,
- or the
YIELD
output from a coroutine.
When iterating over an array, the iterator variable is given values from the array itself. When iterating over a struct, the iterator variable is given values from the struct; to iterate over struct keys please use variable_struct_get_names()
.
When iterating over instances of an object, the iterator variable is given instance references (the struct representation of an instance that you get from e.g. calling self
in the scope of an instance). Please note that FOREACH
loops behave differently to GameMaker's native with()
loops: the scope of code inside the FOREACH
loop does not change.
When iterating over the output from a coroutine, the YIELD
value is assigned to the iterator variable. The FOREACH...THEN
loop will terminate when the iterable coroutine completes.
Please note that care should be taken not to modify an array or struct that you are iterating over. The total number of iterations is calculated when the FOREACH...THEN
loop begins and if the size of the array or struct changes then this may cause crashes and other errors.
coroutineRootStruct = CO_BEGIN
healthRemaining = oPlayer.hp;
FOREACH heart IN global.heartInstances THEN
heart.sprite_index = min(4, healthRemaining);
healthRemaining -= 4;
if (healthRemaining <= 0) BREAK;
END
CO_END
Analogous to GameMaker's own break
command. Escapes either a REPEAT...THEN
, WHILE...THEN
, or FOREACH...THEN
loop immediately without executing the rest of the code in the loop. The remainder of the code in the coroutine will execute as normal.
Please note that the standard GML break
command will not work on coroutine loops.
coroutineRootStruct = CO_BEGIN
FOREACH enemy IN objEnemy THEN
IF point_distance(oPlayer.x, oPlayer.y, enemy.x, enemy.y) > 100 THEN
CONTINUE
END_IF
enemy.vspeed -= 4;
END
CO_END
Analogous to GameMaker's own continue
command. Forces a coroutine loop (either a REPEAT...THEN
, WHILE...THEN
, or FOREACH...THEN
loop) to immediately proceed to the next iteration without executing the rest of the code in the loop.
Please note that the standard GML continue
command will not work on coroutine loops.
coroutineRootStruct = CO_BEGIN
healthRemaining = oPlayer.hp;
FOREACH heart IN global.heartInstances THEN
heart.sprite_index = min(4, healthRemaining);
healthRemaining -= 4;
IF (healthRemaining <= 0) THEN
BREAK;
END_IF
YIELD THEN
END
CO_END
Analogous to GameMaker's own if
and else
commands. An IF must be matched by an END_IF
. It is typically not required to use these particular commands. You should use these macros if the if-branch (or else-branch etc.) contains a coroutine command itself, with the exception of ASYNC_COMPLETE
. ELSE
and ELSE_IF
are also supported.
Please note that ELSE IF
is incorrect syntax and will lead to compile errors, please ensure you use ELSE_IF
.
coroutineRootStruct = CO_BEGIN
WHILE instance_exists(oRainbow) THEN
oRainbow.image_blend = c_red;
DELAY 500 THEN
oRainbow.image_blend = c_orange;
DELAY 500 THEN
oRainbow.image_blend = c_yellow;
DELAY 500 THEN
oRainbow.image_blend = c_lime;
DELAY 500 THEN
oRainbow.image_blend = c_aqua;
DELAY 500 THEN
oRainbow.image_blend = c_purple;
DELAY 500 THEN
END
CO_END
DELAY
is a convenience behaviour that will pause a coroutine for an amount of real world time. The duration of the delay is measured in milliseconds; a second is 1000 milliseconds, and at 60FPS a single frame is (approximately) 16.66ms.
Please note that whilst a coroutine is waiting at a DELAY
command, the .GetPaused()
method will not return true
.
coroutineRootStruct = CO_BEGIN
fireball = instance_create_depth(oPlayer.x, oPlayer.y, 0, oFireball);
fireball.hspeed = -5;
//Wait until the fireball has risen above the player by 30 pixels
AWAIT fireball.y <= fireball.ystart - 30 THEN
//Then shoot the fireball at the nearest enemy!
nearest = instance_nearest(fireball.x, fireball.ystart, oEnemy);
fireball.direction = point_direction(fireball.x, fireball.y, nearest.x, nearest.y);
fireball.speed = 11;
CO_END
AWAIT
is a convenience behaviour that will check its condition before continuing with code execution. If the condition returns true
then execution will continue immediately. However, if the condition returns false
then the coroutine will temporarily stop execution until the next frame (much like YIELD...THEN
, albeit AWAIT
will yield with a value of undefined
).
Please note that whilst a coroutine is waiting at an AWAIT
command, the .GetPaused()
method will not return true
.
coroutineRootStruct = CO_BEGIN
AWAIT_FIRST
CO_BEGIN
DELAY 200 THEN
show_debug_message("First coroutine finished");
CO_END
CO_BEGIN
DELAY 100 THEN
show_debug_message("Second coroutine finished");
CO_END
END
show_debug_message("Race finished (second coroutine should finish first)");
CO_END
AWAIT_FIRST
allows for a parent coroutine to temporarily halt execution until one of the defined child coroutines has finished. Execution in the parent coroutine will continue once any of the child coroutines has completed execution; the remaining unfinished child coroutines will immediately be cancelled. Only coroutines defined inside the AWAIT_FIRST...END
block will be considered for this behaviour and any previously created child coroutines will be disregarded for the purposes of AWAIT_FIRST
logic.
Each child coroutine exists in its own scope such that variables inside each child coroutine are unique to that coroutine. Parent and child coroutines do not share variables, nor do child coroutines share variables with each other. All coroutines will execute CO_ON_COMPLETE
functions regardless of whether that coroutine was the first one to end.
Please note Unlike normal child coroutines, pausing or cancelling the parent coroutine will pause or cancel child coroutines created inside a AWAIT_FIRST...END
block.
Please note that whilst a coroutine is waiting at a AWAIT_FIRST
command, the .GetPaused()
method will not return true
.
coroutineRootStruct = CO_BEGIN
AWAIT_ALL
CO_BEGIN
DELAY 200 THEN
show_debug_message("First coroutine finished");
CO_END
CO_BEGIN
DELAY 100 THEN
show_debug_message("Second coroutine finished");
CO_END
END
show_debug_message("Sync finished (both coroutines should have finished)");
CO_END
AWAIT_ALL
allows for a parent coroutine to temporarily halt execution until all of the defined child coroutines have finished. Execution in the parent coroutine will continue once all of the child coroutines have completed execution. Only coroutines defined inside the AWAIT_ALL...END
block will be considered and any previously created child coroutines will be disregarded for the purposes of AWAIT_ALL
logic.
Each child coroutine exists in its own scope such that variables inside each child coroutine are unique to that coroutine. Parent and child coroutines do not share variables, nor do child coroutines share variables with each other.
Please note Unlike normal child coroutines, pausing or cancelling the parent coroutine will pause or cancel child coroutines created inside a AWAIT_ALL...END
block.
Please note that whilst a coroutine is waiting at a AWAIT_ALL
command, the .GetPaused()
method will not return true
.
coroutineRootStruct = CO_BEGIN
//Rotate the door 30 degrees so it's ajar
WHILE image_angle < 30 THEN
image_angle += 5
YIELD
END
//Wait for the player to push right...
AWAIT_BROADCAST "push right" THEN
//...then open the door all the way!
WHILE image_angle <= 90 THEN
image_angle = min(90, image_angle + 5);
YIELD
END
CO_END
///Elsewhere in the player object...
if (keyboard_check(vk_right))
{
CoroutineBroadcast("push right");
hspeed = 2;
}
AWAIT_BROADCAST
is a useful command that allows for indirect, if basic, control over coroutines. When a coroutine encounters an AWAIT_BROADCAST
command, the coroutine will pause at that command. In order for the coroutine to proceed, CoroutineBroadcast()
must be called using the same name as is given in AWAIT_BROADCAST
command. The coroutine will then continue execution the next time CoroutineEventHook()
is called in a Step event (usually on the next frame). If multiple coroutines are awaiting a broadcast with the same name, only one call to CoroutineBroadcast()
is necessary to resume all those coroutines.
Please note that whilst a coroutine is waiting at an AWAIT_BROADCAST
command, the .GetPaused()
method will not return true
.
AWAIT_BROADCAST
will, by default, only respond to native Coroutines broadcasts. To listen for GameMaker broadcasts from sprites and sequences then please use AWAIT_ASYNC_BROADCAST
.
coroutineRootStruct = CO_BEGIN
show_debug_message("Starting leaderboard pull");
handle = steam_download_scores("Game Scores", 1, 10);
AWAIT_ASYNC_STEAM
if (async_load < 0)
{
show_debug_message("Leaderboard request timed out");
}
else if (async_load[? "id"] == handle)
{
show_debug_message("Leaderboard data received");
global.scores = array_resize(0);
var _list = map[? "entries"];
var _i = 0;
repeat(ds_list_size(_list))
{
var _data = _list[| _i];
array_push(global.scores, {name : _data[? "name"], score : _data[? "score"], rank : _data[? "rank"]});
_i++;
}
ASYNC_COMPLETE
}
THEN
show_debug_message("Leaderboard pull complete");
CO_END
AWAIT_ASYNC_*
commands (for the full list, see below) allows a coroutine to interact with GameMaker's native async event system. When the coroutine encounters an AWAIT_ASYNC_*
command, the coroutine will pause at that line of code and wait until the relevant async event is triggered. Once an async event of the correct type is surfaced by GameMaker's runtime, the async code in the AWAIT_ASYNC_* ... THEN
block is executed. If the code block calls ASYNC_COMPLETE
then the coroutine immediately executes further code, otherwise AWAIT_ASYNC_*
continues to listen for new events.
The standard GML function located between AWAIT_ASYNC_*
and THEN
is executed every time that async event is triggered, regardless of whether that async event is relevant for the coroutine. This is unfortunate, but it's also the way GameMaker is designed. You should always check if the async_load
or event_data
ds_map you have received matches the async event you're expecting.
The code that follows the AWAIT_ASYNC_*
command cannot contain any coroutine macros (with the exception of ASYNC_COMPLETE
). This is because async_load
and event_data
may contain volatile data will not persistent beyond the end of the async event. If you'd like to perform extensive operations on data that's returned by an async event, you should make a copy of it and then process that data outside of the AWAIT_ASYNC_*
code block.
AWAIT_ASYNC_*
code may be executed when an operation has timed out. By default, no timeout duration is set and operations may hang forever. You can customize the timeout duration using the ASYNC_TIMEOUT
macro (see below). When an async operation times out, async_load is a negative number. You should always write code to check if an async operation has timed out i.e. you should always handle the cases where async_load
or event_data
is negative.
Please note that AWAIT_ASYNC_BROADCAST
will pick up specifically GameMaker sprite and sequence broadcasts; it will not pick up broadcasts native to the Coroutines library (use AWAIT_BROADCAST
instead for that). Additionally, inside a AWAIT_ASYNC_BROADCAST
code block you should be checking against event_data
instead of async_load
.
The following async await commands are supported:
AWAIT_ASYNC_HTTP
AWAIT_ASYNC_NETWORKING
AWAIT_ASYNC_SOCIAL
AWAIT_ASYNC_SAVE_LOAD
AWAIT_ASYNC_DIALOG
AWAIT_ASYNC_SYSTEM
AWAIT_ASYNC_STEAM
AWAIT_ASYNC_BROADCAST
These are the most common async events that get used in GameMaker. If you'd like additional async events to be added then please let me know and they'll go into an official release.
coroutineRootStruct = CO_BEGIN
handle = get_string_async("Please enter your name", "Juju Adams");
result = "";
AWAIT_ASYNC_DIALOG
if (async_load[? "id"] == handle)
{
if (async_load[? "status"]) result = async_load[? "string"];
ASYNC_COMPLETE
}
THEN
RETURN result;
CO_END
ASYNC_COMPLETE
is an essential component of the AWAIT_ASYNC_*
command. It indicates that the async operation has completed and the coroutine should continue execution of code. If you do not call ASYNC_COMPLETE
inside your async code block then the async operation may hang indefinitely.
Please note that ASYNC_COMPLETE
should not be called outside of AWAIT_ASYNC_*
code blocks otherwise you will see unpredictable behaviour.
coroutineRootStruct = CO_BEGIN
show_debug_message("HTTP GET started");
handle = http_get("https://www.jujuadams.com/");
AWAIT_ASYNC_HTTP
if (async_load < 0) //Handle the timeout case
{
show_debug_message("HTTP GET timed out");
ASYNC_COMPLETE
}
if (async_load[? "id"] == handle)
{
if (async_load[? "status"] == 0)
{
show_debug_message("HTTP GET succeeded");
show_debug_message(async_load[? "result"]);
ASYNC_COMPLETE
}
else if (async_load[? "status"] < 0)
{
show_debug_message("HTTP GET failed with error code " + string(async_load[? "http_status"]));
ASYNC_COMPLETE
}
}
ASYNC_TIMEOUT 6000 THEN //Wait 6 seconds before timing out (6000 milliseconds)
show_debug_message("HTTP GET complete");
CO_END
Async operations, especially those to servers, often run into issues and requests time out. ASYNC_TIMEOUT...THEN
adds a timeout behaviour to an AWAIT_ASYNC_*
command to handle cases where unreported failure is possible. By default, AWAIT_ASYNC_*
commands have no timeout duration and it is possible for operations to hang forever. The timeout duration (the number between ASYNC_TIMEOUT
and THEN
) is measured in milliseconds.
When async code is executed but the operation has timed out, async_load
will be set to a negative number. You should always write behaviour into your AWAIT_ASYNC_*
blocks to handle cases where async_load
is a negative number to avoid unexpected problems.
@jujuadams 2021