JavaScript でテトリスを作成する

高洛峰
高洛峰オリジナル
2017-02-04 16:21:273813ブラウズ

私はかつて Turbo C++ 3.0 を使用して DOS 用のテトリスを作成し、その後すぐに VB を使用して別のバージョンを作成しました。今回、JavaScript で別のプロジェクトを作成することにしたのは完全に気まぐれではありません。技術的に言えば、主に webpack + babel で構築された純粋な es6 フロントエンド プロジェクトを試してみたかったからです。


プロジェクト構造

これは純粋に静的なプロジェクトであり、HTML のページは 1 つだけあり、index.html です。スタイルシートにはそれほど多くのコンテンツはなく、私はまだ LESS で書くことに慣れています。私が Sass を使いたくない理由は、実際には非常に単純です - 見せびらかしたくないからです (Ruby)。

当然のことながら、焦点はスクリプトにあります。1 つは、インポート/エクスポート モジュール管理を含む完全な ES6 構文を試すことです。2 つ目は、静的言語プロジェクトを構築するような考え方を使用して、Webpack を介して es5 を構築することです。 + babel 構文ターゲット スクリプト。

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

プロジェクトでは jQuery を使用しましたが、習慣により、ターゲット スクリプトに jQuery をパッケージ化したくなかったし、手動でダウンロードしたくなかったので、単純に bower を試してみました。手動でダウンロードする場合と比較して、bower を使用すると、少なくとも bower インストールでビルド スクリプトを作成できるため有利です。

最初はプロジェクトのディレクトリ構造をあまり明確に考えていなかったので、作成されたディレクトリ構造は実際には少し乱雑でした。全体のディレクトリ構造は以下の通りです

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

ビルド構成

フロントエンドのビルドスクリプト部分はwebpack + babelを使用し、スタイルシートは使用量を減らしてgulpでまとめています。すべてのフロントエンド ビルド構成とソース コードはアプリ ディレクトリに配置されます。アプリ ディレクトリは、gulpfile.js や webpack.config.js などのビルド構成を含む npm プロジェクトです。

以前gulpを使ったことがあるので、fulpfile.jsは比較的書きやすいですが、webpackの設定に少し手間がかかります。

最初にインターネットから設定をコピーしました

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

そして、書く過程でjQueryを導入する必要があることがわかり、ネットで長い間検索して文章をコピーしました

externals: {
        "jquery": "jQuery"
    }

しかし、後でProvidePluginが推奨されているのを見ました後で勉強します。

初めてコードが完成して実行したとき、コンパイル後にes6でエラーが発生したソースコードの場所が見つからず、デバッグが非常に面倒であることがわかりました。そのときになって初めて、非常に重要なソース マップが欠落していることに気づきました。そこで、インターネットで長い間検索し、

devtool: "source-map"

プログラム解析

を追加しました。以前に書いたので、データ構造にはまだイメージが残っており、ゲーム領域は2次元配列に対応しています。各グラフィックは、相対的な位置関係を含む座標の集合であり、もちろん色の定義も含まれています。

すべての動作はデータ (座標) の変更によって実現されます。障害物(固定小四角)の判定は、現在のグラフィック位置と定義内のすべての小四角の相対位置から各小四角の座標を計算し、対応する座標に小四角データが存在するかどうかで判定します。大きなマトリックスの。これには、現在のグラフが次の形式で占める座標のリストを事前に計算する必要があります。

ブロックの自動立ち下がりはクロックサイクルによって制御されます。除去アニメーションも処理する必要がある場合は、2 クロック サイクルの制御が必要になる場合があります。もちろん、2 つのクロック サイクルの大公倍数を共通のクロック サイクルにマージすることもできますが、テトリスのアニメーションは非常に単純であり、そのような複雑な処理は必要ないようです。削除時に立ち下がりクロックサイクルが発生し、削除が完了し、再起動します。

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

方块模型

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

JavaScript でテトリスを作成する

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

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

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

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

为了偷懒, 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);
});

游戏区模型

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

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

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

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

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

逻辑控制

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

预览图形的生成的显示

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

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

边界及障碍判断

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

下落动画处理

消除动画处理

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

Game Over 判断

......

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

游戏区方块绘制

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

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

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

小结

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

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

还没有引入计分

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

更多JavaScript でテトリスを作成する相关文章请关注PHP中文网!


声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。