A quick beginner's guide to ACS
This tutorial will guide the creation of your first ACS scripts, hopefully helpful if you are new to basic ACS scripting. It is assumed that you know some mapping fundamentals, and that you are using the Hexen or UDMF map format.
Creating a script
Open up Doom Builder and create a new map (call it MAP01) in ZDoom (Doom in Hexen) format. Create one sector and put a Player 1 Start thing.
For GZDoom Builder:
You should search for "Script Editor", found on the main toolbar/"View" menu, or by pressing F10.
For Older versions of Doom Builder:
Go to the "Scripts" Menu and choose "Edit BEHAVIOR Lump". You should see a button that says "Make New Script". Go ahead and click on that.
The structure of a script
Here is the structure of a basic script:
#include "zcommon.acs"
script <script identifier> <script type> (<script arguments>)
{
<statement>;
}
Let's break down the script's textual components, aka its syntax:
- #include "zcommon.acs"
- Put this as the first line in the script, copied exactly. This imports ACS data for the script to compile and execute succesfully.
- <script identifier>
- Each script has an identifier, which will be either a number, or a "name in quotes" (called a string). Note: all numbered scripts must be between 1 - 32767.
- <script type>
- Each script has a type, which mostly determines when a script will automatically execute. See Script types for the list of available types.
- (<script arguments>)
- Scripts can accept values called arguments when executed. These are like passing in values to a function. Not all script types need arguments. These should be between parentheses.
- {
- All of the script's code must be {between curly brackets}.
- <statement>;
- This would be the code the script executes. Each line of code must have a semicolon at the end; else the code compiler will give an error. It does not read the new lines of text exactly as a human does.
- }
- Remember to put an ending curly bracket to mark the end of the script.
First example script
Here is the first example script we will create, which will display the message "Hello World!" onto the player's screen when the first enter the map:
#include "zcommon.acs" script 1 ENTER { print(s:"Hello World!"); }
This example script will be identified as script 1. The script has the ENTER type, which will execute when a player first enters the map. ENTER scripts don't need any script arguments.
The print() statement
In the example script above, a print() statement/function is used. The print statement enables you to display a message on the player's screen.
The print statement's syntax is like this:
print(<cast type>:<expression>);
Syntax breakdown:
- print(
printis the function name to be executed (called) on this line, and all function calls must be followed by a parenthesis pair. The print statement requires arguments inside the parenthesis.
- <cast type>:
- The message content needs to be interpreted with a letter code. For now, you can use
s:for text (called strings) ord:to display variable numbers (explained later in the tutorial). See print for all possibilities.
- The message content needs to be interpreted with a letter code. For now, you can use
- <expression>
- This will contain the message content to display. Text should be enclosed in "quotation marks".
- );
- Make sure to put the ending parenthesis, and semicolon.
New lines
To put a new line into a string's message, add the text \n inside the quotations. Example:
print(s:"Hello\nWorld!");
This will put the word "World!" on the line below the word "Hello". The compiler will not read the newline from your text editor and print it in the message the way you may expect, you must use the special \n code.
Multiple expressions
You can also provide multiple expressions to a print statement, separated by commas. Example:
print(s:"Hello", s:" World!");
This is an alternate way to print "Hello World!" on the screen. Make sure the space is included in the quotes before the word "World!".
Second example script
Here is another script you can add after script 1:
script 2 (void)
{
print(s:"Bye World!");
}
Unlike the ENTER script in example 1, this script will not automatically execute when the player enters the map, and must be executed explicitly. void inside the parentheses here signifies to the compiler that there are no arguments for this script; the keyword is required for any script not of a special type that takes no arguments.
A (void) script type is probably the most common type of script. You can also use scripts with arguments, but those will be discussed later.
To activate a (void) script, you must either have a thing with it's thing special set to 80: ACS_Execute (monster dies, health/weapon/powerup/key picked up), a linedef who's special is set to 80: ACS_Execute (on line crossed, player presses use, or shot hits or goes past), or activated by another script (you will learn about that later).
The first argument for action special 80: ACS_Execute should be 2, which will execute the above script 2. You can leave the other arguments at 0.
It is very common among mappers to place linedefs that you cannot detect and will not block your path. These lines are used for scripting.
Variables
This section will teach you about variables and basic usage. Variables are used to store data (like a number) for use later in the script, similar to the symbolic representation of numbers in algebra.
Here is an example:
script 3 (void)
{
int a = 9;
print(s:"a is ", d:a);
}
This script does two things:
- It declares
ato be an integer and sets its value to 9. - It prints the string "a is " followed by the value of a. In this case, the output would be "a is 9".
Here is the syntax breakdown for a variable declaration:
<datatype> <variable name> = <value>;
- <datatype>
- The data type of the variable.
intwill be the most common, which is an integer number. There are also other types, listed in Data Types.
- The data type of the variable.
- <variable name>
- The name the declared variable will use. It's best to respect the capitalization of the name each place it is used.
- =
- An initial value can be assigned to the variable with the equal sign, although it is not mandatory.
- Example declaration without a default value: "int a;"
- An initial value can be assigned to the variable with the equal sign, although it is not mandatory.
- <value>
- This will be the initial value of the variable. For an
inttype, a number.
- This will be the initial value of the variable. For an
- ;
- Don't forget the ending semicolon on all code statements!
With integers, you can do basic math (and more advanced math if programmed correctly).
script 3 (void)
{
int a = 9;
int b = 17;
print(d:a + b);
}
This script is an example of basic addition. This code declares an integer a and sets its value to 9, declares another integer b and sets its value to 17, and prints out the value of a + b. The cast type d: is used to display a number value for the print statement. Since a is 9 and b is 17, it will print out the value of 9 + 17, which is 26. If you activated the script while playing, you would see the number 26.
If you wanted to explicitly print out the text "9 + 17", and not "26", you could set up the print like this:
script 3 (void)
{
print(s:"9 + 17");
}
The s: cast type is used here since it is printing a string instead of a variable's value.
Useful integer operators
(In these below examples, a is 9 and b is 17)
- Assignment:
a = b- Sets variable
ato the value ofb, soabecomes 17. A single equal sign does NOT represent a check for equality.
- Sets variable
- Equality:
a == b- This expression checks if the value of a equals the value of b. Results in 1 if true, or 0 if false. In this case, 0. Remember that doubled equal signs are required for these types of checks.
- Inequality:
a != b- Same as equality, but results in 1 if the values are not equal, and 0 if the values are equal.
- Addition:
a + b== 26 - Subtraction.
a - b== -8 - Multiplication.
a * b== 153 - Division.
a / b== 0- The result is not 0.5294, as integer division will "round down", and only result in another integer.
- Dividing by 0 will result in a script error.
- Modulus:
a % b== 9- Gives the integer remainder of attempting to divide 9 by 17.
- Unary Increment:
a++== 10- Simply adds 1 to the variable. a + 1 == 10.
- Unary Decrement:
a--== 8 - Parentheses:
(a + 1) * b== 170- Variables and values can be grouped inside parentheses. Values inside parentheses will evaluated first before outside values.
See Operators for all available operators and more information.
Script actions and parameters
There are of course other possibilities besides printing text and numbers.
Suppose we put a red key in the map. In vanilla Doom, picking it up gives you a red key. With ZDoom ACS scripting, you can make the red key do a whole lot more! You can have the red key kill the player, give him the BFG9000, do absolutely nothing, or raise dead monsters! If you have two red keys, you can make each one do something completely different!
Create a sector, and put a player start, and a red key. Now, give the key a special action of 80: ACS_Execute with a script number of 1.
Type the following code (be aware this code won't compile because of intentional missing parameters):
#include "zcommon.acs"
script 1 (void)
{
Thing_Damage();
}
Now, for the Thing_Damage statement, we need some parameters:
- tid: TID of the thing you want to damage.
- amount: The amount of damage the thing will receive.
- mod: Means of death. Determines the obituary message that will appear if a player is killed. Relevant damage types for the means of death are found on the Damage_types page.
The first parameter is who we want to damage. Since we want to damage the player, we will need a way identify the player for the script: a Thing ID (aka TID). Most ACS functions have no way to reference a player with no TID set, so we will need to set a TID on the player with the Thing_ChangeTID function.
Use this following script:
script 2 ENTER
{
Thing_ChangeTID(0, 1000);
}
When the player enters the map, we will assign them a TID of 1000 (the second parameter). The first parameter of Thing_ChangeTID is who to change the TID of; we use 0 to refer to the script activator, which for an ENTER script, is the player that joined. Using 0 as the script activator TID is common for very many ACS functions, so keep that in mind.
- Note: Player TIDs numbered starting at 1000 is the general standard, but be warned not all Doom mods follow this convetion (unfortunately)...
Now that the player has a TID assigned, we can complete our Thing_Damage function call:
#include "zcommon.acs"
script 1 (void)
{
Thing_Damage(1000, 2, 0);
}
We are damaging the player with TID 1000, with 2 damage, and the default means of death 0 (Gives a generic "Player died." obituary).
Test your level, pick up the red key, and watch your health. It will deplete when you pick up the red key.
Reference this page for more usable action statements: Action specials
Commenting your code
You can add notes next to your script's code (called comments). You can purpose them however you want, but they are most recommended for annotating complex behavior or reasoning for decisions.
You can add a comment after a line of code with a space and two forward slashes, followed by text. Any of this text after the slashes is ignored by the compiler, and not treated as code, until the next line.
Adding comments to a previous example:
script 3 (void)
{
// I'm a comment. Here's were we initialize the variables:
int a = 9;
int b = 17;
print(d:a + b); // Print the sum of a and b
}
Using functions to make simple changes to a map
You can use ACS functions and line actions to make changes to a map, not just things.
Simple swimmable water
To start, open Doom Builder and create a new map in ZDoom (Doom in Hexen format). Create a 512x512 square sector. We'll call this the "big" sector. Put a player 1 start somewhere in the big sector.
Create a second 64x64 "small" sector within the first sector:
- Drop its floor height to
-64so it looks like an empty pool. - Be sure to texture the edges of the pool.
- Assign sector tag 1 to this sector.
- Set the sector's floor texture to
FWATER1.
After this, create another separate sector outside of the big sector, and not touching. This will be our "control" sector. Size doesn't matter for this sector, though you can keep it small for manageability's sake.
- Ensure that the ceiling height of this sector matches the other sectors'.
- Set the sector floor height to
-8 - Set the sector's ceiling texture to
FWATER1.
Edit any linedef in the control sector and assign it line action 209:Transfer_Heights. Transfer_Heights accepts two parameters (sector tag, effect). Set the first parameter to 1 (sector tag 1) and the second parameter to 8 (underwater portion is swimmable).
Save and run this map. If you've done everything correctly, you'll see the pool is "full"... or rather, that the FWATER1 texture is now only 8 units below the floor instead of 64. This is because the *height* of the control sector was *transferred* to the pool. If you jump into the pool, you should be able to swim in it (go forward and move your mouse up and down. You'll float in the water instead of simply looking up and down). Exit the map and go back to Doom Builder.
Next, we want to create a script that adds color and fog to the control sector. For a script to work, the control sector needs a tag, so we'll give it sector tag 2. For the ACS script, we need two functions: Sector_SetColor() and Sector_SetFade(). Both functions take the same 4 arguments (sector tag, red, green, blue). Each color can take a value from 0 to 255, depending on how much of that color you want. Example: setting blue to 0 will mean no blue at all, but setting it to 255 will mean as much blue as possible.
#include "zcommon.acs"
// The OPEN script type executes once when the map first loads script 1 OPEN { // we want to use the sector tag of the control sector, 2 Sector_SetColor(2, 0, 0, 205); // this tints the sector with a blue color Sector_SetFade(2, 0, 0, 205); // this creates a blue fog effect }
Now when you swim in the pool, the underwater portion will look deep blue and murky.
Door opening upon condition
The next tutorial will allow you create a door that opens when you kill an enemy.
To do this, create a new map and make two rooms connected by a door. The first room should contain a monster, lets say a zombieman, and the player 1 start. In the other room there can be an item of your choice as a reward. The door connecting the two should not have any line actions so that you cannot open the it manually, but it must have a sector tag. Set the door sector's tag to 1.
We are going to use the Door_Open() ACS function to open the door. The Door_Open() function takes three arguments:
- tag: Tag of affected sector, in our case,
1 - speed: How quickly the door opens, set to
64to open quickly - lighttag: Tag of sector to perform a gradual lighting effect in, you can leave it at
0
script 1 (void)
{
print(s:"You killed a zombieman!");
Door_Open(1, 64, 0); // Open sector tag 1 as a door, with speed 64
}
Now, in order for this to work we must give the zombieman a thing special. In this case we will use 80:ACS_Execute(). Set the first argument to 1, for script 1, and leave all the other arguments as 0.
Test the map, and the door should open upon killing the zombieman.
Flow control
Conditional execution (if / else)
We can perform a logic check in a script to decide between different code to execute, using if and else statements.
Here is a simple if check:
script 1 ENTER
{
int condition = 1;
if(condition)
{
print(s:"The condition was true.");
}
}
The if statement gets its own pair of {curly braces} to denote what code it controls the execution of. Make sure each beginning brace has an appropriate closing brace, and not to mix these up with the script's braces.
This will execute the print function, since we set the condition variable to 1, which is truth. All numerical values aside from 0 will result in truth, while 0 will result in falsehood.
If you set the value of condition to 0 instead of 1, the print will no longer execute.
Here is an example of else statements to pair with an if:
script 1 ENTER
{
int condition = 0;
if(condition == 0)
{
print(s:"The condition was 0.");
}
else if(condition == 1)
{
print(s:"The condition was 1.");
}
else
{
print(s:"The condition was something else entirely...");
}
}
Try setting the value of condition to 0, then 1, then 123, and see for yourself which branch and print statement executes as a result.
Conditional repeating loops (while)
A "while" loop will do the same thing as an if comparison, except it will restart when finished. The script will stop repeating when the condition(s) no longer are met.
NOTE ON ENDLESS LOOPS: A loop intended to execute forever MUST contain at least one Delay() function with a parameter of 1 or more. If not, ZDoom will terminate the script for its own good (a forever looping script that won't take a break between restarts will go on for eternity without giving time for other code to do anything). If your script is terminated, ZDoom will let you know through a message like "Runaway script # terminated" at the top of your screen.
script 1 OPEN
{
int i = 5;
while(i > 0)
{
command or series of commands that change i;
}
}
This while loop will run as long as the variable i is of a value greater than 0.
If you want to create a while loop that never ends (perhaps to create an ongoing effect in the map), you can use an expression that never returns false to do this:
script 1 OPEN
{
while(TRUE) // Or while(1), or while(4 == 4), etc.
{
commands
delay(1); // wait 1 tic between loop executions
}
}
Note: The unary not operator ! can be used to check for a condition's falsehood, example: !(true). This will evaluate to "not true", aka false.
Count-controlled loops (for)
A "for" loop will loop a portion of a script a defined number of times:
script 1 (void)
{
for(int i = 0; i < 10; i++)
{
log(d:i);
}
}
This script will print 0 through 9 in the console log.
ACS uses what's known as a three-expression for loop, so called for the three expressions:
- The initializer
int i = 0 - The loop-test
i < 10 - The counter
i++
Each expression has a semicolon ; separator between them.
You don't have to declare a variable for the first time in the initializer, and there is some more flexibility if desired with the expressions, but this example is very typical usage of a for loop.
With the first pass of the loop, i == 0, which satisfies the loop test of i < 10 and then proceeds to the commands nested within the loop. i is then incremented by 1 (due to i++) and the next iteration (step) of the loop begins. This continues until i == 10, at which point the loop-test of i < 10 is no longer satisfied, and the nested commands are not executed. The code execution then proceeds beyond the for-loop.
Further reference
- ACS action specials: Action_specials
- ACS functions: Built-in_ACS_functions