 |
| In this part
of the series, we'll discuss some basic collision options.
We'll also learn how to control the camera. |
| |
| There are several
different ways to determine what bumps into what within your
W3D member. Collision, when one virtual object intersects
with another virtual object, is at the heart of most 3D games.
This is true whether you are trying to adjust the camera to
follow a race car or to determine whether or not a bullet
has hit its target. |
| |
In this article,
we talk about how to keep that crazy camera in line using
Director's modelsUnderRay
function to detect models that are in the path of the player's
motion and prevent the camera from flying through the walls.
|
| |
|
The demo movie in Figure 1 uses the same basic scripts
from the previous two articles, and adds only two additional
scripts. The first of these scripts sets up and handles
camera motion. The second handles camera rotation. The source
file for the movie below is called maze_magic_2.dir. You
can download this file from the first
page of this tutorial.
|
| |
To view this demo, you need the latest version of the Macromedia Flash Player.
Download the free Macromedia Flash Player now.
|
|
| Figure 1. The
maze with a collision detecting camera |
| |
| The camera animation
behavior uses five handlers: |
|
· |
beginSprite() |
|
· |
exitFrame() |
|
· |
delayBox() |
| · |
mUpdatePosition()
|
| · |
castRay()
|
|
| |
The setup routines
are handled by the delayBox
handler, and it is triggered using a custom timeout. The timeout
is created during the beginSprite
handler as shown in the animation script startup below: |
| |
|
global w
-- Declare the global variable that
points to the w3d member
on beginSprite(me)
timeout("delayBox").new(500,
#delayBox, me)
-- This just creates the
camBox and dummy last.
-- It prevents them from interfering with the
other
-- models.
end
|
The syntax to create the timeout callback is very simple.
The timeout object is created, named delayBox and the
time between calls is set to 500 milliseconds (or half of
one second). When the half second has passed, the timeout
will call the script #delayBox,
as shown in the dummy box handler code example below: |
| |
on
delayBox(me)
tRes = w.newModelResource("camboxRs", #box)
-- create a new box model resource
tRes.width = 5
tRes.height = 5
tRes.length = 5
-- set the width, length and height of the
new box resource
w.newModel("camBox", w.modelResource("camboxRs"))
-- create a box for the camera to follow
w.newModel("dummy", w.modelResource("camboxRs"))
-- create an extra box to handle rotation
timeout("delayBox").forget()
-- kill the timeout script that called this
handler
w.model("camBox").visibility = #none
-- make the boxes invisible
w.model("dummy")Visibility = #none
end |
| |
Normally this would
trigger a call to the delayBox
handler every half second, but the delayBox
script will turn the clock off, once it has done its setup.
Why are we spending so much energy tinkering with time? Because,
rather than rewriting the texture routines from the previous
tutorial, I wanted to make certain that these were the last
two models created. |
| |
The delayBox
handler just creates a couple of simple boxes that we can
use to facilitate interpolation of the translation and rotation
of the camera. If you could see the boxes (heck, hold down
the "b" key to see the boxes) you could see that
they jump to the interpolated position and rotation of the
camera, but that the camera slowly moves into those positions |
| |
The first two handlers
just created the boxes and made them invisible by setting
their face visibility to #none. The real work begins
with the exit frame script. Basically I check to see which
key, if any is depressed. If a key is down, the left arrow
key for example, then I call the first collision subroutine,
mUpdatePosition. You can
see this going on in the animation script exit frame handler
shown below: |
| |
| on
exitFrame(me)
case TRUE of
-- If the following conditions are TRUE
(keypressed(numToChar(28))):
-- the left arrow key is down
if mUpdatePosition(me, #left) then
-- if there is no model within 10 units
of the model's left side
w.model("camBox").translate(-5,0,0)
-- move the camera box 5 units left
end if
(keypressed(numToChar(29))):
-- right arrow key is down
if mUpdatePosition(me, #right) then
-- if there is no model within 10 units
of the model's right side
w.model("camBox")Translate(5,0,0)
-- move the camera box 5 units right
end if
(keypressed(numToChar(30))):
-- up arrow key is down
if mUpdatePosition(me, #up)
then
-- if there is no model within 10 units
of the model's front side
w.model("camBox")Translate(0,0,-5)
-- move the camera box 5 units forward
end if
(keypressed(numToChar(31))):
-- down arrow key is down
if mUpdatePosition(me, #down) then
-- if there is no model within 10 units
of the model's back side
w.model("camBox")Translate(0,0,5)
-- move the camera box 5 units back
end if
end case
if NOT voidP(w.model("camBox")) then
-- if the cam box model has been created
then
w.model("camBox").transform.position.y = 4.00
-- make sure that the height of the box
is 4 units along the positive y
tTrans = w.model("camBox").transform
-- store the transform of the cam box in
a variable
w.camera(1).transform.interpolateTo(tTrans, 20)
-- interpolate the camera toward the box
transform 20%
end if
end
|
| |
After checking
for a relevant key and calling the first level subroutine,
the script checks the subroutine's reply. The subroutine is
called as the key element in the conditional statement. If
the subroutine returns TRUE, then the model will translate
in the direction that the player requested, but if the returned
value is FALSE, the model will not translate. In other words,
if the mUpdatePosition
handler returns FALSE, the model (and subsequently the camera)
won't go anywhere. |
| |
You may have noticed
while holding down the "b" key that the box stops
at the wall, long before the camera reaches it. The last part
of the exitFrame handler
just cleans up the height of the camera, so you can't go flying
out of that open top. Finally the transform of the box is
loaded into a variable, tTrans,
and the camera is interpolated toward that transform by a
percentage of the total distance. |
| |
| Note:
The percentage of translation is actually handled with a variable
in the finished movie. This is what contributes to that wacky
spin at the beginning of the demo. |
| |
The mUpdatePosition
handler is the first of two subroutines used by the exitFrame
handler to determine whether or not a translation is authorized.
This level simply sorts directions for the rays that will
be cast. The ray must be cast in the direction that we are
moving, so convert the key input into direction vectors in
the handler found in the First Ray Casting Subroutine below: |
| |
| on
mUpdatePosition(me, direction)
-- if the direction is
case direction
of
#left:
-- cast a ray to the left
result = castRay(w, vector(-1,0,0),
w.model("camBox"), 1, #detailed)
#right:
-- cast a ray to the right
result = castRay(w, vector(1,0,0),
w.model("camBox"), 1, #detailed)
#up:
-- cast a ray to the front
result = castRay(w, vector(0,0,-1),
w.model("camBox"), 1, #detailed)
#down:
-- cast a ray to the back
result = castRay(w, vector(0,0,1),
w.model("camBox"), 1, #detailed)
end case
if result.count > 0 then
-- if the ray hit something
tRes = result[1]
-- store the model that was hit in the variable
tRes
if tRes.distance < 10 then
-- if the distance to the model that the
ray hit is less than ten units
RETURN 0
-- answer FALSE, NOT ok to move
else
-- the distance is greater than ten units
RETURN 1
-- answer TRUE, OK to move
end if
else
-- the ray didn't hit anything
RETURN 1
-- amswer TRUE, OK to move
end if
end
|
| |
This mUpdatePosition
handler will then call the castRay
subroutine—which convert the model relative direction
vectors into world relative direction vectors and return a
list of the model(s) that were hit by the ray. The returned
list is stored in the variable result as a list of lists.
|
| |
That list of lists
thing will burn you every time, unless you remember to pluck
out the list you want by index, then poll for the individual
property values. Put the first list within the list into a
temporary variable called tRes,
and then check the distance between the intersected model
and the origin point of the ray. |
| |
| If the distance
is too close, the script returns a FALSE value, blocking the
proposed translation. If the distance is not too close, it
returns a TRUE value, allowing the translation. |
| |
The real magic
of the script is found in the second subroutine, which converts
the model relative direction vectors to world relative direction
vectors. The castRay handler
(found in the Second Ray Casting Subroutine below) begins
by defining some basic variables. |
| |
|
on castRay(whichWorld,
whichDirection, whichModel, howMany, detail)
-- set the variable m to
= a reference to the model
m = whichModel
-- set the variable wp to
= the world relative position of the model
WP = m.getWorldTransform().position
-- set the variable md to = the model relative
direction
MD = whichDirection
-- set the variable wd to
= the world relative direction
WD = ( m.getWorldTransform() * MD ) - WP
-- cast the actual ray and
return the result
tCollisions = whichWorld.ModelsUnderRay(WP,
WD, howMany, detail)
return tCollisions
end
|
| |
A reference to
the model is stored in the temporary variable, m.
The world relative position of the model is stored in WP
The direction of the model is stored in MD,
and then the world relative direction vector is calculated.
|
| |
Finally the actual
ray is cast and the result is stored in the variable tCollisions.
The result is then returned to the mUpdate
handler. |
| |
At this point everything
is in place to handle the forward, backward and strafe controls
for camera motion. The motion of the camera is dampened by
using the interpolateTo
command, which moves the camera only a percentage of the total
distance desired at each frame update. |
| |
|
The last significant script in this movie handles the rotation
of the camera. You'll find the script in the Camera Rotation
Script. After a bit of error checking to make certain that
the model is ready, the script checks to see if the mouse
is down. It's annoying to have a constantly jostling camera,
so this script uses the mouseDown
event to trigger rotation. This can be a bit of a problem
if you plan to use the mouse for weapons fire, but I've
always preferred using the space bar for firing anyway.
;)
|
| |
|
global w
-- declare the w3d reference global
on exitFrame
-- if the model cam box already exists then
if NOT voidP(w.model("camBox")) then
-- if the mouse
is down then
if the
mouseDown then
-- calculate the trajectory of the players
mouse in 3D space
trajectoryW = (w.camera[1].spriteSpaceToWorldSpace(the mouseLoc))
-- put the dummy box where the camera is
w.model("dummy").transform.position = w.camera[1].transform.position
-- point the dummy box at the players mouse
in 3D space
w.model("dummy").pointAt(trajectoryW)
-- rotate the camera slowly to match
the player's gaze / point
w.model("camBox").transform.interpolateTo(w.model("dummy")Transform, 20)
end if
end if
end
|
| |
| If the mouse is
down then the script stores the player's pointing trajectory
in a temporary variable. It moves the dummy box model into
the correct position, then points the dummy at the player's
trajectory. Finally, the camera box model is interpolated
toward the dummy model's transform. This softens the effect
of the rotation a bit. You've now completed the full working
demo with movement and collision detection. |
| |
| Still hungry for
more? Check out my tutorial on projectiles, gravity and drag
on director-online.
Also, see my book on the subject: Real-Time
Interactive 3D Games: Creating 3D Games in Macromedia Director
8.5 Shockwave Studio. |
| |
| |
| |
| |