# 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()
{
TestGame.Start(
"TestBackgroundBoxes",

delegate

{

// Render background
TestGame.game.background.Render();

// Draw background boxes for all the components

// 为所有组件绘制背景框
TestGame.game.backgroundBigBox.Render(new Rectangle(
(
512 - 200- 1540 - 12400 + 23, (768 - 40+ 16));
TestGame.game.backgroundSmallBox.Render(
new Rectangle(
(
512 - 480- 1540 - 10290 - 30300));
TestGame.game.backgroundSmallBox.Render(
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:

Constants

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

{
game.BlockSprite.Render(
new Rectangle(
gridRect.X
+ x * blockWidth,
gridRect.Y
+ y * blockHeight,
blockWidth
-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).

gridRect变量作为一个参数被从main类传递到Draw方法中来指定你要绘制各自的区域。这个是和你用来绘制背景框一样的矩形，只是更小一些。这里你要做的第一件事是为每一个要绘制的方块计算的宽和高。然后你要仔细检查整个数组并使用SpriteHelper.Render绘制每一个方块，使用半透明的暗色来呈现一个空的背景格。你会得到图4-9的效果。因为你使用了组件所以你不需要在单元测试中写完这些代码。单元测试只是绘制背景框然后调用TetrisGrid.Draw方法来显示结果（去看TestEmptyGrid单元测试）。

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
{
Empty,
Block,
Triangle,
Line,
RightT,
LeftT,
RightShape,
LeftShape,
}
// enum BlockTypes

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

public static readonly int NumOfBlockTypes =
EnumHelper.GetSize(
typeof(BlockTypes));

/// <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:

BlockTypes就是我们索道的枚举；它包含所有可能的砖块类型，也用于在NextBlock组件中随机生成新的砖块。一开始所有的格子区域都被空块类型填充满了。格子是这样定义的：

/// <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:

BlockColor[(int)grid[x,y]]

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();
currentBlockRot
= 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);

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;
Sound.Play(Sound.Sounds.Lose);
}
// if
else

{
grid[x
+ xPos, y] = (BlockTypes)currentBlockType;
floatingGrid[x
+ 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.

### 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:

TestFallingBlockAndLineKill单元测试用来测试当前砖块的下落引力。每一次当你调用TetrisGridUpdate方法是当前活动的砖块就会被更新。在第一等级每1000毫秒调用一次Update方法。这里你要检查砖块是否正常向下移动：

// Try to move floating stuff down
if (MoveBlock(MoveTypes.Down) == false ||
movingDownWasBlocked)
{

// Failed? Then fix floating stuff, not longer moveable!
for ( int x=0; x<GridWidth; x++ )

for ( int y=0; y<GridHeight; y++ )
floatingGrid[x,y]
= false;
Sound.Play(Sound.Sounds.BlockFalldown);
}
// 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.

Tetris的逻辑部分大多在MoveBlock辅助方法中完成，这个方法检查在指定方向上的移动是否可能。如果砖块不再能移动它将被固定然后你要清空floatingGrid数组然后播放砖块触底的声音。

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] )
canMove
= 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 )

{
fullLine
= false;

break;
}
// 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++ )
grid[x,yDown
+1= grid[x,yDown];

// Clear top line

// 清除顶行
for ( int x=0; x<GridWidth; x++ )
grid[
0,x] = BlockTypes.Empty;

// Add 10 points and count line

// 增加10分和消除的行数
score += 10;
lines
++;
linesKilled
++;
Sound.Play(Sound.Sounds.LineKill);
}
// if
}
// for

// If we killed 2 or more lines, add extra score
// 如果一次清楚超过2行，有额外加分
if (linesKilled >= 2)
score
+= 5;

if (linesKilled >= 3)
score
+= 10;

if (linesKilled >= 4)
score
+= 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 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.

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.

【但是万一移动的尝试成功了，砖块的位置被更新了倪还清空了floatingGrid数组，最后又一次在gridfloatingGrid中添加了砖块到新的位置。用户同样会听到一声轻微的移动的声效，你使用这个方法来完成。】

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

• 本文已收录于以下专栏：

## CCF 历年真题之俄罗斯方块（_1604_2_Tetris.java）参考答案

CCF 历年真题之俄罗斯方块（_1604_2_Tetris）参考答案 问题描述 试题编号： 201604-2 试题名称： 俄罗斯方块 时间限制： 1.0s 内存限制： 256....
• Dina_p
• 2017年07月10日 16:34
• 410

## JS/Jquery版本的俄罗斯方块(Tetris)

1.前言 写这个Jquery版本的小游戏的缘由在于我想通过从零到有，自己写一个Jquery版本的游戏用来练手。 经历了V1，V2版本，当前版本是V3。 游戏功能：关卡，分数，暂停，虚拟键，状态栏，自定...

## HDU 5374 Tetris(模拟俄罗斯方块）

Tetris God Wu enjoy coding some trivial games like Tetris.His Tetris is made up by 9 columns and 12 ...

## JavaScript jQuery实现Tetris(俄罗斯方块)游戏代码

举报原因： 您举报文章：Chapter 4（2）：Tetris 俄罗斯方块 色情 政治 抄袭 广告 招聘 骂人 其他 (最多只允许输入30个字)