Академический Документы
Профессиональный Документы
Культура Документы
David J. Sushil
David J. Sushil
davidjsushil@gmail.com
David J. Sushil
davidjsushil@gmail.com
David J. Sushil
davidjsushil@gmail.com
David J. Sushil
davidjsushil@gmail.com
At this point, select every object in your scene, and click on the button marked Renumber
Selection A dialogue box will appear, asking for a new number. Type 2, and hit the
OK button. This adds the number 2 to the end of every objects name, committing them
all to a set level-of-detail. Since were not going to bother having multiple detail levels
for this initial model, were done for now.
At this point, we need to add a few helper objects to the scene. We can do this manually,
or we can have the Torque DTS Exporter do it for us. Simply click the button labeled
Embed Shape, but only click it one time. You will get no indication from the plug-in that
anything has occurred, but if you click the Select By Name button in the main toolbar,
youll notice that your scene is now home to base01, detail1, and start01 helper objects
that Torque needs to see in order to process your model. Well see these again when it
comes time to add collision detection.
Next, we need to show Torque where our model begins and ends in the scene. Click on
the Create panel tab (indicated by an arrow pointer), make sure Standard Primitives has
been selected from the dropdown listbox below, and use the Box tool to create an object
that surrounds your entire model. Name this object bounds. It should be a close fit, but it
should also contain all the objects in your scene make sure nothing is poking out.
Next, we need to create collision meshes, which let Torque know when your model has
collided with something in the engine. To do so, create another box that surrounds your
model, but is inside bounds, and name it Collision-1. Once it has been positioned and
scaled, and rotated properly, convert it to an Editable Mesh. Clone it (using Edit >
Clone), and name the cloned copy Col-1.
Collision meshes come in pairs, and they following the naming convention of CollisionN and Col-N, where N is a number from 1 to 8. This means you can have multiple
collision meshes for an object. Keep in mind, however, that collision meshes must be
convex shapes meaning every internal angle must be less than 180. Boxes are fine. So
are cylinders and spheres. In general, since the process of collision detection can be
intensive for a game engine, its best to use as few collision meshes as possible. And
where possible, stick with boxes, as they contain the least number of vertices.
Now would be a good time to select bounds and all of your collision objects, and hide
them. We dont want to accidentally move them now that were happy with their
positions.
With your collision meshes in place and out of sight, we now need to link them to two of
the helper objects we embedded earlier. To do so, click on the Schematic View (Open)
button on the main toolbar (it looks like an orange box pointing down at a grey box).
Using the Connect tool in the top left of the Schematic View window, connect all of your
Collision-N meshes to base01, and all of your Col-N meshes to start01.
At this point, we are ready to export. From the Utility panel, click the button labeled
Whole Shape. Select a destination for your file, and click the Save button. Simple,
right?
5
David J. Sushil
davidjsushil@gmail.com
Importing to Torque
Your model is now saved in DTS format. Before we can load it into Torque, we should
place it somewhere in the Torque file hierarchy. I recommend sticking with tradition, and
putting it somewhere in demo/data/shapes/, preferably in its own folder. In addition, any
files you used to texture the model in 3D Studio Max should be placed in this folder as
well, otherwise the model it will appear gray in your game.
Once your model is placed somewhere in the Torque directories, we can load it into a
mission. There are several methods to accomplish this task, and we will cover two of
them using the Mission Editor, and using code. In general, I would recommend adding
shapes via the Mission Editor wherever possible. However, be aware that there are times
when we need to add shapes via code.
Using Code
While the Mission Editor is a great way of adding Static Shapes (shapes that are purely
decorative), adding shapes via code is also an important skill to master, and doing so is
fairly simple.
First, create a new file, and call it myshape.cs. Open the file with any text editor
(Notepad works well), and add the following code:
datablock StaticShapeData(myShape){
shapeFile = "demo/data/shapes/myshape/myshape.dts";
};
function addShape(){
David J. Sushil
davidjsushil@gmail.com
The first line loads your .cs file into memory. Of course, change the information passed
to the exec() function to reflect the location and name of your file. The second line calls
the addShape() function we wrote. If everything was entered correctly, the shape should
appear at the position you specified.
You could also add these lines of code to another file, one that would execute them
automatically as the level loads. Thats a topic we will explore at a later time.
Now, why would we want to add shapes via code, when it seems so much easier to add
them via the Mission Editor? The answer is versatility. You cant use the Mission Editor
7
David J. Sushil
davidjsushil@gmail.com
to add shapes to your mission once the game has begun. And once we start working with
animated shapes and other objects in later tutorials, a code-based approach makes more
sense. Better to get a handle on the technique now, then struggle with it later.
David J. Sushil
davidjsushil@gmail.com
David J. Sushil
davidjsushil@gmail.com
set. Next, we would reposition the timeline slider to the end of the animation sequence,
maybe on frame 50. With the timeline slider positioned, we could then rotate the fan
blades 360. When we are happy with the rotation, we would set a keyframe at frame 50,
again by clicking the Set Keys button.
The effect of this process is that if we move the timeline slider back and forth between
frames 0 and 50, 3D Studio Max interpolates the position of the blades accordingly. Its
all mathematics at frame 25, for example, the blades should be rotated approximately
180.
We could also test this by playing the animation using the animation controls. However,
since the default length of an animation is 100 frames, and our animation is only 50
frames long, it would appear as though our animation has stopped midway through. We
can remedy this by clicking on the Time Configuration button, which is located in the
bottom-right of the screen, and looks like a window with a clock in front of it.
From this menu, we can set the Start and End times of the animation. In our case, we
would want the End time to be 50, or the last frame in our blade spinning animation.
In either case, once we are satisfied with our animation, we should turn Auto Key mode
off, by click on the Toggle Auto Key Mode button.
The most basic forms of animation are transformations that is, animations that involve
moving, rotating, or scaling objects. Torque supports transformation animation, as well
as a few other types. Well explore those additional animation types in a moment.
10
David J. Sushil
davidjsushil@gmail.com
Otherwise and this is the critical part 3D Studio Max will interpolate from keyframe
50 onward. This could possibly result in a poor animation.
The question is, how do we tell Torque where one animation begins and another ends?
We do so by creating helpers objects called Sequences.
Creating a Sequence
To create a sequence object, click on the Helpers button from the Create panel. In the
dropdown listbox that appears, select General DTS Objects. This option will only appear,
by the way, if you have loaded the DTS Exporter Utility from the Utilities Panel. See the
Importing Assets From 3D Studio Max tutorial for instructions.
From the Object Type rollout, click the Sequence button. Go ahead and create a sequence
by clicking and dragging in a viewport. A simple box will appear. Dont worry if this
sequence isnt within the bounding box of your model its strictly a helper object that
communicates between Max and Torque. It will not be rendered in the engine.
From the Modify panel, give this sequence a name. Also, if you want your animation to
repeat, be sure that the Cyclic Sequence box is checked. Otherwise, uncheck the box, and
the animation will only play one time. Rotating fan blades should be cyclic. On the other
hand, the animation for opening a door should be non-cyclic.
Create a sequence object for each animation on your timeline. Our electric fan has three,
which well call spin, wobble, and die. Spin and wobble are cyclic; die, non-cyclic (it
only has to die once).
Begin/End Controllers
The final step is to indicate where each sequence begins and ends on the timeline. To do
so, we open the Curve Editor. Do so by clicking the Curve Editor (Open) button, located
on the toolbar, and represented by a line chart icon. A curve editor (sometimes referred to
as a dope sheet by animators), is normally used to control the speed and smoothness of an
animation. We will use it to set special keys for each sequence, which Torque will use to
partition our animations in the engine.
To set these keys, locate the first sequence in the panel on the left. In the case of our
electric fan, the sequence is located in the Objects section, and is title spin. Open the spin
object by clicking on the plus sign to its left. From within this object, open up the Object
(Sequence II) sub-object. Finally, click on the Sequence Begin/End controller. Now, set
two keys on the timeline to the right one at the start of the animation (frame 0), and one
11
David J. Sushil
davidjsushil@gmail.com
at the end of the animation (frame 50). You can set keys by selecting the Add Keys
button in the toolbar, and clicking directly onto the timeline.
Repeat this process for the next animation. That is, in the panel on the left, locate the
next sequence (in the electric fan example, its wobble), drill down until you find its
Sequence Begin/End controller, and set keys at the start and end of the animation.
Once each animations beginning and ending frames have been set, were pretty much
done. Sequences, unlike collision meshes, dont need to be linked to anything in our
scene, so we need not bother with the Schematic View, except to make sure that our
sequences are unlinked, and that everything else is embedded and arranged properly.
12
David J. Sushil
davidjsushil@gmail.com
This code should be placed within a .CS file say, fan.cs, located in
demo/data/shapes/fan/, for example. Notice the datablock called fan, which has one
property the location of our .DTS fan model.
We instantiate the fan using our custom addFan() function. Of course, you should change
the position, scale, and rotation properties of the fan object to reflect its transform data in
your level.
See how we store our fan object in a global variable ($fanObj)? Well need that variable
to control our animations later.
In the meantime, to test this code, load Torque, and pull up your console window by
pressing the tilde key (~). Enter the following code:
exec("demo/data/shapes/fan/fan.cs");
addFan();
If everything worked correctly, your fan should appear. Youll notice, however, that it is
not animated! Animations are controlled via a method called playThread. It looks like
this:
$fanObj.playThread(0, "spin");
We call the method from the global variable $fanObj. It takes two arguments. The first
we can ignore (its usually 0), but the second argument is the name of the animation we
want to play. Remember that spin is the sequence in which the blades rotate normally.
Its a cyclic sequence, so once it begins, it continues indefinitely. We can call any
animation sequence we want simply by calling playThread and passing the name of the
animation as an argument.
To make our fan spin when its loaded, insert the playThread code within our addFan()
function. Simple enough. Have fun animating objects!
13
David J. Sushil
davidjsushil@gmail.com
Adding Triggers
To add a trigger through code, create a .cs file and add and modify the following code.
datablock TriggerData(FilingCabinetTrigger){
tickPeriodMS = 1000;
};
function addTrigger(){
%obj = new Trigger() {
position = "100 100 100";
scale =
"1 1 1";
rotation = "0 0 1 0";
dataBlock = "FilingCabinetTrigger";
polyhedron = "0 0 0 1 0 0 0 -1 0 0 0 1";
};
}
As you can see, the first step is to create a datablock called TriggerData. It takes one
argument, which will be the name of the parent trigger. In this case, were calling it
FilingCabinetTrigger.
The datablock has one parameter, labeled tickPeriodMS. This indicates how often the
datablock executes code. Weve assigned it a value of 1000, meaning the trigger will be
executed every second. By the way, the smaller this number, the greater its strain on your
CPU. Deciding on a reasonable interval is important.
Next, we have a function called addTrigger, which, as you might have guessed, creates an
instance of FilingCabinetTrigger.
The first line creates a local variable called %obj, and assigns it the ID of a new Trigger.
This new trigger has several parameters of importance. First, its position is where it
appears in the mission. Its scale is also reported, as is its rotation. These three properties
should be familiar to you, having previously worked with static shapes.
%obj will take on the properties of the FilingCabinetTrigger outlined above, by assigning
it to the dataBlock parameter.
Finally, a property called polyhedron creates the shape of the trigger. It is currently set up
to represent a box, although additional shapes are possible. To date, I have not found any
resource that explains how this polyhedron parameter works. I tend to manipulate the
triggers scale in order to create the proper size and shape.
14
David J. Sushil
davidjsushil@gmail.com
By running the addTrigger() function from the console, we can add an instance of the
FilingCabinetTrigger to our mission.
This code obviously doesnt do much, other than offering you a framework to build on.
Every trigger has two important functions built into it onEnterTrigger() and
onLeaveTrigger(). As you might expect, onEnterTrigger() is called whenever the player
moves into the space the trigger occupies. onLeaveTrigger() is called whenever the
player moves out of the space the trigger occupies. Remember, we set the tickPeriodMS
to 1000, which means these functions will be called only once a second, and only if their
assumptions are met. There is a third method, onTickTrigger(), which we will address
later.
In this case, when the player enters the trigger, Torque will set the value of a global
variable called CabinetInRange to true. It will also print In range in the console.
Likewise, when the player exits the trigger area, CabinetInRange will revert to false, and
the Torque will print Out of range in the console.
Notice that these functions take three arguments, which are supplied by Torque. These
arguments are %this, %trigger, and %obj, which are essentially the following objects:
Variable
%this
%trigger
%obj
What It Represents
The datablock associated with the trigger.
The trigger which executed the function the one we created in
code.
The object which moved into (or out of) the trigger area it could
be the player, for example.
Now, in order to make this code work effectively, we need to execute the .cs file in which
it exists, either through the console, or by placing it in your game.cs file. Either way, the
code to run this is as follows:
exec("demo/data/shapes/filing-cabinet/filing-cabinet.cs");
addFilingCabinet();
15
David J. Sushil
davidjsushil@gmail.com
Of course, you will need to change the first line to reflect the name and location of the .cs
file you have created. You should also add a static shape in roughly the same spot as your
trigger.
This new code should not be unfamiliar to you at this point. We create a static shape
datablock called FilingCabinet, and then create an instance of that datablock in the
addTrigger() function. Notice that we are storing the ID of the static shape in a local
variable called %FilingCabinetObj, which we then assign to our triggers cabinetID
property. In this way, we can modify our onEnterTrigger() and onLeaveTrigger()
functions to control the static shapes animation, like so:
16
David J. Sushil
davidjsushil@gmail.com
Voila.
An Additional Function
Finally, lets look at an addition trigger function, onTickTrigger(), which checks to see if
something is inside the trigger area. This function accepts the same three arguments as
onEnterTrigger() and onLeaveTrigger(), but will be executed not just once, but every time
the tickPeriodMS timer is reset. This could be a good way of assigning damage to a
player who has fallen into a pit of lava, for example.
17
David J. Sushil
davidjsushil@gmail.com
Basic Mapping
The first thing were going to do is check out some of the existing code Torque has to
offer. For this, we turn to demo/client/config.cs. Contained in this file we find three
important functions:
// Torque Input Map File
moveMap.delete();
new ActionMap(moveMap);
moveMap.bindCmd(keyboard, "escape", "", "escapeFromGame();");
moveMap.bind(keyboard, "f2", showPlayerList);
moveMap.bind(keyboard, "a", moveleft);
moveMap.bind(keyboard, "d", moveright);
// Etc., Etc., Etc.
The first line is common courtesy. If an action map named moveMap exists from a
previous game, delete it. Why is this important? Between games (heck, between levels)
the applicable keys may change, and the key responsible for firing a weapon might
become the key responsible for jumping. If we tried to fire a gun that doesnt exist in the
all-jumping level, worlds collide.
In the next line, we create a new action map named moveMap. The new ActionMap()
command function takes one argument, which is the name of our action map. This leads
us to an interesting question. Can we create more than one action map for our game?
The answer is, absolutely! Well explore this a little later.
Next, we have a list of key bindings. These key bindings can be created using one of two
commands.
The first, and easiest, is bind(). Bind() takes three arguments the name of the input
device, the triggering event, and a function to call. The two input devices we are most
concerned with are keyboard and mouse0. As you can see, in line four of the above code,
we are binding a function called showPlayerList to the F2 key on the keyboard. Easy as
pie!
The second method of binding keys is the use of the bindCmd() function. BindCmd()
allows you to add two functions to a key one when the key is pressed, and another when
the key is released. So, in line three, we can see that we are assigning two commands to
the Escape key. The first argument (known as the make command), is blank, indicated by
empty quotation marks. The second argument (known as the break command), is
escapeFromGame(), which as you guessed, escapes from the game. This means that
nothing happens when you press the Escape key, but when you depress it, it calls the
function which quits out of Torque.
18
David J. Sushil
davidjsushil@gmail.com
A Sample Function
Now, lets write a few simple functions to test this. Place the following code in a .cs file
called bindTest.cs, and place it in the demo/data/ folder:
function getPlayerPos() {
echo(localclientconnection.player.position);
}
moveMap.bindCmd(keyboard, "x", "", "getPlayerPos();");
Finally, load the .cs file into memory using the exec() command in Torques console
window, like so:
exec("demo/data/bindTest.cs");
Finally, test your method by walking through the terrain with the Mission Editor
disengaged (that is, hit F11 if you still have the menu bar and heads-up-display visible),
and the console window closed. Press the x button on your keyboard. Open the console,
and you should have the x-, y-, and z-coordinate values of the player recorded.
Now all we need to do is select an appropriate map based on whether the player is in a
vehicle or not. Such code might resemble the following function:
function toggleMaps(%inVehicle){
if (%inVehicle){
playerMap.pop();
vehicleMap.push();
} else {
playerMap.push();
vehicleMap.pop();
}
}
The function toggleMaps is called whenever the player enters or exits a vehicle. This
function takes one argument, %inVehicle, which is either true or false. If it is true, we
need to use the action map for vehicles. We push (activate) the vehicle map, and pop
(deactivate) the player map. If the player is not in a vehicle, we push (activate) the player
map, and pop (deactivate) the vehicle map.
19
David J. Sushil
davidjsushil@gmail.com
GlobalActionMap
It should be noted that Torque has a default action map, labeled GlobalActionMap, which
saves us from at least a few headaches. Some keys should always be available, no matter
whether were in a vehicle or on foot. Keys that display help menus, for example, or
which allow us to exit the game at any time. These should be stored in GlobalActionMap
rather than in our other, mode-dependent action maps. And of course, its a good idea to
never pop (deactivate) GlobalActionMap.
Input Devices
Finally, we should address the existence of additional input devices. Torque can
recognize a single keyboard, multiple mice, multiple joysticks, and multiple unknown
devices.
To bind commands to the keyboard, we simply invoke the keyboard interface. The
keyboard has an extensive range of inputs digits, lowercase letters, uppercase letters,
and F-keys which can be modified by adding the prefixes shift, ctrl, or alt, followed by
a space. Please note that to use the shift, ctrl, or alt keys on their own, use lshift, rshift,
lctrl, rctrl, lalt, and ralt.
We can define multiple mice by appending the word mouse with a digit, beginning with 0
for the first mouse. So, to accept a two-mouse interface, we need to bind keys to mouse0
and mouse1. By default, mouse without a digit indicates the mouse0. The mouse consists
of a series of buttons, labeled button0, button1, button2, etc. The mouse also has an xaxis
and yaxis which may have commands assigned to them.
Likewise, multiple joysticks and multiple unknown devices are invoked the same way as
mice, with joystick0 or unknown0 as the first device in the series, and each subsequent
device increasing its appending value. In addition, these devices, like the mouse, consist
of a series of buttons, labeled button0, button1, button2, etc.
20
David J. Sushil
davidjsushil@gmail.com
21
David J. Sushil
davidjsushil@gmail.com
datablock ParticleData(steam_puff){
textureName = "~/data/shapes/steam/steam_puff.png";
lifetimeMS = 300;
lifetimeVaranceMS = 100;
colors[0] = "0.5 0.5 0.5 0.75";
colors[1] = "0.4 0.4 0.45 0.5";
colors[2] = "0.3 0.3 0.4 0.125";
colors[3] = "0.2 0.2 0.35 0";
sizes[0] = 1.0;
sizes[1] = 1.1;
sizes[2] = 1.5;
sizes[3] = 2;
times[0] = 0.0;
times[1] = 0.3;
times[2] = 0.6;
times[3] = 1;
spinSpeed = 0;
spinRandomMin = -100;
spinRandomMax = 100;
gravityCoefficient = -10;
constantAcceleration = 5;
dragCoefficient = 2;
};
Here, we create a ParticleData object template and call it steam_puff. You will notice
that the first property within our datablock is a texture name, which points to our image
file. When using Torque, its not necessary to include a file extension such as .PNG or .
JPG. Torque will instinctively try to open either one.
Next, we have lifetime information, which indicates how many milliseconds a particle
may remain in the world. In this case, our steam puff particle will last three tenths of a
second, give or take 100 ms, as indicated by the lifetime variance property. Creating
variability is an excellent way of giving your particles some character.
Next, there are lines which control the color and size of the particle throughout its
lifetime. Notice the lines that begin with times[0] times[3]? These are keyframes, and
the values they contain represent percentage values, with 0 being the instant the particle
enters the world, and 1 being the instant that it leaves. Torque will look at the
information contained in the colors and sizes arrays, and interpolate between them based
on the times values.
The colors array accepts four values for each member, which represent red, green, blue,
and alpha values. Remember, our particle image is a transparency map, and otherwise
contains no color, so we have to colorize it ourselves. For color values, 0 always
represents no color, while 1 represents full color. Likewise, 0 is invisible, while 1 is fully
opaque. In this example, or particle begins life as a semi-transparent gray (red, green and
blue are at 50% each, and our alpha channel is at 75% opaque). Over time, it becomes
darker and darker, and more and more transparent. If you notice the blue channel, it
grows darker slightly less than the red and green channels, so our steam puff also appears
bluer as time goes on.
22
David J. Sushil
davidjsushil@gmail.com
The sizes array works exactly like the colors array, with the size of the particle changing
based on the values stored. A value greater than 1 indicates an increase in size, which a
value less than 1 indicates a decrease in size. For our steam puff, it doubles in size from
its initial appearance to its untimely death. Please note that the members of the sizes
array can take values ranging from 0 to infinity.
Now, do you have to include four keyframes? No. In fact, you dont have to include any,
but if you dont, Torque defaults to solid white sprites exactly the size you created them,
which dont change their appearance from the time theyre born to the time they die.
There are three variables that control the rotation of a particle. They are spinSpeed,
spinRandomMin, and spinRandomMax. The spinSpeed property can accept values from
-10,000 to 10,000. In this case, we indicate no spin at all, which is 0. On the other hand,
we specify a spinRandomMin of -100, and a spinRandomMax of 100, which allows
Torque to randomly adjust the spin of each particle. Again, this adds variability, and
variability adds character.
The final three properties control how a particles motion changes over time. The first
property, gravityCoefficient, indicates how gravity affects each particle. Particles with a
positive gravity coefficient will fall, while those with a negative coefficient will rise.
Since were working with steam, we want it to rise, hence the negative value. The
constantAcceleration property indicates how fast the particle moves. It can accept either
positive or negative values, indicating forward or backward motion, respectively. Finally,
the dragCoefficient property indicates how much the particle is slowed per second. We
dont have to worry about this much, since our steam has a shelf life less than one second.
However, its useful to demonstrate how particles can be slowed with time.
Are these all the properties for a particle? No. There are more, but these are enough to
get you started.
Now, a particle on its own does nothing. In order to generate a stream of particles, we
need a particle emitter, which, coincidentally enough, is what we cover in the next
section.
23
David J. Sushil
davidjsushil@gmail.com
datablock ParticleEmitterData(steam_column){
particles = steam_puff;
ejectionPeriodMS = 25;
periodVarianceMS = 2;
ejectionVelocity = 5;
velocityVariance = 2;
thetaMax = 10;
thetaMin = 0;
phiReferenceVal = 0;
phiVariance = 10;
lifeTimeMS = 0;
};
This particle emitter is called steam_column. Youll notice that the first property, labeled
particles, accepts our steam_puff ParticleData template. This means steam_column will
generate multiple steam_puffs, based on the remaining properties in its declaration.
First, we have properties which control how our steam puffs are brought into the world.
In this case, ejectionPeriodMS indicates how often a particle is emitted, and
periodVarianceMS gives us some variability. So our column of steam will generate one
puff of steam approximately every 25 milliseconds. Rather frequent, but hey, its steam.
Next, ejectionVelocity and velocityVariance control how fast each particle travels away
from the emitter. Dont get confused between ejectionVelocity in the emitter and
constantAcceleration in the particle itself. These two motion-based properties could be
forcing the particle along two different paths say, up and right.
In fact, thats where thetaMax and thetaMin come into play. These properties essentially
control the emitters aim, at least around the x-axis. They can accept a range from 0 to
180, but of course, the minimum or maximum depends on its counterparts value. So, if
thetaMin is 10, thetaMax must be at least 10. In any case, a value of 0 sends particles
shooting straight up, while a value of 180 sends them straight down.
Likewise, phiReferenceVal and phiVariance control rotation around the z-axis, and can
each take a range of values from 0 to 360. Yes, its rather inconsistent from the theta
controls, but it works nonetheless. In this case, our phi reference value of 0 keeps the
emitter from rotating around the z-axis, but our phi variance value allows for some
wiggle room. Again, this kind of variety adds spice.
Finally, we have a property, lifeTimeMS, which determines how long the emitter itself
exists in the world. Dont be fooled, a value of 0 means it never leaves. Any other value
for this property must be a positive number. So if you wanted your emitter to work for 5
seconds and then quit, set this value to 5000.
Again, these are not all of the properties for an emitter, but they are enough for now.
24
David J. Sushil
davidjsushil@gmail.com
Yeah, its not much, but this one value affects how often particles are ejected, and their
position when ejected. How this happens is not clear to me, so lets assign it the identity
value (1), and move on.
Of course, the transformation data and the emitter property arent assigned within this
template. Instead, they are assigned when an instance of the node is created.
25
David J. Sushil
davidjsushil@gmail.com
Now, when you load up Torque, open your console window, and type the following
command:
addSteam();
That was almost as easy as pie, if pie was six pages long, right?
Animating Particles
To animate a particle, add the following code to your particle datablock (steam_puff,
from page one):
animateTexture
framesPerSec =
animTexName[0]
animTexName[1]
animTexName[2]
// Etc., up to
= true;
32;
= demo/data/shapes/steam/steam_puff-0.png;
= demo/data/shapes/steam/steam_puff-1.png;
= demo/data/shapes/steam/steam_puff-2.png;
animTexName[50]
You must set animateTexture to true for this to work. As for the framesPerSec property,
32 is the threshold at which humans see fluid motion, but feel free to choose any value
between 1 (the default), and 200.
Multiple Particles
If you want your broken pipe to emit jets of water as well as steam, theres not necessarily
a need to create separate emitters. All we need to do is define a new kind of particle, say
water_jet, and link it to steam_column, our particle emitter. Ill skip the water_jet
particle data template (this tutorial is long enough already), and instead Ill show you how
to list multiple particles in the emitter. Check out the code:
particles = steam_puff SPC water_jet;
This line lists two particle templates, steam_puff and water_jet, which are separated by
the keyword SPC, which designates a space.
Moving an Emitter
Normally, particle emitters cannot be moved via code. However, a hack exists which will
allow you to move emitters to your hearts content. It goes like this:
26
David J. Sushil
davidjsushil@gmail.com
Conclusion
Although this tutorial doesnt cover every nook and cranny of Torques particle effects
capabilities, its a good overview. The best thing to do now is practice and experiment.
27
David J. Sushil
davidjsushil@gmail.com
Detail Textures
Detail textures are blended with the environmental textures to create subtle variations at
close range. This helps offset the blurry, patchy nature of the environmental textures.
Details exist in a cone around the player, and diminish as they recede into the distance.
Detail texture files should be 256 x 256 .png or .jpg files with no transparency. In order
to add a detail texture to your map, add the following code into your mission files
TerrainBlock section:
detailTexture = "~/data/terrains/details/detail1";
Change the path and filename to reflect the position of your detail file. Load Torque,
open your mission, and observe the ground.
Bump Textures
Bump textures must be black and white 256 x 256 bitmaps in either .jpg or .png format.
Ideally, such a texture will have a high contrast the higher the contrast, the more
dramatic the effect becomes. To add a bump map to your environment, add the following
code into your mission files TerrainBlock section:
bumpTexture = "~/data/terrains/details/bump.png";
bumpScale = "10";
bumpOffset = "0.075";
zeroBumpScale = "1";
28
David J. Sushil
davidjsushil@gmail.com
Of course, you should change the bumpTexture property to reflect the proper path and file
name for your level.
The bumpScale property affects how stretched the bump texture is on screen. A value of
1 is not stretched at all, which values greater than 1 are stretched, and values less than one
are shrunk.
The bumpOffset property indicates how drastic the effect appears on screen. The greater
the offset, the greater the effect. In general, subtle offsets look best.
The final property, zeroBumpScale, creates a cone around the player where the effect is
present. Any areas outside of the cone will not have the effect applied. This lessens the
strain on the CPU, and quite honestly, past a certain point onscreen, the effect is
indistinguishable from noise, and should therefore be limited.
Once the bump texture properties have been added to your mission file, save it, and load
the mission in Torque. By the way, you can use the World Editor Inspector panel to
tweak the bump settings in the engine without having to restart the engine.
29
David J. Sushil
davidjsushil@gmail.com
Playing Sounds
Generating audio in the Torque engine is a snap. There are very few hoops to jump
through when setting up and playing sound effects.
Descriptions
While it may seem confusing at first, playing a sound requires establishing two objects in
memory an AudioDescription and an AudioProfile. (Yes, I know profile and
description mean the same thing. Tell it to the people who created Torque.)
Lets start with descriptions, since in many ways they are a parent to our profiles. An
AudioDescription is a set of properties that determines how sounds are played in
general. That means that several sounds can share the same AudioDescription.
This will become clear when we look at some code, which well do in just a moment.
First, we need to establish how our sound will be used within Torque. Will it be
localized to our computer or accessible across a network? The answer to this question
will determine which keyword we use to create our sound. Localized audio is created
using the new keyword, whereas networked audio is created using the datablock
keyword, like so:
// Create a localized sound effect, which
// can only be heard on your computer.
new AudioDescription(localized_3D_sound){
volume = 1;
isLooping = false;
is3D = true;
referenceDistance = 1;
maxDistance = 10;
type = $DefaultAudioType;
};
// Create a networked sound effect, which
// can be heard by multiple computers.
datablock AudioDescription(networked_3D_sound){
volume = 1;
isLooping = false;
is3D = true;
referenceDistance = 1;
maxDistance = 10;
type = $DefaultAudioType;
};
Descriptions, whether created via new or datablock, take one argument the name of
the description. In the first description, its localized_3D_sound, and in the second, its
networked_3D_sound.
Lets look at the properties within these descriptions. First we have volume, which is
set to 1, or 100% of the actual volume of the sound file in question. Values greater than 1
will increase the volume of the sound, while values less than 1 will decrease it.
30
David J. Sushil
davidjsushil@gmail.com
Next, the property isLoop determines if the sound in question loops, or plays one time.
Music will probably loop, so a value of true is appropriate. On the other hand, explosions
should only play one time, so a value of false is appropriate.
The is3D property determines whether the sound is ambient or specific to a particular
source. For example, the sound made when you click on a button in a popup window is
ambient it exists outside of the game world. Music is also ambient (unless tied to a
source in-game, such as a phonograph or radio). For these effects, wed set the is3D
property to false. On the other hand, a door opening, glass shattering and dynamite
exploding are sounds tied to a particular source. Their volume and stereo position may
change depending on where they happen relative to the player. In this way, the is3D
property should be set to true.
The next two properties, referenceDistance and maxDistance, are only relevant
if our sound is a 3D effect. The first property, referenceDistance, establishes the
distance at which the audio is played at full volume. In this case, the sound effect will be
played at full volume as long as it occurs within 1 world unit (about 3 meters) of the
player. maxDistance indicates at what point the volume drops to zero. In this case,
the audio will not be heard if it occurs more than 10 world units from the player.
Finally, we have the type property, which indicates what channel the audio will play
from. Torque has three built-in channels, but others can be created. Our example
description uses $DefaultAudioType, which is channel 0. Two other channels are
$GuiAudioType (channel 1), used for the graphical user interface; and
$SimAudioType (channel 2), used for in-game mission objects.
Profiles
Notice how neither of these descriptions reference a particular file? Thats what our
AudioProfile is for. An AudioProfile essentially specifies which sound to play,
and which AudioDescription to play it with.
Profiles, like descriptions, also fall victim to the stigma of being either localized or
networked, and so we again need to determine if we use the new or datablock
keyword. Lets looky codey:
new AudioProfile(explosion){
filename = demo/data/sounds/explosion.ogg;
description = localized_3D_sound;
};
Profiles accept one argument the name of the profile which in this case is
explosion.
Profiles have two properties, filename and description. Filename is simply the
path to our intended sound file. Description is a reference to an
AudioDescription, in this case localized_3D_sound, which we created a few
pages ago.
31
David J. Sushil
davidjsushil@gmail.com
Simple enough!
Wow, that was ridiculously easy! alxPlay() takes one argument, the name of the
AudioProfile for our sound. It returns an ID number, which we store in %obj. This
only works, however, for localized, ambient sounds. Networked or 3D sounds must be
played some other way.
32
David J. Sushil
davidjsushil@gmail.com
datablock ProjectileData(arrow_shaft){
// Here we would normally list all of this
// datablocks properties, however we are
// really only interested in
sound = whistle;
};
Here, we create an arrow as a projectile. As the arrow travels through the air, it generates
a whistling sound.
datablock ExplosionData(bloody_arrow_hit){
// Here again wed normally list all of this
// datablocks properties, however we are
// really only interested in
soundProfile = messy_thump;
};
Here, we create an explosion actually, an explosion of gore when an arrow strikes its
victim (yuck!). When the explosion occurs, it plays a messy thump sound, indicated by
the soundProfile property.
Of course, if were playing music, we might want to store its ID number in a global
variable, like $music_ID or something catchy.
33
David J. Sushil
davidjsushil@gmail.com
The Body
OK, lets begin with a simple enough character a Scuttlebot. Scuttlebots resemble
horseshoe crabs. They are four-legged robots with a tail and a carapace-like frame. They
are also fairly easy to model in 3D Studio Max, so lets get down to it.
Start by clicking the Box tool in the Create panel. Using the Keyboard Entry rollout,
create a box that is 1 x 1 x 1.
In Torque world units, this is about three cubic feet. Its incredibly tiny. In fact, its so
tiny that 3D Studio Max has trouble working with models this small! You will notice
undesirable behavior mostly interface inconsistencies quite often as we work with
character models at this scale. This leaves us wondering, why dont we just build the
model at whatever size we want, and then scale it down later? Unfortunately, doing so
can cause behavior that is even worse than.
Click on the Modify panel, and name your box scuttlebot. Under the Parameters
rollout, give the box 3 height, 3 length, and 3 width segments. This will allow us to
modify the shape of the box. Convert our scuttlebot to an editable mesh by right-clicking
on it, and selecting Editable Mesh from the Convert submenu. You should have a model
that looks like Figure 1.
Now we can begin deforming the shape to our liking. Before we do so, make note of the
world axis in your Perspective viewport. The side of the box that is facing towards the
positive y-axis is the front of our model. Its important that we keep track of where our
model is facing, because Torque doesnt like objects that have been rotated post hoc.
34
David J. Sushil
davidjsushil@gmail.com
35
David J. Sushil
davidjsushil@gmail.com
Congratulations! Youve created a scuttlebot body. I wish I could say the worst is behind
us, but sadly, it is not. Lets take a moment to save our work, lest disaster strike us
unprepared.
The Legs
Our scuttlebot has four cylindrical legs which move in a shuffling sort of way. In part
due to their inconspicuous placement under the model, we can skimp a bit on precise
animation. This is, after all, more a tutorial on interfacing with Torque than it is a treatise
on picture-perfect animation.
Begin by creating a cylinder that is 0.25 units
high, with a radius of 0.1. You can do so by
clicking on the Create panel and choosing the
Cylinder tool. I recommend using the Keyboard
Entry method of creation creating it by hand at
this scale in 3D Studio Max can be problematic.
Under the Modify panel, give the cylinder 1
height segment and 12 sides. By default, the
cylinder will be placed dead-center in your
model. Using your Select and Move tool,
position the cylinder roughly where the frontright leg should be. The numerical position of
this front-right leg is (-0.35, 0.35, -0.25), if
youre keeping score. Check out Figure 6 for a
reference.
Next, we need to clone this leg three times, positioning each clone where a remaining leg
should be. We can do so by clicking on the Select and Move tool, holding down the shift
key, and clicking and dragging on our original leg. Doing so pulls out a cloned copy of
the leg. You will be presented with a Clone Options dialog box simply make sure the
Object is set to Copy, and click OK. The remaining legs should be positioned according
to the following coordinates:
(0.35, 0.35, -0.25)
(0.35, -0.35, -0.25)
(-0.35, -0.35, -0.25)
At this point, your model will look similar to Figure 7. If so, we can attach the legs to the
body. Do so by first selecting your scuttlebot body. From the Modify panel, click Attach
List, located in the Edit Geometry rollout. Select all the cylinder objects from the list
these are our legs and click the Attach button. Now, our four leg cylinders have been
merged into the body mesh, and our scuttlebot is one solid piece.
If youd like, you can take the time to further refine your scuttlebot model, but for our
purposes, its lookin good, and we can move on to animating the object.
36
David J. Sushil
davidjsushil@gmail.com
Adding Animation
Torque has some nifty animation options available. First and foremost, if youre
animating a humanoid character, theres a better than average chance you dont even need
to worry about animation. The reason for this is that Torques poster boy and stock
character, Kork, comes with preset animations stored outside of his .DTS model file.
These stock animations can be utilized by any biped character that possesses a similar
bone structure to Kork.
Our scuttlebot, on the other hand, is not a biped (its a quadruped), so we need to create
our own custom animations. We begin by setting up a skeleton.
37
David J. Sushil
davidjsushil@gmail.com
conventions Torque expects for such an occasion. The names of bones in Torque are
borrowed from 3D Studio Maxs Biped character, so a handy reference is available at any
time from the Create panels Systems mode. In general, each bone begins with a Bip01
prefix, such as Bip01 Pelvis. Bip01 is always followed with a space (one of the few
times a space is allowed to be exported from a 3D Studio Max model to Torque), and
then the name of the bone. Repetitive bones, such as vertebrae, follow a pattern like this:
1. Bip01 Spine
2. Bip01 Spine1
3. Bip01 Spine2
4. Bip01 SpineN (where N is the total number of bones in the chain, minus one)
The only bones that dont follow this naming convention are special objects, three of
which well address in a moment. Lets go ahead and name these bones, like so:
1. Bip01 Pelvis
2. Bip01 Spine
3. Bip01 Spine1
4. Bip01 Spine2
5. Bip01 Head
6. eye
That last one isnt a mistake. Eye is a special bone which Torque uses to control the
placement of the camera when in first-person mode. By linking eye to our Bip01 Head
bone, we can make sure that when the character moves its head, the camera moves with
it.
Before we proceed, check to make sure that this bone chain is not only centered in the
Top viewport, but passing through the middle of the bottom row of polygons in your Left
viewport.
Now, lets set two bones for our scuttlebots
front right leg. These bones are Bip01 R
UpperArm and Bip 01 R ForeArm (notice the R
qualifier on the opposite side, well use L
instead). The first bone will begin at the top of
the cylindrical leg and end about halfway down,
at which point the second bone begins and
continues to the bottom of the cylinder. Be sure
to check your Top viewport as well, and
position the bones central to the cylinder according to that perspective as well. Use
Figure 9 as a reference if youre unsure of how to proceed.
Just like we did with our original leg cylinders, we can clone this bone chain and position
the copies in their respective locations. The names of the remaining bones are:
1. Bip01 L UpperArm and Bip01 L ForeArm
2. Bip01 R Calf and Bip01 R Foot
3. Bip01 L Calf and Bip01 L Foot
38
David J. Sushil
davidjsushil@gmail.com
The final bone chain is the tail. Add three bones, one for each of the extrusions we
created. Name them Bip01 Tail, Bip01 Tail1, and Bip01 Tail2, respectively. When you
are finished, your bone structure should look like that which is pictured in Figure 10.
Finally, we must link our bone chains together
into one skeleton. We do so by first selecting
the Bip01 Pelvis bone. In the Bone Tools
dialog, click on the Connect Bones button.
Moving the mouse over your active viewport,
you will notice a dashed line that follows your
cursor around. Click on the Bip01 L Calf bone,
and a new bone will appear. Rename the bone
Bip01 L Thigh. Reselect the pelvis, and click
the Connect Bones button again. This time,
click on the Bip01 R Calf bone. Again, a new
bone will appear. Rename it Bip01 R Thigh.
Repeat this process for the font legs, connecting
the Bip01 Spine2 bone to the UpperArm bones.
The connecting bones should be renamed Bip01
L Clavicle and Bip01 R Clavicle. Finally, create
a Bip01 TailBase bone that connects the pelvis
to the tail. Your skeleton will now resemble the
one pictured in Figure 11 (Ive hidden the model
so you can see the entire bone structure).
Were almost done with dem bones. We need
only create one more chain, consisting of two
bones: unlink and cam. These bones dont
animate our character at all. Instead, Torque
uses them to position the over-the-shoulder camera when not in first-person mode. To
create this chain, click on the Create Bones button, and in your Left viewport, create a
bone which begins just before the back legs, and rises up through our scuttlebot and
above the shoulders. After setting unlink, right click to set cam. Name both of the bones
accordingly. Your finished model will look like the one pictured in Figure 12.
39
David J. Sushil
davidjsushil@gmail.com
Now, the model acts like skin around the bones. We can test this by clicking on our
Bip01 L ForeArm bone, and using the Select and Move tool to reposition it. If our Skin
modifier was setup properly, the front left leg should move in unison with the bone.
You may notice that the front right fender around the leg also moves. Thats OK for now.
This is not a tutorial on animation, so much as it is a tutorial on properly interfacing with
Torque. We can always refine the animation later.
PS: Save your work!
Keyframing Animation
With our recently skinned scuttlebot (that sounds gross, doesnt it?), we can begin to craft
animation for use with Torque. We will create two animation sequences a default idle
animation, and a walk cycle animation. Lets begin with the more difficult walk cycle.
For simplicitys sake, select the eye, cam, and unlink bones, and hide them from the scene
by right-clicking and selecting Hide Selected from the popup menu. Now, were only
working with the bones that deform our model.
To animate our model, well need to use the timeline at the bottom of the screen. See the
button marked Auto Key? Click it. Our timeline becomes red, which serves as a warning
that were no longer working with a static model, but a dynamic model that changes over
time. If youre done animating at any point, click the Auto Key button again to return our
timeline to its normal gray color.
Next, click on the Bip01 R ForeArm bone. Were going to animate the bone swinging
forward, then back, then forward again. Using your Select and Move tool, position the
bone slightly closer to the head of the model, which will deform the front right leg into a
forward-stepping position.
When you are satisfied with the placement, click the button below the timeline that looks
like a key. Notice how a box has been added to the timeline on frame 0? Thats a
keyframe. 3D Studio Max and Torque create animation by computing the look of the
model based on transitions between keyframes. This saves us time instead of animating
each frame, we animate a few keyframes, and Max and Torque do the rest of the work for
us. Fancy that!
Now, we need to drag our timeline slider out to about frame 15. Using your Select and
Move tool again, swing that front leg bone into a backwards position. Set the keyframe
by clicking the key button below the timeline.
Now, you can test your rudimentary animation by dragging the timeline slider back and
forth between frame 0 and 15. If youve followed this tutorial correctly, the leg should
swing forward and backward with the slider.
40
David J. Sushil
davidjsushil@gmail.com
Preparing to Export
Its important that we properly prepare our model for use with Torque. There are a
handful of hoops to jump through, but once youve practiced the routine a few times,
41
David J. Sushil
davidjsushil@gmail.com
those jumps will begin to make sense. By the way, whenever Torque starts to make
sense, immediately shut down your computer, visit a pleasant park in your city or town,
and consider therapy.
Initial Preparations
In order to export your model, youll need to access the Torque DTS Exporter utility. If
youve never used this tool before, I recommend reading the Importing Assets From 3D
Studio Max tutorial.
Like every other model, we need to renumber and embed the shape. So make sure youve
selected our scuttlebot, and click the Renumber Selection button from the Utilities panel.
In the Renumber dialog box, type 2 and click OK. This renames our scuttlebot to
scuttlebot2. Then, click the Embed Shape button, but only one time. This button
creates a few helper objects in our scene Start01, Base01, and Detail2. It otherwise is
not apparent that it worked, so check your Select by Name tool to make sure theyve been
added.
Next, we need to add a bounding box to our scene. Using the Box tool from the Create
panel, create a shape that encompasses the entire model. Name this box bounds. Its
important to note that we should not resize bounds using the Select and Uniform Scale
tool. Although the size will change in 3D Studio Max, it will not change in Torque. This
is because scaling geometry is much like adding a modifier to the object, which Torque
doesnt understand. Instead, use the properties in the Parameters rollout, located within
the Modify panel, to change the size of our bounding box.
42
David J. Sushil
davidjsushil@gmail.com
Animation Sequences
Next, we need to add a few helper objects which tell Torque where to locate our
animation sequences. These helper objects can be accessed from the Create panel by
clicking on the Helpers button (it looks like a white box with a circle in it). From the
dropdown list just below the Helpers button, select General DTS Objects. Next, click the
Sequence button, and create a sequence objects in the Perspective viewport by clicking
and dragging. Name the object run. Run is a special name that Torque recognizes.
Run animation is played you guessed it whenever the player causes his avatar to run.
Create a second sequence object and call it root. Root is the special name Torque
understands for idle animation. There is a whole list of these special animation names,
and you can of course create your own. Its best to plug in to the engine as often as
possible, so utilizing existing names for animations is highly recommended.
A comprehensive list of the existing animation names is available in the player.cs file,
which is located in demo/data/shapes/player/. What follows is a partial list of some of the
more common animation names, and a brief description of their purposes:
43
David J. Sushil
Name
root
run
back
side
fall
land
jump
standjump
deathN
davidjsushil@gmail.com
Description
Idle animation the avatar is not doing anything
Moving forward
Backpedaling animation (running backwards)
Strafing animation
Falling animation
Landing animation
Jumping animation when the avatar is moving
Jumping animation when the avatar is standing still
Death animation (replace N with a number from 1 to 10)
Once we have created our sequences, we must take one final step to let Torque know
where they begin and end on our timeline. We do so by opening the Curve Editor, which
is accessed by clicking the Curve Editor (Open) button on the toolbar, which is located
left of the Schematic View (Open) button.
With the Curve Editor window open, click the plus sign to the left of the Objects item on
the left side of your screen. Locate root, and expand it as well. Within root is an item
called Object (Sequence II). Expand it, and click on Sequence Begin/End. Now, we need
to add keys on the chart which designate where the root animation begins and ends. To
do so, make sure you have the Add Keys button highlighted in the toolbar, and click on
the timeline at positions 31 and 41. If you accidentally misplace a key, you can move it
by highlighting the Move Keys button.
Next, repeat the above steps with the run animation, but set the beginning and end keys to
0 and 30. When youve done so, you can close the Curve Editor window. Were almost
done!
One final note on animation sequences you need to designate whether your animation
loops or whether it plays only once. For example, running is a cyclic sequence, whereas a
death animation is non-cyclic. It would look rather silly if our character died over and
over again, or if our character ran through one walk cycle and then glided everywhere.
We can designate a sequence as cyclic or non-cyclic by toggling the Cyclic Sequence
check box in the Modify panel of any given sequence object. By default, all sequences
are cyclic, so we dont have to worry about root or run.
David J. Sushil
davidjsushil@gmail.com
goes as planned, you should be able to open Torque and immediately see that your player
model has replaced Kork. If so, congratulations on building your very own scuttlebot! If
not, well, the next section is for you. Good night and good luck
Troubleshooting
There are any number of ways to screw up a character model in Torque, and if youre
reading this section, you most likely discovered at least one of them. Take a deep breath.
Ill walk you through some of the more common errors you may encounter.
45
David J. Sushil
davidjsushil@gmail.com
46
David J. Sushil
davidjsushil@gmail.com
Unhide Door, and modify its dimensions so it is not quite as thick as the surrounding
frame (most doors aren't flush with their frames, after all).
We also need to create a doorknob for each side of the door. This can be done by placing
a couple of boxes, cylinders, and spheres in the proper positions. You can see the
finished door (with textures applied) in Figure 3. Name the additional objects Panel,
Shaft, and Knob. To designate which side they belong on, append the names with Front
or Back. Remember that adding trailing numbers (like 2, 3, 4, etc.) designates levels of
detail, and may result in objects missing in your game.
To make things easier for animation, go ahead and group Door and your doorknob objects
by selecting them all and selecting Group > Group from the menu bar. Name the
selection Doorswing.
We've now completed our door model. In the next section, we'll look at how to animate it
properly.
Animation
Now that we have our door object constructed, we need to create two animation
sequences - one where the door swings open, and the other where the door swings closed.
Before we begin keyframing these animations, we need to edit the center of gravity for
Doorswing. At the moment, if we rotate the door, it will rotate around the center, much
like a revolving door would. This is because the Pivot point of our door is aligned to its
center. To align the Pivot point to the doorframe, select Doorswing, and from the
Hierarchy panel, select Affect Pivot Only. The Pivot will appear in the scene as a thick
red, blue, and green tripod. You can then use your Select and Move tool to align the
Pivot to the edge of Frame. With the Pivot repositioned, turn off Affect Pivot Only.
We're now ready to animate the door.
As with any animation, we begin by turning on Auto Key mode, located below the
timeline in 3D Studio Max. Select Doorswing. We will set four keyframes on the
timeline, representing the door at four stages:
Frame
0
35
36
71
Physical Description
The door completely closed
The door completely opened
The door completely opened
The door completely closed
Sequence Position
Start of the Open sequence
End of the Open sequence
Start of the Close sequence
End of the Close sequence
Once you have finished animating these sequences, be sure to turn Auto Key off otherwise additional changes to your scene might be contingent on the timeline, which is
never good. By the way, if you are unfamiliar with how to keyframe animation, please
see Animating Objects in Torque for additional instructions.
47
David J. Sushil
davidjsushil@gmail.com
In addition, we need to create a bounding box (bounds), which encompasses the all
visible objects in our scene. Bounds is not linked to anything in our Schematic View, and
it may be hidden from sight once created.
The next step is to create our collision meshes - Collision-1 and Col-1 - and animate them
to match the motion of Doorswing. From the Create panel, select the Box tool and create
a primitive that roughly matches the size of Doorswing. Name it Collision-1, and convert
it to an Editable Mesh. Clone it, and name the copy Col-1. Animate both shapes using
the same techniques outlined in the Animation section above - including moving the
Pivot point.
When you have finished, be sure to link Collision-1 and Col-1 to their parents in the
Schematic View - base01 and start01, respectively.
Finally, we can create our sequence objects. These objects will be named Open and
Close, and they correspond with the animations we created previously. Use the Sequence
button - located in the General DTS Objects dropdown section of the Helpers panel. It is
very important that you set both sequences to be non-cyclic - that is, uncheck the Cyclic
Sequence box in the Modify panel for both sequences.
48
David J. Sushil
davidjsushil@gmail.com
You also need to use the Curve Editor window to designate where each sequence begins
and ends on the timeline. Do so by clicking the Curve Editor (Open) button on the
toolbar. Find the Open sequence, and drill down through its menus until you find the
Sequence Begin/End controller. Set keys on the timeline at 0 and 35. Repeat the process
for the Close sequence, but set the keys to 36 and 71.
At long last, we are ready to export our model. Of course, it won't function properly yet,
but we can at least check it out in the Torque Game Engine and see if it looks nice.
Export the shape by click the Whole Shape button in the Utility panel. Save the model in
your Torque file hierarchy, preferably in demo/data/shapes/door/door.dts.
Next, we'll explore how to code door behavior in TorqueScript.
These statements create two datablocks - a StaticShape called door, which references our
door model, and a Trigger called doorTrigger, which we will use to execute our
animations in the engine.
Next, we need a function that will create instances of these datablocks. We'll call this
function addDoor(), and it looks like this:
49
David J. Sushil
davidjsushil@gmail.com
function addDoor(){
%doorObj = new StaticShape(){
position = "233.3 -206.45 191";
scale = "0.05 0.0575 0.0505";
rotation = "0 0 1 -90";
datablock = door;
};
%obj = new Trigger(){
position = "232.3 -205.25 191";
scale = "1 1 1";
rotation = "0 0 0 0";
dataBlock = doorTrigger;
polyhedron = "0.0000000 0.0000000 0.0000000 2.0000000
0.0000000 0.0000000 0.0000000 -2.0000000
0.0000000
0.0000000 0.0000000 2.0000000";
doorID = %doorObj;
};
}
These two statements basically instantiate copies of our datablocks, assigning them to a
particular location in the level. You will of course need to alter the position, scale, and
rotation of both objects to correspond with their location in your level.
Notice our new Trigger stores the ID number of the StaticShape door we just created. If
you wanted, you could create a whole network of doors in your level, simply by copying
these two statements and altering the transform data in each (position, scale, rotation). So
long as your Trigger stores the ID number of its accompanying StaticShape, the system
will work.
Now that we have a door and trigger in our level, we need code to animate them. The
code is as follows:
function openDoor(%id){
%id.playThread(0, "open");
}
function closeDoor(%id){
%id.playThread(0, "close");
}
Each function takes one argument - the ID number of the door - which it uses to call
either the Open or Close animation sequence we created in 3D Studio Max.
Now all we need are the trigger events which call these two functions. They are:
50
David J. Sushil
davidjsushil@gmail.com
This file is finished. You can test it in Torque by loading your mission, pulling up the
console (~), and running the following two lines of code:
exec("demo/data/shapes/door/door.cs");
addDoor();
By walking up to the door, it should open. When you walk away, it should close.
Mission accomplished!
51
David J. Sushil
davidjsushil@gmail.com
52
David J. Sushil
davidjsushil@gmail.com
This may be useful, depending on how youre planning on using the baking technique.
For example, if youre creating a desk object with all sorts of widgets sitting on it, then
we want to make sure that each widget leaves a dark mark on the surface of the desk.
Of course, one light probably isnt enough. Think about when youre getting your portrait
taken at a studio. You have ambient lighting from the room in general, as well as a
spotlight off to the side somewhere, and the flash from the camera. Thats a minimum of
three light sources. Likewise, we may need to light our object from various angles.
When youre finished placing and adjusting lights, were ready to move on to the actual
baking of textures.
53
David J. Sushil
davidjsushil@gmail.com
Overview
The code we will work on in this exercise is only one possible way of adding AI to your
project. I state this only to alleviate any future frustrations you might encounter in using
the code provided below. Its a start a jumping off point for your future masterpieces
but its not a total solution. In many cases though, it should work in a pinch.
What we want to create is a system where AI bots can be spawned with a single line of
script, which, for example, could be called when the mission begins, or when the player
passes through a trigger.
We also want to establish a system that allows for multiple AI styles. That is, if all bots
acted the same way, the game would become boring. This tutorial will demonstrate how
we can control (and even switch) AI styles.
Finally, we need a way to periodically update our AI. Thankfully, Torque makes this easy
with the schedule function, which we will utilize in this example.
Adding a Bot
Torque comes with preset dummy characters known as PlayerBodies. These are
represented by Torques poster boy, Kork the Orc (quite a sense of humor there at
GarageGames, eh?). Much of the PlayerBody functionality can be found in aiPlayer.cs, a
file located in the demo/server/scripts/ folder. I recommend looking over this file for
additional help with activites such as pathfinding. Since our AI bots are more of a
deterministic variety, well forego much of this file for a more simplistic approach.
To add a bot, use the following code:
%bot = new AIPlayer(){
datablock = PlayerBody;
position = "100 100 100";
};
In this way, the ID number of the bot will be stored in %bot, we inherit characteristics
from the PlayerBody datablock, and place the bot at 100, 100, 100 on the map. Of
course, that position needs to be updated to reflect your particular level. This will spawn
an instance of Kork in the level, however he wont do much. He just kinda starts there,
all awkward and stuff.
Lets take this code a bit further and add it to a custom function, called addBot. AddBot
takes four arguments, an AI style, and position information.
54
David J. Sushil
davidjsushil@gmail.com
Now, if you want to add a bot to your level, you can simply call the addBot function with
a style and position for each bot, like so:
addBot(0, 100, 100, 100);
Still, no action on the part of Kork. But we have added two new properties. The first is
botID, which we will use throughout the exercise. Currently, its set to zero. After the
new block that creates our bot, add the following line:
%bot.botID = %bot;
Still, no Kork just sits there. Thats where the updateBot function comes in handy. Well
write the function in the next section, but for now, add the following line:
updateBot(%bot.botID);
Updating the AI
The updateBot function takes one argument, the ID number of an AI bot. In this function,
well make sure the agent always has ammunition for the crossbow. Also, well
determine how far the bot is from the player. Then, well execute an IB behavior based
on style. Finally, well schedule the updateBot function to be called sometime in the near
future.
First, lets create a new function called updateBot, like so:
function updateBot(%id){
// CODE GOES HERE
}
Within the function, you can give the bot ammunition for the crossbow by calling:
%id.incInventory(CrossbowAmmo, 1);
This ensures at least one shot per update, however depending on your implementation,
you may want to adjust the number of shots between executions.
55
David J. Sushil
davidjsushil@gmail.com
Next, we need to determine how far away the player and the bot are from each other.
This is sadly more complicated than it needs to be in Torque. The engine stores position
information as a single string, instead of three separate numerical values. To parse out
each x-, y-, and z-coordinate, we have to use the getword function, like so:
%botX = getword(%id.getTransform(),0);
%botY = getword(%id.getTransform(),1);
%botZ = getword(%id.getTransform(),2);
%playerX = getword($gspPlayer.getTransform(),0);
%playerY = getword($gspPlayer.getTransform(),1);
%playerZ = getword($gspPlayer.getTransform(),2);
For the moment, well skip executing AI behaviors, and instead look at how we can recall
this function automatically. We do so by using the schedule function, like so:
schedule(200, 0, updateBot, %id);
The first argument in this function represents how many milliseconds to wait before
calling the third argument, which in our case is the name of our updateBot function. Any
arguments after the third represent arguments for the function to be called (hence %id,
which is the sole argument for updateBot). So, a translation of this statement in plain
English would read In 200 milliseconds, call updateBot for the bot indicated by %id.
So now we have a function that gives the bot ammo, determines its distance from the
player, and schedules another update in the future. In the next section, well explore AI
behaviors.
Styles
After we determine a distance from the player, we will steer the behavior of the bot
according to its style. We will add three styles of AI to this example a grunt, which
pursues the player, and when in close range, stops to fire; a sniper, which remains
stationary and fires if the player is within a wide range; and a berserker, who always
pursues the player, firing as it goes regardless of distance.
Within the updateBot function, we will execute these behaviors using a switch statement,
which looks like this:
switch (%id.style){
// CASES GO HERE
}
Within the switch statement, the property %id.style will be evaluated, and a case will be
selected. Each case represents a particular style of bot AI. Going in reverse order, the
berserkers case looks like this:
case 2: // BERSERKER AI
%id.fire(true);
56
David J. Sushil
davidjsushil@gmail.com
%id.setMoveDestination($gspPlayer.getTransform());
%id.setAimObject($gspPlayer);
The fire function, which takes a Boolean value for an argument, determines whether the
bot is firing or not. It is not a request for a single shot, but rather a toggle switch that sets
the bot to fire indefinitely, or remain passive. The setMoveDestination function takes the
players position as an argument, and tells the bot to move towards that spot on the map.
Finally, the setAimObject function tells the bot to always aim towards the player.
This is a very simple AI bot, which always follows the player, and always shoots.
Though simply, hes pretty darn lethal, and you might want to consider going easy on the
number of berserkers in your game!
A more realistic opponent is the grunt:
case 0: // GRUNT AI
if (%distance < 15){
%id.fire(true);
%id.setMoveDestination(%id.getTransform());
}else if (%distance > 30){
%id.fire(false);
%id.setMoveDestination($gspPlayer.getTransform());
}
%id.setAimObject($gspPlayer);
As you can see, here we are utilizing the distance variable we created previously. If the
player is more than 30 units away, put your gun away and chase him; if he is less than 15
units away, stop and shoot.
Finally, we have the sniper, who would do well to stay out of range of the player
perhaps in on the top of a building somewhere:
case 1: // SNIPER AI
if (%distance < 100){
%id.fire(true);
}else{
%id.fire(false);
}
%id.setAimObject($gspPlayer);
This case tells the AI, if the player is within 100 units, fire; otherwise, put the gun away
and wait.
The Players ID
Finally, you may have noticed a global variable in there called $gspPlayer. We need to
add this variable ourselves. Although there are other ways of tracking the players ID, I
find this method most useful.
Open the demo/server/scripts/ folder, and edit the file named game.cs. Find the
createPlayer function, and add the following boldfaced line, just below the new Player
command, and before the call to MissionCleanup:
// Create the player object
%player = new Player() {
57
David J. Sushil
davidjsushil@gmail.com
dataBlock = PlayerBody;
client = %this;
};
$gspPlayer = %player;
MissionCleanup.add(%player);
Now, whenever you want to refer to the player, you can use the global $gspPlayer
variable.
Complete Code
Because were dealing with quite a bit of code, heres the total package. I would
recommend placing it in a single .cs file, and adding a reference to it in the
onServerCreated function, within demo/server/scripts/game.cs.
// FUNCTION FOR ADDING A SINGLE BOT AT (X,Y,Z) ON THE MAP
function addBot(%style, %x, %y, %z){
%bot = new AIPlayer(){
datablock = PlayerBody;
position = %x @ " " @ %y @ " " @ %z;
botID = 0;
style = %style;
};
%bot.mountImage(CrossBowImage, 0);
%bot.botID = %bot;
updateBot(%bot.botID);
}
// UPDATES THE BOT AI
function updateBot(%id){
%id.incInventory(CrossbowAmmo, 1);
%botX = getword(%id.getTransform(),0);
%botY = getword(%id.getTransform(),1);
%botZ = getword(%id.getTransform(),2);
%playerX = getword($gspPlayer.getTransform(),0);
%playerY = getword($gspPlayer.getTransform(),1);
%playerZ = getword($gspPlayer.getTransform(),2);
%distance = mSqrt((%playerX - %botX)*(%playerX - %botX) + (%playerY
- %botY)*(%playerY - %botY));
switch (%id.style){
case 0: // GRUNT AI
if (%distance < 15){
%id.fire(true);
%id.setMoveDestination(%id.getTransform());
}else if (%distance > 30){
%id.fire(false);
%id.setMoveDestination($gspPlayer.getTransform());
}
%id.setAimObject($gspPlayer);
case 1: // SNIPER AI
if (%distance < 100){
%id.fire(true);
}else{
%id.fire(false);
}
%id.setAimObject($gspPlayer);
case 2: // BERSERKER AI
%id.fire(true);
%id.setMoveDestination($gspPlayer.getTransform());
58
David J. Sushil
davidjsushil@gmail.com
%id.setAimObject($gspPlayer);
}
schedule(200, 0, updateBot, %id);
59
David J. Sushil
davidjsushil@gmail.com
David J. Sushil
davidjsushil@gmail.com
16. Ceilings are created by cloning your floors and aligning them with the tops of
your walls. Select all floors at once using Maxs Select by Name tool in the main
toolbar.
17. Create doors by clicking on the Create Door-Window button in the GLB interface.
Click on a wall to place a door. When you are finished with the tool, right click to
turn it off.
18. Carefully adjust the position of your doors to your satisfaction.
19. It is very important to note that all extraneous doors must be deleted by first selecting them, and then clicking the Delete key in the GLB interface. Deleting
them using any other method will result in errors during export.
20. Apply material textures in the form of bitmaps to all surfaces in the model.
NOTE: Each material must be named identically to the bitmap it implements (excluding the file extension). You also need to include create a default texture
called notex, which will be applied to all surfaces without a texture. This is important, because when we create door spaces in QuArK, each doorframe will need
the notex texture applied to it.
21. If your interior does not have any doors or windows, you must create omni lights
in 3D Studio Max, or else the interior will be pitch black in Torque.
22. Export the scene to .MAP format by clicking the Export to Map! button in the
GLB interface. (Use all default settings when prompted)
23. Now, we will use QuArK 6.3 to subtract doors and windows from your interiors
walls. Open QuArk. Choose Open from the File menu, and locate your .MAP
file.
24. Your interior file may be very small in the viewports, so click on the Zoom button
in the toolbar.
25. Using your mouse, click on one of the door objects you created in Max. You may
have to click on this door several times before it is selected. This is because you
are cycling through elements from the top of your interior to the bottom, and the
door may be third in line.
26. With the door selected, choose Brush Subtraction from the Command menu. This
will subtract the door space from the wall in which it is embedded.
27. Using Brush Subtraction will deselect your door. Click to reselect your door.
28. Hit the Del key on your keyboard to remove the door object, leaving just the wall
and a new door space.
29. Repeat steps 25 through 28 for each door in your .MAP file.
30. When you are finished, save your .MAP file by choosing Save from the File
menu.
31. Now, we need to convert our interior to .DIF format using Map2DIF Plus. Make
a copy of Map2DIF Plus and place it in the same folder as your .MAP file. This
folder should also contain all textures, including your notex image file.
32. Map2DIF Plus is a command line processor, so we have to manually point it to
our .MAP file. Do this by creating a shortcut to the Map2DIF_plus.exe file.
33. Right click the shortcut and hit Properties.
34. In the Target field, add the name of your file (including the .MAP extension) to
the end of the line (so it should read something like:
C:\myhouse\map2dif_plus.exe myhouse.map
35. Close the Shortcut Properties dialog, and run the shortcut by double-clicking it.
61
David J. Sushil
davidjsushil@gmail.com
36. After a brief pause, you should see two new files, console.log and a .DIF file.
37. Copy the .DIF file and all textures into the Torque file hierarchy (preferably into
the data/interiors folder).
38. Open your .MIS mission file, and add the following code:
new SimGroup(Buildings) {
new InteriorInstance() {
position = "230.8 -200 191.005";
rotation = "1 0 0 0";
scale = "1 1 1";
interiorFile =
"~/data/interiors/exampleroom/exampleroom.dif";
useGLLighting = "0";
showTerrainInside = "0";
};
};
39. Of course, replace the interiorFile pathway to reflect the location of your .DIF interior. Also, the position, rotation, and scale parameters may need to be altered.
40. All subsequent interiors should be created as separate new InteriorInstances within the SimGroup Buildings. That is, the line:
new SimGroup(Buildings)
Troubleshooting
1. If your interior does not appear in Torque, first check that the position and scale
parameters are correct in your .MIS file. A good strategy for testing interiors is to
place them at your players spawn point. Look for the PlayerDropPoints SimGroup in your mission file, and copy the position parameters. Also, try increasing
the scale a larger building is easier to bump into.
2. If your interior structure has giant black blocks where there should be doors, you
most likely forgot to delete the door structures in QuArK. Repeat steps 23
through 30.
3. If your door frames dont have a texture, you most likely forgot to create a notex
texture. This texture should be a .jpg or .png file. You must create it as a material
in 3D Studio Max, and give it the name notex in the Material Editor. The notex
texture should also be located in the same folder as your .MAP file when you convert it to .DIF, and in the same folder as your .DIF when you load it into Torque.
4. If your building is too big or too small, adjust the scale parameter for your InteriorInstance in the .MIS file. A scale of 1 1 1 accurately reflects the size you
specified in 3D Studio Max.
62