AI Scripting in Medal of Honor: Pacific
Assault
Contents
1.3������ Triggering Script Threads
1.4������ Running Threads on Triggers
1.5������ Treating Multiple Triggers as a
Single Trigger
2.2������ Triggering AISpawners
2.3������ Triggering AISpawners in Script:
the Basics
2.4������ Triggering AISpawners in Script:
Beyond the Basics
3����� Running Threads on Spawned Actors
3.1������ Running Threads on Individual
Actors
3.2������ Running Threads on Multiple Actors
3.3������ Working With Individual Actors in
a Squad Structure
4����� AI: Parameters, Combat Modes and
Orders
4.1������ Allied Assault AI Parameters
4.3������ Basic Enemy Engagement
4.6.1������� stalklastseenposition
4.7������ Captain, Medic and Squad AI
4.7.1������� Working with Captains
4.7.2������� Working with Medics
5.1.2������� Clearing movement orders
5.1.3������� Testing for Arrival
Just about every event in Medal of Honor begins with a trigger.� You�ll use triggers to spawn actors, to alert them, and to set off scripted events involving them.� We�ll take a look at several methods of placing triggers in levels, each of which has its advantages and disadvantages.
The first thing to do in placing a trigger is construct a brush to represent that trigger, then apply the Common::Trigger texture to the entire brush.� Now, with the brush still selected, right-click it to bring up the context menu.� Select Classes�Trigger, and select the type of trigger you�d like.� (Actually, you can take a brush with any texture, and if you apply the trigger class to it, it�ll apply the trigger texture for you, but it�s not a bad idea early on to get used to doing it by hand.)� You have a number of options here, but you�ll really only use three of these with any frequency.
Triggers support a number of properties, but again, only a few of these factor into everyday use.� Bring up a trigger�s entity information window by hitting �N� with the trigger selected. To set properties, type the property name in the Key field, and the value you�d like to apply in the Value field, then hit Enter to apply it to the entity.
|
|
|
Three commonly used properties remain, and these tie into the next discussion: what happens when the trigger goes off?� What hears it, and what happens next?
There are several ways to tie a trigger to the events it triggers.
target: The first of these is the simplest.� If the targetname of another entity is placed in a trigger�s target property, that trigger will send its activate event to that entity.� If that entity knows what to do when triggered, it�ll execute that behavior.
For example, the bats pictured above each have a $targetname �fauna01.�� By entering that value in the trigger�s target property, we tell it to send its activate event to any entity with that targetname, which in this case, sends the message to any object named �fauna01.�
You can connect an object with its target quickly by selecting the object, then selecting the object you�d like it to target, and hitting Ctrl-K to connect the two objects.� An arrow will be drawn between those objects in Radiant to show the connection.� All the connect function has really done, though, is place the second object�s targetname value in the first object�s target property.� You can do this manually if you�d like, and if you find a bogus or mysterious connection between two objects, you�ll debug it here.
When triggered, it�s up to the target object to know what to do when it receives the activation message.� Many classes have default listeners built in, which listen for activation events from triggers with certain targetnames.� In this case, the bats are members of the fauna_bird class, which knows what to do when it gets a message from a trigger named �faunaspawnertrigger,� so the trigger starts that behavior (in this case spawning and flying).
For many standard behaviors and interactions, this is the simplest way to set them up.� For tighter control, however, you�ll want to take these operations into script.
There are two major ways to tie a trigger to a thread in your level�s script.
setthread: The first is to use a trigger�s setthread property to call a thread in your script.
For example, if you set up a trigger_multiple, and put �gagFoo� in its setthread property, that trigger will launch the gagFoo thread (if it can find it in your level script) whenever it�s triggered.
This is a simple way to use a trigger to get a thread going, but it causes a few problems down the road.� When you, or someone else, tries to decipher your level script later on, there�s no way to know from reading the script that gagFoo is launched by a trigger in the level.� If the designer�s been conscientious and commented the script well, this may be indicated somewhere (it should be, at least), but otherwise, the person trying to decipher the script is left hunting for whatever calls gagFoo, which could be anything � another thread, maybe, or a trigger hiding somewhere in the level geometry with a setthread property.� It�s easy to set up, but can be tough to debug.
When using the setthread property, you�ll have to scope your thread explicitly to the file holding it, if the thread isn�t resident in the level�s master .scr file.� If your level is called myLevel.map, and the thread�s in myLevel.scr, you can get away with the name of the thread alone.� If, on the other hand, the thread lives in a file named myLevel_gags, in the �gags� folder, you�ll need to enter gags/myLevel_gags::gagFoo, to tell the engine where to find the thread.� The double colon [::] is called a scope resolution identifier.� You�ll use it throughout your work to tell the engine where to find specific objects.� Bear in mind that if you move gagFoo, or change your directory structure, your call will break until you update your trigger�s setthread.
There�ll be instances where the setthread method really is the fastest and most convenient way to set up a call, but you should think about using it sparingly.
This trigger� |
�launches this thread |
$targetname waittill trigger: A better method, though it requires a little more comfort with the scripting language, is to launch a thread within your level�s script to listen for the trigger.� This allows you to control exactly when the level script starts and stops listening for the trigger, allows you to initialize whatever else you might want to make ready before the script starts listening, and most importantly, leaves no mystery within your level script about which blocks of code depend on triggers to run.
This thread� |
�watches this trigger. |
To set this up, simply give your trigger a $targetname ($targetname works just like targetname except that targetnames preceded by the $ symbol can be accessed as objects in the script by preceding the object�s name with the $ symbol).
Then, in your script, refer to that object and tell the script to wait for it to be triggered.� If your trigger, for example, has $targetname triggerFoo, in the script, write
$triggerFoo waittill
trigger
and the thread will stop there and wait for that trigger to activate.
If you�d like the thread to go back after triggering and watch a trigger_multiple again, simply loop it back on itself to ask it to watch again, and break out of the loop when you�re done watching the trigger.
while (true) {������ // loop will continue running until we break out manually
//-----------------------------------
$triggerFoo waittill trigger
//-----------------------------------
������
thread gagDoSomethingInteresting
if (level.bDone) {
������������� $triggerFoo nottriggerable // shut off the
trigger
������������� break� // the break keyword
gets us out of the while loop
}
}
Another example of the same method, more concise:
while (level.bListen) { // loop will continue running as long as bListen != 0
//-----------------------------------
$triggerFoo waittill
trigger
//-----------------------------------
�������������
thread
gagDoSomethingInteresting
}
$triggerFoo remove�� // destroy this
trigger now that we're done
In the first instance, the while loop continues running until level.bDone is set to true by some other thread or event, at which point, we get inside the if statement, use the nottriggerable keyword to tell the system to stop listening to the trigger, and then use the break keyword to bail out of the loop.
In the second, we set the while loop to keep on running as long as level.bListen is true, so we don�t need to use the break keyword by hand.
Only once we�re out of the loop will execution reach the line after the loop�s closing brace, which in this instance uses the remove keyword to remove the trigger from the level entirely, rather than simply telling the level to ignore it.
Most of the time, you�ll want to use the second syntax, as it�s more concise and easier to debug (it can be easy to miss break and end statements buried deep in code), but there�ll be times when you�ll need to set up more complex conditions and use break manually.
The triggerable and nottriggerable keywords, respectively, make a trigger responsive to triggering elements, or tell them to ignore input.� The remove keyword, as with any other object in the game, removes the object from the level.� If you�ve given your trigger a cnt of 1, or set up a trigger_once, you don�t have to remove it explicitly after it�s been used � it�ll clean itself up.
$triggerFoo nottriggerable // turns triggerFoo
off
$triggerFoo triggerable��� // turns
triggerFoo on
$triggerFoo remove�������� // deletes
triggerFoo
You can run any thread on an object by preceding the call to the thread with the object�s name.� If, for example, I want to run a thread called watchTriggerFoo on $triggerFoo, I put
$triggerFoo thread watchTriggerFoo
watchTriggerFoo will treat $triggerFoo as its self object.� The following thread sets a flag called level.bWatchingFoo to true, turns on $triggerFoo for listening, activates a thread whenever $triggerFoo gets tripped, waits 5 seconds before letting it trip again, and cleans up the trigger and stops listening when a level variable called level.bWatchingFoo gets flipped to false.� We�ve included a comment in level.bWatchingFoo�s initialization that tells us where we plan to change the value to aid us later on in debugging.
//----------------------------------------------------------------------------
watchTriggerFoo:
//
// watches triggerFoo
//----------------------------------------------------------------------------
��
�� dprintln
"Thread launched: watchTriggerFoo"
��
�� level.bWatchingFoo
= true� // switched off in gagDoSomethingInteresting
self triggerable
��
�� while (level.bWatchingFoo) {
��������� self
waittill trigger
��������� thread
gagDoSomethingInteresting
��������� wait
5
�� }
��
�� self
remove
��
end
The advantage to this approach is that the trigger�s operations are now fully encapsulated within a single routine � you can see easily when it gets turned on, and you know unequivocally that if you�re watching it, it has been turned on, because you took care of both of those operations in one thread.� Your cleanup is handled here too.� All neatly wrapped up in a single package, and easy to debug.
Of all methods of watching triggers, this offers the greatest degree of control, flexibility and readability.
If you have a number of triggers in your level that should all do the same thing, you can essentially treat them as a single trigger by giving them all the same $targetname.� When you call a thread on an object with a given $targetname, that thread runs on every object in the level with that $targetname.� That�s a powerful feature, and one you�ll use a lot in handling AI.
If, in the above example, instead of one $triggerFoo, there were 26, everything would work exactly the same.� All 26 would be turned on when watchTriggerFoo was launched, they�ll all throw their messages, and handle their delays independently, and all clean themselves up when level.bWatchingFoo�s status changed.
Technically, what happens when you give multiple objects the same $targetname is that all those objects get put into $targetname array, and when you run operations on those objects, those operations run on every member of that array.� You don�t need to know this to use it, however.� All you need to remember is that a thread launched on an object with a given $targetname runs on every object in the level with that $targetname.
For the most part, we�ll use triggers to spawn actors in our environments.� We�ve spent a bit of time discussing triggers.� Let�s get down to business now and take a look at actors.
The simplest way to place an actor in a level is simply to plant that actor in the scene and leave him there.� To do this, right click wherever you want the actor to go, and from the context menu, select models � human, and then select whichever model you�d like, usually from the axis or allied submenus.
The resulting model will appear in your scene, and if you do nothing else, he�ll be usable as a basic actor, executing his default behaviors.
Simply setting and forgetting an actor this way presents a number of problems, however, especially in larger levels.� First, every actor you place this way is alive all the time, right from the level start � if you have 200 AI�s in your level, all 200 of them will be running, whether you can see them or not, and your framerate will suffer for it.� You�ll also run into issues wherein actors will alert before you really want them to enter the scene � they�ll hear a firefight going on nearby, and that carefully set up ambush you�d planted around the corner goes out the window when they all come running.
Enter the aispawner.� Think of an aispawner as a spawn point from which an AI will emerge whenever you activate it (usually this will be only once, but it doesn�t have to be).
To change an ordinary actor to an aispawner, simply give the actor a $targetname of �aispawner�
�aispawner� is a reserved targetname, which instructs the system to hold off spawning that entity until instructed to do so.
The aispawner system thinks of spawners in terms of sets.� If you tell the system to spawn set 100, for example, any aispawner with that value in its #set property will be activated.� Those with other set numbers won�t.� To put your aispawner in a set, add a �#set� key, and set its value to whatever you want that actor�s spawnset number to be.� In the above example, #set = 100.
If you�d like your spawned actor to run to a specific location when it appears, put the targetname of a pathnode or script origin in the aispawner�s target key.
The easiest way to do this is simply to highlight the aispawner, then highlight the pathnode you�d like it to approach, and hit CTRL-K to connect them.� We�ll look at pathnodes more closely later on.� For now, just be aware that this behavior is available to you.
Once you�re done setting this up in Radiant, you have to do a brief bit of scripting before it�ll run.� The aispawner routines rely on a few other scripts, and you have to get these running before your aispawners will work.� We�ll take care of this by executing global/auto.scr from a level script.
This isn�t hard to do.� Create a text file in your maps directory (or wherever you�re putting your bsp), and give it your bsp�s name, but with the .scr extension.� If your bsp, therefore, is named test_foo.bsp, name your script test_foo.scr, and put it in the same directory.
Here�s an example script:
//
***************************************************************************
//
test_foo.scr
//
Architecture: Giuseppe Blough
//
Scripting: Giuseppe Blough
//
***************************************************************************
//
***************************************************************************
//
level init
//
***************************************************************************
//
enable scripts
exec
global/auto.scr
The last line is the only one that does anything � you could simply drop that line in your otherwise empty script file and it�d still run.� Everything preceded with the // operator is a comment, which has no effect on execution, but explains to others (and to yourself later on) what you�re doing and helps you find things in your code.
If you run your level now, having placed an actor, named him �aispawner� and executed global/auto.scr in your script, you�ll see that the actor no longer appears.� The system is waiting to be told to spawn him.
There are several ways to activate an aispawner, and as with everything else, there�s a simplicity/power tradeoff to the different methods.
One simple (if inadvisable) way to trigger an aispawner is to create a trigger, give it a targetname of �aispawnertrigger� and set its target to �aispawner�.� As mentioned in the trigger discussion, many objects have default behaviors which they�ll execute when they receive a trigger�s activate message, and an aispawner�s default behavior, predictably, is to spawn.
Simple as this method is, however, it causes a pile of problems.
First, and most significantly, every aispawner in your level will, of course, have the $targetname �aispawner.�� Target one, therefore, and you�ve targeted them all.� They�ll all spawn when the trigger goes off, regardless of their #set value.� You could get around this by giving your spawner a different targetname, and because the engine sees that a trigger with $targetname �aispawnertrigger� is targeting that spawner, the engine will hold off the spawn until the trigger goes off.� It�ll work, but it�s a hack.
A number of non-obvious errors will arise as well if you give an aispawnertrigger a target value, and it can�t find that target.� This method has been described here for completeness� sake, but you shouldn�t use it.
Here�s what you should do:
Create a trigger (trigger_multiple, trigger_use, whatever), and give it a targetname of �aispawnertrigger�.� Now give it a #set key, and set its value to the #set value of whichever actors you�d like to spawn.� To activate an aispawner whose #set value is 100, set the corresponding aispawnertrigger�s #set value to 100 as well.
If you�ve planted five aispawners and given them all #set values of 100, they�ll all spawn when the corresponding trigger sends its message.
Simple enough, and for many basic applications, this method�s all you�ll need.
If you need greater control over your spawning, however (and to use the AI effectively, you will), you�ll need to move your spawn methods into script.� Let�s take a look at how this works.
Set up your aispawners the same way whether you intend to spawn them by trigger or script.� As long as they have $targetnames of �aispawner,� and #set values, you�re in business.
All you need to do to spawn them from script is call the spawnset thread with the #set number of those aispawners you plan to activate.� To spawn #set 100, then, put
waitthread
global/aispawn.scr::spawnset 100
in your script, and when the script�s execution hits that line, any aispawner with 100 as its #set value will activate.� Because the spawnset thread resides in another .scr file, we have to tell the system where it is.� That�s what global/aispawn.scr:: is doing � telling the script parser to look in global/aispawn.scr for the spawnset thread.
Always use the waitthread method, rather than the thread method to call spawners.� Waitthread ensures that the operation completes before it lets the calling thread move on to subsequent instructions.� If you fail to do this, you�ll probably run into strange, intermittent errors when spawns take longer than expected, but the calling routine gleefully moves on and executes code that erroneously assumes the spawn has finished.
Let�s take the earlier example and convert it to this method.
First, we�ll need to change our trigger.� Delete its targetname key (and, of course, its target key if you were masochistic enough to try that method), and give it a $targetname value, which can be anything that�ll make sense to you when you read your code later on.� It�s not a bad idea to use a consistent naming convention for objects in your level � it�ll make debugging easier later on.� In this example, the trigger�s $targetname has been set to �triggerSpawnRidgeGuys.�
That�s all we need to do to the trigger, unless we want to set its cnt value or make it responsive to monsters (non-player actors).
Now, let�s get the script watching it.
// ***************************************************************************
// test_foo.scr
// Architecture: Giuseppe Blough
// Scripting: Giuseppe Blough
//
***************************************************************************
// ***************************************************************************
// level init
//
***************************************************************************
// enable scripts
exec
global/auto.scr
//----------------------------------------------------------------------------
main:
//
//
//----------------------------------------------------------------------------
������
������ dprintln "Thread
launched: test_foo::main"
������
������ //-----------------------------------
������ level waittill prespawn
������ //-----------------------------------
������ // empty for now
������
������ //-----------------------------------
������ level waittill spawn
������ //-----------------------------------
������ $triggerSpawnRidgeGuys thread
watchForSpawnRidge
�������������
end
//----------------------------------------------------------------------------
watchForSpawnRidge:
//
//���� Spawns a few guys on
the ridge when player rounds the bend.
//----------------------------------------------------------------------------
������
������ dprintln "Thread
launched: watchForSpawnRidge"
������
������ //-----------------------------------
������ self waittill trigger
������ //-----------------------------------
������ dprintln "Triggered:
triggerSpawnRidgeGuys"
������
������ waitthread global/aispawn.scr::spawnset
100
������
end
As before, we begin by executing global/auto.scr (nearly all your level scripts will begin this way), but we�ve added a few new elements.
The first is the �main� thread.� A level script begins execution automatically when its level launches, and will keep on running until it hits an end keyword.� This script, then, will launch, execute auto.scr, then skate right on into the main thread and execute its instructions until it hits the end keyword.� Technically, you could call this first thread anything you wanted, or even omit the thread name entirely, but it�s a good idea to stick to convention and call it �main,� as that�ll keep it clear that this is the first thread to execute.
Ordinarily, you�d use your main thread to initialize your level, possibly setting a few worldspawn values after prespawn and before spawn, and initializing variables and objects once your level�s spawned.� We haven�t done any of that here for simplicity�s sake � everything�s been left in its default state.� All this main thread does is spit out a debug message telling us that it started, and launch the watchForSpawnRidge thread.
If we wanted to, we could dump the contents of watchForSpawnRidge right into main without changing how our level runs, but wouldn�t be a great practice to do that.� You�ll have an easier time modifying and debugging your code later on if you keep discreet operations on discreet threads � especially if you intend to listen for multiple events simultaneously.
watchForSpawnRidge contains the stuff we care about for this example.� At its core, it does two things: it waits for $triggerSpawnRidgeGuys, which it treats as its self object, because it was called on that object, to send its activation message, and then when that happens, moves on to launch the spawnset method on #set 100.� Everything else in the thread is a comment or a debug message.
(Debug outputs like this, by the way, are a good way to track your script�s execution as things start to get more complex � you�ll be able to see from the message output in qconsole.log if something occurred sooner or later than you expected.)
So far, however, all it looks like we�ve done is add a whole bunch of script to replace a behavior that already worked fine in its simpler non-script state.� Let�s change that.
In the previous script example, we called the spawnset thread to spawn our actors.� All spawnset really does is call the spawn thread for us, with a few of the less frequently used arguments left out.� Spawngroup does the same thing, but offers a slightly different set of parameters.� (�Parameter� and �argument� mean more or less the same thing, and are used interchangeably here.)� Let�s look at Spawnset first, as it�ll be most frequently used.
Spawnset allows us to specify four parameters:
spawnset
local.set local.enemyname local.get_there local.run_to_target
Let�s examine each to see what it does.
local.set: This first parameter is simply the set number of the aispawners you plan to spawn.� You must pass in a value here � otherwise the spawn thread will have no way to know who you intend to spawn.� The rest of the parameters are optional.
local.enemyname: You�ll use this one frequently in managing AI.� Because all your actors initially had to be named �aispawner� to be recognized by the spawn system, you lost the ability to refer to them individually in script by $targetname.� This fixes that.� If you spawn an actor using spawn, spawngroup or spawnset, and pass in an enemyname, it�ll rename the actor to that name.� For example, if we put
waitthread
global/aispawn.scr::spawnset 100 axRidgeGuys 0 0
in our script, it�ll spawn everybody in #set 100 and rename every member of that set to $targetname axRidgeGuys, which will allow us to refer to that group by name in script later on.� (We�ll be doing a lot of this.)
local.get_there: We�d mentioned earlier that if an aispawner is given a pathnode or script origin as a target, the spawned actor will run to that location when spawned.� These next two parameters refine that behavior.� If local.get_there is set to 1, the actor will ignore all other inputs until it arrives at the destination.� If you want an actor to spawn and run around a corner when the player hits a trigger, for example, regardless of whatever else might be going on in the environment, set get_there to 1.� If you want the actor to behave normally; i.e., engaging the player when seen, dodging when shot at, etc., whether or not he�s reached his destination, set this to 0, or just leave it .� The value defaults to 0.� In the example above, we�ve told the actors spawned by this method to run their normal AI from the start.
local.run_to_target: By default, a spawned actor who�s been given a target will run to that target.� If you�d like the actor to walk instead, pass a 0 in this parameter.� We�ve told the axRidgeGuys in the example to walk to their destinations.
Spawngroup works like spawnset, but takes a group argument, and omits the get_there and run_to_target arguments.
spawngroup
local.set local.group local.enemyname
local.set: As with spawnset, this is required.� It�s the set number you intend to spawn.
local.group: Spawnsets can be subdivided into groups which you can spawn separately from each other.� If, for example, we put two actors in #set 100, but put one of them in #group 10 and the other in #group 20, this line:
waitthread
global/aispawn.scr::spawngroup 100 10
Will spawn the first, but not the second, while this one:
waitthread
global/aispawn.scr::spawnset 100
will spawn them both.
waitthread
global/aispawn.scr::spawngroup 100 10 Spawn this guy |
waitthread
global/aispawn.scr::spawngroup 100 20 Spawn this guy |
waitthread
global/aispawn.scr::spawnset 100 Spawn both |
local.enemyname: Again, allows you to rename the actor on spawn.
Spawngroup doensn�t offer the option to pass get_there or run_to_target arguments.
If you check global/aispawn.scr, you�ll see that all spawnset and spawngroup really do is accept the arguments you�ve passed to them, then pass these arguments on to the spawn thread.� Spawnset and spawngroup exist for your convenience � the arguments they omit are rarely used, but if you need them, here they are:
spawn
local.set local.pause local.thread local.off local.group local.enemyname
local.get_there local.run_to_target
local.set: the #set you intend to spawn.
local.pause: a delay, in seconds, between the moment you give the spawn order and the moment the actor appears.
local.thread:
if you specify a thread name here, that thread will be executed on the spawned
actor when it spawns.� Useful for
initializing actors, though there are other ways to achieve this, which we�ll
examine shortly.� *For now, use those
other ways.� This isn�t functional in the
current build.
local.off: Set this to true (1) if you want your actor to spawn with its AI shut off.� The spawned actor won�t respond to game events until you activate its AI.� Useful, for example, if you�d like to hide someone in a closet, and ensure that he won�t respond to a nearby firefight and jump out until you tell him to.
local.group, enemyname, get_there, run_to_target: these work exactly as they did in spawngroup and spawnset, respectively.
Here�s a summary of the available arguments:
spawn |
spawnset |
spawngroup |
local.set |
local.set |
local.set |
local.pause |
|
|
|
|
|
local.off |
|
|
local.group |
|
local.group |
local.enemyname |
local.enemyname |
local.enemyname |
local.get_there |
local.get_there |
|
local.run_to_target |
local.run_to_target |
|
Now that we�ve covered the essentials of getting actors spawned into levels, let�s get down to the stuff that brought us here in the first place: AI.
We�ll achieve virtually everything we do in AI scripting by running scripts on individual actors or on groups of actors, so let�s take a look at how this works.
Earlier, when we ran watch threads on triggers, we saw that we could run a thread on an object by preceding the call to the thread with the object�s name.� This works for actors (or any other object) just as it did for triggers.
To run a thread on an individual actor, then, call the thread after the actor�s name.� To run a thread called initMyGuy on an actor named myGuy, put
$myGuy
thread initMyGuy
in your script.� initMyGuy will launch, and $myGuy will, as we�ve seen, be accessible to that thread as the self object.
If we want to use initMyGuy to set our actor�s health, for example, the following thread will handle it:
//----------------------------------------------------------------------------
initMyGuy:
//
//���� Initializes an actor
//----------------------------------------------------------------------------
������
������ dprintln "Thread
launched: initMyGuy"
������
������ self.health = 10
������
end
When we call this thread on $myGuy, a pointer to $myGuy is passed into initMyGuy as the self object, so when we set self.health to 10, that�s equivalent to setting $myGuy.health to 10.� If we now run the thread on $myOtherGuy, it works identically:
$myOtherGuy
thread initMyGuy
initMyGuy will now set $myOtherGuy�s health to 10.
If we wanted to watch for an event on this actor, we could set this up here as well.
//----------------------------------------------------------------------------
initMyGuy:
//
//���� Initializes an actor
//----------------------------------------------------------------------------
������
������ dprintln "Thread
launched: initMyGuy"
������
������ level.axisCount++��� // increment counter when he spawns
self.health =
10
������
������ //-----------------------------------
������ self waittill death
������ //-----------------------------------
������ level.axisCount����� // decrement counter when he dies
������ if (!level.axisCount) {
������������� thread gagSquadWipedOut axis
������ }
end
This thread, in addition to initializing the actor�s health, waits for it to die, decrements a counter (which we incremented when he was spawned), and launches a thread called gagSquadWipedOut if that counter reaches 0 (The ! (not) operator simply checks to see whether a value is equal to zero.).
The same method works on groups of actors just as it did on individuals.� Just as with triggers or any other objects, if you run a thread on an object with a given $targetname, that thread will run on any object with that $targetname.
Let�s use what we�ve learned so far to create a squad of axis soldiers.
To begin with, let�s create a squad:
waitthread
global/aispawn.scr::spawnset 100 axSquadRidge01
$axSquadRidge01
thread initSquadRidge01
When this thread executes, any spawner in the level in #set 100 will spawn, and its $targetname will be set to �axSquadRidge01.�� We then run initSquadRidge01 on each of these guys, allowing us to count them on spawn, watch for their deaths, set properties, etc.
If we�d like to issue a command to the entire squad now, we can do this easily.� For example, if we wanted to disable the squad�s enemy awareness, to prevent them from attacking before we were ready, we could now turn it off with the following structure:
$axSquadRidge01
thread aiEnableEnemy 0
...
//----------------------------------------------------------------------------
aiEnableEnemy
local.bOn:
//
//���� sets an actor�s enableEnemy property to the
received parameter
//----------------------------------------------------------------------------
������ if (isalive self) {
������������� dprintln "Thread
launched: aiEnableEnemy, with " local.bOn
������������� self.enableEnemy =local.bOn
������ }
������
end
The script above runs aiEnableEnemy on each member of our squad, passing 0 as its argument.
aiEnableEnemy accepts this argument, and each individual squad member as self, and if that actor is still alive, sets that actor�s enableEnemy property to the received value, in this case, 0.
You�ll want to use this structure consistently to pass orders to your actors, for a few reasons:
If you get into the habit of using encapsulated, reusable structures in your code, you�ll save yourself a lot of bug hunting later on.� At first, it might look simpler to write these routines procedurally, and you may be tempted to save yourself some time by writing something like this:
$jimmy.enableEnemy
=0
$joe.enableEnemy
=0
$fred.enableEnemy
=0
But if you add another actor, you�ll have to update your code manually to accommodate the change, and if Fred�s dead, you�ll throw an error.� Spend the time up front to architect your solution, and you�ll save considerable time later on.
It will occasionally be true that a structured, encapsulated thread is simply overkill for the method you�re looking to apply � either it�s a simple, low-risk method, with no real error checking required, or it�s something you don�t expect to use elsewhere.� In these instances, you can access the $targetname array manually through a for loop:
for (local.i
= 1; local.i <= $pfAxis02.size; local.i++) {
if (isalive
$pfAxis02[local.i]) {
������������� $pfAxis02[local.i] nodamage
������ }
}
The preceding iterates through $pfAxis02, and for each living member, applies the nodamage method.� One important quirk to note: in defiance of convention, MOH arrays begin at 1, not the usual 0.� Remember to begin your iterator at 1, and terminate when your iterator value exceeds, not meets, the array size.
Your own judgment and experience will tell you when you�re better off writing a thread and calling it on the squad, and when you�re better off just applying the method in a for loop.� In general, however, you�ll find your code marginally cleaner and easier to debug if you get into the habit of writing and reusing modular methods.
All this is well and good so far � we�ve learned how to get actors running around our levels, and learned how to issue orders at the squad level (we�ll look in the next section at the specific orders themselves), but what if we need to call out an individual actor from amid that squad, and give him a specific order?
We can�t target that actor directly using his $targetname, as that value is shared by his squadmates, but we can give him a distinguishing characteristic, and run a thread on his squad which looks for an actor with that characteristic.� Traditionally, we�ve used $actorname to fill this role, but in truth, any identifying property will do the job.� The tradition works, though, so it�s a good idea to stick to it to keep everything consistent and readable.
If we wanted to stage an ambush now, for example, but wanted individual actors to run to known positions before that ambush, we could give each of those actors an individual $actorname, then run the following thread on those actors� squad:
//----------------------------------------------------------------------------
gagAllyAmbush03:
//
//
//----------------------------------------------------------------------------
������
������ dprintln "Thread
launched: gagAllyAmbush03"
������
������ switch (self.actorname) {
������ case
alliedCaptain:
������������� self runto $ambush03pos01
������������� break
������ case
alliedCorpsman:
������������� self runto $ambush03pos02
������������� break
������ case
alliedInfantry:
������������� self runto $ambush03pos03
������������� break
������ default:
�������������������� //
unhandled or missing actorname
������ }
�������������
������ //-----------------------------------
������ self waittill movedone
������ //-----------------------------------
������ dprintln "Ally
at ambush position.� Starting ambush
behavior."
self ambush
$originAmbushTarget03������� //Activate ambush behavior
//
(find nearest cover, wait for cue to fire)
end
This thread will run for each member of the squad, look at that member�s $actorname property, and send him to an individual pathnode based on the value it finds there.
If we intend to communicate with a given actor frequently throughout our level, we can set a level variable to that actor, and reference it whenever we want.
if (self.actorname == "alliedSolo") {
������ level.alliedSolo00 = self
}
We can now use level.alliedSolo00 exactly as we would a direct $targetname reference, issuing orders, setting properties, or anything we�d like.� For example:
level.alliedSolo00
runto $wherever
Using level variables as pointers to your actors is in many ways preferable to repeated $targetname references strewn about your code, as if you change your object names in Radiant, you�ll only need to propagate the change to the variable assignment - everything else in your code will remain the same, and of course, as we�ve seen, when using objects with reserved $targetnames, such as aispawners, we haven�t really got a choice.
We�ve learned a fair bit now about spawning actors and issuing orders to them, collectively or individually.� Let�s start to examine the ways AI behave in the game, and the things we can do to affect this behavior.
First off, we have a couple of parameters held over from Allied Assault, which still substantially affect the way an AI behaves in combat.� You can set these in Radiant and in script.� Set them in Radiant either by entering the key and its value directly into the entity (N) window, or by hitting I to launch the AI Parameters window.� Most of these can be accessed in script by accessor functions, or as object properties using the dot syntax.� self sight 2500, for example, is equivalent to self.sight = 2500.
type_idle
The AI�s type_idle determines what that actor does when not engaged in combat.
In script, you�ll set this as follows:
self
type_idle "idle"
$joeBlow
type_idle "balcony_idle"
...etc.
type_attack
Type_attack determines the AI�s behavior once he alerts to an enemy.� It too can be set in script as follows:
self
type_attack "alarm"
local.ent
type_attack "turret"
$joeBlow
type_attack "cover"
...etc.
hearing and sight
These determine the ranges within which an actor responds to stimuli.� If an actor�s sight is set to 2048, he can see that far ahead of him in game units.� To deafen or blind an actor, set the appropriate value to 0.
Script examples:
self hearing
2500
local.dude.hearing
= local.spawner.hearing
self sight
2500
local.dude.sight
= local.spawner.sight
fov
An actor�s FOV is his field of view horizontally.� If an actor�s FOV is set to its default value of 90, for example, that actor can see 45 degrees to the right of his sightline, and 45 degrees to the left.� FOV is only applied horizontally.� If he�s on a cliff 200 feet above an enemy, but that enemy�s still in his horizontal FOV, he�ll alert (unless of course the cliff is blocking his visibility).� Setting an actor�s FOV to 360 gives him eyes in the back of his head.
Script examples:
local.spawner.fov
= local.dude.fov
sound_awareness
sound_awareness determines the percentage chance that an AI will respond to a sound within his hearing radius.� An actor with an awareness of 50 notices sounds half the time.� This value drops off to zero as a sound�s source approaches the edge of the actor�s hearing radius.
Script examples:
local.dude.sound_awareness
= local.spawner.sound_awareness
noticescale
noticescale scales the time an AI takes to see an enemy.� At 50, it takes half as long.� At 200, twice as long.
Script examples
self
noticescale 50
self.noticescale
= 200
local.dude.noticescale
= local.spawner.noticescale
leash and fixedleash
An actor�s leash essentially keeps him from straying all over the map.� He�ll remain within this radius of his current leash home, which, originally, is his spawn point, ignoring covernodes outside that radius.� The leash home is automatically reset to the actor�s current position when he engages an enemy, if his fixedleash property is set to its default of 0.� An actor�s fixedleash property can be set to 1 in Radiant, if you�d like him to remain bound to his home no matter what happens, or can be set in script as follows:
$joeBlow
fixedleash 1
$joeBlow.fixedleash
= 1
The actor�s leash, similarly, can be set and reset in script:
self.leash
=1024
self leash
1024
You can reset an actor�s leash home manually to his current position by issuing a resetleash command:
$joeBlow
resetleash
Resetleash will ignore the actor�s fixedleash value, but once the leash home has been set, fixedleash, if true, will still keep the actor from resetting his own home when he engages.
You can tie an actor�s leash home manually to a given object or location using the tether command:
self tether
local.targ
$joeBlow
tether $tank01
$joeBlow
tether $wpRidgeFightingPos
If you tether an actor to a moving object, his leash home will move with that object, regardless of his fixedleash value, and he�ll try to stay within his leash radius of that object.
Actors will ignore their leash values when responding to grenades, following move commands given in script, patrolling a path, or running to an alarm.
mindist and maxdist
mindist and maxdist collectively define the range within which an actor will engage an enemy and use covernodes.
If the enemy is further than his maxdist, the actor will attempt to close before engaging.� A small maxdist will cause the actor to charge his enemy.
If an enemy gets closer than the actor�s mindist, the actor will try to get some distance between himself and his target.
Always keep an actor�s maxdist at least 128 units greater than his mindist.� Covernodes outside this range will be ignored.
In general, you�ll want to give actors wielding short-range weapons such as SMG�s smaller mindist and maxdist values than their rifle-weilding counterparts.
Appropriate leash, mindist and maxdist values are essential to smart-looking AI.� Choose values that make sense with the weapons the actor�s carrying, and that allow the actor to find appropriate covernodes within their bounds.
Script examples:
self mindist
128
local.dude.mindist
= local.spawner.mindist
enemysharerange
enemysharerange determines the radius within which an actor receives information about a buddy�s engagement of an enemy.� If a fellow actor gets into a fight within this actor�s enemysharerange, this actor will become aware of his buddy�s enemy as well, and may choose to engage if circumstances warrant.� An enemysharerange of 0 causes the actor to use his default share range of 512 units.� If you don�t want an actor to join his friends� fights, set his enemysharerange to 1.
Script examples:
local.dude.enemysharerange
= local.spawner.enemysharerange
health
An actor�s health simply determines how much damage he can take before he goes down.� Default is 100.
Script examples:
local.soldier.health
= 1
self.health
=level.stdActorHealth
accuracy
An actor�s accuracy determines the percentage chance of a hit, discounting weapon spread and range effects.� An actor�s default accuracy of 20 allows that actor to hit 20% of the time, given an accurate weapon and target in range.
Script examples:
level.alliedSolo00.accuracy =60��� // make him a better shot
self accuracy
100
local.dude.accuracy
= local.spawner.accuracy
gren_awareness
An actor�s grenade awareness determines the likelihood that he�ll notice and respond to a nearby grenade.� The default gren_awareness of 10 gives an actor a 10% chance of spotting a grenade and trying to toss it back or otherwise respond.
Script examples:
self.driver.gren_awareness
= 0
local.dude.gren_awareness =
local.spawner.gren_awareness
interval
An actor�s interval value determines the minimum distance he wants to maintain between himself and his buddies.� It defaults to 128.� Raise it to spread them out further, reduce it to let them bunch up (which will look terrible, so do this sparingly).
Script examples:
self.interval =256
An actor�s mood determines the suite of animations he�ll play while idling, walking and running.� It has no effect on actual behavior, but puts a bit of story sense into the actors in the field.� Experiment with these to see what they do with different characters.
The current system provides for four possible moods.
Script examples:
self.mood =
nervous������� //
curious, alert, bored, nervous
self.mood =
local.pointman.mood
***Not yet implemented
Script examples:
// not yet implemented
An actor�s personality affects his selection of aggressive and defensive maneuvers, and impacts his response to morale-changing events.
Three personalities are provided:
Script examples:
self personality local.personality
$joeBlow personality 2
You�ll use a few methods frequently to control or tune your actors� responses to combat events, a few of which are outlined here.
ai_off and ai_on
ai_off and ai_on deactivate and activate an actor�s AI entirely.� An actor whose AI has been shut off won�t play any animation, and won�t respond to events.� Use this only for actors who aren�t currently visible to the player.
In script:
self ai_off
$joeBlow ai_on
enableEnemy
enableEnemy is one of the most frequently used methods.� An AI whose enableEnemy has been set to 0 won�t alert to enemies in his sphere of perception.� You�ll use this all the time, for example, to ensure that an actor running to a patrol point doesn�t start shooting before he gets there, or that someone hiding behind a tree doesn�t jump out until you want him to.
In script:
level.alliedSolo00.enableEnemy
= 1 // allow him
to react to enemies
self.enableEnemy = 0������������� //
prevent reaction to enemies
bulletaware
Actors in MOHPA react when shot at, trying to dive out of the way, get to cover, etc.� If you want to suspend this behavior (to run your own scripted reaction, for example), shut off the actor�s bulletaware bit.
In script:
level.alliedSolo00.bulletaware = 0
holdfire
An actor whose holdfire bit has been set will alert normally, aim at an enemy, attempt to take cover if available, etc., but will not fire until fired upon (i.e., until his bulletaware reaction triggers), or until his holdfire bit is shut off (as we�ll do when triggering an ambush, for example.� If you want him to hold his fire even when fired upon, set his holdfire bit to 1 and his bulletaware to 0.
In script:
level.alliedSolo00.holdfire = 0
forceattack
If you want to force an actor to attack the player or another actor specifically, use the forceattack method to do this.� It accepts the $targetname of another object, or a level variable pointing to that object, as its argument.
In script:
level.alliedSolo00 forceattack
level.axisSolo00
$joeBlow forceattack $player
threatbias
Actors run a pretty detailed heuristic to determine who they�ll engage when presented with multiple targets.� Check MOH_AI_tips.html, from the Spearhead SDK for more information about how this works.
If you need to tip the balance one way or another, use threatbias to do this.
A greater threatbias value applied to an actor makes that actor a more attractive target to enemies.� A lower threatbias makes him less attractive.� If you want an actor to be treated normally, set his threatbias to his default value of 0.
�ignoreme� is a constant which can be used in place of the numeric argument to specify that you don�t want the actor to be attacked at all.
In script:
$player threatbias ignoreme // hide player from
ai.
$joeBlow threatbias 10����������� //
everybody shoots at joe
level.ordinaryguy threatbias 0��� // ordinary target
aimat
aimat does what it sounds like: instructs an actor to aim his weapon at a specified target.
In script:
level.alliedSolo00 aimat
level.axisSolo00
shootat
Just as aimat instructs an actor to aim at a target, shootat instructs the actor to open fire on that target.� You can use this in conjunction with enableEnemy 0 to force an actor to attack only those targets you specify.
In script:
level.alliedSolo00 shootat
level.axisSolo00
MOHPA adds several elements to the AI properties provided by earlier versions of the engine.� Among these are Combat Modes and Orders.
Actors in MOHPA may be placed in one of three combat modes, which determine the actor�s preferences when looking for cover and engaging an enemy.� Each of these relies on the presence of aggressive covernodes (covernodes with their aggressive property set to 1) to function.
Here�s an example of the above modes applied in a reusable, squad-friendly thread:
//----------------------------------------------------------------------------
aiSwitchCombatMode
local.cMode:
//
//
//----------------------------------------------------------------------------
������
������ if (isalive self) {
������������� dprintln "Thread
launched: aiSwitchCombatMode, with param: "
dprintln
local.cMode ", for " self.actorname
������������� switch
(local.cMode) {
������������� case
combat_advance:������ //Puts AI into advancing combat mode.
�������������������� self combat_advance
�������������������� break
������������� case
combat_fallback:������������ //Puts AI into fallback combat mode.
�������������������� self combat_fallback
�������������������� break
������������� case
combat_holdsteady:��� //Tells AI to holdsteady.
//(Cancels
combat_fallback/combat_advance)
�������������������� self combat_holdsteady
�������������������� break
������������� default:
�������������������� dprintln "ERROR: Unhandled parameter passed to �
dprintln �aiSwitchCombatMode: " local.cMode
�������������������� break
������������� }
������ }
������
end
It accepts a string as its argument, and can be run on a targetname array:
����������� $axSquadRidge01 thread
aiSwitchCombatMode combat_advance
(The dprintln�s in the thread above, by the way, were split solely to fit the text on the page.� In your executable version, you�d probably want to recombine them.)
You can always issue any of these commands directly if it makes sense to do so.
$joeBlow
combat_advance
The above, for example, will put a single actor named $joeBlow into combat_advance mode, but won�t work for a group.� If you did want to apply the command to a group without writing a thread to handle it, a simple for loop, as we�ve seen, will take care of it.
Orders are issued to actors by exactly the same mechanisms as combat modes, so we�ll just jump straight into the available orders and what they do.
An actor can be instructed explicitly to stalk the last seen position of an enemy.� As long as he�s previously seen an enemy whom he considered to be his target, and no longer sees that enemy now, the stalklastseenposition command will instruct him to go to the last place he saw that enemy to try to seek him out.
If you want him to stalk a specific enemy, you can pass that actor in as an argument to the command.� Otherwise, he�ll go after whomever he targeted last.
In script:
level.axisSolo00
stalklastseenposition level.alliedSolo00���� // Come after him
$joeBlow
stalklastseenposition
An ambush order instructs an actor to run to a covernode, wait for a triggering event, and initiate an attack when the activation message is received.
It accepts two optional arguments:
Here�s an example of an ambush initialization that sends the squad leader to a specified location, but lets everyone else go wherever they can find cover.� Notice as well that the squad leader�s mindist has been reduced to lessen the likelihood that he�ll break cover to get away from the enemy once engaged.� We want this one up close and personal.
switch (self.actorname) {
������ case
axisAmbushLeader02:
������������� self.mindist = 64
������������� self ambush $originAmbushTarget02
$nodeAmbushLeaderCover02
������������� break
������ default:
������������� self ambush $originAmbushTarget02
������������� break
������ }
Once set, the ambush can be triggered in two ways:
If a trigger targets these ambushers directly (i.e., their $targetnames are in its $target property), the resulting triggering event will activate the ambush.� The catch here is, of course, that our ambushers probably have reserved $targetnames of �aispawner,� and so does everyone else in the level, so we won�t be able to use the CTRL-K method of connecting the trigger to its targets.� Instead, we�ll want to rename the spawned actors on spawn (which we should be doing anyway), and in Radiant, we�ll put their final $targetname in the trigger�s target property.� We won�t be able to see the connection in Radiant, but once we�ve spawned and named our actors, it�ll work.� If you change your mind about a squad�s destination $targetname, make sure you remember to change it in whatever triggers might be targeting that squad � you won�t have a visual aid to remind you that these connections are there, so you�ll just have to keep your work organized and remember it for yourself.
If you�d rather handle this in script, where you can more easily see what you�re doing, there�s an easy way to do this.
The ambush method sets the ambushing actors� holdfire bits to 1 when the ambush command is given.� Simply flipping this bit to 0 will trigger the ambush if an enemy is within range of the actor.� This is ultimately a more flexible triggering method, as anything can precipitate it � you could launch it from a trigger, run a proximity check, or wait for a phase of the moon - whatever.� Handling this in script also affords you the flexibility to manage the combat more closely, perhaps specifying a target for those opening fire.� If, for example, you�d like them all to train their fire on the player�s escort first, to give him or her time to react, the following:
$pfAxis02
thread aiOpenFire $pfAlly02[1]
will do the trick, provided that $pfAlly02 has at least one member, if we�ve written a thread to handle it:
//----------------------------------------------------------------------------
aiOpenFire
local.forceEnemy:
//
//
//----------------------------------------------------------------------------
������
������ if (isalive self) {
������������� dprintln "Thread
launched: aiOpenFire: " self.actorname
������������� self.holdfire =0
�������������
������������� if
( local.forceEnemy!=NIL )
{
�������������������� self forceattack
local.forceEnemy
������������� }
������ }
������
end
The preceding thread, as we�ve seen before, checks to ensure that its target actor is alive, then flips off that actor�s holdfire bit.� It accepts an optional argument (local.forceEnemy) as well, and if a targetname is provided there, it forces the actor to open up on that named enemy.
Not yet fully implemented.
global/skirmishline.scr
//
//���� usage:
//����������� thread
global/skirmishline.scr::MoveToSkirmishLine LeftPos RightPos NumMembers Members
WalkFlag DepartureThread ArrivalThread MoveFinishedThread
//
//������������������ LeftPos������������� - leftmost position of line
//������������������ RightPos����� - rightmost position of line
//������������������ NumMembers��� - number of members in formation
//������������������ Members������������� - array of members in formation
//������������������ WalkFlag����� - 1 = walk, 0 = run
//������������������ DepartureThread - thread to
be called before moving each squad members - currently called with waitthread
//������������������ ArrivalThread - thread to be
called for each member upon arrival in line - currently called with waitthread
//������������������ MoveFinishedThread - thread
that blocks until the move is finished - called with waitthread - can be custom
or
//�������������������������������������� - one of
the move finished threads in this file
//�������������������������������������� - called
with NumMembers and Members as parameters
//
//���� NOTES:
//������������������ -����� DepartureThread, ArrivalThread, and MoveFinishedThread are
optional and can be NIL
After all, what�s a MOH game without guys jumping out of the furniture?
Here�s how you set it up:
Place one of the three possible animat/furniture_hidden-cabinets, and give it $targetname �cabinet�, and a #set value of whatever you want.
Place an actor nearby, and target the cabinet to that actor.
Create a trigger_multiple, give it $targetname �cabinettrigger�, and the same #set value you used for the cabinet.
That�s it.� Not particularly useful in a jungle, but could be reworked for use with spiderholes.
MOHPA provides for a few special classes of soldier: the Captain, and the Medic, each of whom has his own special abilities and behaviors.
Captains are defined and managed in script by global/captain.scr.� You�ll need to scope your calls explicitly to that script file, but that minor inconvenience is offset by your ability to read the script and see exactly what it does.
A Captain is paired with a squad, and must have the same $targetname as his squadmates.� If you�ve spawned a squad and renamed it during the spawn, you�re already good to go in this regard.
CaptainInit
Now, to identify the Captain, you�ll run CaptainInit on that actor, which places him in a morale group with his squad, and binds those sharing the provided $targetname into a logical group.
level.alCap03
waitthread global/captain.scr::CaptainInit 1 "pfAlly03"
The preceding runs on an actor previously placed in a level variable (you�ll absolutely want to do this for your Captains � you�ll be talking to them frequently), initializes him as a member of morale group 1, and binds all others sharing $targetname �pfAlly03� (which should be his $targetname as well) into a squad.
By default, CaptainInit will set the Captain as the squad�s pointman, which works if you�re defining a Japanese squad.
CaptainSetPointMan
Allied captains, on the other hand, didn�t take point � for an Allied squad, you�ll want to nominate some other poor schlep to screen for bullets and landmines.� You�ll do this by running the CaptainSetPointMan thread on the Captain, with the designated pointman as its argument.
������ level.alCap03 waitthread
global/captain.scr::CaptainSetPointMan level.alPointMn03
CaptainMoveOut
When it�s time to get the squad moving, you�ll use the CaptainMoveOut thread to When the squad is given a movement order, that order will in fact be given to the pointman, and everyone else will follow that actor.
������ level.alliedCaptain03 waitthread
global/captain.scr::CaptainMoveOut
CaptainSquadHalt
To stop the squad, issue the following:
������ level.alliedCaptain03 waitthread
global/captain.scr::CaptainSquadHalt
The captain will execute a halt hand signal, and the squad will stop.
canhealpatient
Returns TRUE if can heal our current patient.
heal_amount( Float heal_amount )
Amount to heal the guy.
heal_patient
Heals patient.
heal_radius( Float heal_radius )
Radius to look for friendlies to heal.
heal_threshold( Float heal_threshold )
pct of health to heal a guy at.
medic_disable
Disable medic healing.
medic_enable
Enables medic healing.
medic_heal( Entity injuredGuy )
Pass who to heal.
medic_searchtime( Float searchtime )
How often to search for injured.
patient
Returns current patient.
Not yet fully implemented
morale( Integer morale, Integer bReact )
Sets actor's morale level (0-19). If bReact==1, chance ai might react to it.
morale( Integer morale )
Gets actor's morale level (0-19).
morale( Integer morale, Integer bReact )
Sets actor's morale level (0-19). If bReact==1, chance ai might react to it.
morale_basemin( Integer base )
Sets actor's min base morale level (0-19).
morale_basemin( Integer base )
Sets actor's min base morale level (0-19).
morale_basemin( Integer base )
Gets actor's min base morale level (0-19).
morale_baserate( Integer baserate )
Gets actor's base morale level (0-19).
morale_baserate( Integer baserate )
Sets actor's rate at which the base morale is update towards.
morale_baserate( Integer baserate )
Sets actor's rate at which the base morale is update towards.
morale_off
Tells actor to turn off morale behavior checking.
morale_on
Tells actor to turn on morale behavior checking.
self
morale_off
Now that we�ve triggered our spawners and gotten actors appearing in our level, let�s get them using the terrain.
Actors in MOH actually know very little about the terrain they�re walking on.� They get around their environment by moving from info_waypoint to info_waypoint.� This has been well-documented in other tutorials.� Place your waypoints where you want your AI�s to be able to walk.� Don�t put them where you don�t want them to go.
You can order an AI to a specific waypoint by giving that waypoint a $targetname, and using one of several movement commands to order him to get there.� Remember that if he alerts to an enemy or reacts to a bullet, he�ll suspend his movement behavior and deal with that situation before continuing on.� If you want him to ignore those distractions and make sure he gets there, set his enemyEnabled to 0 while he�s in transit.
Three major movement commands handle most situations, and their names are more or less self-explanatory.
In script:
self runto
$nodeAlliedSolo00Peek
$joeBlow
walkto $wherever
self crawlto
self.target
You�ll frequently want to abort ordered moves before the moving actor has reached his destination � if he intercepts an enemy during his move, for example, or if another event precludes continued movement.� Since a new movement order supersedes an earlier order, all you have to do to stop the actor is give him a NULL patrolpath.
level.axisSolo00
patrolpath NULL� //
dump any remaining patrol instructions
An actor�s default behavior when given a movement instruction is to complete that instruction, even if other events, such as an enemy engagement, intervene.� The following thread, run on an actor, clears the actor�s movement order once he engages an enemy.
//----------------------------------------------------------------------------
StopWalkOnEnemy:
//----------------------------------------------------------------------------
������ self waittill hasenemy
������ self patrolpath NULL // clear movement so
AI doesn�t resume after fight
end
Generally, once you�ve sent an actor to a destination, you�ll want to know when he�s gotten there.� Most commonly, you�ll do this by waiting until his movedone event registers, which will occur when he gets within a certain radius of his destination pathnode.� That radius is his movedoneradius, which can be reset in script.
For example, the following:
self runto
$nodeAlliedSolo00Peek
//-----------------------------------
self waittill
movedone
//-----------------------------------
dprintln "Ally at dest01."
�instructs an actor to run to a named pathnode, and then outputs a debug line when he gets there.
If we needed to ensure that he got within a specific range of the pathnode before he considered himself to have gotten there, we could adjust his movedoneradius.
$bunker2reinforcement.movedoneradius
= 40������ // he
really needs to get there
If an actor�s getting too finicky about reaching pathnodes, and getting hung up without registering a movedone, you may want to loosen up his movedoneradius.
By default, this range is 96 units.
Testing range
manually
In certain instances, you�ll want to test range to an object manually - if, for example, you�re testing proximity to something other than a waypoint.� Movedone will handle nearly all cases, but if it doesn�t, you can use the vector_length function to find distance between objects manually, and simply wait for that distance to drop below a threshold.� A standard while loop containing a waitframe will do the trick:
$b2reinforcement
runto $bunker2mg4node
while (vector_length($b2reinforcement.origin - $bunker2mg4node.origin) > 50) {
������ waitframe
}
$bunker2mg4
thread global/mg42init.scr::AttachGuyToMG42 $b2reinforcement NIL 1
The waitframe function does exactly what it sounds like it does � it waits for a single game update to occur.� If you forget to put this inside your while loop, it�ll attempt to iterate infinitely within a single frame, and you�ll discover the joy of a stack overrun.� So don�t forget it.
Aggressive Covernodes
Covernodes have been well-covered in other tutorials, but you�ll need to do two additional things to get a covernode working with the aggressive cover thinker:
Everything else about covernodes works as it has in the past.