Tech

GML Updates in 2019

As you may have seen in our GMS2 2019 roadmap we have ‘GML Updates’ listed for Q4. When we polled our community last year, the extension of GML received overwhelming support. In anticipation of these improvements, I would like to share with you an overview of the features we’re introducing and the thinking behind our approach. Updates to GameMaker Language and the other changes coming in version 2.3 will be available later this year. We will be going into more depth on the rest of version 2.3 in future tech blogs.

Chained Accessors

Historically array accesses in GML have only been 1 deep; a variable can only be dereferenced once in an expression. This has meant that an array foo could only be accessed like this:

foo[index]

This could not be expanded to multiple dimensions (as many users have desired over the years), and we are changing this to allow any number of dimensions to be added so that it will be possible to do:

foo[index1][index2][index3]

This will perform as expected (if the multiple levels are arrays) and will cause errors if any part of the chain is not an array.

We will be expanding this to include general accessors, and not just the array accessor, so that mixed accessors can be chained together, e.g.:

foo[ index1 ][# index2, index3 ][? “key” ]

This would dereference the array foo at index1 and interpret the result as a grid index, which will be dereferenced at index2, index3, the result of which is then interpreted as map index that is dereferenced with the string “key” as the key - the value of that will be returned.

A side effect of this feature is that all arrays in GML will now be 1-dimensional, and the 2-dimensional syntax of:

foo[ index1, index2 ]

Will be the equivalent of doing:

foo[index1][index2]

This should be transparent for older projects and everything will just work as normal.

As an implementation detail, all GML arrays have been 2-dimensional at runtime, and the runtime has jumped through hoops to hide that from users. This change should speed up all array accesses as it does not need to index two arrays anymore – it should also help memory fragmentation as an array now only has one axis to be allocated.

The only projects that will have issues here are projects that have taken advantage of implementation details in historic GML and have hardcoded any of the following:

Mixing use of 1-dimensional and 2-dimensional accesses on the same array Using the internal 32,000 axis limit in some way when accessing the array (this limit has been removed) Using array_length_2d functions (these will need to be changed to get the correct 1-dimensional size)

We will also be adding an array_length function that will be the equivalent of array_length_1d (which we will be keeping for backward compatibility) and deprecating the array_length_2d and array_height_2d functions.

Method Variables

The ability to declare a function in an expression is being added to GML. This function will be bound with the self reference when the expression is evaluated, and the result is something that we call a method. This method can be called with the usual function call syntax and passed parameters just like any other function (built-in function, script or extension).

This means that it will be possible to do:

var log = function(a) { show_debug_message( a ) } log( “Hello World” );`

The important things to note here are that the function is anonymous and the arguments have user-defined names (not just argument0…argument15). All functions can have properly named arguments now!

A named function works similarly, and you could write the above like this:

function log(a) { show_debug_message( a ); }

In this case, the function log would be an instance variable and not a local variable (assuming this code existed inside an event).

The contents of these functions can be anything that could be inside a normal script as they exist now. The important thing is that the self used when these are called is the self that was active at the time of binding, e.g.:

with( objEnemy ) { function foo( a ) { //…. } }

This would declare a variable foo that is a method in all the instances of object objEnemy that are active.

Functions declared this way do not have access to the local variables (declared with var ) that are active at the point of declaration, but they do have instance variable access.

Methods can be assigned to any variable and all rules for variables apply to them: they can be passed around as arguments and can be used as callbacks easily for library functions. For example:

my_load_async( “filename”, function( success, error ) { if (success) { show_debug_message( “filename successfully loaded” ); else show_debug_message( “error loading filename - “ + string(error) ); });

This calls a library function my_load_async where the second parameter is a function that can be called when the main work has been done within the library function.

Methods can also be used in preference to User Events and effectively gives the ability to create interfaces that can be inherited through the normal parent-child relationships of objects.

A new function method( <instance>, <original-method> ) is provided to rebind an existing method variable to be called in the context of another instance (the bound self ). This can be used to re-target and copy methods into new contexts, which will be useful for low-level libraries to use.

Multiple scripts in a single source file

As we now have a syntax which requires function declaration, we will be changing script resources. Instead of a script being a single body of code, it can contain multiple named functions. Any existing projects will be automatically updated for this new format.

For example, a project that contains two script resources Foo and Bar will be updated so that they are still two separate files but their content is wrapped in a named function that is the name of the script.

In the script Foo :

function Foo( argument0 ) { /// contents of the Foo script in here... }

In the script Bar :

function Bar( argument0, argument1, argument2 ) { // contents of the Bar script in here... }

This change will mean that the name of the script resource does not need to be the same as the functions inside it, and many functions can be present inside a single script resource (it is more of a source file resource now).

The script resource is now executed at global scope, so the Foo and Bar functions above are, in addition to being accessible as just Foo and Bar (without a prefix), also declared in the global.Foo and global.Bar variables. Any code between (and outside) the function declarations will be executed at global scope – identical to gml_pragma( “global”, … ) fragments – and thus also executed before any room is created.

Lightweight Objects

We are introducing the concept of a lightweight object into GML. Lightweight objects are accessed like an instance but have no built-in variables or behaviours associated with them, like the events of an object resource. Because of this, they can be used to create your own data structures. They can be viewed as a JavaScript object, a C# Dictionary or a C++ std::map.

An example of a lightweight object in GML:

var a = { foo : “Hello”, bar : “World”, func : function( a, b ) { // insert function code here. } ex : global.my_variable, num : 10 * argument0, // in general the syntax is // <variable-name> : <expr-to-assign-to-variable> };

Any expression is evaluated in the current context of when the object literal is executed – i.e. when the local variable a is initialised.

This is an object literal syntax, and, like the array literal syntax (introduced early on in GMS2), it is relatively simple and allows the user to declare an object in line with the code.

Lightweight objects are similar to instances in that they can hold variables (and these are accessed and used in the same way), but unlike instances they cannot be added into rooms – they are pure data and not instances of object resources. Lightweight objects can be passed around like any other variable in GML and can be used as a data structure with methods and essentially act as a class in its own right. There is no connection between a lightweight object and an object resource.

The typeof function will return the string “object” when given a lightweight object.

“Static” variables on a lightweight object will be supported.

‘New’ operator

By introducing a new operator, we are bringing constructor-like functionality to GML. The basic operation of the new operator is to create a lightweight object, as seen in the previous section, and then passing that empty lightweight object into a function (that can take arguments) and the resulting object that has been manipulated by that function is then returned as the resulting new variable.

Given this Vector3 function:

function Vector3( _x, _y, _z ) { x = _x; y = _y; z = _z; add = function( _v ) { x += _v.x; y += _v.y; z += _v.z; } } var vec = new Vector3( 10, 20, 30 );

We can create an empty lightweight object, pass that as the self into the Vector3 function with 3 parameters _x, _y, _z (here set to 10, 20 and 30) which assigns them to variables x, y and z on the lightweight object and also adds a method variable add to it. This is then returned and assigned to the local variable vec .

This gives a simple and consistent method for the creation of lightweight "class"-like objects that can be provided through simple libraries.

We will also be providing a delete function that can be used to explicitly garbage-collect the lightweight object (it will otherwise be garbage-collected automatically when no longer used).

Exception support

Runtime errors in GML have traditionally stopped an executing game abruptly and shown a corresponding error dialog to users. In this update we are promoting runtime errors in GMS2 to be exceptions and providing new functionality to allow users to create try-catch-finally blocks. These allow exceptions to be tried and caught in a sensible way, and we are also introducing the throw keyword to allow users to create their own exceptions and handle them themselves.

Our current error dialog will be used for any uncaught exceptions that are created when running a game, and if no code catches them then you will continue to see the current dialogs at runtime.

You can use try-catch-finally as follows.

try { // code to execute while monitoring exceptions } catch ex { // do something if an exception is caught // note the variable ex will hold the exception that has been caught } finally { // this code in here will always be executed after the try (or the catch) sections have executed }

This is similar to other languages and will provide a user mechanism to intelligently handle errors in a better way within their own code.

We will also be providing a mechanism to replace the unhandled exception handler so users can handle and render any errors themselves.

It is important to note that some errors will still be catastrophic and cannot be handled as exceptions (e.g., some pathological “Out of Memory” errors and OS-level exceptions) as they do not always happen within a GML context.

Garbage Collector

Between method variables and lightweight objects, we are seeing more memory traffic and less obvious ways of manually managing it, so we have added a fully multi-threaded, multi-generational garbage collector that handles the lifetime of all variables and instances inside the GMS runtime.

This runs once a frame to collect the first generation and cleans up any variables and instances that have been forgotten about in the last frame. All the collection itself is done on another thread, so this frees the main thread to concentrate on the execution of the game.

The collector is very fast and has minimal impact on the running game, and additionally, we will be introducing a suite of functions for controlling some of the behaviour – but the specifics of that has not been finalised at this time.

Conclusion

With these new features, GML is becoming a more mature language, gaining features that should give our users a higher quality of life and allow for more and better GameMaker Studio games. Following the release of this blog we post a thread on the GameMaker Community forums that will aim to field questions about these GML improvements and this blog. The post will detail how to submit questions and later we will return with a follow-up post that answers some of these questions.