|
Dynamic content
With this tutorial you are going to learn how to create dynamic objects
and interactive gameplay content. We are going to cover the basics of dynamic
content, and also spill out some more advanced suggestions about how some things
should be done to keep your Finite State Machine (FSM) scripts and all the complex machines
maintainable. We are also going to create a door and turn it into a "prefab"
that can be easily copied around. Download the example level.
You can also download some prefabricated
dynamic objects by Remedy.
FSMs, Messages and the hierarchy
The basis of all the dynamic content are so-called Finite State Machines
(FSM). FSM scripting is especially evident in Max Payne because there are no
pre-made dynamic entities such as "a door" or "a lift" that
you could use, but rather all the dynamic content is created with dynamic
objects (DO's) and few other simple entities like triggers. These entities
can detect various events and send messages to each others to
control all the things you see happening in the game. For example when the player opens a door, it can
send
a message to, say, a huge boulder to make it roll towards the player, and when
the boulder crashes into something, it can send messages to the nearby enemies
to make them come and investigate all the noise. As you will learn in this
tutorial, it's a flexible system and can be used to construct pretty much
anything you can think of.
Basic message can look like this:
::Cellar_Room2::Enemy1->C_SetStateMachine(
CrouchAndShoot );
It consists of the receiver "::Cellar_Room2::Enemy1",
the message itself "C_SetStateMachine", and finally the
possible parameters "CrouchAndShoot".
Other examples:
::Room2::Door1->DO_StopAnimation();
parent::Enemy4->C_GoTo( ::Street1::Waypoint1,1);
Button->A_Play3DSound( dynamic, switch ,""
);
this->C_PickUpWeapon( deserteagle );
If messages point to objects that are inside MaxED, they require the hierarchy
to be defined for the receiver. You can use absolute hierarchy or
relative hierarchy from the message sender.
Example: Object A is a top-level object. It has children B
and C, and B has child D.
A simple
hierarchy example
The fully qualified names are:
A ::A
B ::A::B
C ::A::C
D ::A::B::D
When you define relative names, you have keywords "this"
and "parent" at your disposal. An object can refer to itself by
"this", and to its parent with "parent". An object can refer
to its child directly by using the child's name.
| Object |
Can Refer to: |
With relative name |
| A |
A
B
C
D |
this
B
C
B::D |
| B |
A
B
C
D |
parent
this
parent::C
D |
| C |
A
B
C
D |
parent
parent::B
this
parent::B::D |
| D |
A
B
C
D |
parent::parent
parent
parent::parent::C
this |
About whether you should use absolute or relative names, it
really depends on the situation. Usually when you create an enclosed machine,
such as a door, all the messages that the door sends to the different parts of
itself should be relative to ensure that when you copy the door around, the copy
will still have its messages defined correctly (=The new copy won't send
messages to the original door).
But then again if the machine sends messages outside of
itself, for example if the door sends a message to an enemy somewhere else in
the level, you might want to use absolute naming, which will ensure that the new
copies of the door will do all the same things as the original. (=All the copies
of the machine will send the same messages outside themselves)
In addition to these, there's two more keywords that you
can use some situations as the receivers; "Activator" and
"Player". For example a collision trigger can send messages to
the Activator when it's been touched, and some messages can be sent simply to
the player (The character that represents the player).
activator->C_SetHealth( 0 );
player->A_StopAll3DSounds( head );
As you can see, there's no hierarchy for these receivers. Likewise there's no
hierarchy involved if the entities are sending messages to the game modes, for
example to maxpayne_gamemode for changing the level, or maxpayne_hudmode for
hudprint, or x_modeswitch to perform a modeswitch (between game, menus and
graphic novels).
maxpayne_gamemode->gm_init(The_Next_Level):;
maxpayne_hudmode->mphm_printdirect("Don't play
with guns");
x_modeswitch->s_modeswitch(game);
The complete list of messages can be found from the
MaxFX-Tools Help.
The dynamic entities and their purposes
Here is a list of entities that you can create in MaxED and use as a part of
your FSM scripting. You can create any of these entities by pressing N
in F3 mode.
Waypoint - This entity is used for AI scripting.
Enemies can be commanded to move to waypoints.
Jumppoint - This is the entity where player spawns
to. The level can include multiple jumppoints, and you can jump between them
with insert and delete in the game, if developermode is on. The initial jumppoint is
defined in levels.txt. The level won't run in normal mode if it isn't defined,
although in developer mode you will just get an error message about it.
Enemy - A character, usually an enemy, although due
to the flexible nature of the AI scripting, any enemy can be used as a
non-harmful NPC. You can select what type of an enemy or NPC you want this to be
from the properties of the entity. (Press enter in F5 mode or click RMB
to it in the hierarchy tree and select "Properties".)
Level item - Any item you can find in the game, like ammunition, weapons or
painkillers. You can choose the type of the
level item from the properties.
Player collide trigger - A spherical trigger that
activates when the player touches it. You can change the radius from the
properties.
Character collide trigger - A spherical trigger that
activates when any character (including the player) touches it. You can change
the radius from the properties.
Projectile collide trigger - A spherical trigger
that activates when any damage-causing projectile touches it. You can change the
radius from the properties.
Action button trigger - A trigger that has to be
used by the player in order for it to activate.
Look-at trigger - A trigger which will cause the
player's character to look towards its center when player is inside it's radius.
Floating FSM - An invisible entity that can be used
as a part of the FSM logics of a system or as an emitter for sounds and particle
effects. It's basically an all-around relay entity for messages.
Pointlight - A Gouraud light for characters and the
desired dynamic objects. The light itself is static in the world and cannot be
moved. More about it in Dynamic lighting section of the advanced
lighting article. (Note that you
can make moving dynamic lights that affect the geometry by using such a particle
effect)
Creating DOs
In addition to the entities listed above then there's so called dynamic objects, which
make up a big portion of all the dynamic content . These are basically meshes that have some functions. All the doors, cars, crates, lifts and
choppers you see in the game are dynamic objects which have different FSM
logics and animations put in by the mapper. As mentioned before, there is no separate entity for
"Door" or "Car", everything is the same. A breakable crate is a
dynamic mesh which hides itself and creates a particle- and sound effect when
it's hit. A chopper is a dynamic mesh which has lots of invisible parents that
animate and move the chopper in the sky in a believable manner. Any model that
moves or interacts with something in the game is a dynamic object.
Then into the creation of DO's. Let's try and make a breakable window.
First we need some sort of a level to play with. Just create two rooms with a window opening between them (and a 1.5m x
2.5m doorway), and create a
flat polygon there to act as the glass for the window (You can get the
materials from the example level of this tutorial). Create a flat polygon by drawing the shape in F3 mode onto the grid - just like you would
create any object - but hold down shift when pressing RMB.

When you import the materials from the example level, click on the glass
material in the material window with RMB and make sure Dualsided and Alpha
test material flags have been set.

If it happens that you can't see the glass in the editor when it's Alpha
test, check MaxED's "Local Preferences". There's a value called
"AlphaTestReference", change it to, say, 128 (restart of MaxEd may
be needed for this to take effect). This will change the
way the material is rendered in MaxED, but don't forget that the game engine
itself will draw Alpha test materials differently, so don't worry if it
doesn't look very good in the editor.
The glass polygon is still static and doesn't really do anything special
yet, except let the bullets go through it because of the material category
properties. Let's turn the object dynamic by selecting it in F5 mode, and
pressing D. The same button also turns dynamic meshes into static ones.
MaxED asks if you want to keep the lightmaps of an object when you turn it
dynamic. Let's answer yes. DO's generally look better with lightmaps.
Setting DO properties
Now let's check out the rest of the object properties for dynamic objects.
Just like with any object's properties, select the object in F5 mode, and
press Return. You can rename the object now into whatever you desire,
and under the Statistics tab there's some flags for us to check.
Something like this:

Explanations:
Export geometry - This defines whether or not to export the geometry
of the object to the .LDB file. By disabling this flag you can, for example,
keep pieces of furniture around the .LVL -file while they won't get exported
to the game. This flag is enabled by default.
Export lights - When you make a polygon emit light (other than
black), a dynamic
cone-like light (that affects only characters and the desired DO's) will also
be created onto it. With this flag, these lights can be excluded or included
to the export. These cone-like lights are however slower in runtime than pointlights,
so it is suggested you use them instead. You
can see the dynamic lights by selecting View > Visualize lights. This flag is disabled by default.
Cast No Shadows - Even if an object has lightmaps of it own, it can
be made not to cast any shadows. This should be set on any DO's that move or
disappear at some point (Doors, breakable crates, moving vehicles). Disabled
by default. All objects will cast shadows in preview rendering.
Insert into collision BSP - This affects only static objects. A
static object can be excluded from the collision BSP by this. If you make very
small objects with lots of faces, it is usually a good idea to set this flag.
It's good speed wise, and also in many situations it makes a smoother gaming
experience when you don't get stuck onto all the little things lying on the
floor. Enabled by default.
Block explosions - This defines whether or not the DO blocks
explosions. Big things like doors should block the explosions, but small stuff
like little bottles shouldn't. Also if an object is blocking explosions, it
will also block them from itself, thus not getting any damage from explosions.
Enabled by default.
Bullet collisions - You can set the bullet collisions with this
flag. Disabled by default.
Dynamic collisions - You can set all the other collisions but bullet
collisions with this flag. Disabled by default.
Lightmapped - This defines whether or not the DO uses lightmaps.
If it doesn't use lightmaps, it is lit merely by dynamic lights.
Pointlights affect - With this flag you can make the dynamic lights to
affect the DO even if it uses lightmaps. This is a bit of a hack and it's not
recommended to be used often.
Cont. upd. - Continuous update, If the object is animating, setting this flag will make
the object animate all the time even when it's not in the view cone. This flag
doesn't have any effect on non-animating DO's.
TIP: You can make all the names of the DO's visible in the rendering window
by selecting View > Dynamic Object Text Modes > Name
Finite state machine scripting
By finite state machine scripting, we mean putting in the messages that link
the entities together. Or as it is in the case of our example, the breakable glass, putting
in messages that the glass sends to itself. We are going to add messages to make
the glass disappear and to play some breaking particle effects. First select the glass, and press 4
to bring up the FSM dialog.
In this first dialog we can add states to the object. The FSM scripts
we need for a breakable glass won't require any states, so we won't add any. We
can also bring up the Custom strings dialog from here, but we won't need
any customs strings now either. Let's just press Messages-button.
The tabs you can see in the upper part of this dialog are events that
can occur to the object. In this case there's 5 of them:
DO_BulletCollides - Send messages if a bullet collides the object, or
it receives damage by explosion.
DO_CharacterCollides - Send messages if a character collides the
object.
DO_MovedToInvalidPosition - Send messages if the object is animating
and something blocks it's path.
DO_OnDeath - Send messages when the hitpoints of the object are
depleted.
Startup - Send messages at startup.
There's three similar message boxes in all the tabs. The first box is for
messages which are to be sent first if the given event occurs. The second box is
for messages that are sent only if the DO is in a specific state when the
even occurs. The third box is for messages which are to be sent after
the state-specific messages (Sometimes the order does matter). Within the
message boxes, the messages are also sent in the order they are listed, from up
to down. You can change the order of the messages with PageUp and PageDown.
Try adding some messages into the message boxes just to see how it works. For
example this->do_stopanimation(); or this->a_stopall3dsounds("");
You can add messages from the Add-buttons. You can delete messages by
selecting them from the message boxes, and pressing Delete. You can cut,
copy and paste messages directly from and into the message boxes with CTRL-X,
CTRL-C and CTRL-V.
Note however that you can't add any messages to the state-specific message
boxes before you have some states defined.
Notice also that pressing TAB completes object and message names up to
as far as they logically can be completed. Example: you're editing a new
message on an object with a child called "the_door". Typing in
"t" and pressing tab will complete the line to read "th"
(because after that the strings "this" and "the_door"
differ). You get the picture.
Ok, after you've got the hang of it, delete all the messages you added, and
let's make the glass breakable. We can make it break by first hit, in which case
we'll simply use DO_BulletCollides -tab. When the glass breaks, we want
it to disappear, make a breaking sound, and emit a particle effect. Let's add
these messages:
About the messages we used; DO_Hide(true); obviously hides the
receiver. DO_Hide(false); would unhide it.
A_Play3DSound plays the desired sound from the receiver. Breakable
is the sound category here, and glass_shatter is the sound block inside
the category. You can see all the sounds there is at your disposal by extracting
the Max Payne .RAS files with RASMaker, and browsing your way to the Max Payne
database sound directory ...\data\database\sounds\. There's multiple .txt
files in there, which all represent a sound category. Opening
"breakable.txt", you will find sound blocks, firstly
"videotape_break", and also somewhere in there
"glass_shatter". The third parameter for this message is the bone
where to emit the sound from. It makes a difference only for characters. For
DO's, where there are no bones (only characters have bones), we just leave it empty with quotation marks.
PS_StartEffect emits a particle effect from the object. There are no
categories there, just the name of the particle effect. You can see the names of
all the particle effects from the Max Payne database once again ...\data\database\particles\particles.txt.
Particle effects are always emitted from the center of the object, and their
orientation is adopted from the orientation of the object's pivot point. There's
no way of directly seeing in the editor what is the internal orientation of the
given particle effect though, so that is just something you need to know or try
out. For most effects in Max Payne database, if there's a direction
for a particle effect, it's towards the Z-axis. You can see the pivot point of a
DO by selecting it in F5 mode. The yellow line is the Z-axis. Now, in
order the glass_shatter effect to have a correct orientation, let's set the pivot point
of the glass to a correct orientation. We need to have the Z-axis pointing straight
outwards from the surface normal. Align the grid on the surface of the glass,
and move it in F12 mode so that the grid center is positioned on the glass surface. The
origin of the grid shows its axis's, and now you can see that the yellow line is
pointing straight outwards from the glass surface. Then go to F5 mode, select
the object, and press P. This will make the object to adopt the pivot
point from the grid.

Hopefully that was not too confusing. Now we get to try out our level, so
export the level and load it up to the framework.
At this point, we should see some
tips about exporting the levels. Generally when
doing dynamic content, you need to export often, and you want to make it as easy
to you as possible. Firstly, you should run the game with command line options
as below:
maxpayne.exe -developer -developerkeys -screenshot -window -nodialog -skipstartup
This will make the game run in developer mode with developer keys, it will be
windowed, and it will skip the startup dialog and the video. Also if you are
running Windows2000, you want to make a .bat file that runs Max Payne as below:
start /low maxpayne.exe -developer -developerkeys -screenshot -window -nodialog -skipstartup
This will make the game run in the lowest priority setting automatically
always when you run it, thus when the game is on the background, it won't take
any resources from MaxED. Although depending on your video hardware and drivers,
you might not be able to run MaxED and Max Payne simultaneously, or at least
either one of them can be very prone to crash. Using low color depths to save
video memory might help somewhat.
Also it is sometimes a good
idea to add your level to the menu.txt so that you can conveniently load it up
through the menu (Take a look at menu.txt of the BasicRoomExample that came with
the game).
Now when you get the game running, you can try shooting the window and it
should break. Needless to say, it's usually a good idea to quicksave as soon as the level has
loaded up if you are testing some dynamic content.
If you still want to play around with the window, you can try adding
hitpoints to it. Add this->DO_SetHitpoints(15); to it's StartUp
tab, and then move all the messages from it's DO_BulletCollides tab to DO_OnDeath
tab. This is a special event which is triggered only when a DO has hitpoints and
they are depleted in startup. Obviously you can add hitpoints to an object upon any event
that occurs in the level, not just at its own startup.
Another DO example, creating a door
Next let's create a functional door. First, if you didn't make a
doorway to your level yet, let's do that. Simply Boolean a hole between the
two rooms. Do not forget though that you can't boolean two rooms together
if they are sharing an exit, so if you made an exit to the window opening, you need to delete
it (in F4 mode) before you can Boolean another hole between the rooms. In Max
Payne the standard doorway size is some 1.5 x 2.5 meters.
Model a door for the doorway, and turn it dynamic by pressing D.
Then check the object's properties, and make sure it's got the following flags
enabled.

Now it might be a good idea to render lightmaps onto the door by selecting
it in F5 mode and pressing R. Just make sure you don't have Hide
other objects enabled in the preferences, or the door won't take any
light sources into account. In general you should relentlessly exploit the
ability to render lightmaps on single objects or single rooms at a time, and
fix the possible anomalies by copy/pasting lightmaps.
Now you should be seeing something like below, and we can start adding
functionality to our door.
Do notice that there is countless ways of making doors in MaxED. You can
make sliding door (the easiest door type) or you can make regular doors that
rotate open. The doors can be swinging "kitchen doors" that close
automatically. The doors can open only one way or both ways. There can be dual
doors. You can require the player to use the door before it opens, or you can
make a control panel for the door. You can do almost anything your imagination can conceive. You can try
and make all the super complex sci-fi doors you ever imagined. You know, the ones that
open for 10 minutes with a lot of hissing noise and moving parts. But for this
tutorial, we'll settle for doing the most typical door we had in Max Payne.
That's a regular door that opens when any character runs against it. It can
open both ways and it stays open.
Let's start by adding animations to our door. Briefly, the animation system
in MaxED works so that you define keyframes (different positions in the
worldspace) for an object, and then define animations between the keyframes.
And since the door is going to be rotating, we are going to have to place
its pivot point correctly. All the rotating objects always rotate around their
pivot point, and if the pivot point is in the wrong place in our door, the
door will move around from it's location while it's rotating. The location of
the pivot point of the door of course depends on what kind of door we are
making, but since we are making a door that opens both ways, we should place
it as below. In real life, you do not see much doors that are hinged like
this. Also if you make the door frame to match the door exactly like in here,
the meshes are going to overlap when the door opens -- nobody will notice :-)
So move the grid center as it is in the picture, select the door, and press
P to adopt the pivot from the grid.
Next we get to add the actual keyframes. Select the door, and press TAB to bring up the keyframe dialog. There's one
existing keyframe there. Rename it as "closed". Then press Add.
It asks for a name for the new keyframe, so let's name it as
"opened1".
Next we need to define what is the position of the door in the
"opened1"-keyframe. Double click on the name of the keyframe to make
sure it's the current one, and close the dialog. Now we need to rotate the
door, and there's few things to consider when rotating objects in MaxED. First
of all, around which point in the the world to rotate the object? We
can rotate objects around any of their vertices, or around their pivot
point. Rarely used is the option to keep Shift pressed while
rotating, which makes the object rotate around it's geometry center. And we can select whether to rotate by world axis, or by the object's local
axis. Or obviously we can make a temporary object anywhere in the world and
rotate any other object around any of it's vertices. It's easier to just try
out rotating things than to try to explain it. First take a look at the lower
right corner of the rendering window in F5 mode. There it indicates the
current rotation mode. In this picture it says VTX (vertex) and
WLD (world), which means if you now rotate the door, it will rotate around
the selected vertex and by the world axis. Try it around, you can rotate with
keys X, Y and Z, or optionally with 1, 2,
and 3. Might be a good idea to save before you mess around too much
though.
You can toggle between vertex and pivot rotation with A, and between
world and local rotation with W. Change the modes and try out rotating
the door around again.
Got it? Ok, good, now as your door is "all over the place", it's
a good idea to revert to that save you had before you started messing around
with rotating things. Then make sure pivot rotation by world axis is selected
(PVT WLD). Select the door, press down Y (or 2), and move the
mouse sideways until the door is in its desired position.
TIP: You can change the angle snap from the Preferences. It's a setting
called Kbd Tilt angle, but it affects to all rotations in MaxED,
including polygon tilting. Also if you've got an exit on the doorway, the door
might appear to partially disappear. This is nothing to worry about though,
but if it cause problems, you can turn the exit acceleration off from the
Preferences.
Now we have the first keyframe in position. Since we want to door to open
both ways, we add another keyframe to the opposite direction. Open up the
keyframe dialog (TAB), add a new keyframe called "opened2",
close the dialog, and rotate the door 180 degrees around.
Got it? Good, then we can add the animations. Select the door, and press Shift+TAB.
You get the animation dialog, in which you can add, delete and rename
animations. You can change the keyframes and the length of the
animations, and you can manipulate the animation graphs. You can also
enter the message dialog from here.
Press Add-button to add an opening animation. Select
"closed" as the Start keyframe and "opened1" as the
End keyframe, and press OK. Now it might be a good idea to rename the
animation as "open1", and alter it's length to, say, 1 second.
Also add the animation for opening to the other direction. Obviously
otherwise do all the same, but select "opened2" as the End keyframe.
Now you can view the animations by double-clicking on the name of the
animation. Also try manipulating the rotation graph to see what it does. It's
actually pretty self-explanatory. Try pressing Ctrl+Q in the rotation
graph dialog. Also you can change the sample rate of the animation by pressing
Ctrl+numpad/ and Ctrl+numpad*. You can also copy and paste
animation graphs with Ctrl+C and Ctrl+V
Next let's add triggers for the door. You should add triggers to the both
sides of it. So align the grid on the door, and add a character collision
trigger (N in F3 mode) on the middle of the door. Name it as
T1, and change it's radius to 0.2m from its properties, and then lift
it up from the door a bit. Repeat to the other side. It's also be a good idea
to group the triggers to the door directly. (Select a trigger, press G,
and click LMB to the door)
Then open the FSM message dialog of either one of the triggers (4 in
F5 mode). When any character touches the trigger, we want it to animate the
door. So, add message parent->do_animate(open1); to it's T_Activate
tab. Make sure though that the animation indeed is "open1" that you
want the door to execute. It's obviously "open1" for the other
trigger and "open2" for the other.
Then open the FSM message dialog of the door, and there you will find tabs
called Animation(open1) and Animation(open2). Add these messages
to the both tabs:

T_Enable(false); messages are for disabling the triggers as the door
opens
A_Play3DSound(dynamic, door_wood_medium_open, ""); is for
playing a sound effect as the door opens.
These messages above are in a message list called Leaving 1st keyframe,
which means the messages are sent at the start of the animation. There's also Reaching
2nd keyframe message list that sends messages at the end of the animation,
but we won't need it now.
The rest of the messages are in a message list called Returing to 1st
keyframe, which means these messages are sent if the given animation
starts to play, but is ordered to return back to the beginning for some
reason. So T_Enable(true); messages obviously enable the triggers if it
happens that the door closes. And also a sound effect is played here. Now
don't forget to copy these messages to the "open2" tab as well.
Still one more message to add, and it goes to DO_MovedToInvalidPosition
tab. As you might remember, this is event is triggered if something blocks an
animation. Let's add message this->DO_InvertAnimation(); there. This is the
message that makes a currently playing animation to return to the first
keyframe.
Now you can export the level and try it out! You should add a jumppoint to
the other side of the door as well so that you can jump there in the game
(with insert or delete) to make sure the door works from both
ways. Also try typing DrawFSM to the console to see visualizations of
the triggers and DO's, and to see if the triggers disable themselves as you
intended them to. You can disable it with DrawFSM_Off.
Then we have just one problem left, we need to make sure there's no AI net
passing through the door when it's opened. Otherwise enemies might run against
the opened door like a bunch of suicidal lemmings. There's a special material
category called ai_node_collision_nodraw, and if you texture an object
with a material in this category, it means that AI nodes will collide against it. Without
going into the details yet, you fix this problem by adding these little AI
blocks to the positions where the door would be when it's open. Remember to do
this to the both sides of the door.

Let's not get into calculating AI network yet, we'll get to that with the AI
tutorial, but for now, let's settle for this.
TIP: Now that you've got a DO with keyframes with it, you might wonder how
to move and rotate the whole object around, not just the selected keyframe.
You can do that by pressing down Ctrl while moving objects. Otherwise
all the moving and rotating commands are just the same. Although notice that
you can't use Z or X for rotating objects while keeping Ctrl pressed, because
these would equal to Undo and Cut. Even though there is no undo in MaxED,
Windows takes over when pressing Ctrl+Z and it doesn't work. You should
use Ctrl+1, Ctrl+2 and Ctrl+3 instead when rotating
objects with keyframes.
Coder attitude with FSM's
You have probably already realized that there are countless of ways to make
same things happen in Max Payne, and it might be that we did things here a bit
differently as you would've liked to do them. For example, you might want to
disable the door triggers in their own t_activate tabs. However if you do
that, you still need to disable both triggers always when either one is
touched. In general when doing FSM's and you aren't sure how you should do
something, try to do scripts so that messages that do the same thing aren't
sent from multiple different places or objects. For example the door we
did, as the
animation itself disables the triggers, it is easy to add more triggers all
over the place. All you need to add is that you send DO_Animate(); message to
the door from the new trigger, and disable the trigger via the door's
animation (and enable it back if the animation returns to the 1st keyframe). This
way you can keep things structured logically and in the same place even if you
do additions to your FSM's.


The door we did is not the best example to describe the importance of the
structure in FSM scripts, but above is a door that is controlled by five
triggers, and two ways of enabling and disabling the triggers. As you can see,
you can make simple things surprisingly complicated if the structure is wrong.
Because of the flexibility of Max Payne's FSM scripting, it is perfectly
possible to create spaghetti and get yourself into troubles if you don't pay
attention to how you do things. This is especially true when we get to
advanced AI-scripting. Putting some time into thinking about this is
definitely worth it. If you just fire away and send messages from whatever
object first comes to mind, you will soon find yourself typing in a lot more
messages than you should, and eventually you will spend 10 times more time
fixing bugs than it would've taken to structure it properly.
Talk about fixing bugs, there's some methods of finding them from your FSM
scripts. You can try this with the level you've done so far. One aid is taking
an FSM dump of the level. You do this in movement mode (space) from Mode
commands > Dump FSMs as XML. You can seek for errors from there. Also
in the game, you can type messagefilter_levelonly to the console to get
a continuous dump of the messages moving in the level to the console. You can
get a filedump of the console with x_consolemode->cm_dumptofile(
my_dump1.txt );. And don't forget Draw_FSM that was mentioned
above.
Also something you will be using a lot for testing and debugging your
dynamic content is Partial export. You do this simply by selecting a
room or multiple rooms (by pressing down Shift) in F5 mode, and then
selecting Export selection from the Mode commands dialog. Notice
however that the selection must contain a jumppoint.
Exit disabling
Exit optimization is a powerful optimizer already on it's own. In some cases you might want to help it
even more to gain some extra speed by manually opening and closing exits. The most obvious use for disabling an exit is when there's a
door that is closed on top of the exit, especially if the door happens to be a swinging door that
closes automatically.
Let's add exit disabling functionality for our door.
First make sure the exit is totally enclosed by the door at it's
"closed" keyframe. If it isn't, either delete the exit and create a
new one to the correct position, or move the door so that it encloses the
exit. Then enter the door's keyframe dialog (TAB) and double-click on
either one if it's "opened" keyframes (we do this just so that we
can actually see the exit).
Now make sure that "Select culled polygons" is not enabled in the
Preferences. If it was enabled, it would make it harder to do what we'll do
next. Enter F4 mode, point to the exit and press Enter. This will bring
up the exit renaming dialog. Name it as toggle1. Now go to the other
side of the exit, point to the exit, press Enter, and rename it with
the same name. That's right, there's not actually just one exit there, but
two! The reason we can name them with the same names is that they belong to
the different rooms. And now as there's two exits, it is possible to create
doorways that are disabled only from the other side. The reason we disabled
"Select culled polygons" was that if we had it enabled, MaxED could
select either one of the exits regardless of in which side we were ourselves.
Let's still rename the rooms in this level so that we know what they are
called. The other one is probably already called Startroom (if it
isn't, rename it), and let's rename the other one as room2. Now open up
the FSM message dialog of the door, and to its Startup tab add
messages:
::startroom::toggle1->EnableExit(false);
::room2::toggle1->EnableExit(false);
This will close the exit at the startup. Then we need to make sure it gets
enabled as the door opens, so to both of the animation tabs add
::startroom::toggle1->EnableExit(true);
::room2::toggle1->EnableExit(true);
Obviously put these into the Leaving first keyframe message list.
Then still just to make it perfect, let's disable the exits if the door closes
because of something. Just copy the messages from the Startup tab to the Returning
to 1st keyframe message list.
Now just export the level and try it out. You can make sure the exit is
disabled when the door is closed by typing maxpayne_gamemode->gm_drawoutlines(1);
to the console. You can see all the outlines that the engine draws through
walls, and if you did things correctly, you can see it doesn't draw any
outlines through the door before it's opened.
FSM states and custom strings
Let's use an existing object for clarifying the use of FSM states
and custom strings. Open up the example level of this tutorial,
which you hopefully downloaded before you got started. If not, download it
from here. You will find a breakable crate
from the level. As you can see, the crate breaks in 5 different pieces. 4
sides and the top. What we had to achieve here was that the top piece falls
down if any of the two other pieces were destroyed. Opening up the FSM dialog
of the top piece, you can see it has some states and custom strings.
If you open up the message dialog of any of the side pieces, you can see
that when they break, they send FSM_Send( add ); to the top piece.
Opening up the message dialog of the top piece again, you'll find a tab there
called FSM_Send(Add). Yes, there's a connection between there. This is
a custom event for the object that is triggered when it receives FSM_Send(Add);
from somewhere.
As you can see from the FSM dialog of the object, it's in state called 1
by default. In it's FSM_Send(Add) tab you can see that if it triggers the
event while in state 1, it will send a message FSM_Switch(2);
This means it will change its state to 2. In other words, this
happens when the first one of the side pieces is broken. Then let's see what
would happen if the top piece would receive FSM_Send(Add); now again,
when another second side piece breaks. Select "2" from the "state-specific"
drop-down list of the FSM_Send(add) tab.

As you can see, when the second piece breaks, the top piece receives
FSM_Send(Add); for the second time, and it will animate itself to drop down.
It will also change its state to "disabled" so that none of the "Add" custom strings it will be receiving from now on has any effect.
Custom strings are especially useful for structuring things properly. You
can keep the messages where they'd logically belong. The camera prefab (look
below) is a good example of this, there is a number of messages to send when
you startup a cinematic. Now you can either send them from whatever triggers
the cinematic (there can be multiple events that do that), or then you can just send
one custom string to the camera from wherever and you'll know where the messages
are when you need them. If you have multiple events that might trigger the
camera, you can easily use "enabled" and "disabled" states
for the camera, so that it switches itself to "disabled" when the
cinematic starts, and all the custom strings it might receive while in the
cinematic, it will ignore. And/or you can of course disable all the triggers
from the camera's "start" custom string tab.
Exploiting prefabs
Prefabs are some commonly used objects that are prefabricated and
ready to be put into the levels. This obviously saves a lot of time and nerves
of the mapper as he doesn't have to make every single door from scratch, for
example. For the end of this tutorial, let's cover some preferred ways of
using prefabs. As already mentioned, There is no special prefab-system or UI
in MaxED. But if you make your dynamic objects cleverly and prepare them
properly, you can reach something that can be described as a prefabs
system.
How to make one? Open up the level where you made your door again. After you have tested your door and you
are sure it's 100% working and all, let's
make a prefab out of it. First we should create a handy parent for the
whole machine, and group everything to it. Create an object next to the door
with dummy material (If you don't have one yet, add dummy material
category and import some texture in there). Then set its pivot point to some
convenient corner that you want to use as a reference point when
copying the object. Note that you can't see the pivot points of the static
objects, but that doesn't mean they don't have one. Yu can set
it just like you would for a dynamic object; move the grid's pivot to the
desired position, select the object and press P.
TIP: If you have to group objects that are in different rooms together, you
can do it by first selecting the desired children, then moving to the room
with the parent, entering F5 mode, and selecting Mode commands >
Grouping > Group selected. In other words; don't use the keyboard
shortcut.

Next select the door parent dummy in F5 mode, and copy it to the
clipboard (Ctrl+C). Now you can either make a copy of the door by
pasting in F5 mode, when the new door appears to the same position as the
original and you have to move it a new position with arrow keys, or you can
paste it in the F3 mode, when it will get pasted to the grid tick by the
reference point. Try it out. You can delete objects with their children in F5
mode by Ctrl+Delete; a handy feature once you've got dozens of doors
around the room.
To create yourself a library of prefabs, import all the materials that you
need to separate empty document (insert them from your current file) paste the object (in this case,
the door) into that somewhere around the world origo. Make sure,
that in that level there just the door object with all the necessary stuff,
but no room or something other extra around it. Finally it is wise to purge all
unused materials to make sure that the prefab has just the textures it needs.
Then, as you need a good working door when you are working with some other
level, just use File > Insert document... to get it in. This
inserts the door and those materials that are not yet there. If the your door
prefab was near the world origo, you should be able to locate it pretty easily
(Use F12/C to reset the grid and the camera) after insertion. Then, just cut
it and paste around as usual (F3/Paste to is good for this). If you do lot of
different general objects and put them into some folder with descriptive
names, you have something that can be described as a library of prefabs.
With a door though there's typically one thing that causes problems when
it's prefabricated; it's usually placed on top of an exit. The game will complain
if you've got static objects going through exits, or static objects that are
grouped to an object in some other room (in this case the door parent might go
through an exit, and the other one of the AI net block definitely will be in a
wrong room for its grouping). It is perfectly fine for dynamic objects to go
through exits, but when there's static objects involved, you might have to do
some regrouping after you've pasted the prefab to its location. Also if you
need to rotate the prefab, don't forget to rotate all of it's keyframes (press
down Ctrl while rotating).
Prefabs from Remedy
Here is also some prefabs that were used in Max
Payne, you can take a look at them and try to figure out how they work.
Cameras - Two cameras that work by custom strings. It's easy to copy
more cameras for your cinematic. The animation in the camera defines the
length of the cut for each camera, and obviously you can just add keyframes to
the camera to move it. Or you can make a "camera_mover" dummy to
which the camera is grouped to.
Ceiling fans - Three breakable ceiling fans
Crates - A set of breakable crates.
Exploding barrels - Four different kind of exploding barrels.
Fire extinguisher - Breaks when the valve is hit.
Gas bottles - Various gas bottles. Break when the valve is hit. Some
fly out, some rotate, some just explode.
Gasoline canister - A simple gasoline canister that explodes.
Small breakables - Some bottles, a drinking glass and a porcelain
plate.
Soda machine - A vending machine.
Sodacans - Four cans of soda. Two of these fly in the air when hit.
Swing door - Kitchen doors that close automatically.
TV's - Four similar TV's with just different colors.
Valkyr containers - Breakable Valkyr containers. The one that is
standing tips over when it breaks.
Wooden door - The door we did in this tutorial as a prefab.
--- --- ---
Chopper - Due to numerous
requests by modding community, the police helicopter prefab used in the game
is now available. You should be warned that this is one of the most complex
dynamic objects used in Max Payne. Understanding the operation requires fluent
animating and FSM scripting skills.
|