Scripting basics
This page is in urgent need of attention because: Unfinished sections
SCRIPT files define a scripted sequence of events, the behavior of weapons, or the AI of actors. The files themselves are ascii (plain text) and can be edited/created in a text editor like notepad.
Scripts use a proprietary language similar to C++, Java, and other languages but it’s very rudimentary and nowhere near as complex.
Basics of Programming
Programming and scripting are essentially the same task. The only distinguishable difference between the two is scripting is instructing a specific program to perform a task where as programming involves writing a program itself.
The general concept behind scripting is the same and so, in order to script you need to learn to program. But, it’s not nearly as scary as it sounds.
When you are scripting you are telling objects to perform certain tasks. But these objects don’t understand English. You need to talk to them in a manner that they can understand.
For the time being we will relate to the way people perform tasks. Let’s say you want to open a door. We all know how to do so but this simple task can be broken down into a series of basic steps.
Opening a Door
- Extend your hand to reach the door.
- Grasp the knob.
- Turn the knob.
- Pull the knob.
To even make things worse those basic steps can be broken down even further.
Extend your hand to reach the door.
- Raise your arm.
- Rotate your forearm.
- Open your hand.
You can see how even a simple task like opening a door is actually comprised of a lengthy list of basic actions. This is the basis of scripting in Doom 3 and programming in general. You cannot just tell a machine to run. You have to tell all its moving parts to perform basic actions.
General Syntax
The following is a list of special characters with a special meaning; they need to be remembered.
;
Follows all commands. More than one command may exist on a single line.
Informs the engine that the following is the end of a command.
sys.print;
//
Comment. Declares the text from this point to the end of the line a
comment and is not executed. Can also be used to null out a line of code
for debugging.
// This text will not be executed.
\*
and */
Comment. Declares the text from /* to */ a comment and the text
contained therein is not executed. Can also be used to null out a
portion of code for debugging.
/* This text will not be executed. */
Getting started
First of all, it is recommended to use a good editor with syntax highlighting. This allows you to see typing errors because the highlighted color changes if the editor doesn’t know your input. To find a suitable text editor, see tools for editing script files .
Now to start with something, we will make a small and easy level script. These are the easiest to start with and make a nice introduction to scripting. What do we need?
- A map with a player spawn point in it. (Use anything you have, be it a simple box with a light in it.)
- A script file with the same name as the map, placed in the maps folder.
Open the map and create a new brush. Select it, then open the “Entities” inspector/window. Now add a key “call” with the value “main”. What this means for this example is that a function named “main” will be called when the map loads.
Now open the script file and paste this code fragment in it.
void main() {
sys.print("^1Hello ");
sys.println("^3World!");
}
If you now run the map, open your console by pressing Ctrl + Alt + ~. If all went well, there should be a line with the text “Hello World!” in two different colors. This example might be far from impressive, but it should be enough to let you experiment once you have read more about scripting.
With that out of the way, let’s look at some theory first. If you already know C or some other procedure-based language, feel free to quickly read the following 2 parts. If you are completely new to it, these sections should teach you the basics.
Variables
A variable is a container used to store a value. Think of it as an alias used to refer to a value without referencing the value directly.
Declaration
Before a variable can be used it must be declared for a specific data type . By assigning a data type you are defining what kind of values can be stored in a specific variable.
Say we want to create a variable called “foo” and store a number in it. The variable declaration would look like this:
float foo;
If you would need more then one variable of the same type, you could write
float foo1;
float foo2;
like this:
float foo1, foo2;
To actually put a number in it, you would do this:
foo = 2;
More examples of can be found on the Data types page, see the common types.
Now, how to put variables to use? The nice thing is that you can write the variable’s name where you would normally put a value of that type. This way, you can use the same code over and over without changing that line of code. You just assign a new value to the variable instead of copying your code and changing the value there.
Operations
Now you basically know what a variable does. However, the assignment operation is just one of many.
Imagine that you want to make a counter. You would first need to check the old value and then assign the new one to it. Of course, this would take a lot of work.
Luckily, we can do basic operations with values. They can be as easy as this:
1 + 1
A few of the supported operations are:
+
Addition
-
Subtraction
*
Multiplication
/
Division
%
Modulo (Results in the remainder of a division)
Since an operation results in a value of the same type, you can store the results of an operation in a variable:
foo = 1 + 1;
Now back to the counter example:
Remember that a variable is an alias to a value? That way, we can use a variable with an operation:
foo = foo + 1;
Variables are especially useful in functions, so make sure you understand this part before going to that section.
Logical structures
So far we explained how to define variables and do a little arithmetic with them. While this is certainly useful for making counters, those counters can never be put to use if you couldn’t compare their value against another to do something. This is where logical structures come in. They allow you to exclude or repeat certain instructions based on a logical condition.
We’ll start with a basic one, the if structure.
If structure
The name says all: ” if the condition is true, do …“. Take the following code for example:
boolean condition1 = true;
boolean condition2 = false;
float var1 = 0;
if( condition1 ) {
var1 = var1 + 2;
}
if( condition2 ) {
var1 = var1 - 2;
}
Seeing how only condition1 is set to true, only the statements inside the first if structure will be executed. In this case, var1 will be set to ‘2’. Of course, using static variables as conditions is not of great use because we could as well comment out all statements for which the condition was false. To really harness the power of logical structures we need logical operations, which we’ll discuss next.
Logical Operations
Logical operators are symbols which return a boolean value, just like the arithmetic operators return a number. However, these operators work on more than just float -type variables. See the list below for details, the data types the operators work on are between parentheses.
==
Equals (float, boolean, string?, entity?)
<
Less than (float)
<=
Lesser or equals (float)
>
Greater than (float)
>=
Greater or equals (float)
!
Negation (boolean)
&&
Logical And (boolean)
||
Logical Or (boolean)
Here are some examples:
float var1 = 5;
float var2 = 10;
float var3 = 5;
if( var1 == var3 ) {
// Execute statements
}
if( var1 >= var2 ) {
// Will never get executed
}
if( var1 <= var2 && var3 <= var2 ) {
// Execute this one as well
}
// etc.
Also note that you can mix arithmetic and boolean operators, for example:
var1 + 5 == var2
The script compiler will process var1 + 5 first and the compare the float values. Because they’re both 10, the resulting value will be true .
While structure
A while structure executes a set of instructions as long as a given statement is true. It analyses conditions the same way if structures do. For example:
float myVar = 0;
while (myVar < 10)
{
sys.print("myVar: " + myVar + "\n");
myVar++;
}
myVar starts out with a value of 0. As long as its value is smaller than 10, it will be displayed in the console, then incremented by 1.
The while loop will do two things:
- First, it will print the text “myVar:” followed by myVar’s value, then start a new line.
- Then, it will add 1 to myVar’s value. The “++” after the variable name means the variable will be incremented by 1. (This is like typing myVar = myVar + 1 .)
For structure
This structure is the odd one because it combines the repeat condition of the while structure with the initializer statements, the condition and the increment combined.
TODO: elaborate use and examples
Functions or Methods
Functions are used to group certain statements and let you reuse them in other parts of the code.
You will use them for one of the following reasons:
- They can just group some statements so that you don’t need to copy/paste them over again.
- They group statements which will need to be called by a map entity.
- They can execute some statements which needs different input values from time to time. (The input values will be called parameters , read on for more.)
- They can execute a few statements to calculate or generate one value.
Declaration
Simple
Just like a variable needs a declaration, a function needs to be declared to be used by the script parser. We will start with the first use, grouping statements. (Which is the easiest) You should be able to figure out what the folowing function does as a whole.
void printHello() {
sys.print("Hello ");
sys.println("World");
}
Let’s take a look at it in detail:
- The first line defines the return type, function’s name and parameters. (More on those later on.)
- The function consists of everything enclosed in the first set of curly brackets (‘{’ and ‘}’) after the function’s definition.
These functions can also be called by map entitities. Just give them a key “call” with your function’s name as a value.
Using parameters
Another reason to use funtions is reusability. What this means is that you can reuse parts of your script instead of writing it again. Reusing code helps in reducing mistakes since you only have to fix a mistake once. If you would have copied and pasted the code, you might need to fix it multiple times.
To make a function be reusable, it will need to operate on different things. So, we’ll simply put those in variables and feed those to the function. You could define all variables globally, but functions have a nicer way of doing things using parameters . Example:
// This function makes an entity rotate around the Z axis, it takes spinTime seconds to complete one turn
void spinZ(float spinTime, entity target) {
target.time(spinTime); // Set the time each spin takes in this example
target.rotate('0 0 90'); // Rotate around the Z axis.
}
void main() {
spinZ(5, sys.getEntity("func_mover_1"));
spinZ(10, sys.getEntity("func_mover_2"));
}
As you can see, the first line now can have one or more variable definitions between the round brackets. The calling statements set the values accordingly.
Return values
Finally the last reason: calculating or choosing values. Sometimes you will need to do some math in several places, or check a few conditions. Wouldn’t it be handy to place them in a function and get one nice value instead of duplicating the code everywhere?
Return values let you do that – the function does its magic and returns one single value. Here is an example:
// Returns true when entities are near each other
boolean isNear(entity first, entity second) {
float dist = first.distanceTo( second ); // Returns their distance in units
boolean near = (dist < 20);
return near;
}
void main() {
entity ent1 = sys.getEntity("func_mover_1");
// Does the same as the statement above, but for "func_mover_2"
entity ent2 = $func_mover_2;
if( isNear( ent1, ent2) ) {
sys.println("They are near each other");
} else {
sys.println("They are away from each other");
}
}
Note that this is just an example: it is only written to show how return values work. Also take a look at the entites: this example shows the use of $entityname and the if / else structure.
More examples
Here is a somewhat longer example, combining a few of the discussed things.
This page is a Stub. You can help us by expanding it.
boolean doorisunlocked;
void main()
{
doorisunlocked = 0; //initialize to 0=false...not really necessary
}
void func_door_001_unlock()
{
doorisunlocked = 1; //set to 1=true
}
boolean doorcheck()
{
if (doorisunlocked)
{
return 1;
}
else
{
return 0;
}
}
void checkdoor()
{
if (doorcheck())
{
sys.println("door is unlocked");
}
else
{
sys.println("door is still locked");
}
}
Entities
If you play a map, nearly everything you see is actually an entity. They form movable furniture, working machines, weapon, monsters and even the player itself. If they are used in so many places, wouldn’t it be logical to have control over them through scripting? The answer is ‘yes’, of course. You can control entities through scripts, as long as you know their name.
Now all we need is an exact way to “talk” to those entities. That is what script events do: send a certain message to an entity, and the entity can do something with it. If you look at them in the script, they look a lot like functions. The only difference is that you always need to specify which entity you are sending ‘the message’ to. Now let’s look at an example, shall we:
$my_entity.setWorldOrigin('57 68 19');
The first part before the dot is the reference to the entity we want to affect. This is called a reference.
In this example we were using an Immediate reference .
Immediate references are simple: if you know the name of the entity in the map, then you just stick a $ and write the name.
In this case there would have to be an entity named “my_entity” in the map.
- Note: Enemy Territory: Quake Wars only:
- Enemy Territory does not have support for immediate references.
The calling
Next, the dot .
It simply indicates that the following will be a function call on the referenced entity.
In this case it’s a script event . setWorldOrigin is an event that sets the position of the entity in the world. We won’t worry further about this because it’s not a functional example.
The other way to reference
Using a named reference. We can declare a variable as an entity reference.
entity myEntityReference;
Then we’ll be able to set it to what we want, reset it to reference another entity, or set it to references returned from functions.
In this case we are going to set it to our pal named “my_entity” which we know is in the map (theoretically, for this example).
myEntityReference = sys.getEntity( "my_entity" );
What sys.getEntity does is look for the entity name we pass in, and returns a reference to it, which we are catching in our own reference.
Now we have our reference set we can call a function on it like in the first example.
myEntityReference.setWorldOrigin('57 68 19');
Fantastic.
sys
In a similar way to accessing entities we can access the system events on the current thread using the sys identifier. It is for events that don’t really make much sense being part of an entity, like game functions and utilities.
Looking at the list for Doom 3 Doom 3 System events we can see there’s a lot of sys events.
In this example we call two events via sys:
getTime, which returns the time in seconds since the game started.
println, which prints a line of text to the console.
void printTime()
{
float tempfloat;
tempfloat = sys.getTime();
sys.println("Current game time is " + tempfloat + " seconds.");
}
You’ll see in the first part we stored the game time in a variable, then added it to the string we were creating as the parameter for println.
The scripting system makes it easy to work with strings, you can append other strings, different variables simply with the + operator.
Spawning entities
(More info on sys.spawn() and an example. See Q4 wiki @ [1] )
Example
void moveEntity(entity tempentity)
{
vector tempvector='-128 -128 0';
tempentity.moveToPos(tempvector);
}
//note: the entity must be a func_mover or this won't work.
void printEntityLocation(entity tempentity)
{
vector tempvector;
string tempstring="Entity is at map location: ";
tempvector=tempentity.getOrigin();
sys.println(tempstring + tempvector);
}
//the following script can use the preceding examples
void main()
{
entity funcmover;
funcmover=sys.getEntity("func_mover_1");
printEntityLocation(funcmover); //display the original location
moveEntity(funcmover);
//now pause so that the game can move the entity using either
//the following statement to allow just one game frame...
sys.waitFrame(); //so that the mover has moved some, but not all the way.
//or with this one
//sys.waitFor(funcmover); //to wait until the entire move is completed.
printEntityLocation(funcmover); //display the current location
}
Overview of script events
A reference of scripting commands (events) can be found on the script events page.
Keep in mind that all commands are case sensitive.
Threads
In all previous parts of code, we assumed that a called function would always return. What if we call a function which will never end, but still want to run some other code?
Let’s take a closer look by means of an example. Suppose we want to make two machines run and write the following code:
void runMachine1() {
vector up = '0 -128 0';
vector down = '0 0 0';
entity part = $machinemover1;
while( 1 ) {
part .moveToPos( up );
sys.waitFor( part );
part.moveToPos(down);
sys.waitFor( part );
}
}
void runMachine2() {
vector up = '-128 -128 0';
vector down = '-128 0 0';
entity part = $machinemover1;
while( 1 ) {
part .moveToPos( up );
sys.waitFor( part );
part.moveToPos(down);
sys.waitFor( part );
}
}
void main() {
runMachine1();
runMachine2();
}
Making them run for the duration of the map might not be so obvious as it appears. The while loop will keep on running. However, this will stop the code for machine 2 to be called because runMachine1 () is still running. Switching the order of the function calls in the main routine is not an option either. So how to make both code blocks execute separated from each other?
That is where threads come into play. Threads execute code without disturbing the flow of other threads. (Exceptions exist, which will be discussed later.) If we would now call a never-ending function, we would run it in a different thread so the function calling it can still execute other statements.
Doom 3 can’t change random parts of code into threads, only functions can be run as a thread. In fact, it is only a matter of putting the thread statement in front of a normal function call.
The updated main routine for the previous example:
void main() {
thread runMachine1();
thread runMachine2();
}
Now both functions run in a separate thread and run happily ever after.
Special uses
in case we want to control a thread, start it, kill it, we have two methods, giving the thread a name, and use it to kill it or using a float variable as thread id and terminate the thread with that float variable:
consider that case:
float t_number = 0; // this will be the thread id number handler
float threadFunction( float whateverVariable ) { //notice the float data type, threads always return a float id number
float numberCopy;
sys.threadname( "superCoolThread" );
while ( true ) {
...
some code using whateverVariable
...
if ( condition to kill the thread is met ) {
numberCopy = t_number;
t_number = 0; // this makes possible to recreate the thread if needed again in the future
sys.terminate( numberCopy ); // this kills the thread form within when it's no longer necessary
}
waitFrame();
}
}
void function_that_initiates_the_thread() {
if ( t_number == 0 ) {
// initiates the thread and stores it's reference (as a number) in the t_number float variable.
// so if t_number is not 0 it means the thread is running
// the number stored changes per thread created
t_number = thread threadFunction();
}
}
void function_that_kills_the_thread() {
if ( t_number != 0 ) { // just attempt to kill the thread if there is one going on
sys.killthread( "superCoolThread" ); //kills the thread that has that name
/*
float numberCopy = t_number;
t_number = 0;
sys.terminate( numberCopy ); //<-- it could be killed that way too
*/
}
}
so threads can be killed form within themselves, and from outside too using those methods
Namespaces
These Declare a block which can include functions and variables, avoiding name conflicts. (similar to C++ namespaces)
These are used in Doom 3 to isolate a map script’s functions and variables from the others.
To get access to a member of a namespace, the following syntax is used:
namespace::member
The main.script and attendant event.script are part of the default, global namespace.
Objects
A script object is a way of organizing variables and functions that are related to an entity . It is analogous to a class in object orientated programming terms.
Its uses are wide, ranging from weapons and AI, to extending existing entities.
While an entity’s spawn class strongly defines runtime behavior, script objects are generally used to create variation on that behavior within its framework.
The spawn class sets up and runs a script object if the key/value pair “scriptobject” on the entity’s spawnArgs is set to a valid object type.
Working on objects
In order to call a function of a script object a valid object reference must be set up.
Example:
monster_base myMonster;
myMonster = sys.getEntity("monster_sam");
myMonster.wake_on_trigger();
Line 1 declares an object reference of monster_base type. The script compiler will issue an error if there is no script object of that type.
Line 2 sets the reference to an entity. For the sake of this example we are assuming the entity “monster_sam” is an entity running a script object of monster_base type, or one that inherits monster_base. An object reference will be set to [\(null\_entity](%24null_entity_%28scripting%29 "\)null entity (scripting)“) if the entity you are trying to set the variable to does not have a object of that type.
Line 3 calls the object function wake_on_trigger(), which we know is a function defined in the script object monster_base. If the function was not declared in the script object the script interpreter would issue a warning .
Creating your own script object
See Script object#Example .
Advanced aspects
(will be moved on page overhaul)
virtual functions
Declares a function on base script object type. Used for more advanced scripting techniques. Should only be declared in global namespace .