Accessibility
 
Home / Developer Center / Director Developer Center /

Director Article

Icon or Spacer Icon or Spacer Icon or Spacer
Dr. Allen Partridge
 
Dr. Allen Partridge
insight.director-3d.com
 
3D maze magic - Part one:  Building a Lingo box maze dynamically

This is the first in a series of three short articles that focus on the development of Lingo generated 3D mazes in Macromedia Director—and the common game elements that you might find in such a maze.

It isn't hard to make these mazes once you understand the basic principles. The resulting mazes are generated incredibly rapidly, making them a very viable option for Shockwave 3D.

 

In this article we'll discuss the basic procedures involved in setting up a box maze. All of the code is provided so that you can build your own maze. The code examples do not include optimization or error checking, in order to keep the handlers as "on topic" as possible.

 
Before you begin, download the sample files for this tutorial:
Windows   Macintosh

maze_magic.zip (272K)

 

maze_magic.sit (244K)

 
·
Part two: Texturing a Lingo box maze dynamically
·
Part three: Camera collision fundamentals
 
Before we begin, let's take a look at the map for the maze and the finished maze, just to make sure that we are all on the same page. You'll need the latest Shockwave plug-in to view the example below. The movie should have loaded by now, since the file is only a few kilobytes.
 

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
 
Once you have had a good look at the map (the colored squares with letters on them) click to move on to the actual maze. The switch happens so quickly that you might easily believe that the maze was pre-built. In reality, the movie builds the maze when you click it. The DIR file displays dynamically and downloads quickly because:
 
·
There are no media elements (other than Lingo scripts) in the movie
·
When the maze is built, there are fewer than 1200 polygons created
·
The application uses the built-in 3D primitives within Director to do all of the work
 
Before beginning any maze, you have to draw a plan. That's right, draw (using a pencil and paper) the plan for the maze. When you are developing 3D applications, it is very important to make notes in advance.
 
Click on the finished maze again so that you can see the map in the movie above once more. The movie just loops back and forth between the map and the maze. The dark line in the map represent the walls of the maze itself. The finished maze will be composed of box primitives. In this view, the boxes are seen from above with the front side of the box toward the top of your monitor. The left side of the box is to your left, and the right side is to your right.
 
On each box, a code indicates the sides of the box that will be removed. As you begin in the upper left-hand corner, you'll see the letters "BR" on the box. This indicates that the box will have its back and right sides removed. There is no black line in these positions, therefore there should be no walls here. Each string of characters represents a shorthand description of the walls that will be removed from that box. An 'L' indicates that the left wall will be removed, an F means that the front must go.
 
Once you've defined a map and identified the type of box you'll need to draw the cell, it's time to start writing the code. The handlers to build the maze are surprisingly simple.
 
I'll start with some prep work for when the W3D (the object file format of Director) sprite initializes. The beginSprite event behavior first creates some shortcut references to the sprite and its member. Everything in W3D starts with the member, so it will be a big time-saver to have a shortcut reference to the member on hand.
 
Once these properties have been defined, it's time to make some model resources. In order for geometry (meshes) to exist as models in a W3D member, they must point to a geometric resource called a model resource. There are several primitive shapes built into Director that may be created dynamically by calling the newModelResource() command and providing it with appropriate values as arguments. This takes the form:
 
pMember.newModelResource("resourceName", \
#primitiveType, #visibility)
 
The beginSprite handler below creates a new model resource for each type of box. There's a model resource with the front removed, one with the left side removed, and so on. This happens in two stages. First, create an appropriately named resource. Then, remove the appropriate sides. After this is done for each of the potential side wall variants, remove the tops of all the boxes so that it is easier to see the entire 3D box maze from above. The following code example illustrates this process:
 

property pMember
property pSprite

on beginSprite(me)
     -- These are references to the sprite
     -- and the member
     pSprite = sprite(me.spriteNum)
     pMember = pSprite.member
     --- Create a series of plans for boxes
     --- that have one or more sides missing.
     --- Start with one that has no front.
     -- When creating a model resource you
     -- may use 3 args. The name of the resource,
     -- the geometry type, and the faces that
     -- you would like to be visible.
     pMember.newModelResource ("boxF", #box, #both)
     -- You may remove faces of a box at
     
  -- the resource level. Any face may
     -- be set to 0 to remove it. (top,
...  -- bottom, left, right, front or back.)
     pMember.modelResource("boxF").front = FALSE
...  -- Now one with no back
     pMember.newModelResource ("boxB", #box, #both)
     pMember.modelResource("boxB").back = FALSE
     -- no right side
     pMember.newModelResource ("boxR", #box, #both)
     pMember.modelResource("boxR").right = FALSE
     -- no left side
     pMember.newModelResource ("boxL", #box, #both)
     pMember.modelResource("boxL").left = FALSE
     -- no front and no back
     pMember.newModelResource ("boxFB", #box, #both)
     pMember.modelResource("boxFB")Front = FALSE
     pMember.modelResource("boxFB")Back = FALSE
     -- no left and no right
     pMember.newModelResource ("boxLR", #box, #both)
     pMember.modelResource("boxLR")Left = FALSE
     pMember.modelResource("boxLR")Right = FALSE
     -- no front and no left side
     pMember.newModelResource ("boxFL", #box, #both)
     pMember.modelResource("boxFL")Front = FALSE
     pMember.modelResource("boxFL")Left = FALSE
     -- no front and no right side
     pMember.newModelResource ("boxFR", #box, #both)
     pMember.modelResource("boxFR")Front = FALSE
     pMember.modelResource("boxFR")Right = FALSE
     -- no front, no left side and no right side
     pMember.newModelResource ("boxFLR", #box, #both)
     pMember.modelResource("boxFLR")Front = FALSE
     pMember.modelResource("boxFLR")Left = FALSE
     pMember.modelResource("boxFLR")Right = FALSE
     -- no back and no left side
     pMember.newModelResource ("boxBL", #box, #both)
     pMember.modelResource("boxBL")Back = FALSE
     pMember.modelResource("boxBL")Left = FALSE
     -- no back and no right side
     pMember.newModelResource ("boxBR", #box, #both)
     pMember.modelResource("boxBR")Back = FALSE
     pMember.modelResource("boxBR")Right = FALSE
     -- no back, no left side and no right side
     pMember.newModelResource ("boxBLR", #box, #both)
     pMember.modelResource("boxBLR")Back = FALSE
     pMember.modelResource("boxBLR")Left = FALSE
     pMember.modelResource("boxBLR")Right = FALSE
     -- no front, no back, no left side
     -- and no right side
     pMember.newModelResource ("boxFBLR", #box, #both)
     pMember.modelResource("boxFBLR")Front = FALSE
     pMember.modelResource("boxFBLR")Back = FALSE
     pMember.modelResource("boxFBLR")Left = FALSE
     pMember.modelResource("boxFBLR")Right = FALSE
     -- no front, no back, and no left side
     pMember.newModelResource ("boxFBL", #box, #both)
     pMember.modelResource("boxFBL")Front = FALSE
     pMember.modelResource("boxFBL")Back = FALSE
     pMember.modelResource("boxFBL")Left = FALSE
     -- no front, no back and no right side
     pMember.newModelResource ("boxFBR", #box, #both)
     pMember.modelResource("boxFBR")Front = FALSE
     pMember.modelResource("boxFBR")Back = FALSE
     pMember.modelResource("boxFBR")Right = FALSE
     -- TAKE the tops off -- so you can see the maze.
     repeat with x = 2 to pMember.modelResource.count
       pMember.modelResource[x]Top = FALSE
     end repeat
end

 
The buildMaze() handler is called from a frame script after the user clicks the map to continue. This handler will actually create the box models based on the resources and then move them around the space in order to build the maze.
 
The basic strategy of the handler is to create the appropriate box for each position in a 9x10 grid, and then move the box into a position that correlates to the box position on the grid. In order for this code to work, there must be a meaningful plan, so that the handler can figure out which variation of the box goes where.
 
To do this, use a list stored in a field cast member. The field contains a simple linear list that contains strings that may be used to reference the appropriate model and model resource for the given box. The list looks like this:
 

put value(member("plan").text)
-- ["BR", "LR", "LR", "LR", "LR", "LR", "LR", "LR", "BL", "FB", "BR", "LR", "LR", "LR", "BLR", "LR", "L", "FB", "FB", "FB", "BR", "LR", "LR", "FLR", "LR", "LR", "FBL", "FB", "FB", "FB", "BR", "LR", "BLR", "BLR", "BL", "FB", "FB", "FB", "FB", "FB", "B", "FB", "FB", "FB", "FB", "FB", "F", "FB", "FB", "FB", "FB", "FB", "FBR", "FBL", "FBR", "LR", "FBL", "FB", "FR", "FBL", "FB", "FB", "FB", "FB", "FB", "FB", "FBR", "L", "FB", "F", "FB", "FB", "FB", "FB", "FB", "FBR", "LR", "FLR", "LR", "FL", "FB", "FR", "FL", "F", "FR", "LR", "LR", "LR", "LR", "FL"]

 
Because the list is stored in a field text member, it is a string. That string can be converted into a list if you plan to read its values as list items, so the first step is to convert this field text to a value using (oddly enough) the value() command, and then store that list in a variable.
 
Though they can seem a bit muddy at first glance, a nested repeat loop is a good way to build objects that need to correspond to rows and columns. In this handler, use the x repeat loop to work with each column and the y loop to work with each row. A little math will let us convert the nested references into list positions within that linear list.
 
Once the type of box has been retrieved from the corresponding position in the linear list, I use the createBox() subroutine to build a box of the appropriate type and then return a reference to that box model so that the buildMaze() handler can finish by moving the box into position.
 

on buildMaze (me)
   -- member("plan") contains a list of the
   -- correct wall sets so the maze will lay
   -- out correctly.
   -- At this point you may be thinking, "hunh?"
   -- I draw a sketch (that's right on paper)
   -- of a gridded version of the maze. It has
   -- 9 columns and 10 rows. Now I create a
   -- list in a text field that has
   -- references to the type of box that I need
   -- in each corresponding row / column working
   -- left to right, top to bottom, one row at
   -- a time. "FR" for example indicates a box
   -- where the front and right sides have been
   -- removed
   -- NOTE THAT THIS CORRELATES to the "boxFR"
   -- resource type that was created when the
   -- sprite initialized.
   tPlan = value(member("plan")Text)
   -- For every column
   repeat with x = 1 to 10 -- columns
......-- For every item in this row in each column
      repeat with
y = 1 to 9 -- rows
         -- This creates a pointer to the same point
         -- within the list. Just math, x-1- cuz we
         -- want to start with 0 to avoid the
         -- multiplication by nine for items in the
         -- first row / column and offset each row
         -- by nine
         tBoxResourceItem = (((x-1) * 9) + y)
         -- match the list pos this one gets
         -- the value stored in the corollary
         -- position within the list.
         tResourceId = (tPlan[tBoxResourceItem])
         -- now set the variable tBoxRef to the
         -- active model, while creating that model
         -- using the createBox() routine.
         -- Boxes are named dynamically with unique
         -- #'s and the faces of the boxes are
         -- determined by the list.
         tBoxRef = createBox(me, "boxModel"&\
(y + ((x-1) * 10)), tResourceId)
         -- finally move the box into the correct
         -- position by offsetting all but the
         -- corner box by the width of the boxes.
         tBoxRef.translate(((y - 1) * 50), 0, \
(((x - 1))* 50))
      end repeat
   end repeat
end

 
This createBox() subroutine just makes a box of the type requested, then returns a reference to that box model to the handler that called it.
 

On createBox(me, boxName, boxType)
     -- make a box of the type indicated.
     pMember.newModel(boxName, (pMember.modelResource\
("box" & boxType)))
     -- return a reference to the new box
     return pMember.model(boxName)
end

 
In the next part of this series, we'll discuss how to do some decorating within the box maze. We'll look at some tricks to optimize the number of polygons used to display the maze and learn how to control the appearance with shaders and textures of each wall within the maze.
 
 
  Next

About the author

Dr. Allen Partridge runs Insight Interactive, a game-development company. He is the author of Real-Time Interactive 3D Games; Creating 3D Games with Macromedia Director 8.5 / Shockwave Studio. Allen is the host of dirGames-L, the largest online e-mail discussion community centered on game design and creation using Macromedia Director.

Submit feedback on our tutorials, articles, and sample applications.