# 俄罗斯方块 > 原文: [https://zetcode.com/gfx/java2d/tetris/](https://zetcode.com/gfx/java2d/tetris/) 在本章中,我们将在 Java Swing 中创建一个俄罗斯方块游戏克隆。 ## 俄罗斯方块 俄罗斯方块游戏是有史以来最受欢迎的计算机游戏之一。 原始游戏是由俄罗斯程序员 Alexey Pajitnov 于 1985 年设计和编程的。此后,几乎所有版本的几乎所有计算机平台上都可以使用俄罗斯方块。 甚至我的手机都有俄罗斯方块游戏的修改版。 俄罗斯方块被称为下降块益智游戏。 在这个游戏中,我们有七个不同的形状,称为 tetrominoes 。 S 形,Z 形,T 形,L 形,线形,镜像 L 形和正方形。 这些形状中的每一个都形成有四个正方形。 形状从板上掉下来。 俄罗斯方块游戏的目的是移动和旋转形状,以便它们尽可能地适合。 如果我们设法形成一行,则该行将被破坏并得分。 我们玩俄罗斯方块游戏,直到达到顶峰。 ![Tetrominoes](img/efbdbc6a04f9ffe911bedc9018f65055.jpg) Figure: Tetrominoes ## 开发 我们的俄罗斯方块游戏没有图像,我们使用 Swing 绘图 API 绘制四面体。 每个计算机游戏的背后都有一个数学模型。 俄罗斯方块也是如此。 游戏背后的一些想法。 * 我们使用计时器类创建游戏周期 * 绘制四蛋白 * 形状以正方形为单位移动(不是逐个像素移动) * 从数学上讲,木板是简单的数字列表 我对游戏做了一些简化,以便于理解。 游戏启动后立即开始。 我们可以通过按`p`键暂停游戏。 空格键将把俄罗斯方块放在底部。 `d`键将片段向下一行。 (它可以用来加快下降速度。)游戏以恒定速度运行,没有实现加速。 分数是我们已删除的行数。 `Tetris.java` ```java package com.zetcode; import java.awt.BorderLayout; import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.SwingUtilities; public class Tetris extends JFrame { private JLabel statusbar; public Tetris() { initUI(); } private void initUI() { statusbar = new JLabel(" 0"); add(statusbar, BorderLayout.SOUTH); Board board = new Board(this); add(board); board.start(); setSize(200, 400); setTitle("Tetris"); setDefaultCloseOperation(EXIT_ON_CLOSE); setLocationRelativeTo(null); } public JLabel getStatusBar() { return statusbar; } public static void main(String[] args) { SwingUtilities.invokeLater(new Runnable() { @Override public void run() { Tetris game = new Tetris(); game.setVisible(true); } }); } } ``` 在`Tetris.java`文件中,我们设置了游戏。 我们创建一个玩游戏的棋盘。 我们创建一个状态栏。 ```java board.start(); ``` `start()`方法启动俄罗斯方块游戏。 窗口出现在屏幕上之后。 `Shape.java` ```java package com.zetcode; import java.util.Random; public class Shape { protected enum Tetrominoes { NoShape, ZShape, SShape, LineShape, TShape, SquareShape, LShape, MirroredLShape }; private Tetrominoes pieceShape; private int coords[][]; private int[][][] coordsTable; public Shape() { coords = new int[4][2]; setShape(Tetrominoes.NoShape); } public void setShape(Tetrominoes shape) { coordsTable = new int[][][] { { { 0, 0 }, { 0, 0 }, { 0, 0 }, { 0, 0 } }, { { 0, -1 }, { 0, 0 }, { -1, 0 }, { -1, 1 } }, { { 0, -1 }, { 0, 0 }, { 1, 0 }, { 1, 1 } }, { { 0, -1 }, { 0, 0 }, { 0, 1 }, { 0, 2 } }, { { -1, 0 }, { 0, 0 }, { 1, 0 }, { 0, 1 } }, { { 0, 0 }, { 1, 0 }, { 0, 1 }, { 1, 1 } }, { { -1, -1 }, { 0, -1 }, { 0, 0 }, { 0, 1 } }, { { 1, -1 }, { 0, -1 }, { 0, 0 }, { 0, 1 } } }; for (int i = 0; i < 4 ; i++) { for (int j = 0; j < 2; ++j) { coords[i][j] = coordsTable[shape.ordinal()][i][j]; } } pieceShape = shape; } private void setX(int index, int x) { coords[index][0] = x; } private void setY(int index, int y) { coords[index][1] = y; } public int x(int index) { return coords[index][0]; } public int y(int index) { return coords[index][1]; } public Tetrominoes getShape() { return pieceShape; } public void setRandomShape() { Random r = new Random(); int x = Math.abs(r.nextInt()) % 7 + 1; Tetrominoes[] values = Tetrominoes.values(); setShape(values[x]); } public int minX() { int m = coords[0][0]; for (int i=0; i < 4; i++) { m = Math.min(m, coords[i][0]); } return m; } public int minY() { int m = coords[0][1]; for (int i=0; i < 4; i++) { m = Math.min(m, coords[i][1]); } return m; } public Shape rotateLeft() { if (pieceShape == Tetrominoes.SquareShape) return this; Shape result = new Shape(); result.pieceShape = pieceShape; for (int i = 0; i < 4; ++i) { result.setX(i, y(i)); result.setY(i, -x(i)); } return result; } public Shape rotateRight() { if (pieceShape == Tetrominoes.SquareShape) return this; Shape result = new Shape(); result.pieceShape = pieceShape; for (int i = 0; i < 4; ++i) { result.setX(i, -y(i)); result.setY(i, x(i)); } return result; } } ``` `Shape`类提供有关俄罗斯方块的信息。 ```java protected enum Tetrominoes { NoShape, ZShape, SShape, LineShape, TShape, SquareShape, LShape, MirroredLShape }; ``` `Tetrominoes`枚举拥有所有七个俄罗斯方块形状。 加上此处称为`NoShape`的空形状。 ```java public Shape() { coords = new int[4][2]; setShape(Tetrominoes.NoShape); } ``` 这是`Shape`类的构造函数。 `coords`数组保存俄罗斯方块的实际坐标。 ```java coordsTable = new int[][][] { { { 0, 0 }, { 0, 0 }, { 0, 0 }, { 0, 0 } }, { { 0, -1 }, { 0, 0 }, { -1, 0 }, { -1, 1 } }, { { 0, -1 }, { 0, 0 }, { 1, 0 }, { 1, 1 } }, { { 0, -1 }, { 0, 0 }, { 0, 1 }, { 0, 2 } }, { { -1, 0 }, { 0, 0 }, { 1, 0 }, { 0, 1 } }, { { 0, 0 }, { 1, 0 }, { 0, 1 }, { 1, 1 } }, { { -1, -1 }, { 0, -1 }, { 0, 0 }, { 0, 1 } }, { { 1, -1 }, { 0, -1 }, { 0, 0 }, { 0, 1 } } }; ``` `coordsTable`数组保存我们的俄罗斯方块的所有可能的坐标值。 这是一个模板,所有零件均从该模板获取其坐标值。 ```java for (int i = 0; i < 4 ; i++) { for (int j = 0; j < 2; ++j) { coords[i][j] = coordsTable[shape.ordinal()][i][j]; } } ``` 在这里,我们将一行坐标值从`coordsTable`放置到俄罗斯方块的`coords`数组中。 请注意`ordinal()`方法的使用。 在 C++ 中,枚举类型本质上是一个整数。 与 C++ 不同,Java 枚举是完整类。 并且`ordinal()`方法返回枚举类型在枚举对象中的当前位置。 下图将帮助您更多地了解坐标值。 `coords`数组保存俄罗斯方块的坐标。 例如,数字`{0, -1}`,`{0, 0}`,`{-1, 0}`,`{-1, -1}`表示旋转的 S 形。 下图说明了形状。 ![Coordinates](img/eb82e71fd29d160f11224c41d6d57b90.jpg) Figure: Coordinates ```java public Shape rotateLeft() { if (pieceShape == Tetrominoes.SquareShape) return this; Shape result = new Shape(); result.pieceShape = pieceShape; for (int i = 0; i < 4; ++i) { result.setX(i, y(i)); result.setY(i, -x(i)); } return result; } ``` 此代码将棋子向左旋转。 正方形不必旋转。 这就是为什么我们只是将引用返回到当前对象。 查看上一张图像将有助于理解旋转。 `Board.java` ```java package com.zetcode; import java.awt.Color; import java.awt.Dimension; import java.awt.Graphics; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.KeyAdapter; import java.awt.event.KeyEvent; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.Timer; import com.zetcode.Shape.Tetrominoes; public class Board extends JPanel implements ActionListener { private final int BoardWidth = 10; private final int BoardHeight = 22; private Timer timer; private boolean isFallingFinished = false; private boolean isStarted = false; private boolean isPaused = false; private int numLinesRemoved = 0; private int curX = 0; private int curY = 0; private JLabel statusbar; private Shape curPiece; private Tetrominoes[] board; public Board(Tetris parent) { initBoard(parent); } private void initBoard(Tetris parent) { setFocusable(true); curPiece = new Shape(); timer = new Timer(400, this); timer.start(); statusbar = parent.getStatusBar(); board = new Tetrominoes[BoardWidth * BoardHeight]; addKeyListener(new TAdapter()); clearBoard(); } @Override public void actionPerformed(ActionEvent e) { if (isFallingFinished) { isFallingFinished = false; newPiece(); } else { oneLineDown(); } } private int squareWidth() { return (int) getSize().getWidth() / BoardWidth; } private int squareHeight() { return (int) getSize().getHeight() / BoardHeight; } private Tetrominoes shapeAt(int x, int y) { return board[(y * BoardWidth) + x]; } public void start() { if (isPaused) return; isStarted = true; isFallingFinished = false; numLinesRemoved = 0; clearBoard(); newPiece(); timer.start(); } private void pause() { if (!isStarted) return; isPaused = !isPaused; if (isPaused) { timer.stop(); statusbar.setText("paused"); } else { timer.start(); statusbar.setText(String.valueOf(numLinesRemoved)); } repaint(); } private void doDrawing(Graphics g) { Dimension size = getSize(); int boardTop = (int) size.getHeight() - BoardHeight * squareHeight(); for (int i = 0; i < BoardHeight; ++i) { for (int j = 0; j < BoardWidth; ++j) { Tetrominoes shape = shapeAt(j, BoardHeight - i - 1); if (shape != Tetrominoes.NoShape) drawSquare(g, 0 + j * squareWidth(), boardTop + i * squareHeight(), shape); } } if (curPiece.getShape() != Tetrominoes.NoShape) { for (int i = 0; i < 4; ++i) { int x = curX + curPiece.x(i); int y = curY - curPiece.y(i); drawSquare(g, 0 + x * squareWidth(), boardTop + (BoardHeight - y - 1) * squareHeight(), curPiece.getShape()); } } } @Override public void paintComponent(Graphics g) { super.paintComponent(g); doDrawing(g); } private void dropDown() { int newY = curY; while (newY > 0) { if (!tryMove(curPiece, curX, newY - 1)) break; --newY; } pieceDropped(); } private void oneLineDown() { if (!tryMove(curPiece, curX, curY - 1)) pieceDropped(); } private void clearBoard() { for (int i = 0; i < BoardHeight * BoardWidth; ++i) board[i] = Tetrominoes.NoShape; } private void pieceDropped() { for (int i = 0; i < 4; ++i) { int x = curX + curPiece.x(i); int y = curY - curPiece.y(i); board[(y * BoardWidth) + x] = curPiece.getShape(); } removeFullLines(); if (!isFallingFinished) newPiece(); } private void newPiece() { curPiece.setRandomShape(); curX = BoardWidth / 2 + 1; curY = BoardHeight - 1 + curPiece.minY(); if (!tryMove(curPiece, curX, curY)) { curPiece.setShape(Tetrominoes.NoShape); timer.stop(); isStarted = false; statusbar.setText("game over"); } } private boolean tryMove(Shape newPiece, int newX, int newY) { for (int i = 0; i < 4; ++i) { int x = newX + newPiece.x(i); int y = newY - newPiece.y(i); if (x < 0 || x >= BoardWidth || y < 0 || y >= BoardHeight) return false; if (shapeAt(x, y) != Tetrominoes.NoShape) return false; } curPiece = newPiece; curX = newX; curY = newY; repaint(); return true; } private void removeFullLines() { int numFullLines = 0; for (int i = BoardHeight - 1; i >= 0; --i) { boolean lineIsFull = true; for (int j = 0; j < BoardWidth; ++j) { if (shapeAt(j, i) == Tetrominoes.NoShape) { lineIsFull = false; break; } } if (lineIsFull) { ++numFullLines; for (int k = i; k < BoardHeight - 1; ++k) { for (int j = 0; j < BoardWidth; ++j) board[(k * BoardWidth) + j] = shapeAt(j, k + 1); } } } if (numFullLines > 0) { numLinesRemoved += numFullLines; statusbar.setText(String.valueOf(numLinesRemoved)); isFallingFinished = true; curPiece.setShape(Tetrominoes.NoShape); repaint(); } } private void drawSquare(Graphics g, int x, int y, Tetrominoes shape) { Color colors[] = { new Color(0, 0, 0), new Color(204, 102, 102), new Color(102, 204, 102), new Color(102, 102, 204), new Color(204, 204, 102), new Color(204, 102, 204), new Color(102, 204, 204), new Color(218, 170, 0) }; Color color = colors[shape.ordinal()]; g.setColor(color); g.fillRect(x + 1, y + 1, squareWidth() - 2, squareHeight() - 2); g.setColor(color.brighter()); g.drawLine(x, y + squareHeight() - 1, x, y); g.drawLine(x, y, x + squareWidth() - 1, y); g.setColor(color.darker()); g.drawLine(x + 1, y + squareHeight() - 1, x + squareWidth() - 1, y + squareHeight() - 1); g.drawLine(x + squareWidth() - 1, y + squareHeight() - 1, x + squareWidth() - 1, y + 1); } class TAdapter extends KeyAdapter { @Override public void keyPressed(KeyEvent e) { if (!isStarted || curPiece.getShape() == Tetrominoes.NoShape) { return; } int keycode = e.getKeyCode(); if (keycode == 'p' || keycode == 'P') { pause(); return; } if (isPaused) return; switch (keycode) { case KeyEvent.VK_LEFT: tryMove(curPiece, curX - 1, curY); break; case KeyEvent.VK_RIGHT: tryMove(curPiece, curX + 1, curY); break; case KeyEvent.VK_DOWN: tryMove(curPiece.rotateRight(), curX, curY); break; case KeyEvent.VK_UP: tryMove(curPiece.rotateLeft(), curX, curY); break; case KeyEvent.VK_SPACE: dropDown(); break; case 'd': oneLineDown(); break; case 'D': oneLineDown(); break; } } } } ``` 最后,我们有`Board.java`文件。 这是游戏逻辑所在的位置。 ```java ... private boolean isFallingFinished = false; private boolean isStarted = false; private boolean isPaused = false; private int numLinesRemoved = 0; private int curX = 0; private int curY = 0; ... ``` 我们初始化一些重要的变量。 `isFallingFinished`变量确定俄罗斯方块形状是否已完成下降,然后我们需要创建一个新形状。 `numLinesRemoved`计算行数,到目前为止我们已经删除了行数。 `curX`和`curY`变量确定下降的俄罗斯方块形状的实际位置。 ```java setFocusable(true); ``` 我们必须显式调用`setFocusable()`方法。 从现在开始,开发板具有键盘输入。 ```java timer = new Timer(400, this); timer.start(); ``` `Timer`对象在指定的延迟后触发一个或多个操作事件。 在我们的例子中,计时器每 400ms 调用一次`actionPerformed()`方法。 ```java @Override public void actionPerformed(ActionEvent e) { if (isFallingFinished) { isFallingFinished = false; newPiece(); } else { oneLineDown(); } } ``` `actionPerformed()`方法检查下降是否已完成。 如果是这样,将创建一个新作品。 如果不是,下降的俄罗斯方块下降了一行。 在`doDrawing()`方法内部,我们在板上绘制了所有对象。 这幅画有两个步骤。 ```java for (int i = 0; i < BoardHeight; ++i) { for (int j = 0; j < BoardWidth; ++j) { Tetrominoes shape = shapeAt(j, BoardHeight - i - 1); if (shape != Tetrominoes.NoShape) drawSquare(g, 0 + j * squareWidth(), boardTop + i * squareHeight(), shape); } } ``` 在第一步中,我们绘制所有形状或已掉落到板底部的形状的其余部分。 所有正方形都记在板阵列中。 我们使用`shapeAt()`方法访问它。 ```java if (curPiece.getShape() != Tetrominoes.NoShape) { for (int i = 0; i < 4; ++i) { int x = curX + curPiece.x(i); int y = curY - curPiece.y(i); drawSquare(g, 0 + x * squareWidth(), boardTop + (BoardHeight - y - 1) * squareHeight(), curPiece.getShape()); } } ``` 在第二步中,我们绘制实际的下降部分。 ```java private void dropDown() { int newY = curY; while (newY > 0) { if (!tryMove(curPiece, curX, newY - 1)) break; --newY; } pieceDropped(); } ``` 如果按空格键,则该片段将落到底部。 我们只是简单地尝试将一块下降到另一条俄罗斯方块下降的底部或顶部。 ```java private void clearBoard() { for (int i = 0; i < BoardHeight * BoardWidth; ++i) board[i] = Tetrominoes.NoShape; } ``` `clearBoard()`方法用空的`NoShapes`填充电路板。 稍后将其用于碰撞检测。 ```java private void pieceDropped() { for (int i = 0; i < 4; ++i) { int x = curX + curPiece.x(i); int y = curY - curPiece.y(i); board[(y * BoardWidth) + x] = curPiece.getShape(); } removeFullLines(); if (!isFallingFinished) newPiece(); } ``` `pieceDropped()`方法将下降的碎片放入板阵列中。 木板再次保持了所有碎片的正方形和已经落下的碎片的剩余部分。 当一块完成落下时,就该检查是否可以去除板上的一些线了。 这是`removeFullLines()`方法的工作。 然后,我们创建一个新作品。 更准确地说,我们尝试创建一个新作品。 ```java private void newPiece() { curPiece.setRandomShape(); curX = BoardWidth / 2 + 1; curY = BoardHeight - 1 + curPiece.minY(); if (!tryMove(curPiece, curX, curY)) { curPiece.setShape(Tetrominoes.NoShape); timer.stop(); isStarted = false; statusbar.setText("game over"); } } ``` `newPiece()`方法创建一个新的俄罗斯方块。 作品获得了新的随机形状。 然后,我们计算初始`curX`和`curY`值。 如果我们不能移动到初始位置,则游戏结束。 我们加油。 计时器停止。 我们将游戏放在状态栏上的字符串上方。 ```java private boolean tryMove(Shape newPiece, int newX, int newY) { for (int i = 0; i < 4; ++i) { int x = newX + newPiece.x(i); int y = newY - newPiece.y(i); if (x < 0 || x >= BoardWidth || y < 0 || y >= BoardHeight) return false; if (shapeAt(x, y) != Tetrominoes.NoShape) return false; } curPiece = newPiece; curX = newX; curY = newY; repaint(); return true; } ``` `tryMove()`方法尝试移动俄罗斯方块。 如果该方法已达到板边界或与已经跌落的俄罗斯方块相邻,则返回`false`。 ```java int numFullLines = 0; for (int i = BoardHeight - 1; i >= 0; --i) { boolean lineIsFull = true; for (int j = 0; j < BoardWidth; ++j) { if (shapeAt(j, i) == Tetrominoes.NoShape) { lineIsFull = false; break; } } if (lineIsFull) { ++numFullLines; for (int k = i; k < BoardHeight - 1; ++k) { for (int j = 0; j < BoardWidth; ++j) board[(k * BoardWidth) + j] = shapeAt(j, k + 1); } } } ``` 在`removeFullLines()`方法内部,我们检查板中所有行中是否有完整行。 如果至少有一条实线,则将其删除。 找到整条线后,我们增加计数器。 我们将整行上方的所有行向下移动一行。 这样我们就破坏了整个生产线。 注意,在我们的俄罗斯方块游戏中,我们使用了所谓的天真重力。 这意味着,正方形可能会漂浮在空白间隙上方。 每个俄罗斯方块都有四个正方形。 每个正方形都使用`drawSquare()`方法绘制。 俄罗斯方块有不同的颜色。 ```java g.setColor(color.brighter()); g.drawLine(x, y + squareHeight() - 1, x, y); g.drawLine(x, y, x + squareWidth() - 1, y); ``` 正方形的左侧和顶部以较亮的颜色绘制。 类似地,底部和右侧用较深的颜色绘制。 这是为了模拟 3D 边缘。 我们使用键盘控制游戏。 控制机制通过`KeyAdapter`实现。 这是一个覆盖`keyPressed()`方法的内部类。 ```java case KeyEvent.VK_LEFT: tryMove(curPiece, curX - 1, curY); break; ``` 如果按向左箭头键,则尝试将下降片向左移动一个正方形。 ![Tetris](img/311a3f035b2af79001deca68bde3fd9d.jpg) Figure: Tetris 这是俄罗斯方块游戏。