This little HOWTO is an attempt to make AI scripting for Single Player RtCW levels a bit more accessible.
AI scripting is what makes the AI's or Bot's behave in an intelligent way. Without any script, they have excellent hearing, sight and health, they exist all the time, just stand around, are fully armed all the time, and attack in true kamikaze style. Sort of slightly worse than in "Doom!" As a result, at the first sound the player (or anyone else) makes, the entire mob attemps to fall over you, in a stir-crazy attempt to kill you.
With a script, this behaviour can be altered in nearly any way you want. Some soldiers hear better than others - an engineer looking after noisy machinery could be stone deaf, some smoke or sleep or goof off during guard duty. When attacked they hide behind things, or even surrender. They patrol an area, or man machine-gun nests. They behave as real people.
You, the mapper, will have to teach the AI's how to behave. And that's a tricky business. It is a complex matter, and the documentation is pretty terse.
You'll need a couple of things to do the scripting:
...And some patience, time, an understanding family and some strong coffee ;)
I assume you know your way around the tools, else go away and learn about them first. If you try to do everything
at the same time, you'll just end up totally confused in a chaotic mess...and probably never try mapping again!
We start with a really simple map. Start GtkRadiant and create a simple skybox - 512 * 1024 units ground size, 256 high, grid 8, 'Hollow', a info_player_start (angle 180) in a convenient spot, some grass on the ground, the rest with some sky. Thats quick'n dirty, but will do for us.
We need somewhere for the AI's to go. Unless you make a 'dumb' map, they'll need to be told where to go, and you tell them with the script, an entity called 'ai_marker' and a unique name for each ai_marker.
Here is my map: the markers are called 'm1' trough 'm5' - mark each in turn, press 'n' for the entity window and enter key 'targetname', value 'm1' for the first, the others as appropriate. My markers start top left with 'm1', goes clockwise and end with 'm5' in the middle.
Just to test, try compiling the map - both the q3map (or q3map2) stage, and the bspc stage - and run it. I call my map tut.map, so i start it in a Wolfenstein console with '/spdevmap tut'. You should now stand unarmed and alone on a plain in the middle of nowhere. Ok?
This is no good, of course. You'll need somebody to shoot, but first you'll need something to shoot them with. So, here we go with your first script.
Heat up your favourite editor, and create a new file. It should be named 'yourmapname'.ai - my map is called tut.map (compiles to tut.bsp), so my .ai script file is called tut.ai.
TRAP WARNING: Some operating systems cannot tell the difference between upper- and lowercase letters, but most can. So, please keep ALL letters in the script lowercase - that way, everybody can play your map afterwards.
The first thing you type is the header. That's just a couple of lines for your convenience, when you later need to find out what on earth you have been doing. Trust me, after a couple of days even the most logical script needs decrypting, before you can work out what you tried to achieve.//
Of course you put your own name there, not mine. There is limits to the amount of blame i'm willing to receive ;) The double slashes tell the game engine: this is a remark and can be ignored. Stuff in remarks wherever you can. Everything after a double slash is a remark. The more remarks, the more help you give yourself later on (Only one catch: there _must_ be a space between the last letter in a line and the //, else you get an error). The line indention - see further down - is there for the same reason: clarity. Stick to it.
Next, you need to add something for the most important AI: the player. Yes, in this game, your own intelligence is artificial. Nice eh ? :)player
Each AI is identified with a unique name like 'nazi1' or 'officer_joe' or what you want. Just make sure, that name is used only once in each .map. The name 'player' is reserved for (you guessed it) the player. Every single bit of information relating to the player, has to be written between the brackets. Similar with the AI's - all info must be between the brackets. Pretty much like in the "C" programming language.
Right, we want to give you a gun:player
Now, what does this do?
Inside the 'playerstart' set of brackets is what happens after the player is spawned and put inside the map. First, we need a weapon he can use:giveweapon weapon_sten
All valid weapons is in the entity list under 'weapons'. The player is now in the possession of a Stengun, but it ain't loaded, and it is tucked away somewhere unseen.setammo ammo_9mm 64
Gives two extra full clips of ammo for the Stengun. Still no ammo in it. The game seems to randomize around this number, so the specific number is rarely reached.setclip weapon_sten 32
This specifies how much ammo is in the gun initially. Don't try to stuff more in than a clip can take. Without this you have an undefined amount of ammo. Specify, or you'll get suprises.selectweapon weapon_sten
Finally you have it in your hands! Phew!
At this moment, there should be three files in your /wolfenstein/main/maps/ directory: tut.bsp, tut_b0.aas and tut_b1.aas - if you stuck to my naming. Save your tut.ai there as well, and try to run RtCW - pull a terminal, and write /spdevmap tut
Did you get your gun ?
It's a bit puny with only one gun, right? Under //weapons addgiveweapon weapon_knife
Try to add a point:// misc
Save, and next time you run the map you suddenly have 50% armour and a set of binoculars. Plus a knife. I have - so far - not found a list of acceptable items, but try to sniff a bit around in the .ai scripts that come with the game. They are hidden in the /maps directory , in the /wolfenstein/main/pak0.pk3 file. It's just a funny named .zip file.
Now, how about some music? It's a bit dull with no sound.
There are two kinds of music: what plays when the level is loaded, and the game waits for you to start, and the in-game music.
First, the waiting music (BTW, i can halfway understand that music can make chickens lay more eggs, but what extra do you get out of some people in an elevator?). Add a 'spawn' group as the first thing after the very first bracket, make your script look like this://
Try it. Great soundtrack, eh? You can of course use this, if you like, but usually you change over to some other music, when you start the level. Add these two lines before // weapons in the 'playerstart' section, and we have some in-game music:mu_fade 0 1000 // music fades to 0% in 1 second (1000mSec.)
I guess that is simple enough? These options can be used later on in the script as well, but more about that later.
So many words, and we're still alone in the map. But at least we have our trusty Stengun and some nice music...
I guess it is time to add our first 'real' AI to our map?
Here he is, centered between marker m1, m4 and m5. He got some properties ('n' for the entity window):
As you can see, it's our old friend, Herr Kessler. Try compiling and running the map. He won't do much - as the map starts, he'll whip out a Tommygun, thats it. If you go and kick him, or shoot him in the leg, he'll start waving the gun around, but not really know what to do with it (like shooting you - i would :)
All the AI's have a default weapon - here it's a Thompson. If you don't want the AI to use the default, it's up to you to take it away from him.
Perhaps we should let him have a more proper weapon - for a partisan, that is?
To the .ai script, add:party1
This is slightly different from the 'player' AI. The 'attributes' is the personal settings for the AI - hearing, vision, ability as a marksman, tactic skills and so on. They'll all be set to their default values, if nothing is specified. A description of all the settings comes with GtkRadiant under help/Return to Castle Wolfenstein/scripting definitions, but their proper use is a matter of experience. Look a bit around in the .ai scripts, especially the ones that comes with the sample maps, find a AI, and compare his behaviour to the script.
The 'spawn' thing is pretty much the same, we just don't set any music here, and that's where we handle the AI's weapons. Herr Kessler started up with a Thompson, but that is a rare weapon amongst partisans in WW2 Europe. More likely is a Luger(usually pilfered from a ageing and careless officer), or a Stengun (airdropped by the OSS). We put this in his 'spawn':takeweapon weapon_thompson // removes the tommygun
Run the map. He now has the P-08, but he's not drawn. Kick him, or shoot him in the leg, and he'll whip the Luger out. Ok, thats fine, but what will he use, if he has more than one kind of weapon? There is a whole series of commands to help determine this - 'statechange'
These are separate scripting events, and it is not necessary to use them all. A AI has four 'moods' - relaxed, query, alert and combat - and it is possible to define exactly how the AI behaves when passing from one state to another. Again, look in the 'scripting definitions'. I have only one state change, we put that in the .ai file right after the closing 'spawn' bracket:statechange relaxed combat
Here i just make him draw the gun, but you can put any scripting event here - like make him run to a heavy machine gun and use it, hide behind a bush e.g. If you run the map again, there should be no visible difference, you'r just _sure_ it's the Luger he draws. He'll use the Luger until it runs out of ammo, then change to another weapon. Unless you tell him otherwise :)
The 'statechange' can be used in cunning ways. E.g. you can make a guard stand outside his whatever he guards, perhaps do a little patrol. Then, when he goes from 'relaxed' to 'query', he can change the direction of his patrol, and look around. From 'query' to 'alert' he can whip the gun out - and changing into 'combat' he yells murder and runs for cover, while attempting to shoot you.... work something out yourself.
There is a couple of commands to further refine this... like 'pain' and 'death'. You treat them just as 'statechange', they are separate scripting events:pain 25
You could set your AI up to attack in a bold way - then the above script could make him drop his gun, run away and hide, when he has reached 25% health. Anddeath
He's only standing still, what if we make him move? Let's say, he have to walk the five markers and stop in front of you. That, of course, takes a bit of script. Stick it in between party1's 'spawn' and 'statechange':trigger walkabout_1
There is fourth type: 'gotomarker'. This uses what 'movetype' the AI is in already - if it walks, it keeps on walking, if it ran, it keeps on running etc.
Now we have a description how mr. Kessler is supposed to do the Silly Walks on one single orbit around the track. How to start him? Well, he's supposed to do it, no matter what the player does. So, stick it in as the bottom line in 'party1/spawn':trigger party1 walkabout_1
It looks slightly cryptic, i'll explain later. Your whole script ought to look like this (just to make sure)://
Now he goes trough the course once, and stops in front of you. Just for fun (hrm!) try'n shoot him in the leg: He'll then stop, draw the gun, wait a while, then continue, but with drawn gun and on alert.
TRAP WARNING: there is no 'statechange combat relaxed' ! When he's drawn a gun, it stays drawn !
In my little map, mr.Kessler goes stuck at marker m3 now and again: his bounding box hits the wall, when he's crouching with a gun in his hand. The 'scripting definitions' claim you can extend the ai_marker's bounding box and create softer corners by writing e.g. 'walktomarker m3 nostop', but it seems this is buggy - sometimes that option crashes RtCW with a warning: "Can not find ai_marker 'nostop'". Better avoid it.
TRAP WARNING: A AI will keep moving towards its ai_marker, until it is exactly centered in the middle. If the ai_marker is too close to a wall, a table, a monster_clip or whatever, the AI will just keep walking 'on the spot' towards the marker - forever, or until you shoot him. Make sure the marker has enough space around it.
Right, we have a player set up, he's armed, the music is playing, and Mr. Kessler has been for a nice stroll around the block.
How do we make this interactive ?
First, remove the linetrigger party1 walkabout_1 // starts the walking script
from the script. Then we have to edit the map, so please crank GtkRadiant up again.
For now, it's ok to send mr. Kessler (party1) on his walk. We just do it from a trigger.
Select textures/common/trigger and make a nice brush right in front of the player start. Next thing, right above it, place a entity, a 'target_script_trigger'. Like this:
We want to send mr. Kessler on multiple walks, so select the trigger brush (shift + left click), whip the entity list out (right click on the selected brush) and select trigger/trigger_multiple. Keep the trigger brush selected, and select the target_script_trigger too (It is important that the trigger brush is selected first). Then press crtl + k. This creates the target/targetname connection between the two entities, and uses the first two available names. This way, you don't risk using the same name more than once. It can be messy in a big map.
Make sure that only the target_script_trigger is selected now. We need to set a couple of keys. Pull the 'entity'
window ('n'), give it the key 'ainame', value 'player'. This is to tell exactly which AI the trigger will respond to.
It doesent work without it.
Then it needs to know what part of the script to activate. Give it the key 'target' and the value 'start_walk', or whatever you like, like this:
Then save and compile. The rest is scripting. The rest. Haha! Hahahahahaha! (Hrm!)
In the 'player' part of your script, after the 'playerstart' section, you add the line:trigger start_walk
WTF???? Why is this to go in the 'player's part? Actually, there is logic to it. It is the player that activates
the trigger. So, it is in the player's script, you decide what is going to happen when the trigger fires. See?
What's between the brackets tells what's happening next: it is triggering an event inside party1's part of the script
- the event called 'walkabout_1'. In other words - as long as you don't move, mr Kessler stands still. You take a
step or two forward, and mr. Kessler takes a walk.
Run the map and see what happens.
Nice? AI 'party1' aka mr.Kessler goes for his walk/run. Everytime the player hits the trigger, Kessler goes calmly to the first ai_marker and starts all over again.
Usually, these events are only supposed to happen once, so the choice of a 'trigger_once' would be better. Start up GtkRadiant, and change it. And try the map again. Now the AI goes once, then stops.
Most times, in a Single Player map, the player walks trough a trigger and starts a AI marching script, as we just did. You don't want the AI's to do anything before the player is in the right spot. And that is what we have here. But then, once the AI's have started, you want them to carry on.
This is quite simple. In the script, as the last line in the 'party1/trigger walkabout_1' section, you add:trigger party1 walkabout_1
Huh? It's still logic. You want to trigger an event (keyword 'trigger'). That is supposed to happen in mr. Kessler's script (keyword 'party1'), and the thing that is supposed to happen is the march script (keyword 'walkabout_1'). Computers are dumb, so you have to provide a precise address for the event, you wish to happen.
Try running the level again. Our AI does not move before the player takes a step forward, then he carries on forever.
Wouldn't it be nice if our AI could stop now and again to admire some part of the landscape?
There is commands for that, of course. The first one is 'wait'. Put the line
in the walkabout_1 section, between 'walktomarker m1' and 'runtomarker m2', save and try it.
Our AI now takes a 2-second break (2000 milliseconds) after reaching the first marker. When he arrives, he just stands staring in the direction he was walking.
Of course, this can be changed. There is a few possibillities, like e.g.:wait 2000 player
Now the AI will face the player while waiting. Also try:wait 2000 m3
It will now face ai_marker m3. There is some possibillities in this one. If the ai_marker is well below eye level, the AI will lean forward to look at it. You can use it, if you want the AI to read some papers lying on a table, to inspect a corpse and so on. This is a built-in animation in the 'officer's, but can be useful for all kinds of AI's (more about animations later). If you place a ai_marker above eye level, the AI will stare at that, but be a bit careful with this - the AI will not just lift its head, but bend backwards in the hips, which tends to look silly.
TRAP WARNING: Your ai_marker must be within the hull of your world. If it is necessary to have it sticking out, put a structural 'clip' brush around it, else your map will leak.
If you have another AI, say 'joe_1', you can have mr. Kessler face him too:wait 2000 joe_1
and, finally, you can make your AI face in the direction of the ai_marker he stands on:wait 2000 m1
To make this possible, you have to give your ai_marker a direction. In GtkRadiant, mark the m1 marker, then clik on the compass direction in the bottom left corner of the entity window. Save and recompile, then it will work.
Remove all this stuff again, when you have played a bit with it - now you know what it can do.
Just to make sure, your tut.ai should now look like this://
We start having some control over our AI now, right?
We better ruin that straight away. Heat up GtkRadiant again, and put a new AI in. I'll suggest a ai_soldier, Heinz, and i put him next to marker m5 facing east (270 degrees, which funny enough is 'down' on the Radiant screen - on all charts, except the Spanish medieval ones, north i always up...).
Ok, compile and run the map.
Interesting, but not nice. The moment the two hotheads see each other, they start fighting it out. The player might even catch a few bullets, before someone shoots Heinz. Notice, that even during the shootout, Kessler still sticks to his script (if you triggered it), and Heinz hardly moves. Don't worry about this, there just isn't anything sticking out in this map, that they can use for 'dynamic' behaviour. You want them to play Hide And Seek - well, they'll need something to hide behind...
Now, to quiet them we could make them blind and deaf, but they won't be very eficcient opponents then. We better play a bit with this.
I have added a 'heinz' section to the script:heinz
And also added the lineaim_accuracy 0.0
to Kesslers 'attributes' section. This ought to make them both unable to hit a barn three steps away. Now we have time to see, what's going on.
The best way to prevent a free-for-all dogfight to evolve, is to make sure that only the relevant AI's are active. If they all are active, at the first shot they'll come running from all sides to squash the player. Luckily, we can control where and when they spawn.
Open GtkRadiant, select Heinz and open the entity window for him. Then check the box for 'triggerspawn' - save and recompile. Then we make a little change in the script - disable the linetrigger party1 walkabout_1 // start party1 walking
in the 'player / trigger start_walk' section of our script (put a // in the beginning of the line). And insert:alertentity heinz
right after it. Try to run the map.
Now, that made a difference. First _nothing_ happens. Heinz ain't there, and Kessler all peaceful. Then you touch the trigger, *POP* comes Heinz, and the trouble start.
This is an important thing. To make your map realistic, only spawn AI's within a reasonable distance of the events you want to happen. You can further refine this by appying the 'hearing scale x.x' parameter in the AI's 'attributes' section. If you fine tune this, you can ensure that no AI responds if the player knifes or shoots someone with a silenced weapon, but hell will break loose if you use a normal gun...or miss! The start of 'Escape2' from the game is a fine example of this.
A further advantage is, the less AI's you have spawned at a given time, the less processor power you need to play the map. Gamers with weak machines (like mine) will appreciate this.
TRAP WARNING: Don't do anything as blatantly stupid as i did in the tut map: DON'T spawn a AI ANYWHERE where the player can see it happening. It utterly ruins that illusion of reality we attempt to build into the maps. Spawn him behind a rock, in the next room or whatever and script him to walk around the corner.
Now we are playing with our trigger... try adding these lines, right after 'alertentity heinz':mu_fade 0 100 // music fades to 0% in 0.1 second (100mSec.)
and run it again....
Nice? :) First, nothing happens. Then, Heinz appears, and the orchestra panics... A few seconds, and they are back on the job again. What really happened ?mu_fade 0 100 // music fades to 0% in 0.1 second (100mSec.)
On activating the trigger, this just kills the old music real fast and calls the 'panic'music. Nothing new there. It is just done by trigger now.wait 5000 // gives it time to play out
The 'wait' thing is there to let the 'panic'music finish. Without that, the next command overwrites it and you only hear a small click sound, before the new/old music starts again. And, you have to put a new sound to play in (or a mu_stop to kill it completely), or else the panic music just loops. And, it sounds silly enough on its own.
If you look in the scripting for the 'church' level in the original game, it is done in a different way. The trigger in the 'curch.ai' dont execute directly, but calls a nearly identical section in the 'church.script'. I guess it's up to you to decide where you want it, there seems to be no difference at runtime.
One of the cute little things this game engine has to offer, is animations. You probably have noticed them: A guard taking a puff on a cigarette, another scratches his butt... It all adds to the illusion of the AI's being alive.
To finish this tut, you'll have to start GtkRadiant yet another time. This time, we get rid of our dear mr. Kessler, he has done his duty. Where he stood, stick in a model (i took the vacuum cleaner), create a trigger brush around it, make it a trigger_once and associate it with a target_script_trigger. Like this:
Then make another trigger brush, at the faraway wall - our exit. This time, make it a ai_trigger. This can trigger a script directly, but not in all cases. I'm not sure exactly what the difference is. It is necessary to use this kind of trigger, else it will count the numbers of met objectives wrong, but still let you out...
Compile and check, please. From now on, before you trigger Heinz, you need to pull a terminal and give the switch /notarget. Else he'll start shooting at you, and you need to see him in 'relaxed' mode...
Next thing we have to do, is cleaning our script up. First, delete the entire 'party1' section. It doesent do anything at all, and it is just messy to leave unused chunks of script lying around. Next, in the player section, remove everything in the 'trigger start_walk' section, except 'alertentity heinz' - we still need him around. Then your script ought to look something like this://
Right, now we should be ready for a bit of script. Right after Heinz' 'spawn' section, insert:trigger animation
And right after 'alertentity heinz' in the 'player/trigger start_walk' you puttrigger heinz animation
to start this small march script. So far, there is no animation - when triggered, Heinz spawns, walks to m5 and stares at you. Try it, just to make sure it works. And then, right after 'wait 5000 player', add:playsound cough2
What does this? Try running the map - don't forget /notarget.
'playsound cough2' does just that. It takes a while for the sound to play, so the game engine just launches
the sound and start the next bit: 'playanim coughing_2h both'. The word 'playanim' launches the animation, called
'coughing_2h', and 'both' means that both legs and torso of the AI is active in this animation. Simple,
really. Scout the .ai scripts for more examples. You can find the available animations for each type of AI
(e.g. infantryss, mechanic...) in the models/players/(playertype)/wolfanim.cfg files - look in the // AMBIENT
part of the file.
Try e.g. adding:
right before 'playsound cough2'.... :)
You have already put a vacuum or some other object in the .map, a trigger brush around it, and a target_script_trigger near it. So all we have to do is adding a bit to the 'player' part of the script, i guess after the 'trigger start_walk' part:trigger trigger2
...And that's it ! Save, run the map, and next time you walk past the vacuum (or whatever) it tells you, that you 'found a secret area'.
You can probably guess why i used a 'trigger_once' - else you can rake quite a few secrets up! If you want your secret to be inside a wall, or underneath a box or whatever, create the wall, box or whatever, make it a func_explosive, and make sure that the trigger brush for the secret is hidden a bit behind it. Else the trigger will fire, when you just scrape along that particular thing.
When you leave the map, it brings you on to the next map and tells your score - but ONLY if you have met the objectives.
You already put the 'trigger3' in the map. That's going to be the exit. Please put:trigger trigger3
in the .ai script, right after the 'trigger trigger2' section. 'Changelevel' is the order to bugger off and find another level. 'Escape1' is the name of the level you switch to - leave the .bsp away. 'Persistant' tells it to show all the statistics. You could use 'nostats' instead, if you dont want them.
Then, to the top of the 'player/spawn', you add:objectivesneeded 2 // tells the number of objectives
Run the map. And it tells you 'objectives not complete', when you hit the exit. We still need to tell the damn machine exactly what the objective is!
I want two objectives met. Your mission: Kill Heinz ! And find the vacuum!
to the end of the 'heinz' script, and add:objectivemet 2
at the top of the 'player/trigger trigger2' script.
TRAP WARNING: The 'objectivemet' thing _must_ be at the very beginning of whatever script it is in. Else the game engine will register the objective as met and let you out, but it will show a wrong count in the endmap statistics!
If you have one, or more than one objective, you give them continued numbers, starting at '1'. Make sure you don't miss a single number, or you won't get out!
Right, save and run ! You should be able to exit the level now, as soon as you make Heinz go ART (assuming room temperature :) - and find that damn vacuum!
If you find faults, flaws, something missing etc., please mail me.
Greetings, and happy mappin' -
Kim 'The Pirate' Christensen
Arno and WeblionX from the PlanetWolfenstein mapping forum: thanks for
feedback and ideas.
Eyeronik from Surface for inspiration in his 'AI marching script' tutorial.
Gray Matter Studios for their 'Scripting definitions'.
My wife, Jennifer, for proofreading and just generally putting up with me...
Errors in 'End of level' corrected, second objective added. Various little errors removed. HTML cleaned up and validated.
Original version, posted mainly for feedback.
Newest version can be found at http://pirat.snotboble.net/files/ai_script_HOWTO.zip
Back to surface