Scripting basics

From modwiki

Jump to: navigation, search

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.

Contents

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

  1. Extend your hand to reach the door.
  2. Grasp the knob.
  3. Turn the knob.
  4. Pull the knob.


To even make things worse those basic steps can be broken down even further.


Extend your hand to reach the door.

  1. Raise your arm.
  2. Rotate your forearm.
  3. 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 basicly 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 especialy usefull in functions, so make sure you understand this part before going to the next one.

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 it's 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 eachother");
    } else {
        sys.println("They are away from eachother");
    }
}

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.

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:

void main() {
    $my_entity.setWorldOrigin('57 68 19');
}

(Also refer to Script events, info on what those are should be over there)

Using existing entities

(Introduce $ operator)

The "sys" riddle

(Explain where the global sys variable comes from)

void printTime()
{
 float tempfloat;
 tempfloat=sys.getTime();
 sys.println("Current game time is " + tempfloat + " seconds.");
}

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
}

Special trick: A string can be used with $ to store entity names.

string funcmover;
$funcmover=sys.getEntity("func_mover_1");

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 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 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

TODO: explain threadnames, killThread, thread-ids and terminate.

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

Objects

(Needs more research) An example is provided in the SDK in a script called func_train.script along with an entity type called func_train. Basically, if you add an entity type func_train to your map and give it a model...for example "models/vehicles/marine_drop_pod/droppod_culled.lwo" and set the move_time=1, accel_time=.1, and decel_time=.1 then add four target_null's to your map and target the first target_null with the droppod (select droppod then first target_null and press CTRL-K) then that target_null targets the next target_null, etc. If you target the last target_null to the first target_null then the train will run in a loop, but your script function will never exit, and if you don't and allow the path to dead-end then the train entity will be removed from the map when it reaches the dead-end. Then add a trigger_once and have it call your script which simply has:

  • Example
func_train tmpent;
tmpent=sys.getEntity("func_train_1");
tmpent.train_go();

You will see the droppod move to each of the target_nulls.

Supposedly spline_movers can be used instead of target_nulls, but when this has been tested Quake4 currently crashes.

Personal tools
Main