Note About This Book: Advanced Lingo For Games was written by Gary Rosenzweig in 2000 for users of Macromedia Director 7. It is presented here for free on an as-is basis, with no updating. Most of the information and code here can be used in the most recent version of Director. The book has been reproduced from the final editing files archived in 2000, and not the final proof galleys. So some minor differences between this version and the printed version my exist. The entire contents of this book are Copyright 2000, Gary Rosenzweig. No part may be reproduced or copied without written permission. The text here is provided for individual use only.
Want to thank me for making this book available for free? Just buy Special Edition Using Macromedia Director MX and we'll call it even!

Advanced Lingo For Games
by Gary Rosenzweig


Chapter 9 Section 3

Making the Game

This game requires four behaviors. The first is for the target sprites themselves, and contains the code to animate them. The second is the frame behavior that keeps track of the overall game state and handles events sent to the Stage. The third is a small behavior to allow some sprites to block shots to targets that are behind them. The last behavior is for a small blast graphic that appears when the user takes a shot and misses.

Target Behavior

The behavior that gets placed on any target on the screen is called the "Shooting Gallery Sprite Behavior" in the example on the CD-ROM. It's primarily in charge of the movement of the target. It also accepts mouse clicks on the sprite as hits.

Following the figure, you'll see the property declarations and the on getPropertyDescriptionList handler, which is rather long. It includes a lot of parameters to provide a great deal of versatility in how the sprite can move.

Figure 9.3 shows the Parameters dialog box for this behavior. I have added comments to the individual parameters below to better explain them.

Figure 9.3
The Parameters dialog box for the sprite behavior is shown here.

-- parameters
property pMovement
property pSpeed
property pAmount
property pStep
property pPoints
property pFrequency

-- other properties
property pOrigLoc -- remember the sprite's original location
property pOrigRect -- remember the sprite's original rect
property pOrigStep -- remember the starting step
property pHit -- has the sprite been hit recently

on getPropertyDescriptionList me
  list = [:]

The type of movement that a target sprite exhibits will fit one of the six types explained earlier in this chapter. Each will be seen later in the on exitFrame handler, where the actual code for executing the movement is located.


  -- type of movement
  addProp list, #pMovement,[cc]
    [#comment: "Type of Movement",[cc]
     #format: #symbol,[cc]
     #range: [#upDown, #leftRight, #circle,[cc]
              #layDown, #moveHoriz, #moveVert, #none],[cc]
     #default: #none]

The "pSpeed" parameter is used by all the movement types to determine how many pixels the sprite moves in one frame. In the case of circular movement, the "pSpeed" relates to the number of degrees around the circle that the sprite moves in one frame. With the "#layDown" movement type, the "pSpeed" and the "pAmount" are used to determine a fractional amount of movement.


  -- how many pixels to move at one time
  addProp list, #pSpeed,[cc]
    [#comment: "Speed",[cc]
     #format: #integer,[cc]
     #default: 1]

The "pSpeed" can also be set to a negative value to make the sprite move backwards. This works for the "#circle", "#moveHoriz", and "#moveVert" types. The other types of movement are back-and-forth movements, and don't need to use a negative "pSpeed".

The "pAmount" parameter is used to determine how far a sprite should go in the case of "#upDown" and "#leftRight". In the case of "#circle", the "pAmount" is used as the radius of the circle. In the case of "#layDown", the "pSpeed" and "pAmount" are used as a fraction to determine how much to stretch the sprite in each frame.


  -- total distance to move
  -- for #circle, it is the radius
  addProp list, #pAmount,[cc]
    [#comment: "Amount",[cc]
     #format: #integer,[cc]
     #default: 25]

The "pStep" parameter is necessary if you want the sprites to start at different positions. This property is actually used to increment the movement throughout the life of the sprite. By using it as a parameter as well, it allows you to have the sprite start a part of the way through the animation. So, if the movement type is "#upDown" and the "#pAmount" is 100, then you know that the sprite is supposed to move 100 pixels up, and then 100 back down, for a total of 200 steps. If you set "pStep" to start at 100, then the sprite will start at the top, and appear to move down and then up instead.


  -- where the sprite begins
  addProp list, #pStep,[cc]
    [#comment: "Starting Step",[cc]
     #format: #integer,[cc]
     #default: 0]

The "pPoints" parameter is simply the number of points that the user gets when they hit the target. You should determine how difficult a target is to hit and assign an appropriate number. A fast moving target that appears rarely should be worth more than a slow moving target that is always on the screen.


  -- number of points the sprite is worth when hit
  addProp list, #pPoints,[cc]
    [#comment: "Points",[cc]
     #format: #integer,[cc]
     #default: 10]

The "pFrequency" parameter allows us to make the game more random. When the sprite hits the "pStep" that corresponds to where it started, it stops moving. Then, the "pFrequency" parameter is used to determine when it starts again. A low number, such as 1, means that it starts immediately. A high number, such as 100, means that it has a 1 in 100 chance of starting again on each frame.

When you are using a "pFrequency" of more than 1, make sure that the starting "pStep" for the target has it hiding behind a blocking sprite or off the screen. Otherwise, the target will be very easy to hit because it will be standing still.


  -- how often the sprite should do its movement
  addProp list, #pFrequency,[cc]
    [#comment: "Frequency",[cc]
     #format: #integer,[cc]
     #default: 1]

  return list
end

The on beginSprite handler is responsible for remembering the original location, rectangle, and "pStep" of the sprite. All of these are useful in one type of animation or the other.


-- get the starting location, rect, and step
on beginSprite me
  pOrigLoc = sprite(me.spriteNum).loc
  pOrigRect = sprite(me.spriteNum).rect
  pOrigStep = pStep
  pHit = FALSE
end

The on exitFrame handler is the workhorse of this behavior. It looks at "pMovement" and determines what type of movement to apply to the sprite.

Most types of movement first call "on checkForReset" and then "on allowAnimation". This first handler checks to see if the sprite has completed an animation and should be reset if it was hit. The second handler advances the "pStep" property as long as the sprite is not currently in a stopped mode, waiting for its next chance to appear.


-- perform the animation
on exitFrame me
  case pMovement of

The "#upDown" and "#leftRight" movements are basically the same. If "pStep" is less than "pAmount", then the sprite is on its way up or to the left. If it is greater than "pAmount", then it's on its way back. The sprite's location is set according to the original location and the value of "pStep".


    #upDown: -- move vertically
      checkForReset(me) -- see if animation has completed one sequence
      allowAnimation(me) -- next step of animation
      if pStep > pAmount*2 then pStep = 0 -- sequence complete
      if pStep <= pAmount then -- first half of sequence
        y = pOrigLoc.locV - pStep -- up
      else -- second half of sequence
        y = pOrigLoc.locV - pAmount*2 + pStep -- down
      end if
      sprite(me.spriteNum).locV = y -- set sprite location

    #leftRight: -- move horizontally
      checkForReset(me) -- see if animation has completed one sequence
      allowAnimation(me) -- next step of animation
      if pStep > pAmount*2 then pStep = 0 -- sequence complete
      if pStep <= pAmount then -- first half of sequence
        x = pOrigLoc.locH - pStep -- left
      else -- second half of sequence
        x = pOrigLoc.locH - pAmount*2 + pStep -- right
      end if
      sprite(me.spriteNum).locH = x -- set sprite location

The circular animation works very differently. It uses "pAmount" as the radius, and "pStep" as a number of degrees. It then uses sin and cos to create a location.


    #circle: -- move in circle
      checkForReset(me) -- see if animation has completed one sequence
      allowAnimation(me) -- next step of animation
-- sequence complete, clockwise or counterclockwise
      if pStep >= 360 then pStep = pStep - 360
      else if pStep < 0 then pStep = pStep + 360
      angle = 2.0*pi()*pStep/360.0 -- convert to radians
      x = cos(angle)*pAmount -- calculate x from angle and radius
      y = sin(angle)*pAmount -- calculate x from angle and radius
      sprite(me.spriteNum).loc = point(x,y) + pOrigLoc -- set sprite 
location

The "#layDown" movement type uses "pStep" over "pAmount" as a fraction and then sets the height of the sprite to that fraction. When "pStep" is at 0, the height is 0, and the sprite appears to be flat. When "pStep" is the same as "pAmount", the height is equal to the original height of the sprite, and it appears normal. In between, the sprite looks like it is getting lifted up, and then falling back down again.


    #layDown: -- compress rectangle
      checkForReset(me) -- see if animation has completed one sequence
      allowAnimation(me) -- next step of animation
      if pStep > pAmount*2 then pStep = 0 -- sequence complete
      percent = float(pStep)/float(pAmount) -- percent of shrink
      if percent > 1 then percent = 2.0-percent -- second half of sequence
      height = percent*(pOrigRect.height) -- calculate actual height of rect
      newRect = duplicate(pOrigRect)
      newRect.top = newRect.bottom - height -- calculate new rect
      sprite(me.spriteNum).rect = newRect -- set the rect of the 
sprite

The "#moveHoriz" and "#moveVert" types don't need to use the "pAmount" parameter. This is because they move all the way across the screen. When the sprite reaches the opposite side of the screen, it is simply reset to the other side.


    #moveHoriz: -- move across screen continuously
      x = sprite(me.spriteNum).locH -- current location
      x = x + pSpeed -- add to current location
      spriteWidth = pOrigRect.width
      stageWidth = (the stage).rect.width
      if pSpeed > 0 and x-spriteWidth/2 > stageWidth then -- off screen?
        reset(me)
        x = x - stageWidth - spriteWidth -- move to other side
      else if pSpeed < 0 and x+spriteWidth/2 < 0 then -- off screen?
        reset(me)
        x = x + stageWidth + spriteWidth -- move to other side
      end if
      sprite(me.spriteNum).locH = x -- set the loc of the sprite

    #moveVert:
      y = sprite(me.spriteNum).locV -- current location
      y = y + pSpeed -- add to current location
      spriteHeight = pOrigRect.height
      stageHeight = (the stage).rect.height
      if pSpeed > 0 and y-spriteHeight/2 > stageHeight then -- off screen?
        reset(me)
        y = y - stageHeight - spriteHeight -- move to other side
      else if pSpeed < 0 and y+spriteHeight/2 < 0 then -- off screen?
        y = y + stageHeight + spriteHeight -- move to other side
        reset(me)
      end if
      sprite(me.spriteNum).locV = y -- set the loc of the sprite

  end case
end

Several utility handlers are called from the "on exitFrame" handler. The first is one that checks to see if the "pStep" is back to its original value. If it is, then it's reset to the "Target" member and "pHit" is set to FALSE. This is needed in case the sprite was hit by the player, thus changing the member to the "Target Hit" member.


-- see if sprite is back at the start of a sequence
on checkForReset me
  if pStep = pOrigStep then
    reset(me)
  end if
end

-- reset the sprite if it was hit
on reset me
  if pHit then
    sprite(me.spriteNum).member = member("Target")
    pHit = FALSE
  end if
end

The "on allowAnimation" handler checks to see if "pStep" is set to its original value. If it is, then the handler only advances the movement of the sprite providing that a random number from 1 to "pFrequency" is equal to 1. If "pFrequency" is 1, then this happens 100 percent of the time. If "pFrequency" is more than 1, then the sprite may have to wait for a number of frames before moving past the original "pStep" value again.


-- only advance the animation if it is in the middle
-- of a sequence, or if it is time to start a new sequence
on allowAnimation me
  if pStep = pOrigStep then -- start of sequence
    -- pFrequency used to randomly determine if it is time to start again
    if random(pFrequency) = 1 then
      pStep = pStep + pSpeed
    end if
  else
    pStep = pStep + pSpeed
  end if
end

Almost separate from the animation handlers in this behavior is the on mouseDown handler. This is a simple way to detect if the player has hit the sprite with a shot.

If this happens, the frame behavior is notified via the "on addScore" handler. The sprite's member is changed to reflect the hit, and the "pHit" property is set to TRUE.

This "pHit" property is then used to prevent the player from hitting the sprite again. The "pHit" property is reset when the sprite reaches its home state. So, when a target moves onto the screen, the player can hit it once, and it changes to a new member. Then the player can't hit that target again until it moves off the screen and then back on again.


-- if the user clicks, then it represents a gunshot
on mouseDown me
  if not pHit then -- not already hit
    sendSprite(0,#addScore,pPoints) -- add to score
    sprite(me.spriteNum).member = member("Target Hit") -- new member
    pHit = TRUE
  else
    pass -- pass to frame behavior so it can be recorded as a miss
  end if
end

Frame Behavior

The frame behavior has the responsibility of keeping track of the score, the number of shots fired, and adding effects like the cursor and sounds.

The on getPropertyDescriptionList handler only needs to get four parameters: the number of shots the player has, two sound names, and the frame to jump to when the game is over. These are similar parameters to the ones we have used in most of the previous games. The resulting dialog box is shown in Figure 9.4.

Figure 9.4
The frame behavior Parameters dialog box.

-- parameters
property pNumberOfShots
property pEndGameFrame
property pHitSound
property pMissSound

property pScore -- the player's score

on getPropertyDescriptionList me
  list = [:]

  -- starting number of shots the player gets
  addProp list, #pNumberOfShots,[cc]
    [#comment: "Number of Shots",[cc]
     #format: #integer,[cc]
     #default: 20]

  -- hit something sound
  addProp list, #pHitSound,[cc]
    [#comment: "Hit Sound",[cc]
     #format: #string,[cc]
     #default: ""]

  -- miss sound
  addProp list, #pMissSound,[cc]
    [#comment: "Miss Sound",[cc]
     #format: #string,[cc]
     #default: ""]

  -- frame to jump to when it is over
  addProp list, #pEndGameFrame,[cc]
    [#comment: "End Game Frame",[cc]
     #format: #marker,[cc]
     #default: #next]

  return list
end

The on beginSprite marks the start of the game. Because we want to use a custom cursor for the entire game, we can set it here. Instead of using a cursor from the standard set of cursors that are embedded inside Director, Projectors, and Shockwave, we will make our own custom cursor. We activate this cursor with the cursor command.


-- set cursor to crosshairs, score to 0
on beginSprite me
  cursor([member("cursor")]) -- set cursor to crosshairs
  pScore = 0
  showScore(me)
end

The cursor command can actually be used in three different ways. You can use it with a number to use a standard cursor like a hand or a magnifying glass. You can use it with a list or one or two members to make a simple custom cursor, or you can use it with a special animated cursor member to make a color cursor or an animated cursor. The last option requires the animated cursor Xtra that comes with Director.

The custom cursor, in this case, is an old-fashioned custom cursor that has been around in Director for many versions. Figure 9.5 shows the member for this cursor. It must be a 1-bit bitmap member.

Figure 9.5
A 1-bit bitmap is used as the cursor in this game.

You could also use an animated cursor instead. This would allow you to use color and make the cursor size bigger. However, for this example game, all we need is a simple cursor.

The next handler updates the "score" text member.


-- put score in text member
on showScore me
  member("score").text = "Score:"&&pScore&RETURN&[cc]
                         "Shots Left:"&&pNumberOfShots
end

The "on addScore" handler does more than just increase the score. It also reduces the number of shots remaining, and checks for the end of the game.


-- add points to score
on addScore me, p
  if pHitSound <> "" then puppetSound pHitSound
  pNumberOfShots = pNumberOfShots - 1 -- reduce shots
  pScore = pScore + p -- add score
  showScore(me)
  checkEndGame(me)
end

The "on mouseDown" handler gets called when the user clicks on an empty part of the screen, or when the user clicks on a sprite that has no on mouseDown handler of its own. We also use the pass command in the sprite behavior to pass along "mouseDown" messages to the frame behavior when the player tries to shoot a target that has already been hit.

This "on mouseDown" handler is only called when the player shoots and misses. So we must subtract one from the number of shots left and check for the end of the game. In addition, a message is sent to the blast graphic sprite to position itself where the shot was fired.


-- click missed an object and went to frame
on mouseDown me
  if pMissSound <> "" then puppetSound pMissSound
  pNumberOfShots = pNumberOfShots - 1 -- reduce shots
  sendSprite(40,#display,the clickLoc) -- place blast graphic
  showScore(me)
  checkEndGame(me)
end

The "on checkEndGame" handler needs to see if all shots have been fired to determine if the game is over.


-- see if all shots are gone
on checkEndGame me
  if pNumberOfShots = 0 then
    go to frame pEndGameFrame
  end if
end

In addition to all the previous code, the frame behavior is responsible for creating the one-frame loop. This is all that is needed in the on exitFrame handler.


-- loop on the frame
on exitFrame me
  go to the frame
end

Blast Sprite Behavior

The blast sprite is a little graphic that is used to mark the screen each time the player misses. It acts as visual feedback to the user that the shot has been fired, even though a target was not hit.

This behavior just needs to move into position when the frame behavior tells it to. Then, it should wait at that location for a specific amount of time before disappearing.

The blast sprite behavior does its disappearing by going back to its original location, which should be off the Stage. The time is determined by counting down from 10 every time the frame loops. When 0 is reached, the sprite should disappear.


property pOrigLoc, pTimer

on beginSprite me
  pOrigLoc = sprite(me.spriteNum).loc -- remember original location
  pTimer = 0 -- start property at 0
end

-- called by frame behavior to tell blast graphic to move
on display me, loc
  sprite(me.spriteNum).loc = loc -- new location
  pTimer = 10 -- begin counting down
end

on exitFrame me
  if pTimer > 0 then -- counting down
    pTimer = pTimer - 1
    if pTimer = 0 then sprite(me.spriteNum).loc = pOrigLoc -- done counting, reset location
  end if
end

Blocking Sprite Behavior

Targets should not always be vulnerable to the player's shots. This would make the game pretty easy. One way to protect them is to have them move and rest off the screen. Another way is to hide them behind other sprites.

Figures 9.1 and 9.2 show some of these objects as simple blocks. These blocks need to have a behavior attached to them to eat the "mouseDown" messages so that they do not get to the target sprites.

You can do this by simply having an empty on mouseDown handler. However, we want to make sure that these shots get recorded as misses, just as a shot on the background would be. So, the "mouseDown" message should be passed directly to the frame behavior. Here is the handler that does this:


on mouseDown
  sendSprite(0,#mouseDown)
end