Scripting basics: Difference between revisions
→The sys entity: Change how to kill the player |
|||
(14 intermediate revisions by 6 users not shown) | |||
Line 1: | Line 1: | ||
TDM (Dooom 3) scripts are text files containing script code in a proprietary syntax. By convention, they are given a <tt>.script</tt> extension. Game-wide script files are located in the <tt>script</tt> directory, while map-specific script files are located in the same directory as the corresponding map. | |||
== Starting with scripting == | == Starting with scripting == | ||
Line 7: | Line 7: | ||
== Syntax == | == Syntax == | ||
TDM scripts have a vaguely C++-like syntax, though they are nowhere near as powerful as C++. They have standard braces-and-semicolons syntax, and a notion of "classes". Class members are defined inside a class declaration and defined separately, rather like C++. Unlike C++, there are no separate header files; everything goes into one script file. | |||
Scripts have only a few data types: | |||
* <tt>float</tt> - A number, integer or floating point. | * <tt>float</tt> - A number, integer or floating point. | ||
Line 19: | Line 19: | ||
Note the complete lack of a dedicated integer type (use <tt>float</tt> instead), or any ability to define complex data structures! If data structures are needed, they are typically created in the SDK and ''scriptevents'' (see below) are provided to allow scripts to manipulate them. | Note the complete lack of a dedicated integer type (use <tt>float</tt> instead), or any ability to define complex data structures! If data structures are needed, they are typically created in the SDK and ''scriptevents'' (see below) are provided to allow scripts to manipulate them. | ||
For a more detailed specification of | For a more detailed specification of the script syntax, see [https://modwiki.dhewm3.org/SCRIPT_(file_format) Modwiki's SCRIPT file reference]. | ||
=== The float data type === | === The float data type === | ||
Line 42: | Line 42: | ||
The data type vector implements a three-component vector. As arrays and their native operator [] is not implemented in D3 scripting, the elements of vectors are accessed like this: | The data type vector implements a three-component vector. As arrays and their native operator [] is not implemented in D3 scripting, the elements of vectors are accessed like this: | ||
vector myVec; // declare a new vector | vector myVec; // declare a new vector | ||
myVec_x = 20; | myVec_x = 20; // access only the X part of the vector | ||
myVec_y = 0; | myVec_y = 0; // access only the Y part | ||
myVec_z = 0; | myVec_z = 0; // access only the Z part | ||
// myVec now holds the 3D vector <20,0,0> | // myVec now holds the 3D vector <20,0,0> | ||
Alternatively, you can assign a string to a vector variable, like this (the string gets converted correctly, but be sure to use '''single quotes'''): | Alternatively, you can assign a string to a vector variable, like this (the string gets converted correctly, but be sure to use '''single quotes'''): | ||
myVec = '0 10 0'; | myVec = '0 10 0'; | ||
As an example, the '''getOrigin()''' methods return a vector. | As an example, the '''getOrigin()''' methods return a vector. | ||
You can compare vectors by using the 'X Y Z' syntax: | |||
<pre> | |||
vector myv = '1 0 0'; | |||
if (myv == '1 0 0') | |||
{ | |||
// do something | |||
} | |||
</pre> | |||
=== The entity data type === | === The entity data type === | ||
Line 54: | Line 64: | ||
entity myEnt; // declare myEnt to be of type entity | entity myEnt; // declare myEnt to be of type entity | ||
myEnt = $player1; // Let myEnt refer to the player entity, which by convention has the name "player1". | myEnt = $player1; // Let myEnt refer to the player entity, which by convention has the name "player1". | ||
The entity data type can be compared to the pointers (that's also how they are stored internally) | The entity data type can be compared to the pointers (that's also how they are stored internally). | ||
==== The NULL entity ==== | |||
There also exists a NULL entity, named '''$null_entity'''. It can be used to compare an entity to NULL, to see if it points to a valid entity. Some script events return NULL to signal "no such entity exists". | |||
==== Entity types from Script Objects ==== | |||
If you declare a [[Script objects|script object]] with: | |||
<pre> | |||
object my_entity_type | |||
{ | |||
float getMyCount(); | |||
}; | |||
my_entity_type::getMyCount() | |||
{ | |||
return 2; | |||
} | |||
</pre> | |||
you can then later declare variables to be of that type. This is important to call methods on these entities, as otherwise your own defined methods are not accessible: | |||
<pre> | |||
entity my_name; | |||
my_name.getMyCount(); // error, normal entities do not have a script event getMyCount | |||
my_entity_type my_name_2; | |||
my_name_2.getMyCount(); // works | |||
my_entity_type my_name_3 = my_name; // casts from "entity" to "my_entity_type" | |||
my_name_3.getMyCount(); // works | |||
</pre> | |||
=== The sys entity === | === The sys entity === | ||
Script events always have to be called on an entity, like this: | Script events always have to be called on an entity, like this: | ||
$player1.setHealth(0); // to kill the player, use this instead of trying $player1.kill(); | |||
However, there are a lot of | However, there are a lot of script events which are unrelated to a specific entity, like cos(), sin(), spawn() and such. These "generic" events are invoked on the '''sys''' entity, like this: | ||
entity myLight = sys.spawn("light_moving"); // spawn a new light entity | entity myLight = sys.spawn("light_moving"); // spawn a new light entity | ||
myLight.setOrigin($player1.getOrigin()); // move it to the point where the player is currently residing | myLight.setOrigin($player1.getOrigin()); // move it to the point where the player is currently residing | ||
Line 65: | Line 108: | ||
=== Script Events === | === Script Events === | ||
A script event is a pre-made function that does something for you, usually to some game item you specify, just when you write it in your code (like $item.eventX(); does X to item). Sometimes it wants parameters or arguments to run (the stuff in parentheses) and will give an error if you don't put the arguments in the parentheses in exactly the way it wants, with the right number of them & right data types, etc. And some of them return values too, especially the "get" functions, which are handed over to a variable (e.g., X = getValue();, now X has the value. If X is an entity, you can even act on X as if it's the entity itself. Just be sure the data type of X and Value are the same, e.g. float, or string, or entity, etc, or you'll get an error). | |||
'''Q. What are some common or useful script events for editing?''' | |||
'''A.''' A list of all Doom3 and TDM script events are listed in two files (open with a text editor): scripts/doom_events.script, scripts/tdm_events.script. Proper syntax for the Doom3 events is here [https://modwiki.dhewm3.org/Script_events_(Doom_3)]. Note that TDM has some of its own script events, like setFrobable (in the tdm_events.script list). A quick list of common or useful script events good for editing is here: [[Script_Events_User-Friendly_List]], with syntax and description, and including TDM events. It doesn't list all script events and all details, just common & useful ones for actual mapping IMO. | |||
You can find a list of existing script events in the following file: | |||
* scripts/tdm_events.script | |||
[[TDM_Script_Reference|Latest Script Events]] | |||
== Discovery (or, why is | == Discovery (or, why is TDM ignoring my script file?) == | ||
TDM will automatically find and load some kinds of files, such as .def files. However, it does not do the same for script files; if you merely create a .script file and place it in the <tt>script</tt> directory, it will not be loaded. | |||
There are two ways to | There are two ways to use a script: | ||
* Place it in a map script. '''This is the recommended way for mappers'''. Map scripts are loaded when the corresponding map is loaded. They must be given the same name and placed in the same location as the corresponding map file, but with a .script extension instead of a .map extension. For example, the map script for <tt>maps/mydir/mymap.map</tt> must be called <tt>maps/mydir/mymap.script</tt>. Map scripts are used to implement map-specific behaviours; as such, mappers will often use them but mod coders typically won't. | * Place it in a map script. '''This is the recommended way for mappers'''. Map scripts are loaded when the corresponding map is loaded. They must be given the same name and placed in the same location as the corresponding map file, but with a .script extension instead of a .map extension. For example, the map script for <tt>maps/mydir/mymap.map</tt> must be called <tt>maps/mydir/mymap.script</tt>. Map scripts are used to implement map-specific behaviours; as such, mappers will often use them but mod coders typically won't. | ||
* | * You can place your code in a file called <tt>script/tdm_custom_scripts.script</tt>. Never use '''<tt>script/tdm_main.script</tt>''' - this contains all the scripts that are provided by the main engine. | ||
Most of the TDM-specific scripts, including <tt>tdm_ai_base.script</tt>, are <tt>#include</tt>d from <tt>tdm_main.script</tt>. We could also put further <tt>#include</tt> directives into <tt>tdm_ai_base.script</tt>, and so on. As long as a script gets <tt>#include</tt>d at least once, it will get loaded in all maps. Note that you should be careful not to <tt>#include</tt> the same script twice, or | Most of the TDM-specific scripts, including <tt>tdm_ai_base.script</tt>, are <tt>#include</tt>d from <tt>tdm_main.script</tt>. We could also put further <tt>#include</tt> directives into <tt>tdm_ai_base.script</tt>, and so on. As long as a script gets <tt>#include</tt>d at least once, it will get loaded in all maps. Note that you should be careful not to <tt>#include</tt> the same script twice, or TDM will complain about multiple definitions - one common way to avoid accidental double-inclusion is to place so-called "inclusion guards" in your script file (C/C++ programmers will be familiar with this kind of preprocessor trickery): | ||
#ifndef | #ifndef __TDM_YOUR_SCRIPT_NAME__ | ||
#define | #define __TDM_YOUR_SCRIPT_NAME__ | ||
// your code here will get included exactly once | // your code here will get included exactly once | ||
#endif // | #endif //__YOUR_SCRIPT_NAME__ | ||
== Script invocation == | == Script invocation == | ||
Line 104: | Line 144: | ||
{{infobox|The <tt>tdm_main</tt> function in <tt>script/tdm_main.script</tt> will be called right before the level starts to load. It's not recommended to put map-specific scripting in there. For map-specific script setup, use the map's own script file and place a <tt>void main()</tt> function in there, which will get called by the game.}} | {{infobox|The <tt>tdm_main</tt> function in <tt>script/tdm_main.script</tt> will be called right before the level starts to load. It's not recommended to put map-specific scripting in there. For map-specific script setup, use the map's own script file and place a <tt>void main()</tt> function in there, which will get called by the game.}} | ||
In addition to using your own <tt>main()</tt> function, make sure you first add a <tt>sys.waitFrame()</tt> in it. Otherwise the <tt>init()</tt> function from some scriptobjects has not been run, and you can get strange bugs from uninitialized variables: | |||
<pre> | |||
void main() | |||
{ | |||
sys.waitFrame(); // wait for init() routines to finish | |||
// add your code below: | |||
} | |||
</pre> | |||
=== How do I run a script in-game? === | |||
A few common ways of calling a script object thread in-game are | |||
# creating a target_callscriptfunction entity with the spawnarg: "call" "<name of your script object>", then creating some other entity (such as a button or [[Triggers|trigger brush]]) that "targets" that entity; | |||
# creating a button with the property "state_change_callback" "<script object>", which runs the script when pushed or triggered, | |||
# Using [[Location_Settings#Script_calls]] to call a script when you enter or leave a location; | |||
# triggering a script with the [[Objectives_Editor]] when you fulfill or fail an objective (possibly a hidden objective only for triggering the script); | |||
# A script can call another thread in itself with the "thread" function; see the command in the link below. | |||
# An entity can call a script when it spawns. Add a property/value either in the .def file or as a spawnarg on the entity: "ScriptEvent void <script object>(<parameters>);". | |||
# The script "void main();" is always called at game start. So if you add code under that heading in your script file, it will run at game start. | |||
There are other methods too. (Note, if you call a persistent script, i.e., it runs all game, be especially sure to avoid performance hits, or make it non-persistent if possible, that is, you "kill" it at some point in the game so it doesn't hog resources.) | |||
== Interfacing with the SDK == | == Interfacing with the SDK == | ||
Line 114: | Line 170: | ||
No language can operate usefully in a vacuum - at some point it needs to go out and do stuff. | No language can operate usefully in a vacuum - at some point it needs to go out and do stuff. | ||
'''TODO:''' Describe the various ways in which the SDK can interface with Doom 3 scripts | '''TODO:''' Describe the various ways in which the SDK can interface with Doom 3 scripts: scriptevents, scriptvars (instances of idScriptVar), etc. | ||
Vanilla Doom 3 script reference: | Vanilla Doom 3 script reference: https://modwiki.dhewm3.org/Script_events_(Doom_3) | ||
Also need refs for Dark Mod's scriptevents. | Also need refs for Dark Mod's scriptevents. | ||
== Things that don't exist in | == Thread Use == | ||
This is a non-comprehensive list of things that don't exist | |||
When TDM is run in Debug mode, the order of a thread routine's definition and its use is important. If a thread routine is used before it's defined, a crash can occur. | |||
Be sure to follow the following format when calling a thread: | |||
void RoutineName(); // initial declaration | |||
... | |||
object::RoutineName() {}; // routine definition | |||
... | |||
thread RoutineName(); // use | |||
== Things that don't exist in scripting == | |||
This is a non-comprehensive list of things that don't exist, but may be expected or known from other languages: | |||
* structs | * structs | ||
Line 128: | Line 202: | ||
* arrays (including the array operator[]) | * arrays (including the array operator[]) | ||
* exceptions | * exceptions | ||
* "crashes" (you | * "crashes" (you cannot call scriptEvents on entities, which do not have the event - the script will not compile and TDM will not load) | ||
* char data types, which implies that const char* is also missing (use string instead) | * char data types, which implies that const char* is also missing (use string instead) | ||
* private/public/protected | * private/public/protected | ||
Line 136: | Line 210: | ||
* [[My first map script]] | * [[My first map script]] | ||
* [[Script objects]] | |||
{{scripting}} | {{scripting}} | ||
{{todo}} | {{todo}} |
Latest revision as of 18:00, 23 May 2023
TDM (Dooom 3) scripts are text files containing script code in a proprietary syntax. By convention, they are given a .script extension. Game-wide script files are located in the script directory, while map-specific script files are located in the same directory as the corresponding map.
Starting with scripting
This article is an overview over the basics of scripting. If you are impatient and just want to implement a specific functionality, see My first map script for a start into scripting.
Syntax
TDM scripts have a vaguely C++-like syntax, though they are nowhere near as powerful as C++. They have standard braces-and-semicolons syntax, and a notion of "classes". Class members are defined inside a class declaration and defined separately, rather like C++. Unlike C++, there are no separate header files; everything goes into one script file.
Scripts have only a few data types:
- float - A number, integer or floating point.
- boolean - can be false or true.
- string - A string (sequence of characters).
- vector - Three numbers in one variable; useful for representing locations, velocities, etc.
- entity - A reference to an entity (a pointer, in C++ parlance).
Note the complete lack of a dedicated integer type (use float instead), or any ability to define complex data structures! If data structures are needed, they are typically created in the SDK and scriptevents (see below) are provided to allow scripts to manipulate them.
For a more detailed specification of the script syntax, see Modwiki's SCRIPT file reference.
The float data type
All numeric values are stored in float data types, which is the only numeric type. Neither int, double nor short, unsigned or other numeric keywords known from C exist in D3 Scripting. Still, most of the operators are applicable, like ++, +, -, +=, *=, etc.
The boolean data type
The data type boolean can only hold two values, namely true or false (TRUE or FALSE work as well). Behind the scenes, this is just another float variable.
Note: you can also use float as argument of conditional expressions, like this:
float myFloat = 1; // this will evaluate to TRUE when used in conditional expr. if (myFloat) { // do something, }
The string data type
Strings hold sequences of characters, like the keys or values of entity spawnargs. Their use is quite intuitive, the + operator can be used to concatenate them:
string myStr = $player1.getKey("name"); // Retrieve the value of the "name" spawnarg of the player entity myStr = "The name of player 1 is " + myStr + "\n"; sys.println(myStr); // Print the text to the console
The vector data type
The data type vector implements a three-component vector. As arrays and their native operator [] is not implemented in D3 scripting, the elements of vectors are accessed like this:
vector myVec; // declare a new vector myVec_x = 20; // access only the X part of the vector myVec_y = 0; // access only the Y part myVec_z = 0; // access only the Z part // myVec now holds the 3D vector <20,0,0>
Alternatively, you can assign a string to a vector variable, like this (the string gets converted correctly, but be sure to use single quotes):
myVec = '0 10 0';
As an example, the getOrigin() methods return a vector.
You can compare vectors by using the 'X Y Z' syntax:
vector myv = '1 0 0'; if (myv == '1 0 0') { // do something }
The entity data type
References to other entities are stored in the entity data type. Entities can be looked up by name, like this:
entity myEnt; // declare myEnt to be of type entity myEnt = $player1; // Let myEnt refer to the player entity, which by convention has the name "player1".
The entity data type can be compared to the pointers (that's also how they are stored internally).
The NULL entity
There also exists a NULL entity, named $null_entity. It can be used to compare an entity to NULL, to see if it points to a valid entity. Some script events return NULL to signal "no such entity exists".
Entity types from Script Objects
If you declare a script object with:
object my_entity_type { float getMyCount(); }; my_entity_type::getMyCount() { return 2; }
you can then later declare variables to be of that type. This is important to call methods on these entities, as otherwise your own defined methods are not accessible:
entity my_name; my_name.getMyCount(); // error, normal entities do not have a script event getMyCount my_entity_type my_name_2; my_name_2.getMyCount(); // works my_entity_type my_name_3 = my_name; // casts from "entity" to "my_entity_type" my_name_3.getMyCount(); // works
The sys entity
Script events always have to be called on an entity, like this:
$player1.setHealth(0); // to kill the player, use this instead of trying $player1.kill();
However, there are a lot of script events which are unrelated to a specific entity, like cos(), sin(), spawn() and such. These "generic" events are invoked on the sys entity, like this:
entity myLight = sys.spawn("light_moving"); // spawn a new light entity myLight.setOrigin($player1.getOrigin()); // move it to the point where the player is currently residing
Script Events
A script event is a pre-made function that does something for you, usually to some game item you specify, just when you write it in your code (like $item.eventX(); does X to item). Sometimes it wants parameters or arguments to run (the stuff in parentheses) and will give an error if you don't put the arguments in the parentheses in exactly the way it wants, with the right number of them & right data types, etc. And some of them return values too, especially the "get" functions, which are handed over to a variable (e.g., X = getValue();, now X has the value. If X is an entity, you can even act on X as if it's the entity itself. Just be sure the data type of X and Value are the same, e.g. float, or string, or entity, etc, or you'll get an error).
Q. What are some common or useful script events for editing?
A. A list of all Doom3 and TDM script events are listed in two files (open with a text editor): scripts/doom_events.script, scripts/tdm_events.script. Proper syntax for the Doom3 events is here [1]. Note that TDM has some of its own script events, like setFrobable (in the tdm_events.script list). A quick list of common or useful script events good for editing is here: Script_Events_User-Friendly_List, with syntax and description, and including TDM events. It doesn't list all script events and all details, just common & useful ones for actual mapping IMO.
You can find a list of existing script events in the following file:
- scripts/tdm_events.script
Discovery (or, why is TDM ignoring my script file?)
TDM will automatically find and load some kinds of files, such as .def files. However, it does not do the same for script files; if you merely create a .script file and place it in the script directory, it will not be loaded.
There are two ways to use a script:
- Place it in a map script. This is the recommended way for mappers. Map scripts are loaded when the corresponding map is loaded. They must be given the same name and placed in the same location as the corresponding map file, but with a .script extension instead of a .map extension. For example, the map script for maps/mydir/mymap.map must be called maps/mydir/mymap.script. Map scripts are used to implement map-specific behaviours; as such, mappers will often use them but mod coders typically won't.
- You can place your code in a file called script/tdm_custom_scripts.script. Never use script/tdm_main.script - this contains all the scripts that are provided by the main engine.
Most of the TDM-specific scripts, including tdm_ai_base.script, are #included from tdm_main.script. We could also put further #include directives into tdm_ai_base.script, and so on. As long as a script gets #included at least once, it will get loaded in all maps. Note that you should be careful not to #include the same script twice, or TDM will complain about multiple definitions - one common way to avoid accidental double-inclusion is to place so-called "inclusion guards" in your script file (C/C++ programmers will be familiar with this kind of preprocessor trickery):
#ifndef __TDM_YOUR_SCRIPT_NAME__ #define __TDM_YOUR_SCRIPT_NAME__ // your code here will get included exactly once #endif //__YOUR_SCRIPT_NAME__
Script invocation
TODO: Describe the various ways in which functions in script files can get called.
In addition to using your own main() function, make sure you first add a sys.waitFrame() in it. Otherwise the init() function from some scriptobjects has not been run, and you can get strange bugs from uninitialized variables:
void main() { sys.waitFrame(); // wait for init() routines to finish // add your code below: }
How do I run a script in-game?
A few common ways of calling a script object thread in-game are
- creating a target_callscriptfunction entity with the spawnarg: "call" "<name of your script object>", then creating some other entity (such as a button or trigger brush) that "targets" that entity;
- creating a button with the property "state_change_callback" "<script object>", which runs the script when pushed or triggered,
- Using Location_Settings#Script_calls to call a script when you enter or leave a location;
- triggering a script with the Objectives_Editor when you fulfill or fail an objective (possibly a hidden objective only for triggering the script);
- A script can call another thread in itself with the "thread" function; see the command in the link below.
- An entity can call a script when it spawns. Add a property/value either in the .def file or as a spawnarg on the entity: "ScriptEvent void <script object>(<parameters>);".
- The script "void main();" is always called at game start. So if you add code under that heading in your script file, it will run at game start.
There are other methods too. (Note, if you call a persistent script, i.e., it runs all game, be especially sure to avoid performance hits, or make it non-persistent if possible, that is, you "kill" it at some point in the game so it doesn't hog resources.)
Interfacing with the SDK
No language can operate usefully in a vacuum - at some point it needs to go out and do stuff.
TODO: Describe the various ways in which the SDK can interface with Doom 3 scripts: scriptevents, scriptvars (instances of idScriptVar), etc.
Vanilla Doom 3 script reference: https://modwiki.dhewm3.org/Script_events_(Doom_3)
Also need refs for Dark Mod's scriptevents.
Thread Use
When TDM is run in Debug mode, the order of a thread routine's definition and its use is important. If a thread routine is used before it's defined, a crash can occur.
Be sure to follow the following format when calling a thread:
void RoutineName(); // initial declaration
...
object::RoutineName() {}; // routine definition
...
thread RoutineName(); // use
Things that don't exist in scripting
This is a non-comprehensive list of things that don't exist, but may be expected or known from other languages:
- structs
- pointers (including dereference operators ->, *)
- integers (use floats instead)
- arrays (including the array operator[])
- exceptions
- "crashes" (you cannot call scriptEvents on entities, which do not have the event - the script will not compile and TDM will not load)
- char data types, which implies that const char* is also missing (use string instead)
- private/public/protected
- multiple inheritance