Articles Archive
Articles Search
Director Wiki

Creating a Simple Flight Simulator with Shockwave 3D

November 13, 2003
by Gary Rosenzweig

In preparing some demos for my presentation at next week's Macromedia MAX Conference in Salt Lake City, I ended up building a rather simple flight simulator. So simple, in fact, that I thought I could cram it all into an article.

Use your arrow keys to control the plane. Click on the movie to change camera views.

I started by playing with an old copy of Bryce, the terrain-creator tool. I used Bryce 4 on Mac OS 9, which allows you to select a single piece of terrain and export it as an OBJ file. This is one of the formats that the ShapeShifter 3D Xtra imports.

All I did was to create a simple random mountain using the Bryce terrain creator tools, and then apply the material "Whole Mountain" to it. I then selected it and exported at OBJ. That was basically all I did in Bryce. I'm not sure what the options are in the most recent version of Bryce, but I selected an adaptive grid for the export and a 512x512 texture size. I ended up with a .obj file and a texture image.

I imported the OBJ file into ShapeShifter 3D without any problems. The texture was missing, though. But I fixed that by creating a new material and choosing the texture's image file. Then I applied that material to the group and it mapped fine without any modifications. I also renamed the group "island" to make it easier to reference in my code.

I then saved it as a Shockwave 3D .w3d file. I guess I could have used another tool to create the terrain, but this was so simple and easy, and it looks great.

The movie has the island model in its own member, imported from the .w3d file. I created another member using Insert, Media, Shockwave 3D and named that member "world". I put it on the Stage and stretched it to 640x480. This is where the action will happen.

The behavior on this "world" sprite starts by importing the island. I also move the island down the y-axis by 2 points so it will sit below the water I'll create soon.

on beginSprite me
  -- initialize world
  pWorld = sprite(me.spriteNum).member

  -- import island
  m = pWorld.cloneModelFromCastmember("island", "island", member("island"))
  m.transform.scale = vector(10,10,10)
  m.transform.position.y = -2

Next, I need to create the water. Instead of making one plane of blue water, I decided to make 2 planes. The first would be a semi-transparent blue plane that would be at sea level, or 0 y.

-- create water and sea bottom resources
  r = pWorld.newModelResource("water",#plane,#front)
  r.width = 10000
  r.length = 10000
  r.widthVertices = 10
  r.lengthVertices = 10

-- create water with semi-transparent blend
  s = pWorld.newShader("water",#standard)
  s.diffuse = rgb("0000FF")
  s.texture = VOID
  s.blend = 75
  w = pWorld.newModel("water",r)
  w.shaderList = s
  w.transform.rotation = vector(90,0,0)

The second plane would be a sand-colored plane representing the bottom of the ocean. It will be at -1 y, so as to be lower than the water's surface but higher than the bottom of the island model.

-- create sea bottom with solid color
  s = pWorld.newShader("bottom",#standard)
  s.diffuse = rgb("CCBB99")
  s.texture = VOID
  b = pWorld.newModel("bottom",r)
  b.shaderList = s
  b.transform.rotation = vector(90,0,0)
  b.transform.position.y = -1

Notice that I needed to rotate both planes by 90 degrees around the x-axis. This is because the plane primitives are created as vertical planes, not horizontal ones. So I needed to turn them.

Now I have a complete scene: island, water, and sea bottom. Next, I searched for a plane model to use. I found one in the "free" section of the site, as a 3DS Max file. I imported it into an old copy of 3DS Max and exported it as a .w3d file. I suppose you could use the new Plasma product to do the same. One funny thing you need to do is group the model's parts together in 3Ds Max, or you end up with one model per group instead of one single plane model. I think this free model had a dozen or so groups, like wings, propeller, etc.

I imported the .w3d file of the plane into my movie and then use this to bring it in to the world.

-- import plane
  pPlane = pWorld.cloneModelFromCastmember("biplane", "biplane", member("biplane"))
  pPlane.transform.scale = vector(.001,.001,.001)
  pPlane.transform.position = vector(0,10,100)
  pPlane.transform.rotation = vector(-90,180,0)

Notice that I had to scale the plane down by .001 to get it to seem like the right size. By contrast, I had to scale the island up by 10 to get it right. So the plane was actually 10000 times larger than it needed to be relative to the island. That sort of thing happens all the time when dealing with models from different sources. I also had to rotate the plane by -90 x and 180 y to get it to point forward. I got these numbers by quick trial and error.

So now my behavior needs to make the plane move. I'll start by defining some positioning and movement properties in the on beginSprite handler.

-- set plane properties
  pSpeed = .2
  pDirection = 0
  pRoll = 0.0
  pPitch = 0.0
  pLocList = []

The speed and direction of the plane are pretty obvious. The direction is an angle in the y-axis, or at least that is how I will use it. The roll and pitch show how much the plane is tilted left and right and up and down. The pLocList is something I'll explain later. The funny thing is that I really need two objects here. The first is the physical model of the plane. That will need to be presented at different angles for visual feedback. The second is an object that is pointed in the exact direction of travel. This is what I will "move" in each from. This second object will be a group and the plane itself will be a child of that group.

-- create plane group to hold plane
  pPlaneGroup = pWorld.newGroup("planegroup")
  pPlaneGroup.transform.position = pPlane.transform.position

So basically, I will move the pPlaneGroup around the world. The plane model is a child of that group, so it will follow it. But as I angle and tilt the plane model, the group is no affected by it. So I can now roll the plane left or right without changing the actual angle of movement.

I'm also going to create two cameras for use in this movie. The first will follow behind the plane while the second will provide an overhead view. This code creates both cameras and assigns the following camera as the one currently in use.

-- create and position top camera
  c = pWorld.newCamera("top")
  c.transform.rotation = vector(-90,0,0)

  -- create and position following camera
  c = pWorld.newCamera("follow")
  c.transform.rotation = vector(-90,0,0)
  c.transform.position = vector(0,100,0)
  sprite(me.spriteNum).camera = c

The following camera will be the main one used. For an added special effect, lets add fog to that camera. This will add some realism and illusion of distance.

-- make fog (following camera)
  c.fog.enabled = TRUE
  c.fog.near = 50
  c.fog.far = 100
  c.fog.color = rgb("999999")
  c.fog.decayMode = #linear

That's it for the on beginSprite handler. In the example movie you'll see I've also defined the properties at the top of the script. Can't forget those. Now the on enterFrame handler will provide the frame-by-frame control and movement.

First, we'll look at the left and right arrow keys and change pRoll accordingly. If one of those keys is being held down, we'll roll in that direction. If not, then we'll roll back to the middle. We'll also limit the roll to 40 degrees in either direction.

on enterFrame me
  -- roll with left and right arrows
  if keyPressed(123) then
    pRoll = pRoll + 1
  else if keyPressed(124) then
    pRoll = pRoll - 1
  else if pRoll > 0 then
    pRoll = pRoll - 1
  else if pRoll < 0 then
    pRoll = pRoll + 1
  end if
  pRoll = max(-40,min(40,pRoll))

Same now for the up and down arrow keys and the pitch.

-- pitch with up and down arrows
  if keyPressed(126) then
    pPitch = pPitch + 1
  else if keyPressed(125) then
    pPitch = pPitch - 1
  else if pPitch > 0 then
    pPitch = pPitch - 1
  else if pPitch < 0 then
    pPitch = pPitch + 1
  end if
  pPitch = max(-40,min(40,pPitch))

Now we'll use the roll for two things. First, we'll rotate the plane in the z (forward) axis to tilt it left or right. This is purely for visual effect. We'll also change the pDirection by the current amount of roll. We'll then set the plane's group direction according to pDirection so it faces the right direction.

-- position plane
  pPlane.transform.rotation = vector(pPitch+90,0,pRoll+180)
  pDirection = pDirection + .04*pRoll
  pPlaneGroup.transform.rotation = vector(0,pDirection,0)

Notice that I also changed the rotation of the plane on the x-axis by pPitch. This is also only for visual effect. This next piece of code is what will actually move the plane.

It starts by making a clean, new transform. A transform is a complex object that is a list of numbers that determines a 3D objects location, rotation and scale. Every model has a transform, but in this case we want to create a transform that isn't linked to any model. This clean transform is at location vector(0,0,0) with no rotation.

We'll then change the position of this transform so it matches the plane group's position. However, the plane group's rotation is not matched. Instead, it is set to the pitch and direction from those property variables. Then it uses pretranslate to move the position of this transform with this rotation taken into account. The result is that the new position of the transform is the location of the plane after one step of movement. We can take this position and re-apply it to the plane group, making the plane move.

-- move plane
  t = transform()
  t.position = pPlaneGroup.transform.position
  t.rotation = vector(-pPitch,pDirection,0)
  d = t.position
  pPlaneGroup.transform.position = d

So how did I know to do all of this? Why is this the exact perfect solution to moving the plane? The truth is that it isn't. There are many ways to make the plane move according to its pitch and roll. I just stumbled upon this way first, and it works.

Moving the camera is a lot easier. Remember that array pLocList? We'll store each position of the plane in that array. When the plane gets to 15 positions, we'll trim one off the beginning as we add one to the end. So we always have a list of the last 15 positions of the plane. The following camera will then put itself 15 steps behind the plane. It will use pointTo to make sure it is rotated to point directly at the plane at all times.

I also found it looks better if I only do this in the x- and z-axes. The y-axis, or vertical position, is best left matching the current vertical position of the plane. It seems more natural this way.

-- move cameras
  add pLocList, d"follow").transform.position = pLocList[1]"follow").transform.position.y = getLast(pLocList).y
  if pPlaneGroup.transform.position <> pLocList[1] then"follow").pointAt(pPlaneGroup.transform.position)
  end if
  if pLocList.count > 15 then deleteAt pLocList, 1"top").transform.position = pPlaneGroup.transform.position + vector(0,3,0)

That last line will move the unused top camera so that it is director above the plane model. To let the user switch between cameras we'll use the on mouseUp handler so they can click to do it.

-- click to change cameras
on mouseUp me
  if sprite(me.spriteNum).camera ="top") then
    sprite(me.spriteNum).camera ="follow")
    sprite(me.spriteNum).camera ="top")
  end if

That's it. You can download the example movie here, for both Mac and Windows.

Some notes, before I go. Notice that the intro screen shows the plane and island. These are actually the 3D cast members themselves. I used the model viewer in Director to change the position and rotation of the models to get the view I wanted. I also assigned lights and a background color using the Property Inspector panel. Then I put the island model member on the Stage first, and turned off Direct-To-Stage for it. I put the plane mode over it, turning off Direct-To-Stage and making it background transparent ink so the two models overlapped each other.

But this is form more than just a pretty start screen. It also insures that both models are loaded into memory before my script runs and tries to grab them for use. I could have just used a preloadMember command and some waiting scripts, but this was easy and fun.

Gary Rosenzweig is the Chief Engineer, founder, and owner of CleverMedia, a game and multimedia development company in Denver, Colorado. He is the author of ten books on Macromedia Director and Flash, including his latest, Special Edition Using Director MX.

Copyright 1997-2019, Director Online. Article content copyright by respective authors.