Tech

GameMaker Studio 2.3: New GML Features

001 blog

Posted by Mark Alexander on 18 August 2020

Now that the 2.3.0 Beta of GameMaker Studio 2 is available, you will be able to see there have been a number of significant changes and additions to the GameMaker Language, as well as a number of changes and additions made to the IDE. In this tech blog we'll be going over the changes to GML and we'll give a brief overview of the new language features available. In a follow up tech blog we'll cover the changes to the IDE.


Arrays

The first change to the GameMaker language that we want to discuss is a change to how arrays work. Previously, GML only permitted 1D or 2D arrays, eg:

array_1d[0] = "hello";
array_1d[1] = "world";

array_2d[0, 0] = "hello";
array_2d[0, 1] = "world";

However, this is no longer the case and now all arrays are 1D. So how do we create a 2D array now? Well this is done by chaining arrays together to essentially create arrays within arrays. Here is a small example:

array[0][0] = 1;
array[0][1] = "hello";
array[0][2] = 55.5;
array[1][0] = sprite_index;
array[1][1] = "world";
array[1][2] = -67.89;

The above is now a multi-dimension array with 2 dimensions to it, but you are no longer limited to just 2 dimensions and can create arrays of 3 or 4 or more dimensions as required, eg:

array[0][0][0] = 1; // A three dimensional array

This change means that all platforms will now work the same (including HTML5) and enables much larger arrays, both in number of items as well as number of dimensions. The change also means that the following functions have been deprecated:

array_length_1d()
array_length_2d()
array_height_2d()

And the following new functions have been added:

array_length(array)
array_resize(array, new_size);

Note that the old functionality (and the deprecated functions mentioned above) will still work but under no circumstances should new projects use the old syntax, as these will eventually be removed from the language.


Chained Accessors

Another change to GML is the ability to chain accessors. Much like you can now chain arrays together, you can chain the different Data Structure accessors together too, making accessing the data from nested structures much easier.

A simple example of this would be a DS grid, where each grid entry is a ds list. Previously you'd have to first retrieve the index of the list from the grid, and then access the data in the list, but now it's as simple as doing something like this:

variable = grid[# 0, 0][| 5];

This would retrieve the data from the 6th position in the DS list that is being held in the grid (0, 0) position. Other examples of use are:

// Access a grid that has been added to a list that is part of a map:
var _a = data[? "lists"][| 0][# 0, 0];

// Access an array nested in a list from a function and modify it:
data[| 0][@ 10] = 100;

// Access a map nested in a grid nested in a list nested in an array:
data[0][| 10][# 3, 4][? "key"] = "hello world";

Functions, Scripts and Method Variables

Above we outline some changes to the GameMaker Language, but now it's time to talk about the good stuff... what's been added! To start with we want to talk about in-line functions and scripts. Previously, a script was a single resource that was created on a global scope and used to create a single custom function which would then be called using the script name as the function name. This is no longer the case and scripts, while still global in scope, are now a type of "container" for one or more functions.

What this means is that when you compile your project, scripts are no longer called individually, but are actually all run at the start of the game in the global scope, so all variables that are defined in the script (outside of function definitions) will be considered global. That said, you should still use the globalidentifier to explicitly define and identify global variables in your code.

The reason for this change is that scripts can now contain multiple functions. If you've only used GML to code projects then the idea of functions may be something new to you, but think of them as being the same as scripts, only explicitly assigned to a variable. This variable is called a method and is used to call the code that is in the function, the same as you would have previously called the script name. This is easier to visualise with an example, so let's look at one. Consider this simple script move_follow():

/// @function               move_follow(_object, _speed);
/// @param  {index} _object The Object to follow
/// @param  {real}  _speed  The speed to follow at

var _o = argument0;
var _s = argument1;
if (point_distance(x, y _o.x, _o.y) > 0)
    {
    direction = point_direction(x, y, _o.x, _o.y);
    speed = _s;
    }
else speed = 0;

Now, however, this would be defined as a function like this:

/// @function               move_follow(_object, _speed);
/// @param  {index} _object The Object to follow
/// @param  {real}  _speed  The speed to follow at

function move_follow(_object, _speed)
{    
if (point_distance(x, y _object.x, _object.y) > 0)
    {
    direction = point_direction(x, y, _object.x, _object.y);
    speed = _speed;
    }
else speed = 0;
}

or like this:

/// @function               move_follow(_object, _speed);
/// @param  {index} _object The Object to follow
/// @param  {real}  _speed  The speed to follow at

move_follow = function(_object, _speed)
{    
if (point_distance(x, y _object.x, _object.y) > 0)
    {
    direction = point_direction(x, y, _object.x, _object.y);
    speed = _speed;
    }
else speed = 0;
}

You would call this function just as you would have called the script:

move_follow(obj_Player, 5);

So, as we mentioned above, scripts can now contain multiple functions and these should be defined with the format shown above where you have the @function <name>(<arguments>) JS doc comment to identify the function, then the function definition below, and then the next JS Doc comment, and then the function, etc...

Multiple Functions In One Script

Above, we briefly mentioned that functions as variables were called method variables, but it should be explained in a bit more detail. Basically, when you create a function with a variable like this, you are creating a method variable, and they can have different scopes. Consider this function:

foo = function() { ... }
  • In the case of a script a variable called "foo" is declared at global scope

  • If the function is declared in an event then the variable "foo" is on the instance that ran that event

  • If the function is declared in an event using the var keyword then the variable "foo" is local to the event only

  • If "foo" is declared inside a with then it is declared on the self that is active at that time

This means that if you have some code that you need to use only within a loop in an alarm event (for example), then you can define it as a local scope method variable at the start of the event and then use it within the loop without the need to clutter up the script assets with it.

It is worth noting that while the variable will be in the chosen scope, the actual function will be bound to the scope that it was initially defined in. Going back to script functions, these are all global scope and are considered "unbound" (ie: they are not associated with any instances), but if you have a script function that creates another function within it and then you call this script from an instance, the function used within the script will be bound to the instance. In general this is not something you ever need to think about but for more complex operations with method variables it's worth taking into consideration. This also applies when using other constructs like with - when you create a method variable inside a with, the function will be bound to the instance that is currently in scope.

The following new functions have also been added to deal with methods:

is_method(variable);
method(instance_id, function);

Check the manual for details fo how these functions should be used.


Structs

The next new feature that we want to discuss in GameMaker Studio 2.3 is the creation of structs. A struct is - to put it simply - a variable that holds a collection of other variables - you can think of it as a kind of "light-weight object". The variables that a struct holds can be of any data type and these variables can be read from and written to after the initial struct declaration, and you can also add more variables to a struct after it has been declared. It should also be noted that the contents of a struct are independent of the instance or script that created it, and as such you can - if you wish - use the built-in variable names like image_index or x and y. One of the benefits of using structs is that (unlike instances), after the initial creation, structs have no processing overhead while they exist, although they will take up space in memory. The struct syntax is as follows:

<variable> = {
    <variable> : <value>,
    <variable> : <value>,
    etc...
    };

So, an example of this in practice would be:

mystruct = {
    a : 20,
    b : "Hello World"
    };

The above creates an instance scope struct in the variable "mystruct" and populates it with some values (structs can be created at local, instance and global scope, just like any other variable). Note that you don't have to populate the contents of a struct when it is created initially and you can create an empty struct by simply doing this:

mystruct = {};

This struct can then be added to at a later point in the game code. Here is an example of a struct with various variables and data types:

var _xx = 100;
mystruct = {
    a : 10,
    b : "Hello World",
    c : int64(5),
    d : _xx + 50,
    e : function( a, b )
        {
        return a + b;
        },
    f : [ 10, 20, 30, 40, 50 ]
    };

You'll notice in the above code that you can also define methods and use runtime functions in structs, and that you can use expressions consisting of any variable previously defined within the context of the struct itself, as well as any variable defined within the scope of the struct itself.

Once a struct has been defined, you can access the data within using the "point" notation, like this:

mystruct = {
    a : 20,
    b : "Hello World"
    }
mystring = mystruct.b + string(mystruct.a);

You can also perform operations on the variables within a structure or use them in functions, just as you would any other variable. For example:

mystruct.a += 1;
mystruct.b = mystruct.a + 20;
mydir = point_direction(mouse_x, mouse_y, mystruct.xx, mystruct.yy);

Finally, structs can have other structs nested inside of them, like this:

mystruct = {
    a : {
        aa : "This is an example"
        },
    b : {
        bb : "And another one"
        },
    };

To access such nested structs you would still use the point notation, like this:

 var _str = mystuct.a.aa + " " + mystruct.b.bb;
show_debug_message(_str);

When a struct is no longer required it can be removed from memory using the delete operator, which is another new GML feature added in the 2.3 update. This de-references the struct and hints to the garbage collector that it may be available for collection. This is not strictly required as the garbage collector may do this automatically in the following game steps if the struct is no longer referenced in your code, but it is good practice to do so and we recommend it (for example, call delete in the Clean Up event of an instance to explicitly tell the garbage collector that an instance scope struct is to be deleted).

Also note that structs can be created using functions, which requires the use of the new operator and the keyword constructor (two more new features to GML), as shown in the following example:

Vector2 = function(_x, _y) constructor
    {
    x = _x;
    y = _y;
    static Add = function( _other )
        {
        x += _other.x;
        y += _other.y;
        }
    }

Here we are creating the function Vector2 and telling GameMaker Studio 2 that this is a function for making a struct by adding the constructor keyword after the definition. You can then call this function like this:

v1 = new Vector2(10, 10);

Now the variable v1 will reference a struct with the variables x and y and the static method variable Add.

Note that there are other additional functions for structs, specifically:

instanceof(variable);
is struct(struct_id);
variable_struct_exists(struct_id, variable);
variable_struct_get(struct_id, variable);
variable_struct_set(struct_id, variable, value);
variable_struct_get_names(struct_id);
variable_struct_names_count(struct_id);

Check the manual for details of how these functions should be used.


Exceptions, Try, Catch and Finally

The final new language feature that has been added in the 2.3 update that we want to discuss here is the ability to control to a much greater degree how errors are handled when they are encountered in your code as well as the ability to generate your own error messages.

To start with we have the new throwoperator, which - as the name implies - can be used to "throw" a runtime error message. This has the following syntax:

throw <expression>;

The expression used can be a value or a string or any other data type, and this will then generate an exception error which is - by default - shown on the screen, and on closing the error message the game will end. For example calling this:

throw "Hello World!";

will cause the following unhandled exception error to be shown:

Multiple Functions In One Script

This is the default way the error can be handled, but you can "take over" this error message and use your own handler code by calling the new function exception_unhandled_handler(). This runtime function permits you to supply a custom method to use that will be called whenever any unhandled exceptions occur in your game.

Apart from the ability to throw your own errors and handle them using functions, we also have the new try, catch, and finallyoperators. These can be used in your game for error checking and permit you to test out blocks of code and control what happens if any runtime exceptions occur. Using these will prevent the exception ending the game and will also prevent showing the standard error message to the user, but this means that you will have to handle what happens next in this case, like - for example - saving out log files and ending the game gracefully (note that if you choose to do nothing, your game may become unstable and not perform correctly).

At it's most basic the try syntax is as follows:

try
    {
    <statement1>;
    <statement2>;
    etc...
    }

However, having a try without anything to actually handle any exceptions the code may produce will not be very helpful, so we usually pair it with a catch, with the following syntax:

try
    {
    <statement1>;
    <statement2>;
    etc...
    }
catch(<variable>)
    {
    <statement1>;
    <statement2>;
    etc...
    }

What catch does is permit you to run extra code supplied in the following block when an exception from the previous try has been caught. If this is a runtime exception, then the supplied variable can be used to access a struct which will contain the following information:

{
message : "",               // a string that is a short message for this exception
longMessage : "",           // a string that is a longer message for this exception
script : "",                // a string that describes where the exception came from
stacktrace : [ "", "" ],    // an array of strings that is the stack frame the exception was generated
}

A simple example of use is shown below:

var a = 0, b = 0, c = 0;
try
    {
    c = a div b;
    }
catch( _exception)
    {
    show_debug_message(_exception.message);
    show_debug_message(_exception.longMessage);
    show_debug_message(_exception.script);
    show_debug_message(_exception.stacktrace);
    }

It may be that you want to run some code regardless of whether an exception was thrown or not, and so for that you can add in a finally block. The finally syntax is:

finally
    {
    <statement1>;
    <statement2>;
    etc...
    }

It is worth noting that you can have any combination of these together, ie: try / finally, try / catch or try / catch / finally, and that within the finally block you cannot use break, continue, exit or return statements as they have no meaning in this context and the compiler will generate an error if they are used.


Summary

Well, we've come to the end of this rather long blog post, and as you can see we've been working hard to improve the GameMaker Language and bring it up to par with other more common programming languages, We hope that these new features are of use to you, and that they help your workflow and enable you to make bigger and better games! Note that all the features discussed here are available from GameMaker Studio 2.3 onwards, and we recommend that you read the manual fully before using any of them, as there are details and nuances related to their use that we just haven't got the space to talk about here. Also note that the 2.3 update includes a number of changes and improvements to the GameMaker Studio IDE which we'll discuss a separate blog post.

Happy GameMaking!

Back to Top