|
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
The code for the game is spread over four behaviors. The main behavior is the frame behavior. However, there are smaller behaviors for the rocks, the ship, and the bullets. Frame BehaviorThis frame behavior contains all the options for the game. The on getPropertyDescriptionList is the largest we have seen yet. It contains properties for the speed of the rocks{1}, the number of rocks to start with{2}, the sprites that will be used by the rocks, the ship and bullets{3}, the frames to go to when a level ends{4} or the user dies{5}, and various sounds that can be used. Figure 11.2 shows the resulting Parameters dialog box that.
Figure 11.2
The Parameters dialog box for the Space Rocks frame behavior.
property pStartNumRocks
property pRockSpeed
property pFirstRockSprite, pLastRockSprite
property pShipSprite
property pFirstBulletSprite, pLastBulletSprite
property pFireDelay
property pFireSound
property pExplodeSound
property pDieSound
property pStartingLives
property pNextLevelFrame
property pEndGameFrame
property pLastFireTime
property pSetUpFlag
global gLives, gScore
on getPropertyDescriptionList me
list = [:]
-- how many large rocks appear on the screen at the start{2}
addProp list, #pStartNumRocks,[cc]
[#comment: "Starting Number of Rocks",[cc]
#format: #integer,[cc]
#default: 4]
-- how fast the rocks move{1}
addProp list, #pRockSpeed,[cc]
[#comment: "Rock Speed",[cc]
#format: #integer,[cc]
#default: 3]
-- bank of rock sprites {3}
addProp list, #pFirstRockSprite,[cc]
[#comment: "First Rock Sprite",[cc]
#format: #integer,[cc]
#default: 11]
addProp list, #pLastRockSprite,[cc]
[#comment: "Last Rock Sprite",[cc]
#format: #integer,[cc]
#default: 42]
-- player's ship sprite
addProp list, #pShipSprite,[cc]
[#comment: "Ship Sprite",[cc]
#format: #integer,[cc]
#default: 44]
-- bank of bullet sprites
addProp list, #pFirstBulletSprite,[cc]
[#comment: "First Bullet Sprite",[cc]
#format: #integer,[cc]
#default: 5]
addProp list, #pLastBulletSprite,[cc]
[#comment: "Last Bullet Sprite",[cc]
#format: #integer,[cc]
#default: 9]
-- how many ticks to wait until next shot allowed
addProp list, #pFireDelay,[cc]
[#comment: "Fire Bullet Delay Time",[cc]
#format: #integer,[cc]
#default: 30]
-- sounds
addProp list, #pFireSound,[cc]
[#comment: "Fire Sound",[cc]
#format: #string,[cc]
#default: ""]
addProp list, #pExplodeSound,[cc]
[#comment: "Explode Sound",[cc]
#format: #string,[cc]
#default: ""]
addProp list, #pDieSound,[cc]
[#comment: "Die Sound",[cc]
#format: #string,[cc]
#default: ""]
-- number of lives, only for use in first level
addProp list, #pStartingLives,[cc]
[#comment: "Number of Lives",[cc]
#format: #integer,[cc]
#default: 0]
-- frame to go to when all rocks are gone{4}
addProp list, #pNextLevelFrame,[cc]
[#comment: "Next Level Frame",[cc]
#format: #marker,[cc]
#default: #next]
-- frame to go to when lives run out{5}
addProp list, #pEndGameFrame,[cc]
[#comment: "End Game Frame",[cc]
#format: #marker,[cc]
#default: #next]
return list
end
In this game, each frame represents a level. Therefore, the properties shown previously can be set differently for each frame, making each level harder. The frame for the first level should have its "pStartLives" set to a non-zero value, such as 3. This starts the global variable "gLives" off at that value. All other frames should have the "pStartLives" property set to 0, which causes the on beginSprite handler to not reset "gLives".{6} The on beginSprite handler would normally set up everything in the frame. However, because this would require sending messages to other sprites, it's an impossible task. This is because the on beginSprite handler for the frame behavior executes before the sprites have been initialized. So, any messages sent to a sprite would be ignored. Instead, we will send these messages in the on exitFrame handler. However, because this handler executes every single time the frame loops, we will use a property called "pSetupFlag" to make sure it only sets up the sprites in the frame the first time the frame loops. We set "pSetupFlag" to TRUE in the on beginSprite handler{7}. Then, when the "on setup" handler is called by the on exitFrame handler, it sets "pSetupFlag" to FALSE. After it is set to FALSE, the "on setup" handler knows not to execute its code again. Thus, the setup is only performed once.
-- set score, lives, and prime for setup
on beginSprite me
if pStartingLives <> 0 then{6}
gLives = pStartingLives
gScore = 0
end if
showScore(me)
pLastFireTime = 0
pSetupFlag = TRUE -- set up rocks on next exitFrame{7}
end
The "on setup" handler sends messages to the appropriate number of rock sprites. These sprites then become visible rocks that have a random location and direction.
-- create initial set of rocks
on setup me
repeat with i = 1 to pStartNumRocks
sendSprite(pFirstRockSprite,#randomRock,pRockSpeed)
end repeat
pSetupFlag = FALSE -- setup complete
end
The frame behavior is responsible for detecting the user's input on the keyboard. The left- and right arrow keys are used to rotate the ship.{8} The up arrow is used to fire the thrusters.{9} The down arrow is used to stop the ship. {10} The spacebar is used to fire a bullet.{11} All these events, except the last, result in a message being sent to the ship sprite.
-- accept keyboard input
on keyboardInput me
-- rotate {8}
if keyPressed(124) then
sendSprite(pShipSprite,#rotate,5)
else if keyPressed(123) then
sendSprite(pShipSprite,#rotate,-5)
end if
-- move forward{9}
if keyPressed(126) then
sendSprite(pShipSprite,#shipThrust)
end if
-- stop movement{10}
if keyPressed(125) then
sendSprite(pShipSprite,#stopShip)
end if
-- fire bullet{11}
if keyPressed(SPACE) then
fireBullet(me)
end if
end
When the user fires a bullet, the "on fireBullet" handler is called. This checks to make sure that enough time has elapsed since the last firing to allow another. It then sends location and orientation information to a bullet sprite so that a bullet can be created.{12}
on fireBullet me
-- only allow a fire if enough time has passed since last
if the ticks > (pLastFireTime + pFireDelay) then
if pFireSound <> "" then puppetSound 1, pFireSound
-- use ship's angle and location for bullet
rot = sprite(pShipSprite).pAngle
loc = sprite(pShipSprite).pLoc
angle = 2.0*PI*rot/360.0
-- send message to bank of bullets
sendSprite(pFirstBulletSprite,#fire,loc,angle){12}
-- remember the time
pLastFireTime = the ticks
end if
end
This next handler compares the locations of all the rocks against the locations of all the bullets to see if any make contact. It uses the distance formula to compute if a bullet is close enough to a rock.{13} If a bullet makes contact, then the rock is destroyed, but two smaller rocks are created to take its place. However, if the rock is already the smallest type, then the two new rocks are not created.{14} Although this handler is examining the rock sprites, it makes a note if it can find at least one rock. This is stored in the "rockExists" variable. If no rocks are found, then the level is over.{15}
-- check all bullets and rocks to see if any collide
on checkHit me
rockExists = FALSE -- assume all rocks gone
-- loop through rocks
repeat with rock = pFirstRockSprite to pLastRockSprite
if sprite(rock).pMoving = FALSE then next repeat -- this rock not being used
rockExists = TRUE -- at least one rock still here
-- loop through bullets
repeat with bullet = pFirstBulletSprite to pLastBulletSprite
if sprite(bullet).pMoving = FALSE then next repeat -- bullet not being used
-- calculate distance{13}
dist = sqrt(power(sprite(bullet).locH-sprite(rock).locH,2)+[cc]
power(sprite(bullet).locV-sprite(rock).locV,2))
-- estimate rock radius
radius = sprite(rock).rect.width/2
-- see if the two are close enough
if dist < radius then
if pExplodeSound <> "" then puppetSound 2, pExplodeSound
-- what size should smaller rocks be?
if sprite(rock).pSize = "big" then
newSize = "medium"
else if sprite(rock).pSize = "medium" then
newSize = "small"
else
newSize = "none"
end if
loc = sprite(rock).loc -- get this rock's location
sendSprite(rock,#die) -- remove rock
sendSprite(bullet,#die) -- remove bullet
if newSize <> "none" then{14}
-- add two smaller rocks
sendSprite(pFirstRockSprite,#newRock,loc, [cc]
pRockSpeed,newSize)
sendSprite(pFirstRockSprite,#newRock,loc, [cc]
pRockSpeed,newSize)
end if
-- add to score
gScore = gScore + 10
showScore(me)
end if
end repeat
end repeat
-- if no rocks exist at all, then the level is over{15}
if not rockExists then
go to frame pNextLevelFrame
abort -- don't continue with Lingo commands
end if
end
This next handler is similar to the last. Instead of checking the positions of rocks and bullets, it checks the positions of rocks as compared to the ship. If one is too close{16}, then the ship has been hit.
-- see if a rock is close to the ship
on checkShipHit me
-- allow a grace period for new ships
if the ticks < sprite(pShipSprite).pGracePeriod then exit
-- loop through rocks
repeat with rock = pFirstRockSprite to pLastRockSprite
if sprite(rock).pMoving = FALSE then next repeat -- this rock not being used
-- see if the two are close enough
dist = sqrt(power(sprite(pShipSprite).locH-sprite(rock).locH,2)+[cc]
power(sprite(pShipSprite).locV-sprite(rock).locV,2))
-- estimate rock radius
radius = sprite(rock).rect.width/2
-- see if they hit
if dist < radius then{16}
if pDieSound <> "" then puppetSound 2, pDieSound
-- remove life
gLives = gLives - 1
if gLives < 0 then -- no lives left, game over
go to frame pEndGameFrame
abort -- don't continue with Lingo commands
else -- still some lives left
sprite(pShipSprite).member = member("ship explode")
updateStage -- briefly show explosion
sprite(pShipSprite).member = member("ship")
showScore(me) -- update score
sendSprite(pShipSprite,#reset) -- reset ship
end if
end if
end repeat
end
The "on showScore" handler is a utility for updating the onscreen text with the current score and the number of lives left.
-- update text on screen
on showScore me
text = ""
put "Score:"&&gScore&RETURN after text
put "Lives:"&&gLives&RETURN after text
member("score").text = text
end
Finally, the frame behavior ends with the on exitFrame handler. This calls the handlers previous that need to do something each and every frame. on exitFrame me if pSetupFlag then setup(me) -- initial setup of rocks (done once) keyboardInput(me) -- check keyboard checkHit(me) -- see if bullets hit rocks checkShipHit(me) -- see if rocks hit ship go to the frame -- loop on the frame end Ship Sprite BehaviorThe behavior for the ship's sprite needs to handle all the messages from the frame behavior. These are all reactions to player key presses. In addition, the ship sprite behavior needs to keep the ship moving if the ship has momentum. property pLoc -- sprite location property pOrigLoc -- reset location property pAngle -- sprite angle property pOrigAngle -- reset angle property pShipDX, pShipDY -- momentum property pGracePeriod -- period where ship is invulnerable The last property listed, "pGracePeriod" refers to a short span of time just after a new life for the player begins. During this short span of time, the ship cannot be hit by a rock. This grace period is used every time the ship is destroyed and a new one is created. It insures that the player does not die immediately after being reborn just because a rock happened to be there at that time. The on beginSprite handler saves the original location and direction of the ship for use when the ship is reset after being destroyed. It initializes the "pLoc" and "pAngle" properties as well, and starts the momentum, represented by both the "pShipDX" and pShipDY" properties. -- get starting properties on beginSprite me pOrigLoc = sprite(me.spriteNum).loc pLoc = pOrigLoc pOrigAngle = sprite(me.spriteNum).rotation pAngle = pOrigAngle pShipDX = 0 pShipDY = 0 pGracePeriod = 0 end When the player dies, the "reset" message is sent to all the sprites. Some of our behaviors will handle this message and others will not. In the case of the ship's behavior, the ship is reset to its starting location and direction, all momentum is taken away, and a grace period of three seconds is started. -- reset after a life lost on reset me pLoc = pOrigLoc sprite(me.spriteNum).loc = pLoc pAngle = pOrigAngle sprite(me.spriteNum).rotation = pAngle pShipDX = 0 pShipDY = 0 pGracePeriod = the ticks + 3*60 -- invulnerable for 3 seconds end When the user presses the left- or right arrow keys, the "rotate" message is sent to the ship. The "pAngle" property is changed and the sprite's rotation property is then changed to draw the ship sprite at the proper rotation. -- rotate ship on rotate me, angle pAngle = pAngle + angle sprite(me.spriteNum).rotation = pAngle end When the user presses the up arrow key, it means that he wants the thrusters to push the ship forward. The "on shipThrust" handler determines the horizontal and vertical components of this thrust as variables "dx" and "dy". It then adds this to the "pShipDX" and "pShipDY" properties. No actual movement is performed here, only a change in momentum. It is the on exitFrame handler's responsibility to move the ship according to this momentum. -- apply thrust on shipThrust me angle = 2.0*PI*pAngle/360.0 dx = cos(angle) dy = sin(angle) pShipDX = pShipDX + dx pShipDY = pShipDY + dyend The only responsibility of the on exitFrame handler is to move the ship. To this end, it takes the ship's current location and adds the momentum values{17}. It then checks to see if the ship's position is off the screen and wraps it around if so{18}.
-- move ship according to momentum
on exitFrame me
-- get ship location
x = pLoc.locH
y = pLoc.locV
-- move {17}
x = x + pShipDX
y = y + pShipDY
-- check to see if the ship is off the screen{18}
if x < 0 then x = x + (the stage).rect.width
if x > (the stage).rect.width then x = x - (the stage).rect.width
if y < 0 then y = y + (the stage).rect.height
if y > (the stage).rect.height then y = y - (the stage).rect.height
-- set ship location
pLoc = point(x,y)
sprite(me.spriteNum).loc = pLoc
end
Rock Sprite BehaviorThe rock sprite's behavior is mostly concerned with the same thing as the ship sprite's behavior: momentum. However, it begins with two handlers that deal with initializing the rock sprites. The first creates a big rock at a random position, with a random momentum{19}. This is for the beginning of a level, when several new rocks are created at once.
property pMoving -- is rock moving?
property pLoc -- rock location
property pAngle -- movement angle
property pSpeed -- rock speed
property pSize -- rock member size
property pType -- rock member type
property pRotate -- speed of tumble
-- create a random big rock at the start of a level
on randomRock me, speed
if pMoving then -- pass to next rock
sendSprite(me.spriteNum+1,#randomRock,speed)
else
pMoving = TRUE
x = 0 -- between left and right sides
y = random((the stage).rect.height) -- any vertical location {19}
pLoc = point(x,y)
pAngle = random(2.0*PI*100)/100.0 -- any angle
pSpeed = speed
pSize = "big"
pType = random(2) -- one of two types
pRotate = random(2) -- tumble 1 or 2 degrees at a time
if random(2) = 1 then pRotate = -pRotate -- 50% of negative tumble
end if
end
The second handler that deals with rock creation is called when a large rock is broken into two smaller pieces. This "on newRock" handler creates a rock of a specific size at a specific location{20}. The size is one less than the rock destroyed and the position is the same as the position of the rock that was destroyed.
-- create a new rock at a specific location with specific size
on newRock me, loc, speed, size
if pMoving then -- pass to next rock
sendSprite(me.spriteNum+1,#newRock,loc,speed,size)
else
pMoving = TRUE
pLoc = loc {20}
pAngle = random(2.0*PI*100)/100.0 -- any angle
pSpeed = speed
pSize = size
pType = random(2) -- one of two types
pRotate = random(2) -- tumble 1 or 2 degrees at a time
if random(2) = 1 then pRotate = -pRotate -- 50% of negative tumble
end if
end
When a rock is hit, the sprite needs to note that the rock is no longer moving and move the image off the screen so that it isn't visible. -- take rock out of the action on die me pMoving = FALSE sprite(me.spriteNum).loc = point(-100,-100) end The on exitFrame handler is similar to the ship sprite's on exitFrame handler. It just deals with the change in the ship's position due to its momentum{21}. It also provides the screen wrapping so that the rock is never off the screen{22}.
-- move rock according to momentum
on exitFrame me
if pMoving then
-- get location
x = pLoc.locH
y = pLoc.locV
-- calculate movement{21}
dx = cos(pAngle)*pSpeed
dy = sin(pAngle)*pSpeed
-- move
x = x + dx
y = y + dy
-- see if the rock is off the screen{22}
if x < 0 then x = x + (the stage).rect.width
if x > (the stage).rect.width then x = x - (the stage).rect.width
if y < 0 then y = y + (the stage).rect.height
if y > (the stage).rect.height then y = y - (the stage).rect.height
-- set the location
pLoc = point(x,y)
sprite(me.spriteNum).loc = pLoc
sprite(me.spriteNum).member = member("Rock"&&pSize&&pType)
-- set the rotation
rot = sprite(me.spriteNum).rotation
rot = rot + pRotate
sprite(me.spriteNum).rotation = rot
end if
end
One other thing that the on exitFrame handler does is rotate the rock a bit. This does not affect the momentum at all, but simply makes the rock appear as if it is tumbling. This is purely a special effect and does not affect the game play. Bullet Sprite BehaviorThe bullet sprite behavior does not introduce anything new. It uses the same basic method of handling bullets as the Sprite Invaders game from the last chapter. However, it is using the same horizontal and vertical momentum as the ship and the rocks. The "on fire" handler deals with creating a new bullet. It passes the message on if the current sprite is too busy.{23}
property pMoving -- is bullet being used?
property pLoc -- sprite location
property pAngle -- movement angle
-- fire a new bullet
on fire me, loc, angle
if pMoving then -- pass on to next sprite{23}
sendSprite(me.spriteNum+1,#fire,loc,angle)
else
-- start moving
pMoving = TRUE
pLoc = loc
pAngle = angle
end if
end
When the bullet hits something, or leaves the screen, it is reset. -- remove bullet on die me pMoving = FALSE sprite(me.spriteNum).loc = point(-100,-100) end The on exitFrame handler moves the bullet sprite according to momentum {24} and also checks to see if the bullet has left the screen{25}.
-- move bullet according to momentum
on exitFrame me
if pMoving then
-- get location
x = pLoc.locH
y = pLoc.locV
-- calculate movement
dx = cos(pAngle)*10.0
dy = sin(pAngle)*10.0
-- move{24}
x = x + dx
y = y + dy
-- see if the bullet is off the screen{25}
if (x < 0 or x > (the stage).rect.width) or[cc]
(y < 0 or y > (the stage).rect.height) then
-- bullets do not loop around screen, they die instead
die(me)
else
-- set location
pLoc = point(x,y)
sprite(me.spriteNum).loc = pLoc
end if
end if
end
| |