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.

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:

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).

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.

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:

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:

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

顺便说一下，NumOfBlockTypes显示给你枚举类的用处。你可以轻易决定在BlockTypes枚举中有多少【入口】。

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:

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.

现在你的网格中有了新的砖块，但是现在只能无聊的看着它在顶部不动；它应该不断的下落。

Gravity

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:

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.

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 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.

如果玩家消掉了多于一行他会得到奖励分数。最后AddRandomBlock方法会在顶部创建一个新的砖块。

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.

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:

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.

Testing

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.

TestRotatingBlock，用来测试TetrisGrid类中的RotateBlock方法。

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

TestFallingBlockAndKillLine用来测试引力下落和刚刚学到的用户输入。

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.