This page still under development
The Quake 4 Script system is a powerful method of creating exciting content for your mod. Much like the special entities you can place in a map to create different effects, the script system contains all sorts of tools to control nearly every aspect of all game entities.
From gripping cinematics to run and gun boss battles, the script system allows you to add spice and flavor to the standard behavior of Quake 4’s entities. Used in conjunction with map entities and game code, you can create all new game types!
Many pages could be dedicated to the similarities and contrasts between using script to create gameplay and using map entities, but we won’t do that here. Use this tutorial to unearth the power of scripting, then draw your own conclusions.
What You’ll Need
You will need a licensed copy of Quake 4 and the SDK. You will be using both the map editor, and a script editing tool of your choice.
Script editing can be done in any text editing program. However, since the script system closely resembles C style code, an editor that uses syntax highlighting would be best. Our developers here used both Visual Studio and Ultra Edit 32, but in a pinch you can use notepad.exe.
Setting up your first script file
Script files can be set up two ways. One method is for quick practice and “scratchpad” style development, the other is for distribution. To get started, we’ll use the first method because it’s much simpler. Create a new text file in the directory that has the .map file you want to use the script on. For example, if you’re working out of C:\Program Files\Quake4\q4base\maps\, you’d place the file there.
Name the file [mapname].script. So if your map is called “awesome.map” you’d name the file “awesome.script”
Your first script function, “Hello Stroggos”
Open up the script file you just created with your editor. Enter the following into the file and save the file.
1 void HelloStroggos() {
2
3 //speak to the world
4 sys.println(“Hello Stroggos!”);
5
6 }
If you’re not familiar with C-style programming, that all looks like a lot of jive. If you are familiar, go ahead and skip to the next header because this is probably old hat to you.
What you just wrote is called a function. Functions are called from the map by triggers—something we’ll get into later. Functions can also be called by other functions, but that’s getting way ahead. Let’s break it down.
The void is the return type, this is what a function gives back to you when it’s done. This function doesn’t give anything back, so we use the word “void” to represent that.
The next part is the function name. The () at the end are where 'parameters' go. If we need to send data to a function, we do it with parameters. Right now, we don’t need any, so the parentheses are empty.
You’ll notice the entire function is wrapped in squirrely braces, these right here: { } This is a must. If you forget them, even one of them, your stuff won’t work.
The next line, the one with the // slashes, is a comment. Comments are just for us, the compiler never sees them. You can write whatever you’d like in a comment. They are not mandatory, but good comments help you remember just what you were doing in a function.
Finally we get to the meat. This line calls a function from the SystemObject, called "sys.” Sys represents the game system, and can do quite a lot for us, which you’ll see later. Right now, sys is using the Print Line function, println. println takes in one parameter, which is a line of text that you wanted printed to the console. It’s very simple.
Notice that the line ends with a semicolon! Semicolons are the only way the compiler knows to stop reading commands from that line. You must include them at the end of your lines, or your stuff won’t run. Especially during these early steps of learning scripting, nine times out of ten when your scripts don’t compile, you forgot a semicolon.
Calling your script from game with triggers
At this point, you should save and close the script file. Now it’s time to open up the editor. Open up an empty map suitable for gameplay, a room with a light and an InfoPlayerStart. Create a new brush, right click it and change it to a trigger_once.
A trigger is an invisible brush that is activated by certain conditions. Usually this boils down to the player touching it and something happening. In our case, we want the trigger to call a function for us. So let’s set that key value on the entity.
Very straightforward. Notice here we don’t have to include the void or parentheses from the script file. Just the name of the function will do for now.
That should do it! Save and BSP your map, then run that level in game. If all went well, you should see “Hello Stroggos!” on screen. If you aren’t running in developer mode, or if con_showPrint is set to 0, you’ll have to open the console to see the message.
Further Changes to the Script and the Map
Now that you've got your script running, let's work a little further. We will modify our script and our map to change things up and spawn in a monster.
First, open the editor and add a target_null to the map. Place the target_null on the map in a place where you'd like the monster to spawn. Remember that monster origins are at the centered at the feet of the monster so place the origin target_null even with the ground unless you want the monster to fall out of the sky.
Give that target_null a name you'll remember. For our example we'll use targetMonster.
Go back to your script and add a few lines to the function we wrote earlier.
1 void HelloStroggos() {
2
3 //speak to the world
4 sys.println(“Hello Stroggos!”);
5
6 //new steps! ----------------------------------
7
8 //create a variable to hold the entity handle with.
9 entity newMonster;
10
11 //spawn the monster and store his handle in the variable
12 newMonster = sys.spawn("monster_strogg_marine");
13
14 //move him to where that new target_null lives
15 newMonster.setWorldOrigin( $targetMonster.getWorldOrigin() );
16 }
Let's go over this new stuff!
entity newMonster creates a variable for us of the entity type. A variable is a just a label for a piece of memory. When we spawn in our new creature, we want to have a way to access him later in the script. We use variables to keep track of all sorts of information.
Right now newMonster is empty. We're going to spawn in a strogg marine, and store the resulting handle into the newMonster variable.
newMonster = sys.spawn("monster_strogg_marine"); does just that! We use the spawn command to tell the game to create a new entity of whatever type we tell it. Anytime we enter text between quotes, we call it a string. That string is the parameter-- you remember those from earlier? So we're telling the game to create us a Strogg Marine, and to put the handle to that Strogg Marine inside our variable called newMonster.
At this point, a Strogg Marine is created, but he's been placed at world location 0,0,0. That's no good, we want him to appear at the target_null we made earlier!
To move an entity to a different position in the map instantly, we use the setWorldOrigin event. This is an instant move, the entity will pop into position over the course of a single frame. We do that with newMonster.setWorldOrigin( ).
Up until now we've been calling all our functions on the SystemObject. Functions can be called on all sorts of objects though, the SystemObject is only the beginning. To learn more about these functions, check out the Script Events page.
The function setWorldOrigin needs a parameter though. Let's give it the origin of that target_null we made earlier. We can call a function on that target_null to get it's origin. So we will.
$targetMonster.getWorldOrigin() returns to us the entity's origin in the world as a vector. The $targetMonster part is special. We use that $ symbol to reference any entity that is in the map. In this example, we use $targetMonster because that's what we named the target_null. If you named yours differently, use that different name.
This part is important, and a bit confusing at first. Understand the difference between an entity variable and the $ symbol. That link will take you to a more detailed example if you need it.
setWorldOrigin needs a vector as it's parameter, and luckily getWorldOrigin gives us just that. So this line,
newMonster.setWorldOrigin( $targetMonster.getWorldOrigin());
Will place the monster directly on the origin of that target_null.
Save your script file!
Running the Script Anew
Now, if you've closed the game since you've run the Hello Stroggos example, just load it again and this script should work out fine. However, if you've still got the game running from before, you'll need to execute a Reload Script command in the console.
Once you've saved your script, drop the console in game and enter the command reloadScript. The game should pause for a few seconds, then show you this:
****************************** ERROR: Exiting map to reload scripts ******************************
This is perfectly normal, and it means your script compiled fine. But if you see any other errors, head to the troubleshooting section before moving on.
At this point, run your map again, and you should be able to walk over the trigger and spawn a Strogg Marine into the map without a problem.
What's Next?
From here, you could move on to the AdvancedScriptTutorial, which will show you a little more about the techniques available to you in script. You could also take a deeper look at the ScriptVariable page to learn more about variables and how they are used in scripting.
If something here didn't work out for you, check out the troubleshooting section below.
Tutorial Troubleshooting
Did you forget a semicolon?
- Remember that most command lines in script need to end with a semicolon. Check the examples to make sure your script follows exactly.
Did you forget other punctuation?
Remember that the function needs to be enclosed entirely in {} curly braces. Did you forget one? Did you miss a parentheses in those functions in the second example?
Did you save your script file in the right place?
Check here to make sure you've saved your script where it belongs-- in the same folder as the map you're working on, with the same name as the map.
Did you call Reload Script from the console if necessary?
Remember that if you changed the script file while the game is running, you need to reload them. Check out the example here.
First Example: Hello Stroggos
1 void HelloStroggos() {
2
3 sys.println(“Hello Stroggos!”);
4
5 }
Second Example: Expanding to spawn a monster
1 void HelloStroggos() {
2
3 sys.println(“Hello Stroggos!”);
4
5 entity newMonster;
6
7 newMonster = sys.spawn("monster_strogg_marine");
8 newMonster.setWorldOrigin( $targetMonster.getWorldOrigin() );
9 }