Chapter 4(2):Tetris 俄罗斯方块

Tetris, Tetris, Tetris!

Enough with all the helper classes and game components discussions. It is time to write another cool game. Thanks to the many classes available in the little game engine it is now easy to write text on the screen, draw sprites, handle input, and play sounds.


Before going into the details of the Tetris game logic, it would be useful to think about the placement of all game elements in a similar way you did in the previous games. Instead of drawing all game components on the screen, you just show the background boxes to see what is going to be displayed. For the background you use the space background once again (I promise, this will be the last time). The background box is a new texture and exists in two modes (see Figure 4-7). It is used to separate the game components and make everything fit much nicer on the screen. You could also just reuse the same box for both parts of the game, but because the aspect ratio is so different for them it would either look bad for the background box or for the extra game components, which are smaller, but also need the background box graphic, just a smaller version of it.


Figure 4-7

Rendering the Background

To render these boxes on the screen you use the SpriteHelper class again and test everything with help of the following unit test:



public static void TestBackgroundBoxes()
// Render background;
// Draw background boxes for all the components
// 为所有组件绘制背景框 Rectangle(
512 - 200- 1540 - 12400 + 23, (768 - 40+ 16));
new Rectangle(
512 - 480- 1540 - 10290 - 30300));
new Rectangle(
512 + 240- 1540 - 10290 - 30190));
 // TestBackgroundBoxes()


This unit test will produce the output shown in Figure 4-8.


Figure 4-8

You might ask why the right box is a little bit smaller and where I got all these values from. Well, I just started with some arbitrary values and then improved the values until everything in the final game fit. First, the background is drawn in the unit test because you will not call the Draw method of TetrisGame if you are in the unit test (otherwise the unit tests won’t work anymore later when the game is fully implemented).


Then three boxes are drawn. The upper-left box is used to show the next block. The center box shows the current Tetris grid. And finally, the upper-right box is used to display the scoreboard. You already saw the unit test for that earlier.


Handling the Grid

It is time to fill the content of these boxes. Start with the main component: the TetrisGrid. This class is responsible for displaying the whole Tetris grid. It handles the input and moves the falling block and it shows all the existing data as well. You already saw which methods are used in the TetrisGrid class in the discussion about the game components. Before rendering the grid you should check out the first constants defined in the TetrisGrid class:





There are a couple more interesting constants, but for now you only need the grid dimensions. So you have 12 columns and 20 lines for your Tetris field. With help of the Block.png texture, which is just a simple quadratic block, you can now easily draw the full grid in the Draw method:



// Calc sizes for block, etc.
int blockWidth = gridRect.Width / GridWidth;
int blockHeight = gridRect.Height / GridHeight;
for ( int x=0; x<GridWidth; x++ )
for ( int y=0; y<GridHeight; y++ )
new Rectangle(
+ x * blockWidth,
+ y * blockHeight,
-1, blockHeight-1),
new Color(606060128)); // Empty color
 // for for


The gridRect variable is passed as a parameter to the Draw method from the main class to specify the area where you want the grid to be drawn to. It is the same rectangle as you used for the background box, just a little bit smaller to fit in. The first thing you are doing here is calculating the block width and height for each block you are going to draw. Then you go through the whole array and draw each block with the help of the SpriteHelper.Render method using a half transparent dark color to show an empty background grid. See Figure 4-9 to see how this looks. Because of the fact that you use game components you also don’t have to do all this code in your unit test. The unit test just draws the background box and then calls the TetrisGrid.Draw method to show the results (see the TestEmptyGrid unit test).


Figure 4-9

Block Types

Before you can render anything useful on your new grid you should think about the block types you can have in your game. The standard Tetris game has seven block types; all of them consist of four small blocks connected to each other (see Figure 4-10). The most favorite block type is of course the line type because you can kill up to four lines with that giving you the most points.


Figure 4-10

These block types have to be defined in the TetrisGrid class. One way of doing that is to use an enum holding all the possible block types. This enum can also hold an empty block type allowing you to use this data structure for the whole grid too because each grid block can contain either any part of the predefined block types or it is empty. Take a look at the rest of the constants in the TetrisGrid class:



/// <summary>
/// Block types we can have for each new block that falls down.
/// 每一种可以下落的砖块的类型
/// </summary>

public enum BlockTypes
 // enum BlockTypes

/// <summary>
/// Number of block types we can use for each grid block.
/// 我们可以使用的砖块种类的数目
/// </summary>

public static readonly int NumOfBlockTypes =

/// <summary>
/// Block colors for each block type.
/// 每一种砖块的颜色
/// </summary>

public static readonly Color[] BlockColor = new Color[]
new Color( 606060128 ), // Empty, color unused
    new Color( 5050255255 ), // Line, blue
    new Color( 160160160255 ), // Block, gray
    new Color( 2555050255 ), // RightT, red
    new Color( 25525550255 ), // LeftT, yellow
    new Color( 50255255255 ), // RightShape, teal
    new Color( 25550255255 ), // LeftShape, purple
    new Color( 5025550255 ), // Triangle, green
// Color[] BlockColor
/// <summary>
/// Unrotated shapes
/// 未旋转的形状
/// </summary>

public static readonly int[][,] BlockTypeShapesNormal = new int[][,]
// Empty
    new int[,] 0 } },
// Line
    new int[,] 010 }010 }010 }010 } },
// Block
    new int[,] 11 }11 } },
// RightT
    new int[,] 11 }10 }10 } },
// LeftT
    new int[,] 11 }01 }01 } },
// RightShape
    new int[,] 011 }110 } },
// LeftShape
    new int[,] 110 }011 } },
// Triangle
    new int[,] 010 }111 }000 } },
// BlockTypeShapesNormal


BlockTypes is the enum we talked about; it contains all the possible block types and also is used to randomly generate new blocks in the NextBlock game component. Initially all of the grid fields are filled with the empty block type. The grid is defined as:



/// <summary>
/// The actual grid, contains all blocks,
/// including the currently falling block.
/// 实际的格子包含所有的砖块,也包括现在正在下落的砖块
/// </summary>

BlockTypes[,] grid = new BlockTypes[GridWidth, GridHeight];


By the way, NumOfBlockTypes shows you the usefulness of the enum class. You can easily determine how many entries are in the BlockTypes enum.


Next the colors for each block type are defined. These colors are used for the NextBlock preview, but also for rendering the whole grid. Each grid has a block type and you can easily use the BlockColors by converting the enum to an int number, which is used in the Draw method:



And finally the block shapes are defined, which looks a little bit more complicated, especially if you take into consideration that you have to allow these block parts to be rotated. This is done with help of the BlockTypeShapes, which is a big array of all possible blocks and rotations calculated in the constructor of TetrisGrid.


To add a new block to the Tetris grid you can just add each of the block parts to your grid, which is done in the AddRandomBlock method. You keep a separate list called floatingGrid to remember which parts of the grid have to be moved down (see the following section, “Gravity”; you can’t just let everything fall down) each time Update is called:



// Randomize block type and rotation
// 随机化砖块类型和旋转
currentBlockType = (int)nextBlock.SetNewRandomBlock();
= RandomHelper.GetRandomInt(4);

// Get precalculated shape
// 获得预先计算好的形状
int[,] shape = BlockTypeShapes[currentBlockType,currentBlockRot];
int xPos = GridWidth/2-shape.GetLength(0)/2;
// Center block at top most position of our grid
currentBlockPos = new Point(xPos, 0);

// Add new block
for ( int x=0; x<shape.GetLength(0); x++ )
for ( int y=0; y<shape.GetLength(1); y++ )
if ( shape[x,y] > 0 )
// Check if there is already something
      if (grid[x + xPos, y] != BlockTypes.Empty)
// Then game is over dude!
        gameOver = true;
 // if
+ xPos, y] = (BlockTypes)currentBlockType;
+ xPos, y] = true;
 // else
 // for for if


First you determine which block type you are going to add here. To help you do that you have a helper method in the NextBlock class, which randomizes the next block type and returns the last block type that was displayed in the NextBlock window. The rotation is also randomized; say “Hi” to the RandomHelper class.


With that data you can now get the precalculated shape and put it centered on the top of your grid. The two for loops iterate through the whole shape. It adds each valid part of the shape until you hit any existing data in the grid. In case that happens the game is over and you hear the lose sound. This will happen if the pile of blocks reaches the top of the grid and you cannot add any new blocks.


You now have the new block on your grid, but it is boring to just see it on the top there; it should fall down sometimes.



To test the gravity of the current block the TestFallingBlockAndLineKill unit test is used. The active block is updated each time you call the Update method of TetrisGrid, which is not very often. In the first level the Update method is called only every 1000ms (every second). There you check if the current block can be moved down:



// Try to move floating stuff down
if (MoveBlock(MoveTypes.Down) == false ||
// Failed? Then fix floating stuff, not longer moveable!
  for ( int x=0; x<GridWidth; x++ )
for ( int y=0; y<GridHeight; y++ )
= false;
 // if
movingDownWasBlocked = false;


Most of the Tetris logic is done in the MoveBlock helper method, which checks if moving in a specific direction is possible at all. If the block can’t be moved anymore it gets fixed and you clear the floatingGrid array and play the sound for landing a block on the ground.


After clearing the floatingGrid array there is no active block you can move down and the following code is used to check if a line was destroyed:



// Check if we got any moveable stuff,
// if not add new random block at top!
// 检查是我们是否还有可以移动的砖块,如果没有了,就在顶部随机生成一个砖块
bool canMove = false;
for ( int x=0; x<GridWidth; x++ )
for ( int y=0; y<GridHeight; y++ )
if ( floatingGrid[x,y] )
= true;
if (canMove == false)
int linesKilled = 0;
// Check if we got a full line
  for ( int y=0; y<GridHeight; y++ )
bool fullLine = true;
for ( int x=0; x<GridWidth; x++ )
if ( grid[x,y] == BlockTypes.Empty )
= false;
 // for if
// We got a full line?
// 有一个满行?
    if (fullLine)
// Move everything down
// 将所有的都向下移动
     for ( int yDown=y-1; yDown>0; yDown - )
for ( int x=0; x<GridWidth; x++ )
+1= grid[x,yDown];
// Clear top line
// 清除顶行
     for ( int x=0; x<GridWidth; x++ )
0,x] = BlockTypes.Empty;
// Add 10 points and count line
// 增加10分和消除的行数
     score += 10;
 // if
 // for
// If we killed 2 or more lines, add extra score
// 如果一次清楚超过2行,有额外加分
  if (linesKilled >= 2)
+= 5;
if (linesKilled >= 3)
+= 10;
if (linesKilled >= 4)
+= 25;
// Add new block at top
// 在顶部添加新砖块
 // if


The first thing that is done here is to check if there is an active moving block. If not you go into the “if block,” checking if a full line is filled and can be destroyed. To determine if a line is filled you assume it is filled and then check if any block of the line is empty. Then you know that this line is not fully filled and continue checking the next line. If the line is filled, however, you remove it by copying all the lines above it down. This is the place where a nice explosion could occur. Anyway, the player gets 10 points for this line kill, and you hear the line kill sound.

现在要做的第一件事是是否有正在移动的砖块。如果没有,你会进到if block,”检查是否有已被填满的行,然后将其销毁。要确定一行是否被填满,你首先要假设他是被填满的然后检查这一行中是否有空的格子。然后你就可以知道这一行还没有被填满并继续检查下一行。如果这是一个满行,你就需要把从这一行开始的每一行的上一行往下复制,这样这一行就被消除了。可以显示一个爆炸的场景。玩家每消掉一行就会得到10分,你还会听到消行的声音。

If the player was able to kill more than one line he gets awarded more points. And finally the AddRandomBlock method you saw before is used to create a new block at the top.


Handling Input

Handling the user input itself is no big task anymore thanks to the Input helper class. You can easily check if a cursor or gamepad key was just pressed or is being held down. Escape and Back are handled in the BaseGame class and allow you to quit the game. Other than that you only need four keys in your Tetris game. To move to the left and right the cursor keys are used. The up cursor key is used to rotate the current block and the down cursor key or alternatively the space or A keys can be used to let the block fall down faster.


Similar to the gravity check to see if you can move the block down, the same check is done to see if you can move the current block left or right. Only if that is true do you actually move the block; this code is done in the TetrisGame Update method because you want to check for the player input every frame and not just when updating the TetrisGrid, which can only happen every 1000ms as you learned before. The code was in the TetrisGrid Update method before, but to improve the user experience it was moved and improved quite a lot also allowing you to move the block quickly left and right by hitting the cursor buttons multiple times.

像在重力【gravity】下落中那样检查是否还可以向下移动,也同样检查你是否还能左右移动当前砖块。如果为真,那就可以移动。代码在TetrisGame Update中完成,因为每一帧你都要检查玩家的输入,只有在更新TetrisGrid的时候不用检查,这个只是每1000毫秒发生一次。代码之前是在TetrisGrid Update方法中的,但是为了来两用户体验它被移动了,并且还做了改进允许你通过多次敲击方向键来快速左右移动。

Well, you have learned a lot about all the supporting code and you are almost doneto run the Tetris game for the first time. But you should take a look at the MoveBlock helper method because it is the most integral and important part of your Tetris game. Another important method is the RotateBlock method, which works in a similar way testing if a block can be rotated. You can check it out yourself in the source code for the Tetris game. Please use the unit tests in the TetrisGame class to see how all these methods work:



Move block


There are three kinds of moves you can do: Left, Right, and Down. Each of these moves is handled in a separate code block to see if the left, right, or down data is available and if it is possible to move there. Before going into the details of this method there are two things that should be mentioned. First of all there is a helper variable called movingDownWasBlocked defined above the method. The reason you have this variable is to speed up the process of checking if the current block reached the ground, and it is stored at the class level to let the Update method pick it up later (which can be several frames later) and make the gravity code you saw earlier update much faster than in the case when the user doesn’t want to drop the block down right here. This is a very important part of the game because if each block were immediately fixed when reaching the ground the game would become very hard, and all the fun is lost when it gets faster and the grid gets more filled.


Then you use another trick to simplify the checking process by temporarily removing the current block from the grid. This way you can easily check if a new position is possible because your current position does not block you anymore. The code also uses several helper variables to store the new position and that code is simplified a bit to account for only four block parts. If you change the block types and the number of block parts, you should also change this method.


After setting everything up you check if the new virtual block position is possible in the three code blocks. Usually it is possible and you end up with four new values in the newPosNum array. If there are less than three values available you know that something was blocking you and the anythingBlocking variable is set to true anyway. In that case the old block position is restored and both the grid and the floatingGrid arrays stay the same way.


But in case the move attempt was successful the block position is updated and you clear the floatingGrid and finally add the block again to the new position by adding it both to the grid and the floatingGrid. The user also hears a very silent block move sound and you are done with this method.



With all that new code in the TetrisGrid class you can now test the unit tests in the TetrisGame class. In addition to the tests you saw before the two most important unit tests for the game logic are:


§  TestRotatingBlock, which tests the RotateBlock method of the TetrisGrid class.


§  TestFallingBlockAndKillLine, which is used to test the gravity and user input you just learned about.


It should be obvious that you often go back to older unit tests to update them according to the newest changes you require for your game. For example, the TestBackgroundBoxes unit test you saw earlier is very simple, but the layout and position of the background boxes changed quite a lot while implementing and testing the game components and it had to be updated accordingly to reflect the changes. One example for that would be the scoreboard, which is surrounded by the background box, but before you can know how big the scoreboard is going to be you have to know what the contents are and how much space they are going to consume. After writing the TestScoreboard method it became very obvious that the scoreboard has to be much smaller than the NextBlock background box, for example.


Another part of testing the game is constantly checking for bugs and improving the game code. The previous games were pretty simple and you only had to make minor improvements after the first initial runs, but the Tetris game is much more complex and you can spend many hours fixing and improving it.


One last thing you could test is running the game on the Xbox 360 - just select the Xbox 360 console platform in the project and try to run it on the Xbox 360. All the steps to do that were explained in Chapter 1, which also has a useful troubleshooting section in case something does not work on the Xbox 360. If you write new code you should make sure from time to time that it compiles on the Xbox 360 too. You are not allowed to write any interop code calling unmanaged assemblies, and some of the .NET 2.0 Framework classes and methods are missing on the Xbox 360.

最有一个测试是在Xbox360上进行测试,只需要选择Xbox360平台然后试着在其上运行。所有的步骤在第一章已经解释过了。在Xbox360上你不能写和非托管代码交互的代码,也不能使用那些在Xbox360上不支持的.NET 2.0的类和方法(Xbox360使用的是精简版的.NET2.0.

想对作者说点什么? 我来说一句