【Swing】——俄罗斯方块

一、前言


该俄罗斯方块版本是教主第二次写的,也算是一次重构,主要有三个目标:

  1. 一是“大道至简”,用最简单的逻辑实现俄罗斯方块的功能;
  2. 二是“逻辑与视图分离”,解开游戏运行的逻辑与Swing渲染的耦合;
  3. 三是记录Swing,记录一下Swing的简单使用方法。



二、设计


我们发现大部分俄罗斯方块都是由10x20的小格子组成,然后有7种类型的方块,每种类型方块的对应1~4种可以由旋转而得到的形状。

于是我们可以设置一个10x20的布尔矩阵作为全局矩阵,然后以4x4的布尔矩阵来实现方块。方块的移动即是方块矩阵在全局矩阵上的偏移;方块的旋转即是单位矩阵的旋转。方块的创建可以采用原型模型,创建7种方块的原型,新方块都克隆自这7种原型方块。


建模

综上,俄罗斯方块游戏需要的类有:

  • 布尔矩阵(BoolMatrix):将二维数组封装为矩阵,以方便其它类的访问。
  • 方块(Tetris
  • 抽象游戏逻辑接口(GameHandler):定义数据获取和事件处理的接口,目的是为了以接口的方式解开逻辑与渲染的耦合。
  • 具体游戏逻辑(Game):继承自GameHandler,实现具体的游戏逻辑。
  • 游戏主窗口(GameView):继承自JFrame,组织各种组件以及负责事件的监听。
  • 面板GamePanel:继承自JPanel,负责渲染界面。
  • 入口类App

预览

俄罗斯方块预览


疑问

1、为什么要写这么多类?

可以发现,俄罗斯方块本身的逻辑其实也就哪些,而这些逻辑好像又的确与视图的渲染没什么直接的关系。

也就是说,只要定义好俄罗斯方块游戏逻辑的访问接口(包括但不限于事件的处理、数据的读取),任何按照一定约定来访问该接口的渲染组件,都能够产生正确的交互并渲染出正确的界面。

2、为什么抽象游戏逻辑接口(GameHandler)中的全局矩阵的宽高为12x22?

大道至简。

如果设置为10x20个小格子,那么除了碰撞问题还要处理移动越界的问题。而如果加一层值为1的边框,那么就可以统一的用碰撞问题去处理。

3、统一用一种方法判断会不会降低效率?

会。

但即便这样设计会增加不必要的循环和判断,视图约定以0.5秒的间隔去刷新和访问,对于逻辑运算和界面渲染来说也已经绰绰有余。另外Swing中的监听是单独的线程,但是触发的事件会被放入队列中,不存在多线程访问的同步问题。

4、一直创建方块会不会增加内存开销?

会。

可以使用享元模式来管理方块。将创建的方块放入享元工厂中就不用一直创建方块了。




三、代码


布尔矩阵

  • 为了支持方块的原型模式,布尔矩阵需要实现Cloneable接口并重写Clone()方法,对内置的二维数组进行深克隆。
  • 并且方块都是4阶单位矩阵,通过对单位矩阵进行旋转操作来实现方块的形状变化。
  • 对于方块的碰撞和越界,将全局矩阵加一层值为1的格子就可以统一用碰撞问题来处理,具体到代码就是判断方块矩阵经偏移后是否与全局矩阵有重合点(其中偏移量对应方块的x、y坐标)。
  • 对于方块更替时的处理,可以先将方块矩阵经偏移后或运算到全局矩阵中,然后再将当前方块的引用指向下一个方块。

在这里插入图片描述

public class BoolMatrix implements Cloneable {
    // 内置二维数组
    private byte[][] array;
    // 矩阵的行数
    private int      rows;
    // 矩阵的列数
    private int      columns;

    public BoolMatrix(int rows, int columns) {
        this.rows = rows;
        this.columns = columns;
        this.array = new byte[rows][columns];
    }

    /**
     * 对于传入的二维数组将会进行复制操作以使得矩阵不依赖实参
     * @param array 二维数组
     */
    public BoolMatrix(byte[][] array) {
        this.rows = array.length;
        this.columns = array[0].length;
        this.array = new byte[this.rows][this.columns];
        for (int i = 0; i < this.rows; i++) {
            System.arraycopy(array[i], 0, this.array[i], 0, this.columns);
        }
    }

    /**
     * 判断源矩阵经偏移后是否与目标矩阵重合
     * @param source  源矩阵
     * @param target  目标矩阵
     * @param xOffset 源矩阵在目标矩阵上的水平偏移量
     * @param yOffset 源矩阵在目标矩阵上的垂直偏移量
     * @return 源矩阵经偏移后是否与目标矩阵重合
     */
    public static boolean overlapped(BoolMatrix source, BoolMatrix target, int xOffset, int yOffset) {
        for (int i = 0; i < source.rows; i++) {
            for (int j = 0; j < source.columns; j++) {
                if (source.get(i, j) == 1 && target.get(i + yOffset, j + xOffset) == 1) {
                    return true;
                }
            }
        }
        return false;
    }


    /**
     * 将源矩阵经偏移后或运算到目标矩阵上
     * 修改的是目标矩阵
     * @param source  源矩阵
     * @param target  目标矩阵
     * @param xOffset 源矩阵在目标矩阵上的水平偏移量
     * @param yOffset 源矩阵在目标矩阵上的垂直偏移量
     */
    public static void or(BoolMatrix source, BoolMatrix target, int xOffset, int yOffset) {
        for (int i = 0; i < source.rows; i++) {
            for (int j = 0; j < source.columns; j++) {
                if (source.get(i, j) == 1) {
                    target.set(i + yOffset, j + xOffset, (byte) 1);
                }
            }
        }
    }

    public byte get(int i, int j) {
        return this.array[i][j];
    }

    public void set(int i, int j, byte value) {
        this.array[i][j] = value;
    }

    /**
     * 矩阵右旋
     * @throws Exception 行列不相等的矩阵不支持旋转
     */
    public void rotateRight() throws Exception {
        if (this.rows != this.columns) {
            throw new Exception("Rotate not supported");
        } else {
            int len = this.rows;
            for (int i = 0; i < len; i++) {
                for (int j = 0; j < len - i; j++) {
                    byte temp = this.array[i][j];
                    this.array[i][j] = this.array[len - j - 1][len - i - 1];
                    this.array[len - j - 1][len - i - 1] = temp;
                }
            }
            for (int i = 0; i < len / 2; i++) {
                for (int j = 0; j < len; j++) {
                    byte temp = this.array[i][j];
                    this.array[i][j] = this.array[len - i - 1][j];
                    this.array[len - i - 1][j] = temp;
                }
            }
        }
    }

    /**
     * 矩阵左旋
     * @throws Exception 行列不相等的矩阵不支持旋转
     */
    public void rotateLeft() throws Exception {
        if (this.rows != this.columns) {
            throw new Exception("Rotate not supported");
        } else {
            int len = this.rows;
            for (int i = 0; i < len; i++) {
                for (int j = i; j < len; j++) {
                    byte temp = this.array[i][j];
                    this.array[i][j] = this.array[j][i];
                    this.array[j][i] = temp;
                }
            }
            for (int i = 0; i < len; i++) {
                for (int j = 0; j < len / 2; j++) {
                    byte temp = this.array[i][j];
                    this.array[i][j] = this.array[i][len - j - 1];
                    this.array[i][len - j - 1] = temp;
                }
            }
        }
    }

    @Override
    public Object clone() throws CloneNotSupportedException {
        BoolMatrix bsm = (BoolMatrix) super.clone();
        bsm.array = bsm.array.clone();
        for (int i = 0; i < bsm.array.length; i++) {
            bsm.array[i] = bsm.array[i].clone();
        }
        return bsm;
    }

    public byte[][] getArray() {
        return array;
    }

    public int getRows() {
        return rows;
    }

    public int getColumns() {
        return columns;
    }
}

方块

  • 将字段定义为package-private包访问权限可减少setter方法。
  • 为了支持原型模式,方块需要:
    1. 实现Cloneable接口并重写clone()方法,对方块进行深克隆。
    2. 创建7种方块原型
    3. 私有化构造方法,以静态工厂的形式提供创建方块的途径,随机创建克隆自7种原型方块的新放方块,并初始化其坐标值。

在这里插入图片描述

import java.awt.*;
import java.util.Random;

public class Tetris implements Cloneable {
    // 7种原型单例
    private static final Tetris RED_PROTOTYPE
            = new Tetris(Color.RED, new BoolMatrix(new byte[][]{{0, 1, 0, 0},
                                                                {0, 1, 0, 0},
                                                                {0, 1, 0, 0},
                                                                {0, 1, 0, 0}}));
    private static final Tetris ORANGE_PROTOTYPE
            = new Tetris(Color.ORANGE, new BoolMatrix(new byte[][]{{0, 0, 0, 0},
                                                                   {0, 1, 1, 0},
                                                                   {0, 1, 1, 0},
                                                                   {0, 0, 0, 0}}));
    private static final Tetris YELLOW_PROTOTYPE
            = new Tetris(Color.YELLOW, new BoolMatrix(new byte[][]{{0, 0, 0, 0},
                                                                   {0, 1, 0, 0},
                                                                   {1, 1, 1, 0},
                                                                   {0, 0, 0, 0}}));
    private static final Tetris GREEN_PROTOTYPE
            = new Tetris(Color.GREEN, new BoolMatrix(new byte[][]{{0, 0, 1, 0},
                                                                  {0, 0, 1, 0},
                                                                  {0, 1, 1, 0},
                                                                  {0, 0, 0, 0}}));
    private static final Tetris CYAN_PROTOTYPE
            = new Tetris(Color.CYAN, new BoolMatrix(new byte[][]{{0, 1, 0, 0},
                                                                 {0, 1, 0, 0},
                                                                 {0, 1, 1, 0},
                                                                 {0, 0, 0, 0}}));
    private static final Tetris BLUE_PROTOTYPE
            = new Tetris(Color.BLUE, new BoolMatrix(new byte[][]{{0, 0, 1, 0},
                                                                 {0, 1, 1, 0},
                                                                 {0, 1, 0, 0},
                                                                 {0, 0, 0, 0}}));
    private static final Tetris MAGENTA_PROTOTYPE
            = new Tetris(Color.MAGENTA, new BoolMatrix(new byte[][]{{0, 1, 0, 0},
                                                                    {0, 1, 1, 0},
                                                                    {0, 0, 1, 0},
                                                                    {0, 0, 0, 0}}));

    // 原型的颜色
    Color      prototype;
    // 矩阵
    BoolMatrix matrix;

    // 当前的坐标/在全局矩阵的偏移量
    int x;
    int y;

    /**
     * 私有化构造方法,通过简单原型工厂方法提供创建新方块的途径
     * @param prototype 原型的颜色
     * @param matrix    矩阵
     */
    private Tetris(Color prototype, BoolMatrix matrix) {
        this.prototype = prototype;
        this.matrix = matrix;
    }

    /**
     * 根据随机原型深克隆创建方块
     * @param x 新方块的x坐标/在全局矩阵的水平偏移量
     * @param y 新方块的y坐标/在全局矩阵的垂直偏移量
     * @return 随机新方块
     */
    public static Tetris createTetris(int x, int y) {
        Tetris t = null;
        try {
            int prototypeId = new Random().nextInt(7);
            switch (prototypeId) {
                case 0:
                    t = (Tetris) RED_PROTOTYPE.clone();
                    break;
                case 1:
                    t = (Tetris) ORANGE_PROTOTYPE.clone();
                    break;
                case 2:
                    t = (Tetris) YELLOW_PROTOTYPE.clone();
                    break;
                case 3:
                    t = (Tetris) GREEN_PROTOTYPE.clone();
                    break;
                case 4:
                    t = (Tetris) CYAN_PROTOTYPE.clone();
                    break;
                case 5:
                    t = (Tetris) BLUE_PROTOTYPE.clone();
                    break;
                case 6:
                    t = (Tetris) MAGENTA_PROTOTYPE.clone();
                    break;
                default:
                    t = null;
            }
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        // switch必定能克隆出方块
        assert t != null;
        t.x = x;
        t.y = y;
        return t;
    }

    public Color getPrototype() {
        return prototype;
    }

    public BoolMatrix getMatrix() {
        return matrix;
    }

    public int getX() {
        return x;
    }

    public int getY() {
        return y;
    }

    @Override
    public String toString() {
        StringBuffer sb = new StringBuffer();
        for (int i = 0; i < this.matrix.getRows(); i++) {
            for (int j = 0; j < this.matrix.getColumns(); j++) {
                if (this.matrix.get(i, j) == 1) {
                    sb.append("◼");
                } else {
                    sb.append("◻");
                }
            }
            sb.append("\n");
        }
        return String.valueOf(sb);
    }

    @Override
    public Object clone() throws CloneNotSupportedException {
        Tetris t = (Tetris) super.clone();
        t.matrix = (BoolMatrix) t.matrix.clone();
        return t;
    }
}

抽象游戏逻辑接口

在这里插入图片描述

public interface GameHandler {
    /**
     * 全局矩阵的宽高
     */
    int GLOBAL_WIDTH  = 10 + 2;
    int GLOBAL_HEIGHT = 20 + 2;

    /**
     * 处理向左移动事件
     */
    void handleLeftAction();

    /**
     * 处理向右移动事件
     */
    void handleRightAction();

    /**
     * 处理旋转事件
     */
    void handleRotateAction();

    /**
     * 处理速降事件
     */
    void handleLandAction();

    /**
     * 请求调整(包括下降、清行、更新方块)
     * @return 游戏是否尚未输掉
     */
    boolean requestAdjust();

    /**
     * @return 全局矩阵
     */
    BoolMatrix requestGlobal();

    /**
     * @return 当前方块
     */
    Tetris requestCurrent();

    /**
     * @return 预览方块
     */
    Tetris requestNext();

    /**
     * @return 分数/消行数
     */
    int requestScore();
}

具体游戏逻辑

在这里插入图片描述

public class Game implements GameHandler {
    // 全局矩阵
    private BoolMatrix globalMatrix;
    // 当前方块
    private Tetris     current;
    // 预览方块
    private Tetris     next;
    // 分数/消行数
    private int        score;

    public Game() {
        this.globalMatrix = new BoolMatrix(GLOBAL_HEIGHT, GLOBAL_WIDTH);
        // 边框为1
        for (int i = 0; i < GLOBAL_HEIGHT; i++) {
            for (int j = 0; j < GLOBAL_WIDTH; j++) {
                if ((i == 0 || i == GLOBAL_HEIGHT - 1) 
                 || (j == 0 || j == GLOBAL_WIDTH - 1)) {
                    this.globalMatrix.set(i, j, (byte) 1);
                }
            }
        }
        // 初始化方块
        this.current = Tetris.createTetris(4, 1);
        this.next = Tetris.createTetris(4, 1);
    }

    @Override
    public void handleLeftAction() {
        int preX = this.current.x - 1;
        if (!BoolMatrix.overlapped(this.current.matrix,
                                   this.globalMatrix,
                                   preX,
                                   this.current.y)) {
            this.current.x = preX;
        }
    }


    @Override
    public void handleRightAction() {
        int preX = this.current.x + 1;
        if (!BoolMatrix.overlapped(this.current.matrix,
                                   this.globalMatrix,
                                   preX,
                                   this.current.y)) {
            this.current.x = preX;
        }
    }

    @Override
    public void handleRotateAction() {
        try {
            // 预先右转
            this.current.matrix.rotateRight();
            if (BoolMatrix.overlapped(this.current.matrix,
                                      this.globalMatrix,
                                      this.current.x,
                                      this.current.y)) {
                // 重合则撤销
                this.current.matrix.rotateLeft();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Override
    public void handleLandAction() {
        // 最长的距离为"一"型的方块将要重合的距离
        int maxDistance = GLOBAL_HEIGHT - this.current.y - 1;
        for (int i = 0; i <= maxDistance; i++) {
            int preY = this.current.y + i;
            if (BoolMatrix.overlapped(this.current.matrix,
                                      this.globalMatrix,
                                      this.current.x,
                                      preY + 1)) {
                this.current.y = preY;
                break;
            }
        }
    }

    @Override
    public boolean requestAdjust() {
        // 判断输赢
        for (int i = 1; i < GLOBAL_WIDTH - 1; i++) {
            if (this.globalMatrix.get(3, i) == 1) {
                return false;
            }
        }
        int preY = this.current.y + 1;
        if (!BoolMatrix.overlapped(this.current.matrix,
                                   this.globalMatrix,
                                   this.current.x,
                                   preY)) {
            // 能继续下降
            this.current.y = preY;
        } else {
            // 不能继续下降
            // 1.或运算到全局矩阵
            BoolMatrix.or(this.current.matrix,
                          this.globalMatrix,
                          this.current.x,
                          this.current.y);
            // 2.更新方块
            this.current = this.next;
            this.next = Tetris.createTetris(4, 1);
            // 3.清空满行
            int zeroRowIndex = -1;
            for (int i = GLOBAL_HEIGHT - 2; i >= 1; i--) {
                int num = 0;
                for (int j = 1; j < GLOBAL_WIDTH - 1; j++) {
                    if (this.globalMatrix.get(i, j) == 1) {
                        num++;
                    }
                }
                if (num == GLOBAL_WIDTH - 2) {
                    for (int j = 1; j < GLOBAL_WIDTH - 1; j++) {
                        this.globalMatrix.set(i, j, (byte) 0);
                    }
                    // 每次只清空给一行
                    this.score++;
                    zeroRowIndex = i;
                    break;
                }
            }
            // 4.向下填充
            for (int i = zeroRowIndex + 1; i >= 2; i--) {
                for (int j = 1; j < GLOBAL_WIDTH - 1; j++) {
                    if (this.globalMatrix.get(i, j) == 0 && this.globalMatrix.get(i - 1, j) == 1) {
                        this.globalMatrix.set(i, j, (byte) 1);
                        this.globalMatrix.set(i - 1, j, (byte) 0);
                    }
                }
            }
        }
        return true;
    }

    @Override
    public BoolMatrix requestGlobal() {
        return this.globalMatrix;
    }

    @Override
    public Tetris requestCurrent() {
        return this.current;
    }

    @Override
    public Tetris requestNext() {
        return this.next;
    }

    @Override
    public int requestScore() {
        return this.score;
    }
}

主窗口

  • 为了让代码块的概念更加清晰,可以将窗体的创建拆分为三个部分,分别是buildBase()buildComponent()buildListener()
  • 提供一个start()方法来开始运行游戏。其中在while循环种间歇性的向抽象游戏罗杰接口发起调整请求,并让自己的面板组件重新绘制。当收到游戏输掉的消息后退出循环并弹出JDilalog对话框来结束游戏。

在这里插入图片描述

注意事项
  • 事件的监听需要获得焦点才能触发,可以在调用setVisible()方法前加上requestFocus(),或者在其后加setFocusable(true),以使得组件能够获得焦点。
  • 窗口运行在main线程中并且会阻塞主线程,而事件的监听时运行在独立的线程中,可以调用Thread.currentThread.getName()方法来获取当前线程的名称。
import javax.swing.*;
import java.awt.*;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;

public class GameView extends JFrame {
    // 调整后的窗口最佳宽高
    private static final int FRAME_WIDTH  = 560;
    private static final int FRAME_HEIGHT = 720;

    // 游戏处理者
    private GameHandler handler;
    // 绘制面板
    private JPanel      gamePanel;
    // 提示栏
    private JTextArea   tipText;
    // 游戏是否暂停
    private boolean     paused;

    public GameView(GameHandler handler) {
        this.handler = handler;
        this.paused = false;

        this.buildBase();
        this.buildComponent();
        this.buildListener();

        // 设置窗口可见
        this.setVisible(true);
        // 设置窗口可获取焦点以触发事件监听
        this.setFocusable(true);
    }

    /**
     * 开始游戏
     * 在while循环中以0.5秒的间隔阻塞绘制和调整请求
     */
    public void start() {
        boolean going = true;
        while (going) {
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            if (!this.paused) {
                going = handler.requestAdjust();
                this.gamePanel.repaint();
            }
        }
        // 游戏输掉后以弹出对话框的形式退出游戏并释放资源
        this.setEnabled(false);
        JDialog result = new JDialog(this, "GAME OVER");
        result.setSize(this.getWidth() / 4, this.getHeight() / 8);
        result.addWindowListener(new WindowAdapter() {
            @Override
            public void windowClosing(WindowEvent e) {
                super.windowClosing(e);
                GameView.this.dispose();
            }
        });
        result.setLocationRelativeTo(null);
        result.setResizable(false);
        result.setVisible(true);
    }

    /**
     * 添加窗口基本属性
     */
    private void buildBase() {
        this.setTitle("教主的俄罗斯方块");
        this.setSize(FRAME_WIDTH, FRAME_HEIGHT);
        this.setLocationRelativeTo(null);
        this.setDefaultCloseOperation(EXIT_ON_CLOSE);
        this.setResizable(false);
    }

    /**
     * 添加窗口组件
     */
    private void buildComponent() {
        this.gamePanel = new GamePanel(this.handler);
        this.getContentPane().add(this.gamePanel, BorderLayout.CENTER);

        this.tipText = new JTextArea();
        this.tipText.setFont(new Font("微软雅黑", Font.PLAIN, 14));
        this.tipText.setText("W : 旋转\t" + "A : 向左\t" + "S : 速降\t" + "D : 向右\t" + "Enter : 开始/暂停");
        this.tipText.setBackground(null);
        this.tipText.setEditable(false);
        this.getContentPane().add(this.tipText, BorderLayout.SOUTH);
    }

    /**
     * 添加事件监听
     */
    private void buildListener() {
        this.addKeyListener(new KeyAdapter() {
            @Override
            public void keyPressed(KeyEvent e) {
                super.keyPressed(e);
                switch (e.getKeyCode()) {
                    case KeyEvent.VK_W:
                        if (!GameView.this.paused) {
                            GameView.this.handler.handleRotateAction();
                        }
                        break;
                    case KeyEvent.VK_A:
                        if (!GameView.this.paused) {
                            GameView.this.handler.handleLeftAction();
                        }
                        break;
                    case KeyEvent.VK_S:
                        if (!GameView.this.paused) {
                            GameView.this.handler.handleLandAction();
                        }
                        break;
                    case KeyEvent.VK_D:
                        if (!GameView.this.paused) {
                            GameView.this.handler.handleRightAction();
                        }
                        break;
                    case KeyEvent.VK_ENTER:
                        GameView.this.paused = !GameView.this.paused;
                        break;
                }
            }
        });
    }
}

面板

在这里插入图片描述

可以引入560x720的背景图片于面板类的同级目录下,去掉注释掉的代码并修改图片名,运行时即可渲染背景图片。

import javax.imageio.ImageIO;
import javax.swing.*;
import java.awt.*;
import java.io.IOException;

public class GamePanel extends JPanel {
    // 矩阵单元格的长度
    private static final int   CELL_LEN = 30;
    // 背景图
    private static       Image img;
/*
    static {
        try {
            img = ImageIO.read(GamePanel.class.getResource("cartoon1.jpg"));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
*/

    private GameHandler handler;

    public GamePanel(GameHandler handler) {
        this.handler = handler;
    }


    @Override
    public void paint(Graphics g) {
        super.paint(g);
        this.paintImage(g);
        this.paintGlobalMatrix(g);
        this.paintCurrentMatrix(g);
        this.paintNextMatrix(g);
        this.paintScore(g);
    }

    private void paintImage(Graphics g) {
     //   g.drawImage(img, 0, 0, null);
    }

    private void paintGlobalMatrix(Graphics g) {
        BoolMatrix globalMatrix = this.handler.requestGlobal();
        for (int i = 0; i < GameHandler.GLOBAL_HEIGHT; i++) {
            for (int j = 0; j < GameHandler.GLOBAL_WIDTH; j++) {
                g.setColor(Color.BLACK);
                g.drawRect(j * CELL_LEN, i * CELL_LEN, CELL_LEN, CELL_LEN);
                if (globalMatrix.get(i, j) == 1) {
                    if (i == 0 || i == GameHandler.GLOBAL_HEIGHT - 1 
                     || j == 0 || j == GameHandler.GLOBAL_WIDTH - 1) {
                        // 边框值
                        g.setColor(Color.LIGHT_GRAY);
                    } else {
                        // 由其它方块复制过来的值
                        g.setColor(Color.GRAY);
                    }
                    // 不完全填充会好看一点儿
                    g.fillRect(j * CELL_LEN + 2,
                               i * CELL_LEN + 2,
                               CELL_LEN - 3,
                               CELL_LEN - 3);
                }
            }
        }
    }

    private void paintCurrentMatrix(Graphics g) {
        Tetris current = this.handler.requestCurrent();
        BoolMatrix currentMatrix = current.getMatrix();
        g.setColor(current.getPrototype());
        for (int i = 0; i < currentMatrix.getRows(); i++) {
            for (int j = 0; j < currentMatrix.getColumns(); j++) {
                if (currentMatrix.get(i, j) == 1) {
                    g.fillRect((current.getX() + j) * CELL_LEN + 2,
                               (current.getY() + i) * CELL_LEN + 2,
                               CELL_LEN - 3,
                               CELL_LEN - 3);
                }
            }
        }
    }

    private void paintNextMatrix(Graphics g) {
        Tetris next = handler.requestNext();
        BoolMatrix nextMatrix = next.getMatrix();
        int positionX = (GameHandler.GLOBAL_WIDTH + 1) * CELL_LEN;
        int positionY = CELL_LEN;
        for (int i = 0; i < nextMatrix.getRows(); i++) {
            for (int j = 0; j < nextMatrix.getColumns(); j++) {
                g.setColor(Color.BLACK);
                g.drawRect(positionX + j * CELL_LEN,
                           positionY + i * CELL_LEN,
                           CELL_LEN,
                           CELL_LEN);
                if (nextMatrix.get(i, j) == 1) {
                    g.setColor(next.getPrototype());
                    g.fillRect(positionX + j * CELL_LEN + 2,
                               positionY + i * CELL_LEN + 2,
                               CELL_LEN - 3,
                               CELL_LEN - 3);
                }
            }
        }
    }

    private void paintScore(Graphics g) {
        int score = this.handler.requestScore();
        int positionX = (GameHandler.GLOBAL_WIDTH + 1) * CELL_LEN;
        int positionY = 10 * CELL_LEN;
        g.setColor(Color.RED);
        g.setFont(new Font("微软雅黑", Font.BOLD, CELL_LEN));
        g.drawString("score = " + score, positionX, positionY);
    }
}

入口

public class App {
    public static void main(String[] args) {
        GameHandler handler = new Game();
        GameView view = new GameView(handler);
        view.start();
    }
}



四、Swing结构


  • 其中窗体(JFrame)和对话框(JDialog)都是作为顶层容器而存在的,通常不能直接向里面添加组件。顶层容器的显示需要调用setVisible()方法并传参true
  • 要想向顶层容器中添加组件通常需要借助中间容器,比如面板(JPanel)。还可以通过调用getContentPane()方法来获得默认的中间容器。
  • 窗体(JFrame)默认是边界布局(BorderLaylout);面板(JPanel)默认是流式布局(FlowLayout)。

在这里插入图片描述
在这里插入图片描述




五、参考资料


  • 19
    点赞
  • 51
    收藏
    觉得还不错? 一键收藏
  • 6
    评论
下面是一个简单的 Java Swing 实现俄罗斯方块的代码示例: ```java import java.awt.Color; import java.awt.Dimension; import java.awt.Graphics; import java.awt.Point; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.KeyEvent; import java.awt.event.KeyListener; import java.util.ArrayList; import java.util.Random; import javax.swing.JFrame; import javax.swing.JPanel; import javax.swing.Timer; public class Tetris extends JPanel implements ActionListener, KeyListener { private static final long serialVersionUID = 1L; private final int WIDTH = 10; private final int HEIGHT = 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 Shape.Tetrominoes[] board; public Tetris(JFrame parent) { initTetris(parent); } private void initTetris(JFrame parent) { setFocusable(true); curPiece = new Shape(); timer = new Timer(400, this); timer.start(); statusbar = new JLabel(" 0"); parent.add(statusbar, BorderLayout.SOUTH); board = new Shape.Tetrominoes[WIDTH * HEIGHT]; addKeyListener(this); clearBoard(); } public void actionPerformed(ActionEvent e) { if (isFallingFinished) { isFallingFinished = false; newPiece(); } else { oneLineDown(); } } private int squareWidth() { return (int) getSize().getWidth() / WIDTH; } private int squareHeight() { return (int) getSize().getHeight() / HEIGHT; } private Shape.Tetrominoes shapeAt(int x, int y) { return board[(y * WIDTH) + 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(); } public void paint(Graphics g) { super.paint(g); Dimension size = getSize(); int boardTop = (int) size.getHeight() - HEIGHT * squareHeight(); for (int i = 0; i < HEIGHT; ++i) { for (int j = 0; j < WIDTH; ++j) { Shape.Tetrominoes shape = shapeAt(j, HEIGHT - i - 1); if (shape != Shape.Tetrominoes.NoShape) drawSquare(g, j * squareWidth(), boardTop + i * squareHeight(), shape); } } if (curPiece.getShape() != Shape.Tetrominoes.NoShape) { for (int i = 0; i < 4; ++i) { int x = curX + curPiece.getX(i); int y = curY - curPiece.getY(i); drawSquare(g, x * squareWidth(), boardTop + (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 < HEIGHT * WIDTH; ++i) board[i] = Shape.Tetrominoes.NoShape; } private void pieceDropped() { for (int i = 0; i < 4; ++i) { int x = curX + curPiece.getX(i); int y = curY - curPiece.getY(i); board[(y * WIDTH) + x] = curPiece.getShape(); } removeFullLines(); if (!isFallingFinished) newPiece(); } private void newPiece() { curPiece.setRandomShape(); curX = WIDTH / 2 + 1; curY = HEIGHT - 1 + curPiece.minY(); if (!tryMove(curPiece, curX, curY)) { curPiece.setShape(Shape.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.getX(i); int y = newY - newPiece.getY(i); if (x < 0 || x >= WIDTH || y < 0 || y >= HEIGHT) return false; if (shapeAt(x, y) != Shape.Tetrominoes.NoShape) return false; } curPiece = newPiece; curX = newX; curY = newY; repaint(); return true; } private void removeFullLines() { int numFullLines = 0; for (int i = HEIGHT - 1; i >= 0; --i) { boolean lineIsFull = true; for (int j = 0; j < WIDTH; ++j) { if (shapeAt(j, i) == Shape.Tetrominoes.NoShape) { lineIsFull = false; break; } } if (lineIsFull) { ++numFullLines; for (int k = i; k < HEIGHT - 1; ++k) { for (int j = 0; j < WIDTH; ++j) board[(k * WIDTH) + j] = shapeAt(j, k + 1); } } } if (numFullLines > 0) { numLinesRemoved += numFullLines; statusbar.setText(String.valueOf(numLinesRemoved)); isFallingFinished = true; curPiece.setShape(Shape.Tetrominoes.NoShape); repaint(); } } private void drawSquare(Graphics g, int x, int y, Shape.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); } public void keyPressed(KeyEvent e) { if (!isStarted || curPiece.getShape() == Shape.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': case 'D': oneLineDown(); break; } } public void keyReleased(KeyEvent e) { } public void keyTyped(KeyEvent e) { } } class Shape { 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; } public int getX(int index) { return coords[index][0]; } public int getY(int index) { return coords[index][1]; } } public class TetrisGame extends JFrame { private static final long serialVersionUID = 1L; public TetrisGame() { initUI(); } private void initUI() { Tetris board = new Tetris(this); add(board); setTitle("Tetris"); setSize(400, 700); setDefaultCloseOperation(EXIT_ON_CLOSE); setLocationRelativeTo(null); } public static void main(String[] args) { TetrisGame game = new TetrisGame(); game.setVisible(true); } } ``` 这个代码示例可以直接运行,实现了一个简单的俄罗斯方块游戏。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值