|
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 script for this game is long and complicated. It uses everything from property lists to multi-line comparison statements. You will definitely want to check out the file on the CD-ROM before reading the rest of the chapter. Even though there is a lot of code, it all still fits in just one frame behavior. This behavior controls everything in the game. Instead of using an on getPropertyDescriptionList handler, we will do something different here. Because it's unlikely that a multimedia author will want to drag, drop, and customize this game, we are putting all the game constants in the on beginSprite handler. It's still easy to find one of these properties and change it if necessary. The behavior starts by declaring these properties, as well as a bunch of others. These other properties will be used to keep track of the game state throughout play. Descriptions for each can be read in the following comments. --properties set in on beginSprite property pSpeed property pEntryLocation property pLeftWall property pRightWall property pFloor property pRowMax property pBlockSize property pFirstSprite property pPieceList -- list of piece types property pPieceType -- current piece type property pPieceOrientation -- current piece orientation property pPieceSprites -- sprites used by the piece property pPieceLoc -- location of the piece property pNextMoveTime -- when next drop will happen As promised, the on beginSprite handler starts by setting all the game constants. These constants determine the game speed, playing area, block size, and the sprites used in the Score. on beginSprite me pSpeed = 20 -- 20 ticks need to pass before a drop pEntryLocation = point(200,0) -- starting spot of the drop pLeftWall = 150 -- leftmost block position pRightWall = 250 -- rightmost block position pFloor = 280 -- bottommost block position pRowMax = 11 -- how many rows there are pBlockSize = 10 -- space separating the blocks pFirstSprite = 11 -- first sprite that can be used by a block getPieceList(me) -- initialize piece list pNextMoveTime = the ticks -- set next move for now dropNewPiece(me) -- create a new piece end The on beginSprite handler ends by calling "on getPieceList", setting the "pNextMoveTime" property, and dropping the first piece. The "on getPieceList" handler creates the game constant "pPieceList". This is a list of all the pieces and their configurations for different orientations. For instance, take the first piece type. It shows a [[-1,0],[0,0],[1,0]] as the first orientation. This means there is one block one space to the left of center, on block at the center, and one block one space to the right of center. Figure 7.2 shows this piece if we were using hollow squares for each block. When it is rotated to the right, the next orientation, [[0,-1],[0,0],[0,1]], is used. This has one block above the center, one block at the center, and one block to the right of the center. Figure 7.3 shows the piece with the rotation. Because another rotation to the right returns the piece to the same shape as the first orientation, no more is needed. For other shape types, up to four orientations are needed. Figure 7.2 The piece represented by the list [[-1,0],[0,0],[1,0]]. Figure 7.3-- Initializes pPieceList with a list of all the pieces -- Each piece includes all the different rotations on getPieceList me pPieceList = [] -- three blocks, straight list = [] add list, [[-1,0],[0,0],[1,0]] add list, [[0,-1],[0,0],[0,1]] add pPieceList, list -- three blocks, bent list = [] add list, [[0,0],[0,-1],[1,0]] add list, [[0,0],[1,0],[0,1]] add list, [[0,0],[0,1],[-1,0]] add list, [[0,0],[-1,0],[0,-1]] add pPieceList, list -- two blocks, straight list = [] add list, [[0,0],[1,0]] add list, [[0,0],[0,1]] add list, [[0,0],[-1,0]] add list, [[0,0],[0,-1]] add pPieceList, list -- four blocks, square list = [] add list, [[0,0],[1,0],[0,-1],[1,-1]] add pPieceList, list end The "on dropNewPiece" handler starts a piece dropping at the beginning of the game. It is also used just after a piece lands to drop the next piece. This handler is responsible for picking a random piece and a random orientation. It also looks through the sprites in the Score to find sprites not in use. It uses these sprites to display the blocks of the piece. The property "pFirstSprite" is used to tell the code where a large span of sprites begins. These sprites are available to be used as blocks on the game. In this example, "pFirstSprite" is set to 11. The sprite channels from 11 to 318 contain block bitmap members. Because the game screen is a grid that is 11 blocks wide and 28 blocks high, we know that we will never need more than 308 sprites (11 times 28) no matter how well the player packs in the blocks. If you change the dimensions of the board, you may want to change the number or sprites as well. These sprites are then taken by the "on dropNewPiece" handler and put into the "pPieceSprites" list{1}. The "on drawPiece" handler uses these sprites to draw the current piece as it falls. When the piece lands, the sprites are still used by the same blocks as they lay at the bottom of the screen. If a sprite is in use, it will have a positive locV value. If it is not in use, it will have a negative locV value and thus be out of sight above the top of the Stage. So we start all 308 sprites above the top of the screen. They will all have negative locV values. As we use sprites, they are drawn on the screen and are given positive locV values. If a sprite is removed from the screen, which happens when a row clears, then the sprite is placed at a negative locV. This not only takes the sprite off the Stage visually, but also lets the "on dropNewPiece" handler find it when it's time to drop the next piece.
-- create a new piece and drop from the top
on dropNewPiece me
-- choose a random piece
pPieceType = random(pPieceList.count)
-- find some empty sprites
pPieceSprites = []
numSpritesNeeded = pPieceList[pPieceType][1].count
repeat with i = pFirstSprite to the lastChannel
if sprite(i).locV < 0 then -- sprite not in use
add pPieceSprites, i{1}
if pPieceSprites.count = numSpritesNeeded then exit repeat
end if
end repeat
-- pick a random orientation
pPieceOrientation = random(pPieceList[pPieceType].count)
-- starting location
pPieceLoc = duplicate(pEntryLocation)
drawPiece(me)
end
The "on drawPiece" handler takes the sprites in "pPieceSprites" and uses them to display the blocks in the piece. It uses the "pPieceLoc" as well as the data from the "pPieceList" to draw each block.
-- display a piece in the proper orientation
on drawPiece me
-- loop through all sprites
repeat with i = 1 to pPieceSprites.count
-- set the location of the sprite
sprite(pPieceSprites[i]).loc = pPieceLoc + \
point(pPieceList[pPieceType][pPieceOrientation][i][1]*pBlockSize, \
pPieceList[pPieceType][pPieceOrientation][i][2]*pBlockSize)
end repeat
end
When the time comes, each piece must drop one space down. This is simply done by changing the "pPieceLoc". However, two events must be looked for first. Both of these events result in the piece stopping, and a new piece being dropped from the top. The first event is when the piece lands on top of another piece. The second event is when the piece hits the bottom. Both of these are handled by their own handler functions. In addition, if one of these two conditions is met, then some other checks must be conducted. The rows of blocks in the game need to be examined to see if any rows are complete. All the blocks need to be checked to see if one has landed at the top of the screen, in which case the game is over.
-- move the piece down
on movePiece me
-- see if it should stop
if fallOnPiece(me) or hitBottom(me) then
-- see if any rows are now complete
checkRowsCompleted(me)
-- see if the piece is stuck at the top
checkGameEnd(me)
-- get the next piece set
dropNewPiece(me)
-- return FALSE since no move was made
return FALSE
end if
-- move down
pPieceLoc.locV = pPieceLoc.locV + pBlockSize
-- redraw sprites
drawPiece(me)
-- return TRUE since a move was made
return TRUE
end
The "on hitBottom" handler checks all the sprites in the piece to see if any are in the space just above the floor. If so, then the piece has hit the bottom and must stop.
-- check to see if the piece hit the floor
on hitBottom me
-- loop through sprites
repeat with s in pPieceSprites
-- if a sprite touches the floor
if (sprite(s).locV >= pFloor) then
return TRUE
end if
end repeat
return FALSE
end
The "on fallOnPiece" handler checks all the piece's sprites against all the other block sprites. If the piece appears to be just above another block, then it has landed on it and the block must stop.
-- check to see if the piece hit another piece
on fallOnPiece me
-- loop through sprites
repeat with i = pFirstSprite to the lastChannel
-- see if the sprite is being used
if sprite(i).locV < 0 then next repeat
-- make sure it is not one of the sprites from the current piece
if getOne(pPieceSprites,i) then next repeat
-- loop through the piece's sprites
repeat with s in pPieceSprites
-- see if the sprite is run on top of another
if (sprite(i).locH = sprite(s).locH) and[cc]
(sprite(i).locV = sprite(s).locV+pBlockSize) then
return TRUE
end if
end repeat
end repeat
return FALSE
end
The "on checkRows" handler looks at all the sprites and compiles a list of which sprites are in which row. If any row contains the maximum number of sprites, then that row must be removed{2}.
-- check all sprites to see if any complete rows are formed
on checkRowsCompleted me
-- start an empty property list
list = [:]
-- loop through all sprites
repeat with i = pFirstSprite to the lastChannel
-- if the sprite is in use
if sprite(i).locV > 0 then
-- if this row is not yet in the list
if voidP(getAProp(list,sprite(i).locV)) then
addProp list, sprite(i).locV, 1
else -- add one to the number of sprites in the row
setProp list, sprite(i).locV, getProp(list,sprite(i).locV)+1
end if
end if
end repeat
-- loop through all the rows
repeat with i = 1 to list.count
-- if the row has the maximum number of sprite
if list[i] = pRowMax then{2}
-- clear the row
removeRow(me,getPropAt(list,i))
end if
end repeat
end
To remove a row, the code simply looks at every sprite in use. It takes away and recycles any sprites in the row to be removed. It also moves any sprite in rows above it down by one{3}.
-- clear a row and move everything above it down
on removeRow me, v
-- loop through sprites
repeat with i = pFirstSprite to the lastChannel
if sprite(i).locV < 0 then -- sprite not used
next repeat -- do nothing
else if sprite(i).locV < v then -- sprite above row
sprite(i).locV = sprite(i).locV + pBlockSize -- move down{3}
else if sprite(i).locV = v then -- sprite in row
sprite(i).locV = -100 -- remove
end if
end repeat
end
Checking for the end game is simple. Each sprite is checked to see if it's at the top. If just one block is stopped at the top, the game is over.
-- see if the current piece is stuck near the top
-- and end the game if so
on checkGameEnd me
if pPieceLoc.locV <= 10 then
go to frame "End"
abort -- don't continue
end if
end
The on exitFrame handler calls the "on movePiece" handler to keep the piece dropping. It does this only when the value of the ticks reaches the value in "pNextMoveTime". It also resets "pNextMoveTime" to 20 ticks in the future.
-- every frame, check to see if it is time for next drop
on exitFrame me
if the ticks > pNextMoveTime then
movePiece(me)
pNextMoveTime = the ticks + pSpeed
end if
go to the frame
end
The user interacts with the game through five keys. The left- and right-arrow key move the piece left and right. The "." and "," keys, which also have the symbols ">" and "<" on them, are used to rotate the piece left and right. In addition, the spacebar is used for a special function. It tells the game to drop the current piece as fast as possible, so that the player does not have to wait for it to get to the bottom.
-- takes keyboard input
on keyUp me
case the keyCode of
124: -- right arrow
if not hitOtherPiece(me,pBlockSize) then
pPieceLoc.locH = pPieceLoc.locH + pBlockSize
end if
123: -- left arrow
if not hitOtherPiece(me,-pBlockSize) then
pPieceLoc.locH = pPieceLoc.locH - pBlockSize
end if
end case
case the key of
".": -- rotate right
pPieceOrientation = pPieceOrientation + 1
if pPieceOrientation > pPieceList[pPieceType].count then pPieceOrientation = 1
",": -- rotate left
pPieceOrientation = pPieceOrientation - 1
if pPieceOrientation < 1 then pPieceOrientation = pPieceList[pPieceType].count
SPACE: -- quickly drop
drop(me)
end case
pushAwayFromEdges(me) -- make sure the piece doesn't go past edge
drawPiece(me)
end
The on keyUp handler ends with a call to "on pushAwayFromEdges". This handler takes care of an odd situation in the game in which the user tries to move the piece past the left or right side of the playing area. However, it isn't as simple as just stopping the user from pushing past a limit. When pieces are rotated they can also go past the limit. A piece that occupies column 1 and 2 in the playing area, can attempt to rotate into column 0, for instance. This handler takes care of any situation where the piece is over the edge. It simply recognizes that this is happening{4}, and nudges the piece back toward the center by one column{5}.
-- special handler that looks to make sure the
-- piece is not past the edges, and moves it in
-- if it is
-- needs to be called when the user moves the piece
-- or rotates it
on pushAwayFromEdges me
-- assume no problem
pastLeftEdge = FALSE
pastRightEdge = FALSE
-- loop through piece's sprites
repeat with i = 1 to pPieceSprites.count
-- get sprite location
x = pPieceLoc.locH + pPieceList[pPieceType][pPieceOrientation][i][1]*10
-- see if it is too far right
if x > pRightWall then pastRightEdge = TRUE{4}
-- see if it is too far left
if x < pLeftWall then pastLeftEdge = TRUE{4}
end repeat
-- move piece in if necessary
if pastRightEdge then adjustment = -pBlockSize{5}
else if pastLeftEdge then adjustment = pBlockSize{5}
else exit
pPieceLoc.locH = pPieceLoc.locH + adjustment
end
The "on hitOtherPiece" handler checks for a very unusual occurrence. If the user is moving the piece across the screen, and there are other blocks in the way, the game needs to prevent the user from moving the piece through the existing blocks. To do this, the handler checks the proposed new position of the piece{6}, and looks to see if any blocks are in the way{7}. If so, a FALSE is returned. This tells the handler calling it, in this case the on keyUp handler, not to allow the move.
-- check to see if the user tried to move the piece
-- into another piece
on hitOtherPiece me, changeLoc
-- loop through the piece's sprites
repeat with i = 1 to pPieceSprites.count
-- determine the sprite's location
thisloc = point(changeLoc,0) + pPieceLoc + \
point(pPieceList[pPieceType][pPieceOrientation][i][1]*pBlockSize, \
pPieceList[pPieceType][pPieceOrientation][i][2]*pBlockSize){6}
-- loop through all the sprites
repeat with j = pFirstSprite to the lastChannel
-- make sure sprite is in use
if sprite(j).locV < 0 then next repeat
-- make sure sprite is not part of piece
if getOne(pPieceSprites,j) then next repeat
-- see if the locations are the same
if sprite(j).loc = thisloc then return TRUE{7}
end repeat
end repeat
return FALSE
end
Finally, end with the simple "on drop" handler. This loops quickly with a repeat loop while calling "on movePiece". When "on movePiece" returns a FALSE, it means that the piece has reached a stopping point and the game continues as normal. The updateStage command is used here to force a screen redraw.
-- when user hits space, quickly drop the piece
on drop me
repeat while TRUE
-- move down until hits something
if not movePiece(me) then exit
-- force stage update
updateStage
end repeat
end
| |