Clone wiki

NuclearThroneTogether / Scripting / Language Features

The following are the differences and extensions of scripting language used, compared to GameMaker Language itself:

Script/function definitions

Script/function definition syntax matches the little-known multiscript/extension script file syntax in GameMaker.

That is, each script/function is to be preceded by a #define scriptName header-line and will be considered to span until the next header or the end of file. For example,

#define init
test("Hello!");

#define test
trace("Test: " + argument0);

Depending on mod type, certain scripts will be automatically called by the game. Lists of these can be found on the according mod type page.

Named arguments

While you can use the standard argument# variables to access arguments, there is also a bit of syntax for naming them.

To use it, simply open parentheses after the #define statement and list the arguments:

#define draw_text_bang(rx, ry, text)
draw_text(rx, ry, text + "!");

which would have been exactly equivalent to

#define draw_text_bang
draw_text(argument0, argument1, argument2 + "!");

while a small syntactic enhancement, this allows to spend a bit less time typing out short scripts.

Macro definitions

As with GMS2, you can declare mod-wide macros by using #macro name value syntax,

#macro version 1001
// ...
trace(version); // equivalent to trace(1001)

Macros are replaced with their values during compile and are a way of implementing constants or shortcuts without any additional overhead.

Ternary operator

Ternary (inline if-then-else) operator is supported. This means that you can do, for example

draw_set_color(instance_exists(Player) ? c_red : c_blue);

Which would be equivalent to

if (instance_exists(Player)) {
    draw_set_color(c_red);
} else draw_set_color(c_blue);

Array write

Array access behavior is "create-on-write", meaning that when you do

var a;
a[0] = 1;
a[1] = 2;

it in fact compiles to something like

var a;
if (!is_array(a)) a = array_create(0);
a[@0] = 1;
a[@1] = 2;

You can always explicitly create arrays via array_create or array declaration syntax (see below) and use [@index] array access syntax to indicate that you are sure that a variable is an array and there's no need to ensure that it is an array prior to writing.

Array declaration

JavaScript-like array declaration syntax is supported, meaning that you can do

var a = [1, 2, 3];

which is a faster equivalent of

var a = array_create(3);
a[@0] = 1;
a[@1] = 2;
a[@2] = 3;

Accessors

Unlike the regular GML (so far), accessors can be chained, meaning that you can do

var m = [[1, 2], [3, 4]];
var v = m[0][1]; // v == 2

without having to assign into m[0] into an intermediate variable.

"in" operator

variable_instance_ functions are supported in the scripting language, meaning that you can do

trace(variable_instance_exists(self, "some")); // 0 or 1, depending on whether variable exists
variable_instance_set(self, "some", "hi");
trace(variable_instance_get(self, "some")); // "hi"

since variable_instance_exists is a pretty long name for something that you may want to do often, another thing that is supported is a (variable name) in (instance) operator, meaning that you can do

if ("time" in self) {
    // variable exists - increment it
    time += 1;
} else {
    // variable does not yet exist - give it a default value
    time = 0;
}

for convenience, you can also use not in:

if ('time' not in self) time = 0;

Lightweight objects

Sometimes you may want to group a bunch of values together, but without the hassle of creating-managing-destroying an instance. In such cases you may want to use lightweight objects instead:

var q = { a: 1, b: "hi" };
trace(q); // { a: 1, b: "hi" }
trace(q.b); // "hi"
trace("a" in q); // 1

Internally, lightweight objects are stored as 2d arrays, meaning that they are managed by the game automatically (no need to explicitly destroy), and the above sample would be stored as

1    "hi"
"a"  "b"
<id>

(where <id> is a field pattern id for faster lookup)

Wait-instruction

One of the scripting language's most notable features is the wait-instruction.

To put it shortly, when executed, the program will pause for the given number of frames while the rest of the game continues executing. So,

for (var i = 1; i <= 5; i++) {
    trace(i);
    wait 30;
}

will count up to 5 while waiting 30 frames (one second) between each step.

This allows to do lots of interesting things. For example, to make an explosive bullet for a custom weapon, you could simply wait for projectile to be destroyed while tracking it's position:

#define weapon_fire
motion_add(gunangle, -4);
weapon_post(6, -7, 5);
// create and configure a projectile:
var qx = x, qy = y;
var q = instance_create(qx, qy, HeavyBullet);
with (q) {
    team = other.team;
    motion_add(other.gunangle + random_range(-5, 5), 1);
    friction = -0.8; // gradual acceleration
    image_angle = direction;
}
// track projectile' position while it exists:
while (instance_exists(q)) {
    qx = q.x + q.hspeed;
    qy = q.y + q.vspeed;
    wait 1;
}
// create an explosion at it's last position once it's gone:
instance_create(qx, qy, SmallExplosion);

Overall, wait is extremely useful for writing sequentially executing code without hassle.

Fork-instruction

Another notable language feature is the fork instruction.

fork() acts like a function that, when called, will create a copy of the currently running script. The copy will have it's own (copied) local variables, but will share global variables, arrays, and game context with the original; The copy will execute it's script and finish while the original will continue it's own way; The function returns true to the copy and false to the original. For a more visual example,

if (fork()) {
    trace("fork");
} else trace("orig");
trace("post");

This would output

fork
post
orig
post

since the copy executes before the original script resumes. If you were to add a wait call into the copy however, the original would resume as soon as the copy pauses:

if (fork()) {
    trace("fork");
    wait 1;
} else trace("orig");
trace("post");

which would output

fork
orig
post [from orig]
[1 frame pause]
post [from fork]

The most common use case scenario for fork is executing something involving wait without interrupting the original program. For that you would insert an exit statement in the end of fork's branch to prevent it from executing the rest of the script. For example,

if (fork()) {
    wait 10;
    trace("fork");
    exit;
} else trace("orig");
trace("post");

which would output

orig
post [from orig]
[10 frame pause]
fork

So, if you wanted to give the earlier shown explosive bullet weapon to also have triple-shot (each projectile being tracked and exploding, obviously), you could do that like so:

motion_add(gunangle, -4);
weapon_post(6, -7, 5);
for (var i = -15; i <= 15; i += 15) if (fork()) {
    var qx = x, qy = y;
    var q = instance_create(qx, qy, HeavyBullet);
    with (q) {
        team = other.team;
        motion_add(i + other.gunangle + random_range(-5, 5), 1);
        friction = -0.8;
        image_angle = direction;
    }
    while (instance_exists(q)) {
        qx = q.x + q.hspeed;
        qy = q.y + q.vspeed;
        wait 1;
    }
    instance_create(qx, qy, SmallExplosion);
    exit;
}

To summarize, fork compliments wait and even further extends what you can do with it.

In conclusion

While scripting language is overwhelmingly compatible with GML and you can write the code exactly the same way as you would in GameMaker, the new tricks that it brings can be used to do tasks even faster and easier.

Updated