Game Development Community

dev|Pro Game Development Curriculum

TGE scripting language reference

by Joel Baxter · 02/15/2002 (3:58 pm) · 18 comments

I'm not aiming to describe the various script functions and how to use them, but rather to describe the language itself (although I do go over a few engine-defined script functions). And actually I don't attempt to describe everything about the language -- this is not a formal grammar by any means -- just some interesting parts.

Please add comments with any corrections or additions that you find necessary.


BASIC ELEMENTS

Reserved words: break, case, continue, datablock, default, else, false, function, if, for, new, or, package, return, switch, switch$, true, while

Other tokens:
- String constant (character sequence enclosed in single or double quotes)
- Variable (begins with % for local variable or $ for global variable, followed by a letter, followed by an optional sequence of additional letters/numbers/underscores/colons; can't end with a colon)
- Identifier (Letter followed by an optional sequence of letters/numbers/underscores)
- Number (decimal integer or float, or hex number starting with 0x)

Operators:
- Straight assignment: =
- Math: + - * / % += -= *= /= %= ++ --
- Bit: ~ | & ^ << >> |= &= ^= <<= >>=
- String concat: @ NL TAB SPC
- Boolean: ! && ||
- Numeric compare: == != < > <= >=
- String compare: $= !$=

Other special chars:
- Conditional ? :
- Array subscripting [ ]
- Grouping ( )
- Blocking { }
- Listing ,
- Namespace ::
- Member .

String constants can be enclosed by single quotes or double quotes. A single-quoted string is a "tagged string". Tagged strings should be used for any string constant that is transmitted across a connection; the entire string is only sent once, then on subsequent transmissions a short tag identifying that string is sent instead.

Strings can contain the usual \r, \n, and \t codes. They can also use \x codes, where \x is followed by a 2-digit hex number. The code will be replaced by the ASCII character with that value. Finally, strings that are to be displayed can use \c codes to suggest color changes (although it is up to the object-specific engine code to use those suggestions). \cr resets the color to the default, whatever that may be for the type of display that is being done. \cp pushes the current color onto a stack, and \co pops the color from the stack. \c followed by a single digit sets the current color to that entry in the color table; the contents of the color table depend on the situation, but for a GUIControlProfile object you can define the whole array with the fontColors field, or entries 0-3 with the fontColor, fontColorHL, fontColorNA, and fontColorSEL fields.


STRUCTURE

A script is a sequence of statements, function declarations, and package declarations.

Most of the syntax is C-like. Although, as is often the case in scripting languages, there's usually no type enforcement on the variables, and you don't declare variables before using them. (If you read a variable before writing it, it will be the empty string or zero, depending on whether you are using it in a string context or a numeric context.)

Mainly it's the use of objects and namespaces that takes some getting used to, so I'm going to focus on those features.


OBJECTS

Every object in the game has a numeric identifier; let's call it a "handle" to avoid confusion with the identifier type of token. When the engine code calls a script function, it passes handles to identify objects that should be operated on; the script functions in turn often pass these handles around among themselves.

Object handles are unique. You can numerically compare two object handles to see if they refer to the same object.

Additionally, the object handle of a datablock can be passed across a network connection and used on the other side; datablocks always exist on the server and all its clients, and they use the same handle regardless of whether they are on the server or on some client. However, handles do not behave that way for non-datablock objects; for a non-datablock object, a server object will have a different handle than its client ghost objects, so there is no point in sending an object handle over a network connection unless it references a datablock object.

Handles can be used to access object fields. If you have an object handle contained in the local variable %object, you can access a field of that object like this:
%object.foo_field = 124.5;
You can also call a function in reference to an object:
%object.foo_function(12, "foo", "bar");
This will cause some function named foo_function to be executed. There may be several functions with the name foo_function in your script, as long as they are in different namespaces; the particular foo_function to be executed will be selected according to the object's namespace and the namespace hierarchy. This process will be covered in detail in the section on namespaces later.

When a function is called in reference to an object, implicitly the function will be passed another argument at the beginning of the arguments list: a handle to the object. Customarily this additional argument is named %this in the parameter list of the function declaration, to indicate that it can be used like the pointer of the same name in C++ objects. So the declaration for our foo_function would have four parameters, the first of which being %this.

Handles are not the only way to access objects. Many objects, including all datablock objects, also have a name. This name can be used to access the object, if you don't have a handle available (it's faster to use the handle if you have it). Objects are named using identifiers, but an identifier is just a string that has a restricted format. So for referencing an object by its name, you can use strings, identifiers, or variables containing strings/identifiers. For example, if the object in question is named MyObject, all of the following snippets are identical:
MyObject.foo_field = 124.5;

	"MyObject".foo_field = 124.5;

	%objname = MyObject;
	%objname.foo_field = 124.5;

	%objname = "MyObject";
	%objname.foo_field = 124.5;
(Although those examples are accessing a field, you can also of course use object names/identifiers to invoke functions, just like using object handles to do so.)

FYI, here are some relevant console functions, defined by the engine so they are available to all scripts:

- When invoked in reference to an object, getID() returns the object's handle.

- An object's handle can also be determined by the global console function nameToID(expression), where expression is any of the things that can be used to identify an object.

- The global console function isObject(expression) is similar, except it just returns a boolean rather than an object handle.

- When invoked in reference to an object, getName() and setName(newname) can be used to read and write the object's name.

- When invoked in reference to an object, getClassName() returns the object's class.

If you're used to C++ there are a few things that probably need emphasizing about TGE scripting objects:

- There is nothing special about %this; the use of %this is never implied. If you want to access a field of an object, you always have to use something that evaluates to an object handle or name followed by a dot followed by the field name, as in the examples above. The only exception to this rule is in the sequence of field initialization statements when creating an object.

- You may see some variable names in the scripts that use double colons, so that they look like C++ static member fields of a class. In TGE script, however, colons have no special meaning in a variable name, and so those variables have no special relationship to any class or object. I suppose that the use of double colons in variable names can either be viewed as a red herring or as a helpful indicator of the variable's purpose.

- Object handles always behave the same way when used to access object fields or invoke functions. There is no sense in which an object handle can ever be "cast to another classtype" to change its behavior. When a handle or object name is used to access a field, it always references the same bit of data. When a handle or object name is used to invoke a function, the same function is always executed (unless a package has been activated... more on packages later).


DATABLOCK CREATION

A datablock is an object that contains a set of characteristics which describe some other type of object. For instance, a PlayerData object is a datablock that contains many parameters describing the speed, mass, and other properties of a Player object. When a Player object is created, it is set to reference some already-existing PlayerData object that will tell it how to behave. Most objects may come and go throughout the course of the game, but datablocks are created once and are not deleted. Datablocks have their own specific creation syntax:
[b]datablock[/b] class_identifier[b]([/b]name_identifier[b])[/b]
	[b]{[/b]
		initialization_statements
	[b]};[/b]
The value of this statement is the handle of the created datablock.

The class_identifier must be a valid datablock class name, like PlayerData. The name_identifier is whatever name you choose to give this datablock (as long as it follows the rules for being a valid identifier, and is not a name already in use).

initialization_statements is a sequence of statements, each an assignment to an identifier. Each identifier specifies a datablock field. It's possible for the contents of these fields to be accessible by both the script code and the engine code, and in fact that is often the case; in that situation, you of course need to assign a value to the field that makes sense for the type of information it's supposed to be holding. The engine has rules for how it converts between the script representation of values and its own internal representation. Most of the time the correct script format for a value is obvious; numbers are numbers, strings are strings, booleans can be 1/0 or true/false. More complicated data types will be represented on the scripting side of things with a string of some format; for example, a point is a string containing three numbers separated by single spaces, like "0.1 5.2 4.44".

You don't have to restrict yourself to only initializing (and later using) fields that are accessible by the engine code. An object can have other fields as well; the engine code can't read them, but the scripts can. In the TGE SDK, inventory management is completely done in the scripts, using object fields that the engine code is not aware of.

Finally, note that there's a variation on the datablock creation syntax:
[b]datablock[/b] class_identifier[b]([/b]name_identifier [b]:[/b] copy_source_identifier[b])[/b]
	[b]{[/b]
		initialization_statements
	[b]};[/b]
copy_source_identifier specifies the name of some other datablock from which to copy field values before executing initialization_statements; this other datablock must be of the same class as the datablock we are creating, or a superclass of it. This is useful if you want to make a datablock that should be almost exactly like a previously created datablock, with just a few changes, or if you want to centralize the definitions of some characteristics in one datablock that can then be copied by multiple other datablocks.

As an example, the Tribes 2 scripts create a SimDataBlock object named LightPlayerDamageProfile, which defines constants related to the effects of received weaponfire. Then the PlayerData object named LightMaleHumanArmor is created to define all sorts of player characteristics, and it copies values from LightPlayerDamageProfile (which it can do because SimDataBlock is a superclass of PlayerData). The PlayerData objects LightFemaleHumanArmor and LightMaleBiodermArmor both copy from LightMaleHumanArmor and then change some graphics- and sounds-related fields. (N.B.: if you look at the T2 scripts to see exactly what they do, note that T2 scripting uses a different format for specifying the copy source.)


OTHER OBJECT CREATION

Creating non-datablock objects is fairly similar to creating datablocks. The basic syntax is
[b]new[/b] class_expression[b]([/b]name_expression[b])[/b]
	[b]{[/b]
		initialization_statements
	[b]};[/b]
The value of this statement is the handle of the created object.

The class_expression can be an identifier specifying a (valid) object class name, as with the datablock declaration. Or, it can be an expression in parentheses that evaluates to an identifier or a string that specifies a valid class name. Similarly for name_expression, although the name_expression can be omitted if you don't want to name the object.

The initialization_statements can consist of two parts. First is an optional sequence of identifier assignment statements, as in the datablock declaration, if you want to assign values to the object fields at this time. Second is an optional sequence of more object creation statements. You can omit either or both. If you omit both, you can also omit the curly braces.

Like the datablock declaration (and using the same format), in the object creation statement you can optionally specify the name of some other object to copy field values from:
[b]new[/b] class_expression[b]([/b]name_expression [b]:[/b] copy_source_identifier[b])[/b]
	[b]{[/b]
		initialization_statements
	[b]};[/b]
There's one more twist to the object creation syntax. After the name_expression (or, if you're using a copy_source_identifier, then after that) you can place a comma followed by a comma-separated list of arguments to be passed to the engine code that creates the object. However no objects except for TCPObject make use of this feature in the TGE SDK.


NAMESPACES

When an object is created, it gets a namespace, and this is used along with the namespace hierarchy to figure out what function to execute when you call a function in reference to that object.

By default, an object's namespace is its class name, and the namespace hierarchy is the class hierarchy of console objects. The default setup will be modified, especially in the case of datablock objects, but let's just look at an example using the default setup first.

Let's say you have a Projectile object. Its namespace is Projectile. Proceeding up the namespace hierarchy from Projectile is the sequence of namespaces Projectile->GameBase->SceneObject->NetObject->SimObject->ConsoleObject, because that's what the class hierarchy looks like.

If you were to invoke foo_function in reference to that Projectile object, the script interpreter sees this as a call to foo_function in the Projectile namespace, i.e. Projectile::foo_function. If Projectile::foo_function doesn't exist, though, then it looks for GameBase::foo_function, since that's the next namespace up the hierarchy. If that doesn't exist either, then it tries SceneObject::foo_function, etc. If it doesn't find a valid foo_function then it complains with an error message.

As an alternative to invoking a function in reference to an object, you can choose to explicitly specify the namespace of the function. Unlike invoking a function in reference to an object, invoking a function with an explict namespace does not implicitly add an object handle to the argument list. However, the function probably expects an object handle at the beginning of the list, and if that's the case then you'll want to explicitly pass it a handle there. For example, both of these invocations do the same thing if %object is a handle for a MyObject object:
%object.foo_function(12, "foo", "bar");
	MyObject::foo_function(%object, 12, "foo", "bar");
Those would both call the same foo_function, a function that is expecting four arguments. And in either case, if MyObject::foo_function does not exist, then the script interpreter will start searching up the namespace hierarchy to find a foo_function that does exist.

The identifier Parent is a special namespace that indicates the next namespace up the hierarchy from the namespace of the current function, whatever that might be. So inside of Projectile::foo_function, Parent would refer to the GameBase namespace. Executing Parent::foo_function would be the same thing as executing GameBase::foo_function. One might wonder why use Parent, then. For one thing, it makes it more clear what you are doing. For another thing, it's more robust... it will continue to work as intended if the namespace hierarchy gets changed by modifications to the classes in the engine code, or by the scripting.

Which brings us to the tidbit that when you create an object, you can change the namespace hierarchy and the namespace of the object. There are a few different ways in which this can be done, depending on the type of object. (I'll be describing the behavior of objects in the unmodified TGE SDK.)

When creating an object of type GameBaseData or a derived class, i.e. a datablock object, you can specify a className field that adds another namespace to the hierarchy, below the class name of the object.

Also, many types of objects (EditManager, GameBaseData, TCPObject, GuiControl, and all their derived classes) use the object's name to add another namespace to the hierarchy, and the object's namespace is set to be equal to its name. So, if I created a PlayerData object with className SuperPlayer and named it Hulk, its namespace would be Hulk, and the path up the namespace hierarchy would be Hulk->SuperPlayer->PlayerData->ShapeBaseData->etc. On the other hand, if I named it Hulk but did not specify a className, then the object's namespace would still be Hulk, but the path up the hierarchy would just be Hulk->PlayerData->ShapeBaseData->etc.

BTW, if you use setName() to change an object's name, this does not change the namespace hierarchy or the namespace of the object.

There's also a generic type of object, ScriptObject, which has two fields named class and superClass. The class field sets the object's namespace (but not its response to getClassName). If you are using the class field, you may also use the superClass field, which inserts another namespace in the hierarchy between the class name and ScriptObject.

Namespaces are a powerful tool to define general behavior for your objects and then more and more specific behavior as needed. The weapon scripting in the SDK is a good example of this. One weapon item datablock is named Crossbow with a className of Weapon. (Its namespace would therefore be Crossbow, and the path from Crossbow up the namespace hierarchy would be Crossbow->Weapon->ItemData->etc.) The other is named Rifle, also with a className of Weapon.

The ItemData::onPickup method defines how to manage the player inventory and item respawning when an item gets picked up. That works for weapons as well as for other items, but when you pick up a weapon it should also be readied for use. So there's a Weapon::onPickup function declared as well, to perform that weapon-readying:
function Weapon::onPickup(%this, %obj, %shape, %amount) 
{ 
   // The parent Item method performs the actual pickup. 
   // For player's we automatically use the weapon if the 
   // player does not already have one in hand. 
   if (Parent::onPickup(%this, %obj, %shape, %amount)) { 
      if (%shape.getClassName() $= "Player" && %shape.getMountedImage($WeaponSlot) == 0) { 
         %shape.use(%this); 
      } 
   } 
}
This function overrides ItemData::onPickup if onPickup is invoked in reference to a crossbow or rifle item, because when running up the namespace hierarchy from Crossbow or Rifle, Weapon comes before ItemData. Note that this code calls Parent::onPickup so that the inventory and respawn management in ItemData::onPickup will be executed.

There are no declarations for Crossbow::onPickup or Rifle::onPickup, because the scripts don't want to do anything specific to the type of weapon picked up. But, they could.

This example nicely shows why the className feature exists; it allows you to specify a behavior for a subgroup of objects of a certain class. Without className, to do weapon-readying we would have had to declare duplicate functions named Crossbow::onPickup and Rifle::onPickup, which would be bad. Or else we would have had to modify the engine code to create a subclass of ItemData specifically for use with weapons.


FUNCTION DECLARATION

The function declaration syntax is
[b]function[/b] function_identifier[b]([/b]params_list[b])[/b]
	[b]{[/b]
		statements
	[b]}[/b]
The function_identifier can either be a single identifier, or two identifiers separated by a double colon. The first format just names the function according to the identifier and puts it into the global namespace. The second format uses the identifier before the double colon to specify the function's namespace, and the identifier after the double colon the function's name.

A function does not have to be passed as many arguments as are in its parameter list. If a shorter argument list is used, then some parameters at the end of the list will be the empty string, like any uninitialized variable in the scripts.

Functions may use a return statement to return any sort of value.


PACKAGE DECLARATION

The package declaration syntax is
[b]package[/b] name_identifier [b]{[/b]
		function_declarations
	[b]}[/b]
A function declared inside a package declaration is initially invisible to namespace hierarchy searches; it can't be used. But when the name of its containing package is passed to the activatePackage global console function, then that function becomes usable, and it overrides any other function that has the same name and namespace. (It can call the overrided function using Parent.) The package can be deactivated by passing its name to the global console function deactivatePackage.

#1
02/17/2002 (8:02 am)
Great quick reference Joel!
#2
03/28/2002 (2:26 pm)
A couple of random additions:

-----

If you carefully read the object creation statement syntax and wonder "why the heck would you want to put another object creation statement inside it?" (I know I did), the answer appears to be that if you create other objects inside the curly brackets of a creation statement for a SimSet or SimGroup, then those objects will automatically be added to the set/group.

Take a look at a mission file to see an example of this (adding objects to the SimGroup named MissionGroup).

-----

Arrays are a flexible beast in TGE scripting; you can throw just about anything you want inside those square brackets. It might help to think of an "array" reference as just another variable name, part of which can be evaluated on the fly without the explicit use of "eval". Certainly there are no restrictions on the array indexing that you might be expecting from experience with arrays in other languages.

For example, if you do this:
%foovar = "a";
   %fooarray[%foovar @ "b"] = "hi";
   echo(%fooarray["ab"]);
it will print "hi" to the console.

You can also use a comma to separate portions of the "array index", so the following code will also print "hi":
%foovar = "blah";
   %fooarray["bleh",%foovar] = "hi";
   echo(%fooarray["bleh","blah"]);
Just FYI, using a comma is equivalent to concatening the portions of the "index" using the underscore character, so here's yet another version of hi-printing code:
%foovar = "blah";
   %fooarray["bleh",%foovar] = "hi";
   echo(%fooarray["bleh_blah"]);
That little tidbit about the underscore probably won't be very useful, but if you happen to be using underscores in some of your array indices and comma-concatentation for some other indices of that same array, you could run into trouble if you're not aware what the comma is actually doing.
#3
03/29/2002 (9:48 am)
I like this thing, I'm using it to help me learn the scripting stuff.
#4
03/30/2002 (8:24 pm)
Another little thing about array variables names: the name with the square brackets represents the same variable as without. So, $a[0] and $a0 are the same variable.

(Of course, you have to use the brackets if you need to evaluate a variable to form the index... $a[%foo] is not the same as $a%foo, and in fact the latter form won't even compile.)
#5
07/15/2002 (10:38 am)
very nice doc!

Thanks, Joel!

Laurent
#6
08/07/2002 (11:35 am)
How would I go about collating a list of class_identifiers?
#7
04/16/2004 (10:02 pm)
just got the engine tonight, thank you very much for this nice reference!
#8
07/17/2004 (11:53 am)
I've been working through Finney's excellent 3DGPai1, but I still was having a lot of trouble with some fundamental aspects of syntax and structure, especially the question of namespace. Your clear and lucid explanation really straightened things out for me. Thanks!
#9
07/29/2004 (12:50 am)
I agree there with Bill. I had a hard time understanding where a object was getting its function from when I looked up the function and saw that it was not a part of that object. Now that I see that functions go up the heirarchy, I see that function in a higher level in the chain. That really explains alot.
Thank you
#10
01/24/2005 (8:23 pm)
A very nice intro to the scripting language Joel!!

I pretty much got how the script language related to the native code straight away, thanks to this doc. It might be worth pointing out the documentation for concoleObject to help people find more info on the native-code/script-code relationship and binding. It should fill in quite a few questions for use newbies to Toprque :o)
#11
08/01/2005 (9:48 am)
So have I interpreted things correctly if i gather that you can't actually create new classe with the scripting language, but you can fake it reasonably well using ScriptObject, declaring obects of the same "class" to have the same name, and use "function name::funcname(..." for defining member functions?
#12
11/03/2005 (5:13 am)
You wrote that datablocks can be transmitted by transmitting their ID. This doesn't work with objects.
And now I want to know how to transmit objects. Any ideas???
#13
02/22/2006 (10:59 am)
This is a great reference for beginners like me, that are trying to make sense of how the scripting works.
#14
07/27/2006 (12:07 pm)
Gr8 resource!

BTW, how can I receive a handle to a specific object in the world, if I only know it's name?

I'm asking because some scripts require referencing objects you don't have handles to, so it's quite important.

Of course, the question is only relevant if I didn't create the object via script (since then I would already have its handle)

Any help would be appreciated!
#15
05/23/2007 (5:40 am)
Finally, I understand how all that object declaration shite sorts out into a namespace hierarchy. I think I even understand why it works that way. This resource just keeps on giving. Thx Mr. Baxter.
#16
09/16/2010 (12:06 am)
Eight years later and I finally know what "copy_source_identifier" does.

Belated TY!
#17
08/16/2011 (5:13 pm)
MY GOD THIS IS GOOD!

got the engine and was starting to get a little overwhealed on where to start and what it all meant. This little trinket has just smoothed things out for me and put a lot of it back into prospective...

Thanks a bunch.
#18
08/19/2011 (10:57 pm)
This should be in the torque wiki area. I've been adding stuff to it occasionally.