|
Want to thank me for making this book available for free? Just buy Special Edition Using Macromedia Director MX
Advanced Lingo For Games
| |
| 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 BehaviorThe 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 BehaviorThe 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 BehaviorThe 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 BehaviorTargets 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 | |