当前位置 博文首页 > 十二翼堕落天使:【Swing】——俄罗斯方块

    十二翼堕落天使:【Swing】——俄罗斯方块

    作者:[db:作者] 时间:2021-09-17 15:22

    一、前言


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

    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]