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 7 Section 3

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