Java Swing 2D俄罗斯方块

https://zetcode.com/javagames/tetris/

在本章中,我们在 Java Swing 中创建了一个俄罗斯方块游戏克隆。源代码和图像可以在作者的 Github Java-Tetris-Game存储库中找到。

俄罗斯方块

俄罗斯方块游戏是有史以来最受欢迎的电脑游戏之一。最初的游戏是由俄罗斯程序员Alexey Pajitnov于 1985 年设计和编程的 。从那时起,俄罗斯方块几乎可以在每个计算机平台上以多种形式出现。连我的手机都有俄罗斯方块游戏的修改版。

俄罗斯方块被称为落块益智游戏。在这个游戏中,我们有七种不同的形状,称为tetrominoes。S 形、Z 形、T 形、L 形、线形、镜像 L 形和方形。这些形状中的每一个都由四个正方形形成。形状正在从板上掉下来。俄罗斯方块游戏的目标是移动和旋转形状,使它们尽可能适合。如果我们设法形成一行,该行就会被破坏,我们就会得分。我们玩俄罗斯方块游戏,直到我们达到顶峰。

特洛米诺

图:Tetromino

发展历程

tetrominoes 是使用 Swing 绘画 API 绘制的。我们使用 java.util.Timer来创建一个游戏循环。形状在一个正方形的基础上移动(而不是一个像素一个像素)。从数学上讲,游戏中的棋盘是一个简单的数字列表。

游戏启动后立即开始。我们可以通过按 p 键暂停游戏。空格键会将俄罗斯方块立即放到底部。d 键会将棋子向下一行。(它可以用来加速下降。)游戏以恒定速度进行,没有实现加速。分数是我们删除的行数。

com/zetcode/Shape.java
package com.zetcode;

import java.util.Random;

public class Shape {

	protected enum Tetrominoe {
		NoShape, ZShape, SShape, LineShape, TShape, SquareShape, LShape, MirroredLShape
	}

	private Tetrominoe pieceShape;
	private int coords[][];
	private int[][][] coordsTable;

	public Shape() {

		initShape();
	}

	private void initShape() {

		coords = new int[4][2];

		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 } } };

		setShape(Tetrominoe.NoShape);
	}

	protected void setShape(Tetrominoe shape) {

		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 Tetrominoe getShape() {
		return pieceShape;
	}

	public void setRandomShape() {

		var r = new Random();
		int x = Math.abs(r.nextInt()) % 7 + 1;

		Tetrominoe[] values = Tetrominoe.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 == Tetrominoe.SquareShape) {

			return this;
		}

		var 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 == Tetrominoe.SquareShape) {

			return this;
		}

		var 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提供有关俄罗斯方块作品的信息。

protected enum Tetrominoe {
    NoShape, ZShape, SShape, LineShape,
    TShape, SquareShape, LShape, MirroredLShape
}

Tetrominoe枚举拥有七项俄罗斯方块形状的名字叫空的形状NoShape

coords = new int[4][2];
setShape(Tetrominoe.NoShape);

coords数组保存俄罗斯方块的实际坐标。

int[][][] 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数组包含俄罗斯方块所有可能的坐标值。这是一个模板,所有部分都从中获取其坐标值。

for (int i = 0; i < 4; i++) {

    System.arraycopy(coordsTable[shape.ordinal()], 0, coords, 0, 4);
}

我们将来自 的一行坐标值 coordsTable放入coords俄罗斯方块的数组中。注意ordinal()方法的使用。在 C++ 中,枚举类型本质上是一个整数。与 C++ 不同,Java 枚举是完整的类,该 ordinal()方法返回枚举类型在枚举对象中的当前位置。

下图将有助于更多地了解坐标值。coords 数组保存俄罗斯方块的坐标。例如,数字 (-1, 1)、(-1, 0)、(0, 0) 和 (0, -1) 表示旋转的 S 形。下图说明了形状。

Coordinates

图:坐标

Shape rotateLeft() {

    if (pieceShape == Tetrominoe.SquareShape) {

        return this;
    }

    var 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;
}

此代码向左旋转一块。正方形不必旋转。这就是为什么我们简单地返回对当前对象的引用。查看上一张图片将有助于理解旋转。

com/zetcode/Board.java
package com.zetcode;

import com.zetcode.Shape.Tetrominoe;
import static javax.swing.JOptionPane.showMessageDialog;
import static javax.swing.JOptionPane.showConfirmDialog;
import javax.swing.*;

import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.Timer;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;

public class Board extends JPanel {

	private final int BOARD_WIDTH = 10;
	private final int BOARD_HEIGHT = 20;
	private final int PERIOD_INTERVAL = 300;

	private Timer timer;
	private boolean isFallingFinished = false;
	private boolean isPaused = false;
	private int numLinesRemoved = 0;
	private int curX = 0;
	private int curY = 0;
	private JLabel statusbar;
	private Shape curPiece;
	private Tetrominoe[] board;

	public Board(Tetris parent) {

		initBoard(parent);
	}

	private void initBoard(Tetris parent) {

		setFocusable(true);
		statusbar = parent.getStatusBar();
		addKeyListener(new TAdapter());
	}

	private int squareWidth() {

		return (int) getSize().getWidth() / BOARD_WIDTH;
	}

	private int squareHeight() {

		return (int) getSize().getHeight() / BOARD_HEIGHT;
	}

	private Tetrominoe shapeAt(int x, int y) {

		return board[(y * BOARD_WIDTH) + x];
	}

	void start() {

		numLinesRemoved = 0;
		statusbar.setText("0");

		curPiece = new Shape();
		board = new Tetrominoe[BOARD_WIDTH * BOARD_HEIGHT];

		clearBoard();
		newPiece();

		timer = new Timer(PERIOD_INTERVAL, new GameCycle());
		timer.start();
	}

	private void pause() {

		isPaused = !isPaused;

		if (isPaused) {

			statusbar.setText("paused");
		} else {

			statusbar.setText(String.valueOf(numLinesRemoved));
		}

		repaint();
	}

	@Override
	public void paintComponent(Graphics g) {

		super.paintComponent(g);
		doDrawing(g);
	}

	private void doDrawing(Graphics g) {
		// vertical line
		g.setColor(Color.black);
		for (int i = 1; i < BOARD_WIDTH; i++) {

			g.drawLine(i * (int) getSize().getWidth() / BOARD_WIDTH, 0, i * (int) getSize().getWidth() / BOARD_WIDTH,
					(int) getSize().getHeight());

		} // g.drawLine(0, (int) getSize().getWidth() / BOARD_WIDTH, 20, 120);
			// (int) getSize().getWidth() / BOARD_WIDTH

		var size = getSize();
		int boardTop = (int) size.getHeight() - BOARD_HEIGHT * squareHeight();

		for (int i = 0; i < BOARD_HEIGHT; i++) {

			for (int j = 0; j < BOARD_WIDTH; j++) {

				Tetrominoe shape = shapeAt(j, BOARD_HEIGHT - i - 1);

				if (shape != Tetrominoe.NoShape) {

					drawSquare(g, j * squareWidth(), boardTop + i * squareHeight(), shape);
				}
			}
		}

		if (curPiece.getShape() != Tetrominoe.NoShape) {

			for (int i = 0; i < 4; i++) {

				int x = curX + curPiece.x(i);
				int y = curY - curPiece.y(i);

				drawSquare(g, x * squareWidth(), boardTop + (BOARD_HEIGHT - y - 1) * squareHeight(),
						curPiece.getShape());
			}
		}
	}

	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 < BOARD_HEIGHT * BOARD_WIDTH; i++) {

			board[i] = Tetrominoe.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 * BOARD_WIDTH) + x] = curPiece.getShape();
		}

		removeFullLines();

		if (!isFallingFinished) {

			newPiece();
		}
	}

	private void newPiece() {

		curPiece.setRandomShape();
		curX = BOARD_WIDTH / 2 + 1;
		curY = BOARD_HEIGHT - 1 + curPiece.minY();

		if (!tryMove(curPiece, curX, curY)) {

			curPiece.setShape(Tetrominoe.NoShape);
			timer.stop();

			var msg = String.format("Game over. Score: %d", numLinesRemoved);
			statusbar.setText(msg);
			// showMessageDialog(null, "This is even shorter");
			// int result = showConfirmDialog(null, "是否重新开始? ", "重新开始",
			// JOptionPane.YES_NO_OPTION,
			// JOptionPane.QUESTION_MESSAGE);
			// if (result == JOptionPane.YES_OPTION) {
			start();
			// } else if (result == JOptionPane.NO_OPTION) {
			// statusbar.setText("You selected: No");
			// } else {
			// statusbar.setText("None selected");
			// }
		}
	}

	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 >= BOARD_WIDTH || y < 0 || y >= BOARD_HEIGHT) {

				return false;
			}

			if (shapeAt(x, y) != Tetrominoe.NoShape) {

				return false;
			}
		}

		curPiece = newPiece;
		curX = newX;
		curY = newY;

		repaint();

		return true;
	}

	private void removeFullLines() {

		int numFullLines = 0;

		for (int i = BOARD_HEIGHT - 1; i >= 0; i--) {

			boolean lineIsFull = true;

			for (int j = 0; j < BOARD_WIDTH; j++) {

				if (shapeAt(j, i) == Tetrominoe.NoShape) {

					lineIsFull = false;
					break;
				}
			}

			if (lineIsFull) {

				numFullLines++;

				for (int k = i; k < BOARD_HEIGHT - 1; k++) {
					for (int j = 0; j < BOARD_WIDTH; j++) {
						board[(k * BOARD_WIDTH) + j] = shapeAt(j, k + 1);
					}
				}
			}
		}

		if (numFullLines > 0) {

			numLinesRemoved += numFullLines;

			statusbar.setText(String.valueOf(numLinesRemoved));
			isFallingFinished = true;
			curPiece.setShape(Tetrominoe.NoShape);
		}
	}

	private void drawSquare(Graphics g, int x, int y, Tetrominoe 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) };

		var color = colors[shape.ordinal()];

		g.setColor(color);
		g.fillRect(x + 1, y + 1, squareWidth() - 2, squareHeight() - 2);
		// System.out.println("Width="+squareWidth());
		// System.out.println("Height="+squareHeight());

		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);
	}

	private class GameCycle implements ActionListener {

		@Override
		public void actionPerformed(ActionEvent e) {

			doGameCycle();
		}
	}

	private void doGameCycle() {

		update();
		repaint();
	}

	private void update() {

		if (isPaused) {

			return;
		}

		if (isFallingFinished) {

			isFallingFinished = false;
			newPiece();
		} else {

			oneLineDown();
		}
	}

	class TAdapter extends KeyAdapter {

		@Override
		public void keyPressed(KeyEvent e) {

			if (curPiece.getShape() == Tetrominoe.NoShape) {

				return;
			}

			int keycode = e.getKeyCode();

			// Java 12 switch expressions
			switch (keycode) {

			case KeyEvent.VK_P -> pause();
			case KeyEvent.VK_LEFT -> tryMove(curPiece, curX - 1, curY);
			case KeyEvent.VK_RIGHT -> tryMove(curPiece, curX + 1, curY);
			case KeyEvent.VK_DOWN -> tryMove(curPiece.rotateRight(), curX, curY);
			case KeyEvent.VK_UP -> tryMove(curPiece.rotateLeft(), curX, curY);
			case KeyEvent.VK_SPACE -> dropDown();
			case KeyEvent.VK_D -> oneLineDown();
			case KeyEvent.VK_R -> start();
			}
		}
	}
}

最后,我们有了Board.java文件。这是游戏逻辑所在的位置。

private final int BOARD_WIDTH = 10;
private final int BOARD_HEIGHT = 22;
private final int PERIOD_INTERVAL = 300;

我们有四个常量。所述BOARD_WIDTH和 BOARD_HEIGHT定义电路板的尺寸。的 PERIOD_INTERVAL常量定义游戏的速度。

...
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 确定了俄罗斯方块形状已经完成下降,然后我们需要创建一个新的形状。该isStarted用于检查,如果比赛已经开始。同样,isPaused用于检查游戏是否暂停。该numLinesRemoved计算是我们迄今删除的行数。该curXcurY确定下落的俄罗斯方块形状的实际位置。

private int squareWidth() {

    return (int) getSize().getWidth() / BOARD_WIDTH;
}

private int squareHeight() {

    return (int) getSize().getHeight() / BOARD_HEIGHT;
}

这些线决定了单个 Tetrominoe 方块的宽度和高度。

private Tetrominoe shapeAt(int x, int y) {

    return board[(y * BOARD_WIDTH) + x];
}

我们确定给定坐标处的形状。形状存储在board数组中。

void start() {

    curPiece = new Shape();
    board = new Tetrominoe[BOARD_WIDTH * BOARD_HEIGHT];
...

我们创建一个新的当前形状和一个新板。

clearBoard();
newPiece();

棋盘被清除,新的落下棋子被初始化。

timer = new Timer(PERIOD_INTERVAL, new GameCycle());
timer.start();

我们创建一个计时器。定时器每PERIOD_INTERVAL 隔一段时间执行一次,形成一个游戏循环。

private void pause() {

    isPaused = !isPaused;

    if (isPaused) {

        statusbar.setText("paused");
    } else {

        statusbar.setText(String.valueOf(numLinesRemoved));
    }

    repaint();
}

pause()方法暂停或恢复游戏。当游戏暂停时,我们会paused在状态栏中显示消息。

doDrawing()方法内部,我们在板上绘制所有对象。绘画有两个步骤。

for (int i = 0; i < BOARD_HEIGHT; i++) {

    for (int j = 0; j < BOARD_WIDTH; j++) {

        Tetrominoe shape = shapeAt(j, BOARD_HEIGHT - i - 1);

        if (shape != Tetrominoe.NoShape) {

            drawSquare(g, j * squareWidth(),
                    boardTop + i * squareHeight(), shape);
        }
    }
}

在第一步中,我们绘制掉到板底部的所有形状或形状的剩余部分。所有的方块都被记住在棋盘阵列中。我们使用shapeAt()方法访问它。

if (curPiece.getShape() != Tetrominoe.NoShape) {

    for (int i = 0; i < 4; i++) {

        int x = curX + curPiece.x(i);
        int y = curY - curPiece.y(i);

        drawSquare(g, x * squareWidth(),
                boardTop + (BOARD_HEIGHT - y - 1) * squareHeight(),
                curPiece.getShape());
    }
}

在第二步中,我们绘制实际下落的部分。

private void dropDown() {

    int newY = curY;

    while (newY > 0) {

        if (!tryMove(curPiece, curX, newY - 1)) {

            break;
        }

        newY--;
    }

    pieceDropped();
}

如果我们按下该Space键,该棋子会掉到底部。我们只是尝试将棋子放下一行,直到它到达另一个倒下的俄罗斯方块的底部或顶部。当俄罗斯方块完成下落时,pieceDropped()调用 。

private void oneLineDown() {

    if (!tryMove(curPiece, curX, curY - 1)) {
        
        pieceDropped();
    }
}

在该oneLineDown()方法中,我们尝试将下落的部分向下移动一行,直到它完全下落。

private void clearBoard() {

    for (int i = 0; i < BOARD_HEIGHT * BOARD_WIDTH; i++) {

        board[i] = Tetrominoe.NoShape;
    }
}

clearBoard()方法用空的 填充板 Tetrominoe.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 * BOARD_WIDTH) + x] = curPiece.getShape();
    }

    removeFullLines();

    if (!isFallingFinished) {

        newPiece();
    }
}

pieceDropped()方法将下落的碎片放入 board数组中。棋盘再次容纳所有棋子的方格和落下的棋子的剩余部分。当棋子落下后,是时候检查我们是否可以从板上移除一些线。这是removeFullLines()方法的工作。然后我们创建一个新作品,或者更准确地说,我们尝试创建一个新作品。

private void newPiece() {

    curPiece.setRandomShape();
    curX = BOARD_WIDTH / 2 + 1;
    curY = BOARD_HEIGHT - 1 + curPiece.minY();

    if (!tryMove(curPiece, curX, curY)) {

        curPiece.setShape(Tetrominoe.NoShape);
        timer.stop();

        var msg = String.format("Game over. Score: %d", numLinesRemoved);
        statusbar.setText(msg);
    }
}

newPiece()方法创建了一个新的俄罗斯方块。这件作品获得了一个新的随机形状。然后我们计算初始值curX和 curY值。如果我们不能移动到初始位置,游戏就结束了——我们顶了。计时器停止,我们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 >= BOARD_WIDTH || y < 0 || y >= BOARD_HEIGHT) {

            return false;
        }

        if (shapeAt(x, y) != Tetrominoe.NoShape) {

            return false;
        }
    }

    curPiece = newPiece;
    curX = newX;
    curY = newY;

    repaint();

    return true;
}

tryMove()方法尝试移动俄罗斯方块。false如果它已到达棋盘边界或与已经掉落的俄罗斯方块棋子相邻,则该方法返回。

private void removeFullLines() {

    int numFullLines = 0;

    for (int i = BOARD_HEIGHT - 1; i >= 0; i--) {

        boolean lineIsFull = true;

        for (int j = 0; j < BOARD_WIDTH; j++) {

            if (shapeAt(j, i) == Tetrominoe.NoShape) {

                lineIsFull = false;
                break;
            }
        }

        if (lineIsFull) {

            numFullLines++;

            for (int k = i; k < BOARD_HEIGHT - 1; k++) {
                for (int j = 0; j < BOARD_WIDTH; j++) {
                    board[(k * BOARD_WIDTH) + j] = shapeAt(j, k + 1);
                }
            }
        }
    }

    if (numFullLines > 0) {

        numLinesRemoved += numFullLines;

        statusbar.setText(String.valueOf(numLinesRemoved));
        isFallingFinished = true;
        curPiece.setShape(Tetrominoe.NoShape);
    }
}

removeFullLines()方法内部,我们检查板中所有行中是否有任何完整行。如果至少有一个完整的行,则将其删除。找到整行后,我们增加计数器。我们将整行上方的所有行向下移动一行。这样我们就破坏了整条线。请注意,在我们的俄罗斯方块游戏中,我们使用了所谓的天真重力。这意味着正方形可能会在空白间隙上方浮动。

private void drawSquare(Graphics g, int x, int y, Tetrominoe 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)
    };

    var 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);
}

每块俄罗斯方块都有四个方块。每个正方形都是用该drawSquare()方法绘制的 。俄罗斯方块碎片有不同的颜色。正方形的左侧和顶部用较亮的颜色绘制。同样,底部和右侧用较深的颜色绘制。这是为了模拟 3D 边缘。

private class GameCycle implements ActionListener {

    @Override
    public void actionPerformed(ActionEvent e) {

        doGameCycle();
    }
}

在 中GameCycle,我们调用doGameCycle()方法,创建一个游戏循环。

private void doGameCycle() {

    update();
    repaint();
}

游戏分为游戏周期。每个周期都会更新游戏并重新绘制棋盘。

private void update() {

    if (isPaused) {

        return;
    }

    if (isFallingFinished) {

        isFallingFinished = false;
        newPiece();
    } else {

        oneLineDown();
    }
}

所述update()表示游戏中的一个步骤。下落的棋子向下移动一行,或者如果前一个棋子已经完成下落,则会创建一个新棋子。

private class TAdapter extends KeyAdapter {

    @Override
    public void keyPressed(KeyEvent e) {
        ...

游戏由光标键控制。我们检查 KeyAdapter.

int keycode = e.getKeyCode();

我们用getKeyCode()方法得到关键代码。

// Java 12 switch expressions
switch (keycode) {

    case KeyEvent.VK_P -> pause();
    case KeyEvent.VK_LEFT -> tryMove(curPiece, curX - 1, curY);
    case KeyEvent.VK_RIGHT -> tryMove(curPiece, curX + 1, curY);
    case KeyEvent.VK_DOWN -> tryMove(curPiece.rotateRight(), curX, curY);
    case KeyEvent.VK_UP -> tryMove(curPiece.rotateLeft(), curX, curY);
    case KeyEvent.VK_SPACE -> dropDown();
    case KeyEvent.VK_D -> oneLineDown();
}

使用 Java 12 switch 表达式,我们将键事件绑定到方法。例如,Space我们用钥匙放下掉落的俄罗斯方块。

com/zetcode/俄罗斯方块.java
package com.zetcode;

import java.awt.BorderLayout;
import java.awt.EventQueue;
import javax.swing.JFrame;
import javax.swing.JLabel;

/*
Java Tetris game clone

Author: Jan Bodnar
Website: http://zetcode.com
 */
public class Tetris extends JFrame {

	private JLabel statusbar;

	public Tetris() {

		initUI();
	}

	private void initUI() {

		statusbar = new JLabel(" 0");
		add(statusbar, BorderLayout.SOUTH);

		var board = new Board(this);
		add(board);
		board.start();

		setTitle("Tetris");
		setSize(320, 660);
		setDefaultCloseOperation(EXIT_ON_CLOSE);
		setLocationRelativeTo(null);
	}

	JLabel getStatusBar() {

		return statusbar;
	}

	public static void main(String[] args) {

		EventQueue.invokeLater(() -> {

			var game = new Tetris();
			game.setVisible(true);
		});
	}
}

Tetris.java文件中,我们设置了游戏。我们创建了一个棋盘来玩游戏。我们创建一个状态栏。

statusbar = new JLabel(" 0");
add(statusbar, BorderLayout.SOUTH);

分数显示在位于图板底部的标签中。

var board = new Board(this);
add(board);
board.start();

板被创建并添加到容器中。该start() 方法启动俄罗斯方块游戏。

图:俄罗斯方块

这是俄罗斯方块游戏。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值