GameMaker: 2.3 syntax in details

With GameMaker Studio 2.3 update out for a bit now and 2.3.1 beta just released, it seems like a great time for a blog post going over the numerous syntactic additions.

This covers the syntax itself, how it works, and what you can do with it.

Also included is a list of breaking changes and how to get around them.

Chained accessors

GameMaker has long allowed for a handful of "accessor" shorthands,

// normal array operations:
val = an_array[index];
an_array[index] = val;
// non-copy-on-write operations:
an_array[@index] = val; // same as array_set(an_array, index, val)
// ds_map:
val = a_map[?key]; // same as val = ds_map_find_value(a_map, key)
a_map[?key] = val; // same as ds_map_set(a_map, key, val)
// ds_list:
val = a_list[|index]; // same as val = ds_list_find_value(a_list, index)
a_list[|index] = val; // same as ds_list_set(a_list, index, val)
// ds_grid:
val = a_grid[#x, y]; // same as val = ds_grid_get(a_grid, x, y)
a_grid[#x, y] = val; // same as ds_grid_set(a_grid, x, y, val)

The update expands on these slightly, allowing to chain the together - so you can now also do

list_of_maps[|i][?"hi"] = "hello";

instead of

ds_map_set(ds_list_find_value(list_of_maps, i), "hi", "hello");

This proves handy for nested data structures and multi-dimensional arrays.

Array changes

Related to above, 2d arrays are now just nested 1d arrays, and you can create arrays with higher dimension count easier.

array_1d[0] = "hi!"; // no change
array_2d[1][0] = "hi!"; // previously array_2d[0, 0] = "hi!"
array_3d[2][1][0] = "hi!"; // new!
// ...and so on

You can also use the legacy array[index1, index2] syntax.

Structs

Structs are like instances, but without any events or built-in variables. Very lightweight.

You can create an empty struct by using {}:

var q = {};
show_debug_message(q); // {  }
q.hi = "hello!";
show_debug_message(q); // { hi : "hello!" }
q.one = 1;
show_debug_message(q); // { hi : "hello!", one: 1 }

you can also pre-populate some fields by specifying name: value pairs:

var q = { a: 1, b: 2 };
show_debug_message(q); // { b : 2, a : 1 }
q.c = 3;
show_debug_message(q); // { c : 3, b : 2, a : 1 }

similar to arrays, structs are managed by GameMaker automatically, meaning that you do not have to explicitly destroy them like you would with instances.

Structs can be used in most places where instances were - for example, you can do with (a_struct), although you cannot iterate over every "instance" of a struct this way - you'll want to add them to an array/list for that.

Structs as maps

Similar to instances, structs have variable_struct_* functions for dynamically managing their variables. This allows to use structs as a garbage-collected alternative to ds_maps:

var q = { a: 1 };
variable_struct_set(q, "b", 2);
variable_struct_set(q, "$", "dollar");
show_debug_message(q); // { $ : "dollar", a : 1, b : 2 }
show_debug_message(q.b); // 2
show_debug_message(variable_struct_get(q, "a")); // 1
show_debug_message(variable_struct_get(q, "$")); // dollar

2.3.1 further expands on this by adding a struct[$key] accessor for convenience:

var q = { a: 1 };
q[$"b"] = 2; // same as variable_struct_set(q, "b", 2)
var v = q[$"b"]; // same as variable_struct_get(q, "b")

combined with arrays, this allows most data structures to be replicated without the need to explicitly destroy them.

A few notes:

  • Direct (a.b) reads/writes are faster than using variable_struct_* functions and can be utilized for cases where you are sure that a struct has a variable. Otherwise variable_struct_* functions have very similar performance to ds_map.
  • Unlike ds_map, which takes just about anything for keys, struct variable names are strings, so variable_struct_set(q, 4, "four") is the same as variable_struct_set(q, "4", "four").

Structs for JSON

2.3.1 adds json_stringify and json_parse functions, which are much alike the existing json_encode and json_decode, but work with structs and arrays instead of maps and lists.

So now you can do

var o = {
    a_number: 4.5,
    a_string: "hi!",
    an_array: [1, 2, 3],
    a_struct: { x: 1, y: 2 }
};
show_debug_message(json_stringify(o));

and this would print

{ "a_string": "hi!", "an_array": [ 1, 2, 3 ], "a_struct": { "x": 1, "y": 2 }, "a_number": 4.5 }

and passing that string to json_parse would give you your nested struct back.

Functions

Previously, each script resource would contain a single snippet of code to be ran when it is called.

/// array_find_index(array, value)
/// @param array
/// @param value
var _arr = argument0;
var _val = argument1;
var _len = array_length_1d(_arr);
for (var _ind = 0; _ind < _len; _ind++) {
    if (_arr[_ind] == _val) return _ind;
}
return -1;

but now, things are different - you can have multiple independent snippets inside the same script resource, told apart through use of function <name>() {<code>} syntax:

/// @param array
/// @param value
function array_find_index() {
    var _arr = argument0;
    var _val = argument1;
    var _len = array_length_1d(_arr);
    for (var _ind = 0; _ind < _len; _ind++) {
        if (_arr[_ind] == _val) return _ind;
    }
    return -1;
}

/// @param array
/// @param value
function array_push() {
    var _arr = argument0;
    var _val = argument1;
    _arr[@array_length(_arr)] = _val;
}

This works as following:

  • function name(){} inside a script becomes a global function, which is equivalent to how this worked pre-2.3

    function name() {
        // code here
    }
    

  • function() {} can be used as an expression, allowing you to do

    explode = function() {
        instance_create_layer(x, y, layer, obj_explosion);
        instance_destroy();
    }
    

    in a Create event or even use it as an argument inside function calls!

    layer_script_begin("MyLayer", function() {
        shader_set(sh_brightness);
        shader_set_uniform_f(shader_get_uniform(sh_brightness, "u_bright"), 1);
    });
    layer_script_end("MyLayer", function() {
        shader_reset();
    });
    

  • function name(){} inside another function or outside of scripts is equivalent of

    self.name = function(){};
    

    and can be used for convenience.

  • Any other code inside scripts but outside functions will be ran on game start; getting/setting variables will work as if you are doing global.variable:

    show_debug_message("Hello!"); // shows before any instances are created
    variable = "hi!"; // sets global.variable
    // ...function definitions
    

    allowing it to be used for any initial setup.
    Do note, however, that this runs before even entering the first room, so, if you want to spawn instances, you'll want to use room_instance_add.

As a pleasant bonus, you can now call functions stored in variables without script_execute!

function scr_hello() {
    show_debug_message("Hello, " + argument0 + "!");
}
/// ...
var hi = scr_hello;
script_execute(hi, "you");
hi("you"); // new! Same effect as above

Now, onwards to even more interesting additions:

Named arguments

Introduction of function syntax also brings another wonderful addition - named arguments!

Previously you would do either

function array_push() {
    var _arr = argument0, _val = argument1;
    _arr[@array_length(_arr)] = _val;
}

or

function array_push() {
    var _arr = argument[0], _val = argument[1];
    _arr[@array_length(_arr)] = _val;
}

but now you can do just

function array_push(_arr, _val) {
    _arr[@array_length(_arr)] = _val;
}


This makes optional arguments easier too - any named arguments not provided to the script will be set to undefined, meaning that you can do:

function array_clear(_arr, _val) {
    if (_val == undefined) _val = 0;
    // previously: var _val = argument_count > 1 ? argument[1] : 0;
    var _len = array_length(_arr);
    for (var _ind = 0; _ind < _len; _ind++) _arr[@_ind] = _val;
    return _arr;
}

do note, however note that argument_count for scripts with named arguments will not be less

Static variables

These resemble local static variables in C++.
That is, a static variable is persistent, but only visible inside the function that it was declared in.

This is good for any cases where you need function-specific state:

function create_uid() {
    static next = 0;
    return next++;
}
function scr_hello() {
    show_debug_message(create_uid()); // 0
    show_debug_message(create_uid()); // 1
    show_debug_message(create_uid()); // 2
}

Static variables are initialized when the execution first reaches them:

function scr_hello() {
    // show_debug_message(some); // error - not defined
    static some = "OK!";
    show_debug_message(some); // "OK!""
}

As result, your static variables would usually reside at the beginning of their respective function.

Methods/function binding

This feature is by most means identical to Function.bind in ECMAScript-based languages.

A function can be "bound" to something, which will change self to that value inside that function call, pushing original self to other (just like with statement does).

This means that if you had

// obj_some, Create event
function locate() {
    show_debug_message("I'm at " + string(x) + ", " + string(y) + "!");
}

, you could do both

var inst = instance_create_depth(100, 200, 0, obj_some);

inst.locate(); // 100, 200

var fn = inst.locate;
fn(); // also 100, 200!

as the function reference you got is bound to that instance.

A function can be bound to a struct, an instance ID, or nothing at all (undefined).

Functions that are not bound to anything will preserve self/other like scripts did in <2.3.
However, if a function is not bound to anything, but you are calling it as some.myFunc, it will be treated as if it's bound to some.

Automatic binding works as following:

  • function name(){} in scripts binds to nothing, maintaining backwards compatibility with <2.3.
  • function name(){} in events binds to self, making for simpler instance method definitions.
    (that is, you can just have series of function definitions in Create event)
  • static name = function(){} also binds to nothing, which is good as you wouldn't want static functions to bind to the first instance that the parent function is called with.
  • Any other uses of name = function(){} bind to self.

Functions can be [re-]bound using the method built-in function. A function that has been bound to something is formally called a "method" (hence the built-in function name).

Overall, this is not only handy for instance/struct-specific functions, but also "creating" functions that are bound to some custom context - for example, you could make a function that returns you a function that generates incremental IDs (like the one shown earlier for static), and have the IDs be independent for each such returned function.

function create_uid_factory() {
    var _self = { next: 0 };
    var _func = function() { return self.next++; };
    return method(_self, _func);
}
//
var create_entity_uid = create_uid_factory();
var create_network_uid = create_uid_factory();
repeat (3) show_debug_message(create_entity_uid()); // 0, 1, 2
show_debug_message(create_network_uid()); // 0

Function calls

Since functions can now be stored wherever, you are also allowed to call them from wherever:

scr_greet("hi!"); // just like before
other.explode(); // works!
init_scripts[i](); // also works!
method(other, scr_some)(); // executes `scr_some` for `other` without using `with`

Built-in function referencing

You can now do

var f = show_debug_message;
f("hello!");

and you can automatically index built-in functions,

var functions = {};
for (var i = 0; i < 10000; i++) {
    var name = script_get_name(i);
    if (string_char_at(name, 1) == "<") break;
    functions[$name] = method(undefined, i);
    show_debug_message(string(i) + ": " + name);
}
// `functions` now contains name->method pairs

which would print

0: camera_create
1: camera_create_view
2: camera_destroy
...
2862: layer_sequence_get_speedscale
2863: layer_sequence_get_length
2864: sequence_instance_exists

Indexing can be very handy for debugging and scripting tools - for example, GMLive now uses this mechanism instead of having a massive file full of scripts wrapping every single known built-in function.

Constructors

A constructor is a function marked with a constructor suffix-keyword:

function Vector2(_x, _y) constructor {
    x = _x;
    y = _y;
}

which enables you to do

var v = new Vector2(4, 5);
show_debug_message(v.x); // 4
show_debug_message(v); // { x: 4, y: 5 }

In short, new keyword automates creating an empty structure, calling the constructor function for it, and then returning it. Just like classes in other programming languages! But there's more:

Static variables

GameMaker will treat static variables inside the constructor as existing in struct instances created from it, provided that the struct instance did not override the variable.

This is similar to how Variable Definitions work for objects, or how prototypes work in other programming languages (such as JavaScript prototypes or Lua's metatables).

This can be used for default values (which you can then overwrite), but, most importantly, to add methods to structs without actually storing them in each struct instance:

function Vector2(_x, _y) constructor {
    x = _x;
    y = _y;
    static add = function(v) {
        x += v.x;
        y += v.y;
    }
}
// ... and then
var a = new Vector2(1, 2);
var b = new Vector2(3, 4);
a.add(b);
show_debug_message(a); // { x : 4, y : 6 }

Note: if you wanted to override a static variable right in the constructor itself (rather than inside a function in it), you would need to use self.variable to distinguish between the static variable and new struct's variable:

function Entity() constructor {
    static uid = 0;
    self.uid = uid++;
}

(which would give each entity a unique ID)

Inheritance

A constructor may inherit from another constructor using : Parent(<arguments>) syntax:

function Element(_x, _y) constructor {
    static step = function() {};
    static draw = function(_x, _y) {};
    x = _x;
    y = _y;
}
function Label(_x, _y, _text) : Element(_x, _y) constructor {
    static draw = function(_ofs_x, _ofs_y) {
        draw_text(_ofs_x + x, _ofs_y + y, text);
    };
    text = _text;
}

which will call the parent constructor first and then the child's.

Static variables defined in child constructor take precedence over those defined in parent constructor, making for a way to override parent fields - so with above you could do

var label = new Label(100, 100, "Hello!");
label.step(); // calls parent step function
label.draw(5, 5); // calls child draw function

Exception handling

GameMaker functions are generally structured around not throwing errors unless it is definitely your fault - so, for example, trying to open a text file that doesn't exist will return a special index -1, but trying to read from an invalid index will throw an error.

Still, it can be handy to write code that is allowed to fail without inserting safety checks on every step of the process. And now you can! This works as following:

try {
    // (code that might raise an error)
    var a = 1, b = 0;
    a = a div b; // causes "division by zero" error
    show_debug_message("this line will not execute");
} catch (an_exception) {
    // do something (or nothing) with the error information that is
    // now stored in the local variable an_exception
    show_debug_message(an_exception);
}

"built-in" errors are structs with a few variables:

  • message: A string with a short description of the error.
    For example, if you tried to do integer division by zero, it would be "DoRem :: Divide by zero".
  • longMessage: A string with a longer description of the error and callstack.
    This is what would appear in the built-in error popup if you were to not handle the error.
  • stacktrace: An array of strings indicating the call stack - a chain of function namess that led up to the problematic spot. When running from IDE or using YYC, line numbers will be included after each function name (e.g. gml_Script_scr_hello (line 5)).
  • script: (technical) name of the script/function that the error originated in.
    This is not too different from grabbing the first item in stacktrace.

You may also throw your own exceptions - either via calling show_error with error text:

try {
    show_error("hey", false);
} catch (e) {
    show_debug_message(e.message); // "hey"
}

or by using the throw keyword (which allows arbitrary values to be "thrown"):

try {
    throw {
        message: "hey",
        longMessage: "no long messages today",
        stacktrace: debug_get_callstack()
    }
} catch (e) {
    show_debug_message(e); // prints the above struct
}

Try-catch blocks can be nested in the same or across different scripts.

When that happens, the nearest catch-block will be triggered.

If you do not want to handle an exception, you can "re-throw" it:

try {
    try {
        return 10 / a_missing_variable;
    } catch (e) {
        if (string_pos("DoRem", e.message) != 0) {
            show_debug_message("Caught `" + e.message + "` in inner catch!");
        } else {
            throw e;
        }
    }
} catch (e) {
    show_debug_message("Caught `" + e.message + "` in outer catch!");
}

If an exception goes uncaught, you get the familiar error popup window. Unless...

exception_unhandled_handler

In what can be considered the last line of defense, GameMaker now also offers the ability to provide a function that will be called when an exception was left uncaught and your game is about to close. This overrides the default error popup window.

exception_unhandled_handler(function(e) {
    show_message("Trouble!\n" + string(e.longMessage));
});
show_error("hey", true);

As documentation notes, there isn't much you can do at this point, but you can save error text (along with any context that might prove useful) to a file so that you can load it on game start and offer the user to send a report.

Smaller additions

Mostly convenience functions,

String functions

string_pos_ext, string_last_pos, and string_last_pos_ext have been added to deal with searching for substrings from an offset and/or from the end of the string, which are great for parsing data - e.g. see my older "split string on delimiter" post.

Array functions

A handful of array functions have been added for dealing with arrays:

  • array_resize(array, newsize)
    Perhaps the most prized addition - this resizes an array to new size, either adding zeroes to the end of an array or removing elements to meet the size.

    var arr = [1, 2, 3];
    array_resize(arr, 5);
    show_debug_message(arr); // [1, 2, 3, 0, 0]
    array_resize(arr, 2);
    show_debug_message(arr); // [1, 2]
    

    Enables various other utility functions to be made.

  • array_push(array, ...values)
    Adds one or more values to the end of an array.

    var arr = [1, 2, 3];
    array_push(arr, 4);
    show_debug_message(arr); // [1, 2, 3, 4]
    array_push(arr, 5, 6);
    show_debug_message(arr); // [1, 2, 3, 4, 5, 6]
    

  • array_insert(array, index, ...values)
    Inserts one or more values at an offset in an array.

    var arr = [1, 2, 3];
    array_insert(arr, 1, "hi!");
    show_debug_message(arr); // [1, "hi!", 2, 3]
    

  • array_pop(array)➜value
    Removes the last element from an array and returns it.

    var arr = [1, 2, 3];
    show_debug_message(array_pop(arr)); // 3
    show_debug_message(arr); // [1, 2]
    

  • array_delete(array, index, count)
    Removes element(s) at an offset in an array

    var arr = [1, 2, 3, 4];
    array_delete(arr, 1, 2);
    show_debug_message(arr); // [1, 4]
    

  • array_sort(array, sorttype_or_function)
    Sorts an array either ascending/descending (just like ds_list_sort),

    var arr = [5, 3, 1, 2, 4];
    array_sort(arr, true);
    show_debug_message(arr); // [1, 2, 3, 4, 5]
    

    or by passing each element through the provided "comparator" function

    var strings = ["plenty", "1", "three", "two"];
    array_sort(strings, function(a, b) {
        return string_length(a) - string_length(b);
    });
    show_debug_message(strings); // [ "1","two","three","plenty" ]
    

script_execute_ext

You know how executing a function with arbitrary argument count usually entails having a little switch-block on argument count doing script_execute in each case? Now it doesn't.

var arr = [1, 2, 3, 4];
var test = function() {
    var r = "";
    for (var i = 0; i < argument_count; i++) {
        if (i > 0) r += ", ";
        r += string(argument[i]);
    }
    show_debug_message(r);
}
script_execute_ext(test, arr); // `1, 2, 3, 4` - entire array
script_execute_ext(test, arr, 1); // `2, 3, 4` - starting at offset
script_execute_ext(test, arr, 1, 2); // `2, 3` - offset and count

Data structure checks

Four functions have been added for checking whether ds_list and ds_map items are maps/lists:

ds_list_is_map(id, index)
ds_list_is_list(id, index)
ds_map_is_map(id, key)
ds_map_is_list(id, key)

This allows to verify that what you are accessing (especially for json_decode output) is indeed a map/list and partially addresses the issues that I made an extension and a blog post for.

ds_map functions

Two functions have been added for enumerating map keys/values:

ds_map_values_to_array(id,?array)
ds_map_keys_to_array(id,?array)

These can be handy for iterating large maps, especially if you desire to modify them while doing so (which is where ds_map_find_* functions have undefined behaviour).

Type checking functions

is_struct, is_method have been added for checking whether a value is a struct or a bound function accordingly, but there's an extra - is_numeric will check whether a value is any of numeric types (real, int32, int64, bool).

Breaking changes

A few things to watch out for:

2d array functions

Since 2d array functions are now deprecated, they translate as following:

  • array_length_1d(arr)array_length(arr)
  • array_height_2d(arr)array_length(arr)
  • array_length_2d(arr, ind)array_length(arr[ind])

The implication here is that array_height_2d does not care about whether your array is truly 2d (has sub-arrays inside) and therefore will return unexpected values when used on 1d arrays - e.g. array_height_2d([1, 2, 3]) is 3.

You can get around this via

function array_height_2d_fixed(arr) {
    var n = array_length(arr);
    if (n == 0) return 0; // empty / not an array
    for (var i = 0; i < n; i++) if (is_array(arr[i])) return n;
    return 1; // no arrays inside
}

(which will only return >1 if the array contains sub-arrays)

But this will still have false positives with 1d arrays containing 1d arrays, because that's what 2d arrays are now.

Default return value

Previously script/function calls would return 0 if the script/function didn't return anything.

They now return undefined.

This is generally a good change since GameMaker still uses numeric IDs in plenty of places (forgetting to return a value could end in you using a valid but unrelated structure with index 0), but may break old code that only really worked through chance (related reading).

In 2.3.1, some built-in functions were similarly changed to return undefined if they are not supposed to return anything (previously also 0).

self/other values

In GameMaker ≤ 8.1, doing

show_debug_message(self);
show_debug_message(other);

would show -1 and -2 respectively, which were treated as a special case in most functions.

This was changed sometime in GMS1 to be equivalent to self.id and other.id.

This had now been changed again and self/other now give you instance "structs" - so

hi = "hello!";
show_debug_message(self);

would now show { hi : "hello!" }. This has a few implications:

  • self-struct is not equal to self.id, so old code relying on that will break. (uses of self are best replaced by self.id for such cases).
  • Unlike referencing by ID, with an instance-struct you can work with instance variables even after the instance has been removed from the room via instance_destroy (but can still check if it's in the room using instance_exists)

Prefix-ops as then-branch

Doing

if (condition) ++variable;

and

if (condition) --variable;

is no longer allowed due to ambiguity created by variety of new syntactic structures, making it hard to tell whether you meant if (condition)++ <expr> (post-increment on condition's expression) or if (condition) ++<expr> (pre-increment on then-branch expression).

If you'd like a personal take, I would rather forbid equating (variable)++ to variable++ - I don't think I've seen this construct intentionally used in any projects.

Regardless, this is pretty trivial to fix up.

array[$hex]

Since a[$b] is now used for struct accessor (see above), trying to do array[$A1] (previously array access with a Pascal-style hexadecimal literal index) will not work like it did before (instead trying to read key from a variable called A1).

You would want to change that to either array[ $A1] (a space for clarity) or array[0xA1] (a C-style hexadecimal literal).

Conclusion & further reading

Rest assured, 2.3 changes are very exciting and broaden the horizons of what can be done in GML. Most notably, a lot of JavaScript code can now be ported to GML easily, as is being demonstrated by user-created libraries such as GMLodash.

For details possibly not covered here, you can check out

Have fun!

Related posts:

7 thoughts on “GameMaker: 2.3 syntax in details

  1. Excellent blogpost, YAL. Thanks for taking the time to write it.This is the first time I see a good explanation of what the static keyword does in modern GML.
    I assume this will allow you to take much saner approaches on sfgml. How well is it working with GMS2.3 new features?

    • It is indeed great news for sfgml! I have implemented struct-based generation, which means that, for the first time, a lot of existing Haxe code works without any GML-specific changes – see my recent gif loader as an example.

      Structs also mean that the generated code can be called from GML much more similarly to how Haxe side is structured – inst.func() / Class.func() and all.

  2. so, a[$A] and a[ $A] are OK, but then they don’t know where you wanted to put space in if(x)++ a; or if(x) ++a; …

    • I think it’s because () are unwrapped too early – you can also do

      var a = 1;
      (a) = 2;
      show_debug_message(a); // 2
      

      which is pretty cursed. Whereas [$ must be merging the two into a single token while parsing.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.