Enemies and AI
With this tutorial you are going to learn how to script the AI for enemies
and other NPC's. That includes messages to change the behavior and the perceiving properties of the enemies, and the messages to make them
move
around in the level. We are going to learn how to calculate AI network to
the levels, and we are going to make the enemies switch weapons. Also deathcams
and custom animations are going to be covered. You can download the
example level from here: AI_scripting.zip
The Statemachine States
One of the basic blocks of the AI scripting in Max Payne is so called statemachine
states. They are different states for enemies that define their behavior. The usable
statemachine states that define the combat behavior are:
MercCombat - General combat statemachine. The enemy will follow player and
perform dodges spontaneously during combat.
MercCombatDefensive - Same as above, but the enemy won't follow the
player very far.
MobsterCombat - The same as MercCombat but the enemy won't perform
dodges.
MobsterCombatDefensive - The same as MercCombatDefensive, but the
enemy won't perform dodges.
MeleeCombat - Combat statemachine for enemies who are using a melee weapon.
CrouchAndShoot - Crouches and shoots. If it looses player from
sight, it will start following.
CrouchAndShootStatic - Crouches and shoots. Won't follow player.
StandAndShoot - Stands and shoots. Follows the player if the sight
is lost.
StandAndShootStatic - Stands and shoots. Won't follow the player.
WalkAndShoot - Walks towards the player while shooting. Will start
running if loses the sight to the player.
CrouchWaitAndShoot - Was used for statemachine
testing purposes. Redundant.
CrouchWaitAndShootStatic - Was used for
statemachine testing purposes. Redundant.
RunWalkAndShoot - Runs towards the player and
once close enough, starts walking and shooting. Redundant.
WalkStopAndShoot - Same as WalkAndShoot but
won't shoot while walking. Redundant.
You can get pretty far with the statemachines above, but every now and then
you want to do some more specific moves, and they are handled with
statemachines as well:
SingleDodgeLeft, -Right, -Forward, -Backward - Performs a dodge and
returns to the previous statemachine in the stack.
SingleShootDodgeLeft, -Right, -Forward, -Backward - Performs a
shootdodge and returns to the previous statemachine in the stack.
SingleShootAccurately - Shoots once (relatively) accurately, and
then returns to the previous statemachine. Quite redundant to be honest, maybe useful
in cinematics.
With the statemachines above you can already do some simple combat. Load up
the level as it was left after the FSM & DO tutorial, or just download the
example level of that tutorial.
Then let's place an enemy into the level. As you might remember, you can
place entities to the grid by pressing N in F3 mode. Select Enemy
from the drop-down dialog. The properties of the newly created enemy will pop
up. Rename it to e1. You can now also change the type of the enemy from the
Entity-tab if you want to. You can also rotate the enemy into the
desired orientation by selecting the enemy, pressing Y in F5 mode and
moving the mouse. But don't forget that you have to be in pivot rotation mode
(A to toggle) in order to rotate any entity.
Now as you've got an enemy in the level, it doesn't do anything by default.
It's just standing there in idle statemachine state (more about idle
states in a moment). You can open the FSM dialog of the enemy (4 in F5
mode) and change its statemachine state at its Startup into any of the states
above with a message called C_SetStateMachine.

Now you can try and export the level and see how it works, although in order
for the enemy to be able to follow the player through rooms and around
obstacles correctly, we are going to have to calculate the ai-network
for the level. You already know how to add your levels to the levels.txt
correctly, so add a new block for this level of ours, but make sure it will
have the flag EnableAI set as true. This will tell the engine to look
for AI network for this level.
The actual calculation of the network happens in the game. Once you've
exported the level .LDB, and you have the levels.txt all sorted out, launch
the game and type the following to the console:
MaxPayne_GameMode->GM_InitAndCalcAI( my_AI_level );
X_ModeSwitch->S_ModeSwitch(Game);
You will need to do this only once if you don't touch the geometry of your
level, as the game will save the AI net to the same directory as where your
.LDB file is, with .AI-file extension.
Once the level has loaded up and your PC has done all the crunching, you
can view the AI network by pressing F7. Or view the AI nodes by
pressing F8 (Of course, only in developer mode with developer keys
enabled).
You can try to combat against the enemy now. Notice how it can navigate
from one room to another without problems.
Perceiving properties
Note how the enemy can hear you if you start shooting even if it
can't see you. We can change the perceiving properties of the enemies by using
the following statemachine states:
Idle - This is the default statemachine. The enemy can hear and see
the player.
IdleUntilEnemySeen - In this statemachine the enemy can't hear the
player at all. Very useful for setting up ambushes.
Nonreactive - In this statemachine the enemy won't perceive the
player at all.
To get an enemy into any of these statemachines, use once again the C_SetStateMachine
-message. And when the enemy perceives the player, it actually means it will
trigger its OnActivate event, thus you can add whatever messages to the
OnActivate -message tab of the enemy to make various things happen. Most
commonly to make the enemy to react to the player somehow (Switch into a
combat statemachine). In all the combat
statemachines that were listed before, the enemies have the same hearing and
seeing properties as in Idle statemachine, thus they will also trigger the
OnActivate event once the player is perceived.
Notice though that the enemy
won't re-trigger its OnActivate event when perceiving player multiple
times for as long as it
has not forgotten the
player (30 seconds from the last sight), or as long as its statemachine hasn't
been re-switched
into Idle, Idleuntilenemyseen or Nonreactive.
Some examples, let's say you want the enemy not to be able to hear
player. You do this by adding the following to the Startup tab:
This->C_SetStateMachine( idleuntilenemyseen);
And to the OnActivate tab:
This->C_SetStateMachine( mobstercombat );
You can also stack the statemachines, so if you want an enemy to do
a shootdodge backwards when it perceives you, and then start chasing you, you
can do it by sending these messages at OnActivate:
this->C_SetStateMachine(mobstercombat);
this->C_SetStateMachine(singleshootdodgebackward);
What this does is that it first sets the statemachine into MobsterCombat,
and immediately after that into SingleShootDodgeBackward. The statemachines
are stacked, so now the enemy has these two statesmachines stacked on top of
the IdleUntilEnemySeen. The statemachines are stacked in the order of
execution, so the enemy will in practice go into SingleShootDodgeBackward
statemachine first (which ends up on top of the stack), and once it's been
executed, it will revert to the previous statemachine in the stack, which is
MobsterCombat. Play around with it. You can stack multiple dodges and
shootdodges in there if you wish.
Here's still some more statemachines you should find useful:
StandAndIdle - Same as idle but faces the player at all times, and
won't cause the re-trigger of OnActivate event.
Crouch - Same as idle, but crouching, and won't cause the re-trigger
of OnActivate event.
Delay500ms - A statemachine that forces the enemy into
"idle" state for half a second and then reverts to the previous
statemachine in the stack.
Delay1s - Same as above but lasts one second.
Delay2s - Same as above but lasts two seconds.
More advanced actions
Let's next try and add a little animation and some sound for the enemy once
it spots you. To the StartUp tab, put these messages:
This->C_SetStateMachine( idleuntilenemyseen );
This->C_RemoveAllWeapons( Deserteagle );
This->C_PickupWeapon( Pumpshotgun );
This->C_PickupAmmo( Pumpshotgun, 3);
To the OnActivate tab replace the existing messages with the following:
This->C_SetStateMachine( mobstercombat );
This->C_SetStateMachine( delay1s );
This->C_SetIdle(1, false);
This->A_Play3DSound(enemy, mobster_alert,head);
And to its OnDeath tab:
This->A_StopAll3DSounds(Head);
This->CAM_AnimateParented( Death_03 );
Now you can export the level and try it out. The enemy will initially have
a pumpshotgun at hand, and when it sees the player, it will shout an alert, mess with
its weapon for a moment,
and then start to shoot at you. Once it has shot you three times with a
pumpshotgun, it will switch to deserteagle.
Here's some explanations about the new messages in Startup tab:
C_RemoveAllWeapons( deserteagle ); - Removes all the weapons from
the character, but gives it the weapon defined in the parameters, plus
infinite ammo for that weapon. With this message, the weapon will just suddenly appear into the hand of
the enemy, so it's mostly useful just for the startup of the level. The valid
parameters are baseballbat, beretta, berettadual, deserteagle,
grenade, ingram, ingramdual, jackhammer, m79,
molotov, mp5, pumpshotgun, sawedshotgun, sniper,
leadpipe and empty. See data\database\weapons\.
C_PickupWeapon( Pumpshotgun ); - Gives the enemy a pumpshotgun in
addition to the desert eagle. With this message, the enemy will take the
weapon out of his pocket with an animation, so it can be sent to an enemy in
the middle of a combat situation if that is desired. Or you can for example
have an enemy with empty weapon at startup, and send this message to
the enemy as it spots the player. The enemy doesn't get any
ammo with this message, and it won't use the weapon it has if it doesn't have
any ammo. The valid parameters are the same as above.
C_PickupAmmo( Pumpshotgun, 3 ); - Gives the enemy three shells for
the pumpshotgun. Once again the valid weapon names are the same as
above.
Whenever an enemy has multiple weapons, it will always use the most
powerful ones first, and if it depletes all the ammo, switch to the second
best weapon, etc. The priority list can be seen from data\database\weaponid.h
What happens in the OnActivate event is that the very first message sets the statemachine into MobsterCombat, and the
second message immediately stacks another statemachine (Delay1s) up on top
of it, thus causing the enemy to idle for 1 additional second after
perceiving the player. This additional second is in there just so that we can
animate the enemy at idle instead of making it start the combat immediately.
With C_SetIdle we change the idle animation to be "1"
instead of the default "0". The idle animation "1" is a
reaction animation to the player for all common mobsters. The
"false" flag in there means that the animation won't be looping. If
we set it to looping, the enemy would restart doing this animation every time
it went idle. As it's false, the enemy will execute the animation only once,
and after that it's idle animation will be whatever it previously was.
There's basically 5 regular slots for idle animations per character, plus 5
more slots to be used if the character is wounded. You can make use for all 10
slots in MaxED by using C_SetIdle and C_SetWoundedAnimations
messages. The default animations are:
Slot 0 - The default standing animation
Slot 1 - Reaction to Max
Slot 2 - Aim (handy for ambushes)
Slot 3 - Get up from sitting
Slot 4 - Using something
To change the animation between any of these you do it with C_Setdle,
as you noticed. In the parameters you define the actual slot, and whether you
want the animation to loop or not. Note that some animations are set to not
loop within themselves.
In order to use the additional 5 slots, you set the slot itself with C_SetIdle,
and then force the enemy into wounded state with C_SetWoundedAnimations(
true );. Don't forget to disable the wounded animations when you want the
enemy to move though.
Slot 0 wounded - The wounded standing animation
Slot 1 wounded - Custom action, varies between skins. Usually
something enemies do while idling bored
Slot 2 wounded - Aiming while crouched
Slot 3 wounded - Sitting
Slot 4 wounded - Talking
All the custom idle animations are defined in the skeleton and skin
scripts.
A_Play3DSound obviously triggers an alert sound from the enemy's
head. Since we are playing the sound through a character, we need to define
the bone. The most obvious bone in this case to be used is head.
As you know, you can see all the sounds you have at your disposal from the
sound script files, but here's a brief list of the most useful enemy sounds:
junkie_alert - Junkie shouting
junkie_noise - Junkie making noise
junkie_talk - Junkie having a conversation with himself
killersuit_alert - Killersuit spotting Max
whistle - Enemy whistling
whistle_quiet - Same, but bit more quiet
mobster_alert - A regular mobster spotting Max
mobster_group_alert - A mobster group spotting Max
mobster_alert_known - A mobster who knows Max spotting him
mobster_group_alert_known - A mobster group who knows Max spotting
him
mobster_noise - Mobsters making noise, used to warn the player
mobster_group_noise - A mobster group making noise
merc_alert - A merc getting alerted
merc_group_alert - A merc group getting alerted
merc_noise - Merc making noise
merc_report - Essentially the same as above
police_alert - Police getting alerted
police_noise - Police making noise
The messages in the OnDeath tab:
A_StopAll3DSounds - Stops the enemy playing any sounds, basically
ensures that
the enemy won't shout its alert in case player kills him immediately with a
headshot.
This->CAM_AnimateParented( Death_03 ); - Runs a deathcam for the
enemy. Basically you can use Death_01, Death_02 or Death_03
here The camera paths are defined in
data\database\camerapaths\camerapaths.txt.
Movement messages
There are four messages you can use to command the enemies move around the
levels:
C_GoTo - Moves to a waypoint.
C_GoToAndShoot - Moves to a waypoint while shooting at the
targetcharacter (usually the player).
C_GoToPlayer - Moves towards the player.
C_Patrol - Patrols between a maximum of three waypoints.
Let's try and make the enemy in our test level to run to a waypoint when
the glass window in there breaks. First add a waypoint entity somewhere
into the level. There doesn't need to be a direct line between the enemy and
the waypoint, for as long as there is a valid AI network in the level, the
enemy can find its way to the waypoint no matter where it is (As long as
it is also possible for the enemy to move to that location). Name the waypoint as w1.
To the DO_BulletCollides tab of the breaking glass, add:
::room2::e1->C_GoToAndShoot(::startroom::w1, 1);
What this does is that it commands the enemy to move to the defined
wayppoint (while shooting) with a speed of 1. That is the maximum speed
and means running, the other option is walking, which would be 0.5. Pay
attention to the object hierarchies in this message though, as they obviously
might not be the same in your level.
Once it reaches the waypoint it will basically start behaving according to
its statemachine. In this case, since the enemy would be in Mobstercombat, it
will start shooting and chasing the player. If it was for example in
CrouchAndShootStatic, it would start crouching and shooting, and wouldn't
chase the player. Notice that enemies do obey the movement messages even if their
statemachine states that they should be static.
Now try out changing the C_GoToAndShoot message into:
::room2::e1->C_GoTo(::startroom::w1, 1);
The parameters are the same. One major difference with C_GoTo over
C_GoToAndShoot is that if the enemy perceives the player while moving, it will
immediately start acting according to its statemachine. For example, if in
combat statemachine, it
will stop moving, forget about the waypoint and start shooting.
So if you now shoot the glass before the enemy has
noticed you, it will start moving to the waypoint until it sees you. If it
won't see you, it will reach the waypoint and stand there, adopting the
orientation from the waypoint. If you howerever shoot the glass while the
enemy is already shooting at you, it won't basically do seemingly nothing (The
enemy starts to move, but at the same time perceives you immediately,
and thus won't actually move anywhere. Unless of course you are hiding...) And
basically if you want to make an enemy to run to a waypoint even if the player
is around, all you have to do is to "blindfold" it by using Nonreactive
statemachine.
Next try changing the movement message into:
::room2::e1->C_GoToPlayer(1);
The parameter indicates the movement speed. This makes the enemy,
obviously, run to the player, and once it spots the player, act according to
the statemachine it is in. Basically all the NPC helpers there was in Max
Payne applied this movement message, and it is also useful in some combat
scripts.
Here's an example of C_Patrol message:
::room2::e1->C_Patrol( ::room2::w1, ::room2::w2, ::room2::w3, 0.5 );
So you define three waypoints and the movement speed. Two of the defined
waypoints can be the same, if you want to patrol between two waypoints. Also
in C_Patrol the enemy will start acting according to the statemachine if it
perceives the player. You can try and make the enemy to patrol at its startup
between some waypoints.
AI net manipulation
Sometimes you might have to manipulate the AI network a bit, for example if
you don't want to have an AI network somewhere on the floor, or if you want to
create an AI network on the air, or if the AI network is not dense enough.
First of all you can control the density of the AI net for any given room
from the properties of the room. Select a room in F5 mode, press enter,
and select the Statistics tab.

The number you can see there on the AI net density slider is meters
per AI node. You can try changing the value, and then re-calculating the
AI network and view the results with F7 and F8 in the game.
Another way of manipulation is something we already applied
when we did the door in FSM & DO tutorial; AI Blocks. AI blocks are
objects that are using a material from the AI_node_collision_nodraw category.
What they do is, that the engine takes them into account as geometry when it
calculates the AI network, even though they aren't drawn or collided into when
playing the game. As you might remember, we created AI blocks by the
door so that AI net wouldn't be created to the locations where the door would
be once opened. Basically in the AI net calculation process, the AI blocks are
considered as just another walls, and obviously no AI net will be passing
through them.

You can also build platforms in the air so that AI net will be
created into mid-air, if that is desired. Try for example building a wedge and
you'll see how it affects the AI net.

Notice how the AI net visualization shows lines pointing
downwards from the upper parts of the wedge. They are indicating that an enemy
can jump down from those locations. If the wedge would be higher, the AI net
wouldn't allow the enemies to jump down from too high. Building AI net into
air is useful sometimes because the AI net calculation doesn't take any
dynamic objects into account at all. You can, for example, build an
AI net inside elevators with AI blocks.
When you experiment with this, don't forget that the game
strips out AI nets when there is no way for any enemy to reach that place.
For example no AI net is created on top of shelves, unless there is an enemy
or a waypoint there (Where enemy could be teleported). Likewise, if there are
no enemies or waypoints in your level, no AI net will be created into the
level at all.
Team combat
Making a combat with several enemies involved might sometimes
be a bit tricky if you don't build the structure of the FSM-script properly.
Next we are going to suggest a way of doing team combat properly, and explain
how to do death cinematic for the last dying enemy of a bunch.
The whole idea here is that when any of the enemies spots the
player, all the others should notice this as well. It is very easy to go wrong
here by making all the enemies to send messages to all the other enemies. As
was already explained in the FSM & DO tutorial, it is always better to
gather all the messages into one location that will then send the required messages
to every entity involved from there. So once again:

Imagine the joy of debugging an FSM structure as shown on the
right. But as the left-hand picture is trying to illustrate, we are going to use a
sort of a FSM relay with a custom string which sends all the messages to the
involved enemies when its custom event is triggered. And obviously it is
triggered as any of the enemies perceives the player.
Before adding any actual enemies, let's first copy/paste one
more room for our level, and add a door between. Rename the room as room3.
Then let's put some
decoration into the room for our enemies to take cover. I'm simply using
crates for this tutorial, although I assume you will come up with something
more interesting in your real levels.

The next time you export the level, don't forget to recalculate the AI net since the geometry of
the level has changed. Usually the game lets you know if the AI net is not
valid, but in some cases it can just crash. Also it can't detect it if the
furniture inside the room has changed, just if the room itself or the room
count has changed.
Then first create one dummy object (object with dummy
texture) into the new room, and turn it dynamic. This will function as the
relay FSM illustrated in the FSM structure pictures before, and also as a
timer for our combat script. We are using a DO instead of a Floating FSM Name
it as, say, script, set its Cont. upd. flag on, and add two
states to it; enabled (default), and disabled. Also add custom
strings called start and initialize.
Then add one enemy into the room. Rename it as e1, and
for its Startup tab add:
this->C_SetStateMachine(nonreactive);
this->C_RemoveAllWeapons(berettadual);
this->C_PickupWeapon(pumpshotgun);
this->C_PickupAmmo(pumpshotgun, 3);
And to its OnActivate tab:
::room3::script->FSM_Send(start);
Now we have the first enemy in, which we can use as a sort of
a template for the others. It is nonreactive from the level start, it's got a
dual beretta and pumpshotgun with it. It's gonna trigger a custom event called
start for the script DO when it perceives the player.
Next. copy/paste the enemy into three (in F5 mode), and place
them around as you wish. You can change the enemy types from their properties
to get some variation. Also you should vary their weaponry somewhat. And let's
also make two of the enemies to run for cover. For that, we are going to have to add
some waypoints into the room. Where should you add them depends wholly on
where do you want the enemies to run and how do you want them to behave after
that. For this example, we'll make e1 to just follow the player, e2
will run for cover and tries to stay there, moving back and forth between two
waypoints, and e3 will run for cover
for a while and run out as soon as e1 is killed. So let's set up
waypoints like this:

Obviously, e1 won't need any waypoints. There's two
waypoints for e2 so that we can make it run between them from time to time;
totally static targets are just too easy to shoot. And one waypoint for e3
since it won't be spending much time in its cover anyway.
But it is now, the enemies would be just standing around with
their weapons, without being able to even perceive the player. They
are standing as nonreactive initially because we don't want them to hear the
player too soon, from behind the door. But we do want them to become aware once
the player opens the door. So, let's do that next. First, add a message to the
door opening animation (pay attention to which one of the opening animations,
if the door opens both ways):
::room3::script->fsm_Send( initialize );
So it's going to trigger this custom event for the script
DO as it is opened. If your door is such a door that it closes automatically,
do not forget to add logics there that will send the message only for the first
time the door is opened.
Next open up the FSM dialog of the script DO, and for
the custom event initialize, add messages:

In other words, once the door is opened, all the enemies will
go into idle, and one of the enemies will walk to the player. Essentially,
since the enemies are going to start a combat as soon as the spot the player,
it will work so that one of the enemies will turn towards the player, or if
the player manages to hide before he is spotted, this same enemy will walk to
the door and towards the player to "investigate" what is going on.
Then to the actual reactions for when they perceive the
player. First of all, we are going to need couple of timers for making the e2
to run between the two waypoints. So add two animations to the script
DO:

And then to the custom even start, add these messages
to the state-specific message list for enabled state:
this->FSM_Switch(disabled);
::room3::e1->C_SetStateMachine(mobstercombat);
::room3::e2->C_SetStateMachine(crouchandshootstatic);
::room3::e3->C_SetStateMachine(crouchandshootstatic);
::room3::e1->A_Play3DSound(enemy, mobster_group_alert, head);
::room3::e1->C_SetIdle(1, false);
::room3::e1->C_SetStateMachine(delay500ms);
::room3::e3->C_GoToAndShoot(::room3::e3w1, 1);
this->DO_Animate(e2_timer1);
The first message switches the state to disabled, so
that the start custom event getting triggered won't have any effect
after it's been triggered once. Then it sets the enemies into combat
statemachines. It makes e1 shout an alert, and mess with its weapon
for half a second. e3 will run to its cover while shooting at the player, and
because of having crouchandshootstatic statemachine set, it will stay there
then as well. Lastly, it will start the first timer animation that we just
added in.
And notice that since e1 is going to shout something, we
should add little something to it's OnDeath tab:
this->A_StopAll3DSounds(head);
Then to the messages that the animation keyframes of the script
DO should
trigger:

As you can see, the movement messages for e2 are sent from
here, because they have to be re-sent over and over until e2 dies. And as you
can see, the two timers start up each others at the end keyframes. We still
have to make sure the animation does end when e2 dies, or otherwise the object
would keep animating until the end of the level, causing some overhead. So to
the OnDeath tab of e2, add:
::room3::script->DO_StopAnimation();
And still to make e3 come out of the cover as e1 dies, to the OnDeath
tab of e1, add:
::room3::e3->C_SetStateMachine(mobstercombat);
::room3::e3->C_GoToPlayer(1);
There, now you can export the level and try it out!
Once you are through with testing and seen that everything
works as is supposed to, let's still add logics to show a cinematic death cam
of the last person who dies. First add a Floating FSM into the room, name it as death_counter,
add states 1 (default), 2 and 3 to it, and add a custom strings
called add1, add2 and add3 to it.
Then to the OnDeath of e1, add:
::room3::death_counter->FSM_Send(add1);
To the OnDeath of e2, add:
::room3::death_counter->FSM_Send(add2);
To the OnDeath of e3, add:
::room3::death_counter->FSM_Send(add3);
So each one of the enemies will trigger their own custom event
to the death_counter. Now make it so that each time any of these events gets
triggered, the custom FSM will change its state to the next one. Basically
upon the custom event add1, when on state 1, send this->FSM_Switch(2);.
When on state 2, send this->FSM_Switch(3); etc... This is
from the FSM dump of the death_counter:
Send on "FSM_Send(add1)"
Send on "1"
this->FSM_Switch(2);
Send on "2"
this->FSM_Switch(3);
Send on "FSM_Send(add2)"
Send on "1"
this->FSM_Switch(2);
Send on "2"
this->FSM_Switch(3);
Send on "FSM_Send(add3)"
Send on "1"
this->FSM_Switch(2);
Send on "2"
this->FSM_Switch(3);
So basically just add all the six FSM_Switch messages to the
correct state-specific message lists. Take a look at the example level if it's
hard to figure out.
Then to the add1 custom event when on state 3,
add:
::room3::e1->CAM_AnimateParented(death_01);
This means if e1 sends custom event add1 to the
death_counter when it's in state 3 (=both other enemies have been killed
already), it will trigger the death cam for e1. As you propably guessed
already, you add similar message to the other custom events as well.
To the add2 custom event when on state 3, add:
::room3::e2->CAM_AnimateParented(death_01);
To the add3 custom event when on state 3, add:
::room3::e3->CAM_AnimateParented(death_01);
And there you have it. Export the level and try it out.
Debugging combat scripts
By now you propably noticed that it's sometimes easy to forget something
when making combat scripts, and when it happens that your enemies aren't
behaving as you intented them to, there are few methods to debug your combat scripts.
One method is to print an XML dump if the FSMs; While in move mode in
MaxED, select Dump FSM's as XML from the Mode commands dialog. You can
investigate the FSM messages that way for errors.
Or in some cases it is useful to dump the information of a given character
to the console in order to see which statemachine it is in and various other
useful information:
::room2::e1->c_dump();
You can scroll the console list with Ctrl+arrow keys. If you can't remember the name of the enemy, you can get it printed into
the console by typing MaxPayne_GameMode->GM_DrawTarget(1); and then
aiming at the character.
Few other useful commands
There are some other commands that are pretty simple to use and you might
find them useful. I'll explain them through simple examples:
P_CreateProjectileToBone( enemy_painkiller, 1, gun ); - The first
parameter is the projectile to create, the second parameter is the number
of projectiles to create, and the third parameter is the bone where
to create the projectile. This message typically used at the OnDeath tab of an
enemy, so that it will drop something when it dies. You can see the valid
projectiles to be used in here from data\database\projectiles\. Basically use
anything with enemy_ prefix.
C_SetTargetCharacter( ::Room3::NPC1 ); - This will make a character
to consider someone else as its enemy than player. So basically you can make
enemies shoot at each others. As a parameter you can define any character in
the level, or then simply player.
C_ForceUpdate( 15 ); - Typically the engine doesn't handle the
behaviour of any enemy unless the enemy is in the view (or has been a while
back), or some movement message has been sent to an enemy. So sometimes you
might need to use this message to force the engine to start handling some
enemy, in order for it to be able to see the player, etc...
The parameter is the time in seconds how long the engine will keep updating
the character even if it keeps staying out of the view.
C_Teleport( ::Room2::w1 ); - Is used to
teleport enemies to the waypoints. It teleports the message receiver to the
waypoint defined in the parameters.
C_SendSpecial(); - This is typically sent to the activator
from a character collide trigger. It is used to make something happen when a
specific enemy touches a trigger. You propably noticed the mysterious event
called OnSpecial in the message dialog of the characters. When
C_SendSpecial(); is sent to the activator, it will trigger the OnSpecial event
for the character, thus if you for example want a character A to die as
it touches a trigger, you send this to the activator, and from the OnSpecial
tab of A, you send this->c_sethealth(0); and also in most
cases disable the trigger as it's of no use anymore.
|