The Bitwise Challenge Post-Mortem, Part 4

This is part 4 of the post-mortem I’m doing for the Bitwise Challenge, in which I made a Tetris-like game called Loftis as an Addon in WildStar in four hours. Here are links to parts onetwo, and three.

Okay, so by the end of part 3 we had written the logic for creating new blocks, testing whether a position is valid, rotating blocks, and moving blocks side to side as they rise through the field.

If when a block is rising it cannot move into the position above, it is time to place it. The code for placing a block is straightforward:

 local tGame = self.tGame
 local tBlock = self.tGame.tBlock
 for y = 1,4 do 
   for x = 1,4 do
     if tBlock[y][x] then
       tGame.arField[tGame.iRow + y - 1][tGame.iColumn + x - 1] = true
       local tPixie = tGame.arPixies[tGame.iRow + y - 1][tGame.iColumn + x - 1]
       local nLeft = (tGame.iColumn + x - 2) * 32
       local nTop = -128 + (tGame.iRow + y) * 32 - 32

       tPixie.loc.nOffsets = {nLeft, nTop, nLeft + 32, nTop + 32}
       tPixie.id = self.wndGame:AddPixie(tPixie)
       tGame.arPixies[tGame.iRow + y - 1][tGame.iColumn + x - 1] = tPixie
     end
   end
 end
 self.tGame.tBlock = nil

Aaaaaaaack! More magic numbers. Bad Bitwise. I’m not going to talk too much about the Pixies API calls here because that is WildStar-specific, and these post-mortems are more about the logic behind Loftis. All that we are really doing here is setting the tGame.arField[y][x] to true for any corresponding true value in our rising block. After doing that we reset the tGame.tBlock member to nil. We’ll see why later.

After placing a new block, we check to see if we have completed any rows. This is done in two steps. First, we make a list of all rows in which all 10 blocks are filled.

 -- check for scoring
 nRowCount = 0
 arRowsToRemove = {}
 for y = 4,21 do
   local nBlocks = 0
   for x = 1,10 do
     if tGame.arField[y][x] then
       nBlocks = nBlocks + 1
     end
   end

   if nBlocks == 10 then
     nRowCount = nRowCount + 1
     arRowsToRemove[nRowCount] = y
   end
 end

Note: the arRowsToRemove variable should be local! I’m really not sure why it wasn’t flagged as local; might have been an edit or just an oversight. Also note that we start our checking at row 4 because rows 1-3 are dummy rows that are always on. But we only go to 21! Another bug! That should be 24. Whenever we find a row that is completely on, we store its index and continue.

After compiling our list of completed rows, we remove them in reverse order, so as to preserve the correct indices of preceding rows.

 if nRowCount == 0 then
   return
 end

 local nScore = 50
 for i = nRowCount,1,-1 do
   nScore = nScore * 2
   self:RemoveRow(arRowsToRemove[i])
 end

 self.tGame.nScore = self.tGame.nScore + nScore

We also double the score for each row and add that temporary value to the game score. Since we started at 50, the scores for completing 1-4 rows will be 100, 200, 400, 800.

Removing a row is simple:

 for y = nRow, 27 do
   for x = 1,10 do
     tGame.arField[y][x] = tGame.arField[y + 1][x]
   end
 end

 for x = 1,10 do
   tGame.arField[28][x] = false
 end

The magic numbers… they burn. You can hopefully see why removing the rows in reverse order is necessary here. Note that this method of counting the rows and then calling RemoveRow for each of the rows is really really inefficient. Because of the way I do this, if I were to remove the first 4 rows due to a particularly well placed I-piece, nearly the entirely field is moved 4 entire times. There are two reasons I did it this way: one, it was very very fast to write it this way. Two, even though it is about as inefficient as possible, it’s still plenty fast enough to get all the work done without a hiccup. Again, in a production environment or with a larger dataset, we wouldn’t do it this way.

Along those same lines, if you look at the code dealing with pixies surrounding the removal code, you can see that I destroy all the blocks in the field and re-create them with every single row removal. Extremely inefficient! I started to do this in a better way but by this point I was really sweating the clock and ended up just hacking this code because I knew performance wasn’t a concern.

That brings us to the last part of this Addon, the update function. Here the function is called OnTimer, because I implemented through the use of an Apollo timer. Normally I would respond every frame and calculate how much time had elapsed for the simulation. Here, however, I just decided to set a 20 Hz timer and always assume that 50 ms had elapsed. Again, a coding speed decision. The result of this is that if WildStar slows down to less than 20 FPS, Loftis will slow down, making it easier to play.

Here’s the OnTimer function:

function Loftis:OnTimer()
 if self.tGame == nil then
   return
 end
 local tGame = self.tGame
 if tGame.bGameOver then
   return
 end
 if tGame.tBlock == nil then
   -- we need to make a new block and put it in the world
   -- if we can't place the block, game over man
   local iBlock = math.random(1, 7)
   local tBlock = InitBlock(karBlocks[iBlock])
   local nRotate = math.random(1, 4)
   for i = 1,nRotate do
     tBlock = RotateBlockRight(tBlock)
   end
   if not self:IsPositionValid(tBlock, 24, 5) then
     self:DoGameOver()
   end
   tGame.tBlock = tBlock
   tGame.iColumn = 4
   tGame.iRow = 24
   tGame.fProgress = 0
   return
 end

 tGame.fProgress = tGame.fProgress + 0.10
 if tGame.fProgress >= 1.0 then
   if tGame.iRow > 1 and self:IsPositionValid(tGame.tBlock, tGame.iRow - 1, tGame.iColumn) then
     tGame.fProgress = 0
     tGame.iRow = tGame.iRow - 1
   else
     self:PlaceBlock()
     return
   end
 end

 -- Update Block pixies
 self:UpdateBlockPixies()
end

Now you can see why I initialized the tGame.tBlock member to nil when starting a new game, and why we reset it to nil when it is placed. Every update, we check to see if it is nil, and if it is, we create a new one from the 7 variations, rotate it 1-4 times, and then place it if we can, or call DoGameOver if we cannot.

Rising is controlled through the fProgress member, as noted previously. Here’s another magic number, 0.10, which determines how fast the blocks are rising. This equates to one block every half a second. If we had used an elapsed time model, we would have used a constant for speed (2.0) and multiplied it by elapsed time. Whenever fProgress gets to 1, we check to see if the block can keep rising and reset progress to 0, or place the block.

Finally, we call UpdateBlockPixies to modify the sprites that are used to draw the block. I won’t go into much detail there (again, WildStar specific) but the method I used does allow for “smooth movement” of the blocks as they rise, using fProgress to determine the relative y-value of the sprites drawn.

That’s pretty much it for Loftis. I’ve gone over all the major logic in the game and hopefully showed my thought process as I wrote the game. Not bad for a 4-hour challenge, though your mileage may vary. Obviously there were bugs that I found as I did this post-mortem, and there might be others as well. If you are in the WildStar beta, please feel free to install Loftis, modify it, etc. I myself will be updating a version with a little more polish and a few less bugs eventually, probably next week, depending on the weather here.

If you are not in the WildStar beta, you can probably follow along with the Apollo API calls and get a pretty good feel for what they do based on their names. I hope to see you in the game eventually!

Bitwise out.

Author: Wiesman

Husband, father, video game developer, liberal, and perpetual Underdog.

2 thoughts on “The Bitwise Challenge Post-Mortem, Part 4”

Disagree?

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s