Accessibility
 
Home / Developer Center / Director Developer Center /

Director Article

3D maze magic - Part three: Camera collision fundamentals

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.

Get Macromedia Flash Player
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.
 
 
  Previous Contents