Home > Web Front-end > JS Tutorial > body text

Write a Tetris in JavaScript

高洛峰
Release: 2017-02-04 16:21:27
Original
3728 people have browsed it

I once wrote Tetris under DOS using Turbo C++ 3.0, and soon after I wrote another version using VB. This time I decided to write another one in JavaScript not entirely on a whim. Technically speaking, it was mainly because I wanted to try a pure es6 front-end project built with webpack + babel.


Project structure

This is a purely static project, and there is only one page of HTML, which is index.html. There is not much content in the style sheet, and I am still used to writing in LESS. The reason why I don’t like to use sass is actually very straightforward – I don’t want to show off (Ruby).

The focus is naturally on the script. One is to try the complete ES6 syntax, including import/export module management; the second is to try to use the idea of ​​​​building like building a static language project, through webpack + babel builds a target script with es5 syntax.

源(es6语法,模块化)==> 目标(es5语法,打包)
Copy after login

jQuery was used in the project, but because of habit, I didn’t want to package jQuery in the target script, nor did I want to download it manually, so I simply tried bower. Compared with manual downloading, using bower is advantageous. At least bower install can write the build script.

I didn’t think very clearly about the project directory structure at the beginning, so the created directory structure was actually a bit messy. The entire directory structure is as follows

[root>
  |-- index.html    : 入口
  |-- js/           : 构建生成的脚本
  |-- css/          : 构建生成的样式表
  |-- lib/          : bower 引入的库
  `-- app/          : 前端源文件
        |-- less    : 样式表源文件
        `-- src     : 脚本(es6)源文件
Copy after login

Build configuration

The front-end build script part uses webpack + babel, the style sheet uses less, and is then organized through gulp. All front-end build configuration and source code are placed in the app directory. The app directory is an npm project with build configurations such as gulpfile.js and webpack.config.js.

Because I have used gulp before, fulpfile.js is relatively easy to write, but it takes a little effort to configure webpack.

First I copied a configuration from the Internet

const path = require("path");module.exports = {    context: path.resolve(__dirname, "src"),    entry: [ "./index" ],    output: {        path: path.resolve(__dirname, "../js/"),        filename: "tetris.js"
    },    module: {        loaders: [
            {                test: /\.js$/,                exclude: /(node_modules)/,                loader: "babel",                query: {                    presets: ["es2015"]
                }
            }
        ]
    }
};
Copy after login

Then I found that jQuery needed to be introduced during the writing process, so I searched online for a long time and copied a sentence

externals: {
        "jquery": "jQuery"
    }
Copy after login

But Later I saw that it was recommended to use ProvidePlugin, and I will study it later.

When the code was first completed and ran for the first time, I found that debugging was very troublesome because after compilation, the source code location of the error in es6 could not be found. Only then did I realize that a very important source map was missing. So I searched the Internet for a long time, and added

devtool: "source-map"
Copy after login

Program Analysis

Because I have written it before, there is still some reflection in the data structure. The game area corresponds to a two-dimensional array. Each graphic is a set of coordinates with relative positional relationships, and of course color definitions.

All behaviors are achieved through changes in data (coordinates). The judgment of obstacles (fixed small squares) is based on calculating the coordinates of each small square based on the current graphic position and the relative positions of all small squares in the definition, and then checking whether there is small square data in the corresponding coordinates of the large matrix. This requires calculating in advance the list of coordinates that the current graph will occupy in the next form.

The automatic falling of the block is controlled by the clock cycle. If you also need to deal with elimination animation, you may need two clock cycles of control. Of course, the great common denominator of the two clock cycles can be taken to merge into a common clock cycle, but the animation of Tetris is quite simple, and it seems that there is no need for such complicated processing - you can consider pausing the falling clock cycle when eliminating, and the elimination is completed Then restart.

交互部分主要靠键盘处理,只需要给 document 绑定 keydown 事件处理就好。

方块模型

传统的俄罗斯方块只有 7 种图形,加上旋转变形一共也才 19 个图形。所以需要定义的图形不多,懒得去写旋转算法,直接用坐标来定义了。于是先用WPS表格把图形画出来了:

Write a Tetris in JavaScript

然后照此图形,在 JavaScript 中定义结构。设想的数数据结构是这样的

SHAPES: [Shape]     // 预定义所有图形Shape: {                // 图形的结构
    colorClass: string,     // 用于染色的 css class    
    forms: [Form]           // 旋转变形的组合}
Form: [Block]           // 图形变形,是一组小方块的坐标Block: {                // 小方块坐标
    x: number,              // x 表示横向
    y: number               // y 表示纵向}
Copy after login

其中 SHAPES 、 Form 都直接用数组表示, Block 结构简单,直接使用字面对象表示,只需要定义一个 Shape 类(当时考虑加些方法在里面,但后来发现没必要)

class Shape {
    constructor(colorIndex, forms) {        this.colorClass = `c${1 + colorIndex % 7}`;        this.forms = forms;
    }
}
Copy after login

为了偷懒, SHAPE 是用一个三维数组的数据,通过 Array.prototype.map() 来得到的 Shape 数组

class Shape {
    constructor(colorIndex, forms) {        this.colorClass = `c${1 + colorIndex % 7}`;        this.forms = forms;
    }
}

export const SHAPES = [    // 正方形
    [
        [[0, 0], [0, 1], [1, 0], [1, 1]]
    ],    // |
    [
        [[0, 0], [0, 1], [0, 2], [0, 3]],
        [[0, 0], [1, 0], [2, 0], [3, 0]]
    ],    
    // .... 省略,请参阅文末附上的源码地址].map((defining, i) => {    // data 就是上面提到的 forms 了,命名时没想好,后来也没改
    const data = defining.map(form => {        // 计算 right 和 bottom 主要是为了后面的出界判断
        let right = 0;
        let bottom = 0;        
        // point 就是 block,当时取名的时候没想好
        const points = form.map(point => {
            right = Math.max(right, point[0]);
            bottom = Math.max(bottom, point[1]);            return {
                x: point[0],
                y: point[1]
            };
        });
        points.width = right + 1;
        points.height = bottom + 1;        return points;
    });    return new Shape(i, data);
});
Copy after login

游戏区模型

虽然游戏区只有一块,但是就画图的这部分行为来说,还有一个预览区的行为与之相仿。游戏区除了显示外还需要处理方块下落、响应键盘操作左、右、下移及变形、堆积、消除等。

对于显示,定义了一个 Matrix 类来处理。 Matrix 主要是用来在 HTML 中创建用来显示每一个小方块的 以及根据数据绘制小方块。当然所谓的“绘制”其实只是设置 的 css class 而已,让浏览器来处理绘制的事情。

Matrix 根据构建传入的 width 和 height 来创建 DOM,每一行是一个

作为容器,但实际需要操作的是每一行中,由 表示的小方块。所以其实 Matrix 的结构也很简单,这里简单的列出接口,具体代码参考后面的源码链接

class Matrix {
    constructor(width, height) {}
    build(container) {}
    render(blockList) {}
}
Copy after login

逻辑控制

上面提到主游戏区有一些逻辑控制,而 Matrix 只处理了绘制的问题。所以另外定义了一个类: Puzzle 来处理控制和逻辑的问题,这些问题包括

预览图形的生成的显示

游戏图形和已经固定的方块显示

进行中的图形行为(旋转、左移、右移、下移等)

边界及障碍判断

下落结束后可消除行的判断

下落动画处理

消除动画处理

消除后的数据重算(因为位置改变)

Game Over 判断

......

其实比较关键的问题是图形和固定方块的显示、边界及障碍判断、动画处理。

游戏区方块绘制

已经确定了 Matrix 用于处理绘制,但绘制需要数据,数据又分两部分。一部分是当前下落中的图形,其位置是动态的;另一部分是之前落下的图形,已经固定在游戏区的。

从当前下落中的图形生成一个 blocks 数组,再将已经固定的小方块生成另一个 blocks 数组,合并起来,就是 Matrix.render() 的数据。 Matrix 拿到这个数据之后,先遍历所有 ,清除颜色 class,再遍历得到的数据,根据每一个 block 提供的位置和颜色,去设置对应的 的 css class。这样就完成了绘制。

Write a Tetris in JavaScript

边界和障碍判断

之前提到的 Shape 只是一个形状的定义,而下落中的图形是另一个实体,由于 Shape 命名已经被占用了,所以源代码中用 Block 来对它命名。

这个命名确实有点乱,需要这样解理: Shape -> ShapeDefinition ; Block -> Shape 。

现在下落中的图形是一个 Block 的实例(对象)。在判断边界和障碍判断的过程中需要用到其位置信息、边界信息(right、bottom)等;另外还需要知道它当前是哪一个旋转形态……所以定义了一些属性。

不过关键问题是需要知道它的下个状态(位置、旋转)会占用哪些坐标的位置。所以定义了几个方法

fasten() ,不带参数的时候返回当前位置当前形态所占用的坐标,主要是绘图用;带参数时可以返回指定位置和指定形态所需要占用的坐标。

fastenOffset() ,因为通常需要的位移坐标数据都相对原来的位置只都有少量的偏移,所以定义这个方法,以简化调用 fasten() 的参数。

fastenRotate() ,简化旋转后对 fasten() 的调用。

这里有一点需要注意,就是有图形在到在边界之后,旋转可能会造成出界。这种情况下需要对其进行位移,所以 Block 的 rotate() 和 fastenRotate() 都可以输入边界参数,用于计算修正位置。而修正位置则是通过模块中一个局部函数 getRotatePosition() 来实现的。

动画控制

前面已经提到了,动画时钟分两个,下落动画时钟和消除动画时钟。对于人工操作引起的动画,在操作之后直接重绘,就不需要通过时钟来进行了。

考虑到在开始消除动画时需要暂停下落动画,之后又要重新开始。所以为下落动画时钟定义为一个 Timer 类来控制 stop() 和 start() ,内部实现当然是用的 setInterval() 和 clearInterval() 。当然 Timer 也可以用于消除动画,但是因为在写消除动画的时候发现代码比较简单,就直接写 setInterval() 和 clearInterval() 解决了。

在 Puzzle 类中,某个图形下图到底的时候,通过 fastenCurent() 为固定它,这个方法里固定了当前图形之后会调用 eraseRows() 来检查和删除已经填满的行。从数据上消除和压缩行都是在这里处理的,同时这里还进行了消除行的动画处理——对需要消除的行从左到右清除数据并立即重绘。

let columnIndex = 0;const t = setInterval(() => {    // fulls 是找出来的需要消除的行
    fulls.forEach((rowIndex) => {
        matrix[rowIndex][columnIndex] = null;        this.render();
    });    
    // 消除列达到右边界时结束动画
    if (++columnIndex >= this.puzzle.width) {
        clearInterval(t);
        reduceRows();        this.render();        this.process();
    }
}, 10);
Copy after login

小结

俄罗斯方块的算法并不难,但这个仓促完成的小游戏中仍然存在一些问题需要将来处理掉:

没有交互方式的开始和结束,页面一旦打开就会持续运行。

还没有引入计分

每次绘制都是全部重绘,应该可以优化为局部(变化的部分)重绘

更多Write a Tetris in JavaScript相关文章请关注PHP中文网!


Related labels:
source:php.cn
Statement of this Website
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn
Popular Tutorials
More>
Latest Downloads
More>
Web Effects
Website Source Code
Website Materials
Front End Template
About us Disclaimer Sitemap
php.cn:Public welfare online PHP training,Help PHP learners grow quickly!