Java 每日一刊(第10期):俄罗斯方块


“人生就像俄罗斯方块,每一步都在积累,挑战虽然不断,但每一个消除的瞬间都让我们更接近成功。”

前言

这里是分享 Java 相关内容的专刊,每日一更。

本期将实现一个经典的游戏——“俄罗斯方块”。

内容包括:

  1. 游戏玩法
  2. 游戏的设计思路
  3. 核心思路的代码实现
  4. 游戏的具体实现

游戏玩法

俄罗斯方块是一款经典的益智游戏,玩法简单却极具挑战。以下是这款游戏的基本玩法:

  1. 游戏界面:游戏界面是一个包含 10 列和 22 行方格的矩形区域。
  2. 方块类型:游戏中有不同形状的方块,每个方块由 4 个小方块组成,统称为 Tetrominoes。它们可以通过旋转、左右移动、快速下降来控制。
  3. 方块操作
    • 左右箭头键:控制方块向左或向右移动。
    • 上箭头键:旋转方块 90 度。
    • 下箭头键:方块快速下降一行。
    • 空格键:方块直接下落到底部。
  4. 消除方块:当一整行方块被填满时,该行会被消除,所有在该行上方的方块会向下移动。
  5. 得分规则:每消除一行得 100 分,连续消除多行有额外奖励,每多消除一行加 50 分。
  6. 游戏结束:当方块堆到顶部,无法生成新的方块时,游戏结束。

游戏的设计思路

为了开发这样一款游戏,我们需要考虑以下几个设计要点:

  1. 游戏界面:如何显示方块?如何处理游戏区域的绘制?
  2. 游戏状态:如何管理游戏的状态,包括暂停、继续、得分更新、游戏结束等?
  3. 方块的控制:如何实现方块的移动、旋转和掉落?
  4. 碰撞检测:如何判断方块是否可以移动或掉落,或已经到达底部?
  5. 消除逻辑:如何检测是否填满一行,并在填满后删除该行?
  6. 游戏的输入控制:如何使用键盘控制方块的移动和旋转?

接下来,我们将具体分析每个设计思路,并展示如何通过代码实现这些功能。

核心思路的代码实现

游戏界面设计

我们需要一个 10 列 22 行的游戏区域,用 二维数组 存储每个格子的状态。我们使用 Swing 中的 JPanel 来绘制这个界面,Graphics 类提供了绘制方块和游戏区域的功能。

public class Tetris extends JPanel implements ActionListener {
    private final int boardWidth = 10;  // 游戏区域宽度
    private final int boardHeight = 22; // 游戏区域高度
    private Tetrominoes[] board;        // 存储游戏区域的方块状态
}

paintComponent 方法中,我们使用循环遍历游戏区域中的方块,并绘制当前固定的方块及下落中的方块。

@Override
public void paintComponent(Graphics g) {
    super.paintComponent(g); // 调用父类的方法,确保组件的正确绘制和刷新
    
    // 绘制已固定的方块,即那些已经不再移动的方块
    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) { // 如果该位置有固定的方块
                // 计算绘制位置并绘制该方块
                // j * squareWidth() 计算在X轴上的位置,i * squareHeight() 计算在Y轴上的位置
                drawSquare(g, j * squareWidth(), i * squareHeight(), shape); 
            }
        }
    }
    
    // 绘制当前正在下落的方块
    if (currentPiece.getShape() != Tetrominoes.NoShape) { // 确保当前方块是有效形状,而非空
        for (int i = 0; i < 4; ++i) { // 遍历方块的四个方块单元
            int x = currentX + currentPiece.x(i); // 计算当前方块单元的X坐标(相对于游戏区域)
            int y = currentY - currentPiece.y(i); // 计算当前方块单元的Y坐标(相对于游戏区域)
            // 计算绘制的实际像素位置
            // (boardHeight - y - 1) * squareHeight() 计算出在游戏界面中绘制的Y位置
            drawSquare(g, x * squareWidth(), (boardHeight - y - 1) * squareHeight(), currentPiece.getShape()); 
        }
    }
}

游戏状态管理

为了管理游戏的状态,我们定义了一些布尔变量,如 isStartedisPausedisGameOver,它们分别用来控制游戏的开始、暂停和结束状态。

// 标记游戏是否已经开始,初始为 false 表示游戏还未开始
// 当玩家触发开始游戏操作时,此变量将被设置为 true
private boolean isStarted = false; 

// 标记游戏是否处于暂停状态,初始为 false 表示游戏没有暂停
// 当玩家触发暂停操作时,此变量将被设置为 true,游戏逻辑应暂时停止
private boolean isPaused = false;

// 新增游戏结束标志,用于标记游戏是否结束,初始为 false 表示游戏尚未结束
// 当达到游戏结束条件(例如玩家失败或胜利)时,此变量被设置为 true
// 游戏结束时应阻止进一步的用户输入或操作
private boolean isGameOver = false;

游戏开始时,方块会开始下落,游戏结束时会停止生成新的方块,并在屏幕中间显示 "Game Over" 信息。

/**
 * 启动游戏的方法。当玩家触发“开始游戏”时调用此方法。
 * 该方法会重置游戏的相关状态,并初始化游戏内容。
 */
public void start() {
    // 将游戏状态设置为已启动,标记游戏正式开始
    isStarted = true;

    // 确保游戏不处于暂停状态,游戏一开始必须是运行状态
    isPaused = false;

    // 重置游戏结束状态为 false,表明游戏尚未结束
    isGameOver = false;

    // 清空游戏板,为新的游戏开始做准备
    clearBoard();

    // 生成一个新的方块以开始游戏
    newPiece();

    // 启动计时器,用于控制方块下落的速度
    timer.start();
}

当游戏结束时,会显示游戏结束信息:

// 检查游戏是否结束。如果游戏已经结束,则执行相应的处理逻辑
if (isGameOver) {
    // 调用方法绘制“游戏结束”消息提示,告知玩家游戏已经结束
    drawGameOverMessage(g);
}

方块控制与碰撞检测

方块的移动、旋转与掉落是通过检测周围方块的状态实现的。我们定义了一个 tryMove 方法,它根据方块的新位置判断是否可以移动,如果可以,更新方块的位置并重新绘制游戏界面。

/**
 * 尝试将当前方块移动到指定的新位置。
 * 检查新位置是否合法(即不会超出游戏板边界且没有与已有方块重叠)。
 * 如果移动合法,更新当前方块的位置并重绘游戏界面。
 *
 * @param newPiece 新的方块形状
 * @param newX     方块移动后的新 X 坐标
 * @param newY     方块移动后的新 Y 坐标
 * @return         如果移动成功返回 true,移动不成功返回 false
 */
private boolean tryMove(Shape newPiece, int newX, int newY) {
    // 遍历方块的四个小方格,检查每个方格在新位置上的状态
    for (int i = 0; i < 4; ++i) {
        // 计算小方格的新 X 坐标(相对于整个方块)
        int x = newX + newPiece.x(i);
        // 计算小方格的新 Y 坐标(相对于整个方块)
        int y = newY - newPiece.y(i);

        // 如果新位置超出游戏板边界,返回 false,移动失败
        if (x < 0 || x >= boardWidth || y < 0 || y >= boardHeight)
            return false;
        
        // 如果新位置已经被其他方块占据,返回 false,移动失败
        if (shapeAt(x, y) != Tetrominoes.NoShape)
            return false;
    }

    // 如果通过所有检查,更新当前方块的位置到新的位置
    currentPiece = newPiece;
    currentX = newX;
    currentY = newY;
    
    // 重新绘制游戏板以反映方块的新位置
    repaint();
    
    // 移动成功,返回 true
    return true;
}

方块通过键盘输入控制,使用 KeyAdapter 监听键盘事件。

/**
 * 键盘事件的适配器类,用于处理玩家的按键操作。
 * 继承自 KeyAdapter,用于捕获并处理键盘事件。
 */
class TAdapter extends KeyAdapter {

    /**
     * 当键盘按下时触发该方法,根据按下的键执行相应的操作。
     * 支持的按键包括:左、右、下、上、空格键,用于控制方块移动、旋转和快速下落。
     * 
     * @param e 键盘事件对象,包含按键信息
     */
    public void keyPressed(KeyEvent e) {
        // 获取当前按下的键码
        int keycode = e.getKeyCode();

        // 根据按键码执行相应操作
        switch (keycode) {
                // 左箭头键:尝试将方块向左移动一格
            case KeyEvent.VK_LEFT:
                tryMove(currentPiece, currentX - 1, currentY);
                break;

                // 右箭头键:尝试将方块向右移动一格
            case KeyEvent.VK_RIGHT:
                tryMove(currentPiece, currentX + 1, currentY);
                break;

                // 下箭头键:让方块下降一行
            case KeyEvent.VK_DOWN:
                oneLineDown();
                break;

                // 上箭头键:旋转方块(顺时针旋转)
            case KeyEvent.VK_UP:
                // 先生成旋转后的方块
                Shape rotatedPiece = currentPiece.rotateRight();
                // 尝试将旋转后的方块放置在当前位置
                tryMove(rotatedPiece, currentX, currentY);
                break;

                // 空格键:立即将方块快速下落到底部
            case KeyEvent.VK_SPACE:
                dropDown();
                break;
        }
    }
}

消除逻辑与得分系统

当方块落到底部或无法继续下落时,系统会检查是否有满行,若有则删除该行并更新得分。

/**
 * 移除已填满的完整行,并将上方的方块下移。
 * 更新分数并在状态栏显示当前得分。
 * 如果有消除行操作,将结束当前方块的下落并重绘游戏界面。
 */
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;

        // 计算得分:每移除一行获得100分
        int points = numFullLines * 100;

        // 如果一次消除多行,则有连击加分,每多一行多50分
        if (numFullLines > 1) {
            points += (numFullLines - 1) * 50;
        }

        // 更新总得分
        score += points;

        // 在状态栏更新当前得分
        statusBar.setText("Score: " + score);

        // 标记当前方块的下落已完成,等待生成新方块
        isFallingFinished = true;

        // 当前方块形状设置为空(无形状),表示其已经被消除
        currentPiece.setShape(Tetrominoes.NoShape);

        // 重绘游戏界面,显示方块的最新状态
        repaint();
    }
}

用户界面与窗口管理

为了使游戏窗口更现代化,我们使用自定义标题栏和关闭按钮,并实现了鼠标拖动窗口的功能。

// 创建自定义的标题栏标签,显示“俄罗斯方块”,并使其居中对齐
customTitleBar = new JLabel("俄罗斯方块", SwingConstants.CENTER);

// 设置标题栏字体为指定的字体(monoFont 可能是已定义的字体对象)
customTitleBar.setFont(monoFont);

// 设置标题栏文本颜色为白色
customTitleBar.setForeground(Color.WHITE);

// 设置标题栏背景颜色为深灰色
customTitleBar.setBackground(Color.DARK_GRAY);

// 启用不透明模式,以便显示背景颜色
customTitleBar.setOpaque(true);

// 设置标题栏的首选大小,高度为 40 像素,宽度为窗口的宽度
customTitleBar.setPreferredSize(new Dimension(getWidth(), 40));

游戏启动

游戏的主类是 TetrisGame,负责初始化游戏窗口、启动游戏并显示得分状态。

/**
 * 应用程序的入口点,启动俄罗斯方块游戏。
 *
 * @param args 命令行参数,未使用
 */
public static void main(String[] args) {
    // 使用 EventQueue.invokeLater() 确保事件调度线程 (EDT) 中的 Swing 组件初始化和操作
    EventQueue.invokeLater(() -> {
        // 创建 TetrisGame 实例,初始化游戏窗口
        TetrisGame game = new TetrisGame();

        // 设置游戏窗口为可见,启动游戏
        game.setVisible(true);
    });
}

游戏的具体实现

前面我们介绍了核心思路的代码实现,现在我们来具体完成这款游戏。

这款游戏一共需要三个文件:

  1. TetrisGame.java:这个文件是应用的窗口部分,继承了 JFrame,负责创建并管理整个游戏的窗口界面,以及标题栏、状态栏等 UI 元素。
  2. Tetris.java:这个文件是核心的游戏逻辑处理部分,继承了 JPanel 类,并实现了 ActionListener 接口。主要作用是控制游戏的状态和方块的运动逻辑。
  3. Shape.java:这个文件负责定义和管理俄罗斯方块的形状以及旋转操作。使用了 Tetrominoes 枚举类来表示不同形状的方块。

文件的依赖关系

本游戏一共 3 个文件,其中:

  • TetrisGame.java 文件依赖 Tetris.java 文件;
  • Tetris.java 文件依赖 Shape.java 文件

TetrisGame

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Font;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionAdapter;

import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.SwingConstants;

public class TetrisGame extends JFrame {

    private JLabel statusBar;
    private JLabel customTitleBar; // 自定义标题栏
    private JButton closeButton; // 关闭按钮
    private int mouseX, mouseY; // 记录鼠标按下时的坐标

    public TetrisGame() {
        setTitle("俄罗斯方块 - 加强版");
        setSize(300, 600); // 更大的窗口尺寸

        // 隐藏窗口默认标题栏
        setUndecorated(true);

        // 设置等宽字体,支持中文
        Font monoFont = new Font("Microsoft YaHei Mono", Font.PLAIN, 16);

        // 创建自定义标题栏并设置样式
        customTitleBar = new JLabel("俄罗斯方块", SwingConstants.CENTER);
        customTitleBar.setFont(monoFont); // 设置与底部 score 样式一致的字体
        customTitleBar.setForeground(Color.WHITE); // 设置字体颜色
        customTitleBar.setOpaque(true);
        customTitleBar.setBackground(Color.DARK_GRAY); // 设置背景颜色
        customTitleBar.setPreferredSize(new Dimension(getWidth(), 40)); // 设置标题栏高度

        // 实现拖动窗口功能
        customTitleBar.addMouseListener(new MouseAdapter() {
            @Override
            public void mousePressed(MouseEvent e) {
                // 记录鼠标按下时的坐标
                mouseX = e.getX();
                mouseY = e.getY();
            }
        });

        customTitleBar.addMouseMotionListener(new MouseMotionAdapter() {
            @Override
            public void mouseDragged(MouseEvent e) {
                // 获取当前鼠标位置并移动窗口
                int x = e.getXOnScreen() - mouseX;
                int y = e.getYOnScreen() - mouseY;
                setLocation(x, y);
            }
        });

        // 创建关闭按钮
        closeButton = new JButton("X");
        closeButton.setFont(new Font("Arial", Font.BOLD, 16));
        closeButton.setForeground(Color.WHITE);
        closeButton.setBackground(Color.RED); // 关闭按钮为红色
        closeButton.setFocusPainted(false);
        closeButton.setBorderPainted(false);

        // 让关闭按钮不获取焦点
        closeButton.setFocusable(false);

        // 添加关闭按钮事件
        closeButton.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                System.exit(0); // 点击关闭按钮时退出程序
            }
        });

        // 创建标题栏面板,将标题和关闭按钮放在一起
        JPanel titlePanel = new JPanel(new BorderLayout());
        titlePanel.add(customTitleBar, BorderLayout.CENTER);
        titlePanel.add(closeButton, BorderLayout.EAST); // 关闭按钮在右侧
        titlePanel.setBackground(Color.DARK_GRAY);

        // 创建状态栏并设置样式
        statusBar = new JLabel("得分: 0");
        statusBar.setFont(monoFont); // 更大的字体
        statusBar.setHorizontalAlignment(SwingConstants.CENTER);
        statusBar.setForeground(Color.WHITE); // 字体颜色
        statusBar.setOpaque(true);
        statusBar.setBackground(Color.DARK_GRAY); // 状态栏背景颜色

        // 将自定义标题栏添加到顶部
        add(titlePanel, BorderLayout.NORTH);
        // 将状态栏添加到底部
        add(statusBar, BorderLayout.SOUTH);

        // 创建游戏区域
        Tetris game = new Tetris(this);
        add(game);

        // 确保游戏面板获得焦点
        game.setFocusable(true);
        game.requestFocusInWindow();

        game.start();

        setDefaultCloseOperation(EXIT_ON_CLOSE);
        setResizable(false); // 禁止调整窗口大小
        setLocationRelativeTo(null); // 将窗口居中显示
    }

    public JLabel getStatusBar() {
        return statusBar;
    }

    public static void main(String[] args) {
        EventQueue.invokeLater(() -> {
            TetrisGame game = new TetrisGame();
            game.setVisible(true);
        });
    }
}

Tetris

import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;

public class Tetris 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 boolean isGameOver = false; // 新增游戏结束标志
    private int numLinesRemoved = 0;
    private int score = 0; // 得分变量
    private int currentX = 0;
    private int currentY = 0;
    private JLabel statusBar;
    private Shape currentPiece;
    private Tetrominoes[] board;

    public Tetris(TetrisGame parent) {
        setFocusable(true);
        currentPiece = new Shape();
        timer = new Timer(300, this); // 控制方块下落的速度
        statusBar = parent.getStatusBar();
        board = new Tetrominoes[boardWidth * boardHeight];
        clearBoard();

        addKeyListener(new TAdapter());

        setBackground(Color.BLACK); // 设置背景颜色
    }

    public void start() {
        if (isPaused)
            return;

        isStarted = true;
        isGameOver = false; // 游戏开始时取消游戏结束状态
        isFallingFinished = false;
        numLinesRemoved = 0;
        score = 0; // 开始游戏时得分清零
        clearBoard();
        newPiece();
        timer.start();
    }

    @Override
    public void actionPerformed(ActionEvent e) {
        if (isFallingFinished) {
            isFallingFinished = false;
            newPiece();
        } else {
            oneLineDown();
        }
    }

    @Override
    public void paintComponent(Graphics g) {
        super.paintComponent(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, j * squareWidth(), boardTop + i * squareHeight(), shape);
            }
        }

        // 绘制当前下落的方块
        if (currentPiece.getShape() != Tetrominoes.NoShape) {
            for (int i = 0; i < 4; ++i) {
                int x = currentX + currentPiece.x(i);
                int y = currentY - currentPiece.y(i);
                drawSquare(g, x * squareWidth(), boardTop + (boardHeight - y - 1) * squareHeight(),
                        currentPiece.getShape());
            }
        }

        // 如果游戏结束,显示“Game Over”居中提示
        if (isGameOver) {
            drawGameOverMessage(g);
        }
    }

    private void drawGameOverMessage(Graphics g) {
        // 绘制半透明背景
        g.setColor(new Color(0, 0, 0, 150)); // 黑色半透明背景
        int rectWidth = 200;
        int rectHeight = 100;
        int rectX = (getWidth() - rectWidth) / 2;
        int rectY = getHeight() / 2 - rectHeight / 2;
        g.fillRect(rectX, rectY, rectWidth, rectHeight);

        // 绘制“Game Over”文字
        g.setColor(Color.WHITE);
        g.setFont(new Font("Arial", Font.BOLD, 36));
        String gameOverMsg = "Game Over";
        FontMetrics fm = getFontMetrics(g.getFont());
        int x = (getWidth() - fm.stringWidth(gameOverMsg)) / 2;
        int y = getHeight() / 2 + fm.getAscent() / 2;
        g.drawString(gameOverMsg, x, y);
    }

    private int squareWidth() {
        return (int) Math.round(getSize().getWidth() / boardWidth);
    }

    private int squareHeight() {
        return (int) Math.round(getSize().getHeight() / boardHeight);
    }

    private Tetrominoes shapeAt(int x, int y) {
        return board[(y * boardWidth) + x];
    }

    private void oneLineDown() {
        if (!tryMove(currentPiece, currentX, currentY - 1))
            pieceDropped();
    }

    private void clearBoard() {
        for (int i = 0; i < boardHeight * boardWidth; ++i)
            board[i] = Tetrominoes.NoShape;
    }

    private void newPiece() {
        currentPiece.setRandomShape();
        currentX = boardWidth / 2 + 1;
        currentY = boardHeight - 1 + currentPiece.minY();

        if (!tryMove(currentPiece, currentX, currentY)) {
            currentPiece.setShape(Tetrominoes.NoShape);
            timer.stop();
            isStarted = false;
            isGameOver = true; // 设置游戏结束状态
            repaint();
        }
    }

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

        currentPiece = newPiece;
        currentX = newX;
        currentY = newY;
        repaint();
        return true;
    }

    private void pieceDropped() {
        for (int i = 0; i < 4; ++i) {
            int x = currentX + currentPiece.x(i);
            int y = currentY - currentPiece.y(i);
            board[(y * boardWidth) + x] = currentPiece.getShape();
        }

        removeFullLines();
        if (!isFallingFinished)
            newPiece();
    }

    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;
            int points = numFullLines * 100; // 每消除一行得100分

            // 连击奖励
            if (numFullLines > 1) {
                points += (numFullLines - 1) * 50; // 每多消除一行,额外加50分
            }

            score += points; // 更新得分
            statusBar.setText("Score: " + score); // 在状态栏更新得分
            isFallingFinished = true;
            currentPiece.setShape(Tetrominoes.NoShape);
            repaint();
        }
    }

    private void drawSquare(Graphics g, int x, int y, Tetrominoes shape) {
        Color[] colors = {
                new Color(0, 0, 0), new Color(102, 204, 255),
                new Color(255, 102, 102), new Color(102, 255, 178),
                new Color(255, 178, 102), new Color(178, 102, 255),
                new Color(102, 255, 255), new Color(255, 255, 102)
        };

        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 {
        public void keyPressed(KeyEvent e) {
            if (!isStarted || currentPiece.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(currentPiece, currentX - 1, currentY);
                    break;
                case KeyEvent.VK_RIGHT:
                    tryMove(currentPiece, currentX + 1, currentY);
                    break;
                case KeyEvent.VK_DOWN:
                    oneLineDown();
                    break;
                case KeyEvent.VK_UP:
                    Shape rotatedPiece = currentPiece.rotateRight();
                    tryMove(rotatedPiece, currentX, currentY);
                    break;
                case KeyEvent.VK_SPACE:
                    dropDown();
                    break;
            }
        }
    }

    public void pause() {
        if (!isStarted)
            return;

        isPaused = !isPaused;
        if (isPaused) {
            timer.stop();
            statusBar.setText("Paused");
        } else {
            timer.start();
            statusBar.setText("Score: " + score);
        }

        repaint();
    }

    private void dropDown() {
        int newY = currentY;

        while (newY > 0) {
            if (!tryMove(currentPiece, currentX, newY - 1))
                break;
            --newY;
        }

        pieceDropped();
    }
}

Shape

import java.util.Random;

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

class Shape {
    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;
    }
}

启动项目

将之前三个文件的内容拷贝到对应的位置,并点击启动按钮启动游戏,我们将会看到如下游戏界面:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值