NTT uses a custom scripting language that is largely equivalent to that of GameMaker: Studio. That means that you can use the official manual to learn the language.
Below are the elements that do not occur in or are different from "vanilla" GML:
GameMaker itself has you create scripts via UI and displays separate (or tabbed) code editors for each of them.
NTT uses one source file per mod, and thus uses a slightly different scheme - you can define an arbitrary amount of scripts per file using the #define directive:
#define scr_1 // scr_1' code here #define scr_2 // scr_2' code here #define scr_3 // and so on
You can also name the script' arguments by specifying them on #define-line:
#define scr_add(a, b) return a + b;
Which is equivalent to having
/// scr_add(a, b) return argument0 + argument1;
in regular GML.
NTT also supports macros, with syntax much akin to that in GameMaker Studio 2.
In short, the syntax is
#macro name value
and simply substitutes uses of name by specified value during compilation.
This is commonly used for constants to avoid having magic numbers/strings:
#macro maxwep 127 trace(maxwep); // shows "127"
Same as with GMS2, specifying a non-constant expression as value will have it evaluated at runtime, allowing to make "shortcuts" for otherwise lengthy function calls.
NTT supports GMS2-specific ternary operator (see manual), meaning that you can do
draw_set_color(team == 2 ? c_blue : c_red);
instead of having to do
if (team == 2) { draw_set_color(c_blue); } else draw_set_color(c_red);
(or making an additional local variable)
NTT supports GMS2-specific array declaration syntax, meaning that you can do
var array = [1, 2, 3];
instead of doing
var array = array_create(3); array[0] = 1; array[1] = 2; array[2] = 3;
Same as regular GML, doing
something[1] = 4;
would replace something by a new 2-element array (containing values 0 and 4) if it wasn't an array before.
However, unlike regular GML, NTT's interpreter does not implement "copy on write" mechanic, meaning that modifying an array will always modify the original (referenced) array instead of making a copy of it based on unclear rules.
If you need to clone an array, you can do so explicitly via array_clone or other functions.
Using [@index] for write-access remains preferable for all cases where you are sure that a value is an array, as it saves from runtime checks for whether it is.
Like with current versions of GM, variable_instance_ functions are supported:
trace(variable_instance_exists(self, "test")); // 0 variable_instance_set(self, "test", 4); trace(variable_instance_exists(self, "test")); // 1 trace(variable_instance_get(self, "test")); // 4
However, the nature of scripting generally implies that often you'll find yourself setting variables on instances only if they aren't set yet, which prompts for a shorter syntax.
Therefore NTT's GML variant supports <variable name> in <instance id> syntax.
So, instead of having
if (variable_instance_exists(self, "count")) { count += 1; } else count = 1;
You can have
if ("count" in self) { count += 1; } else count = 1;
Or even
count = (("count" in self) ? count : 0) + 1;
For cases where you would use
if (!variable_instance_exists(self, "marked")) { marked = true; // do something }
you can also do not in:
if ("marked" not in self) { marked = true; // do something }
Sometimes you may want to group several values together in an easy-to-read way, but without the hassle of creating-managing-destroying instances or data structures. For such cases you can use lightweight objects instead.
To create one of these, use { key1: value1, key2: value2, ... }:
myobj = { num: 1, str: "hi" };
you can then work with it exactly like you would with instances:
myobj.num += 1; trace(myobj.num);
when you are done using it, no further action is required - lightweight are freed as soon as there are no more variables referencing them, just like with arrays.
Also see lightweight object API.
Another quality-of-life feature of the mod's scripting language is template strings (sometimes also called template literals or string interpolation).
Say, you want to display "HP: <current>/<max>" in UI.
Normally you would need to do it like
#define draw_gui with (Player) { draw_text(10, 50, "HP: " + string(my_health) + "/" + string(maxhealth)); break; }
which is... a little inconvenient.
With template strings (denoted via `` quotes), however, you can use ${expression} right inside the string to have values converted to strings and appended:
#define draw_gui with (Player) { draw_text(10, 50, `HP: ${my_health}/${maxhealth}`); break; }
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.
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.