JavaScript로 테트리스 작성

高洛峰
풀어 주다: 2017-02-04 16:21:27
원래의
3727명이 탐색했습니다.

Turbo C++ 3.0을 사용하여 DOS용 Tetris를 작성한 후 곧 VB를 사용하여 다른 버전을 작성했습니다. 이번에는 완전히 변덕스럽지 않고 JavaScript로 또 다른 코드를 작성하기로 결정했습니다. 기술적으로 말하면 주로 webpack + babel로 구축된 순수한 es6 프런트엔드 프로젝트를 시도하고 싶었기 때문입니다.


프로젝트 구조

이것은 순전히 정적 프로젝트이며 index.html이라는 HTML 페이지가 하나만 있습니다. 스타일시트에는 내용이 많지 않고, 아직 LESS로 작성하는 데 익숙합니다. 제가 sass를 사용하지 않는 이유는 사실 매우 간단합니다. 자랑하고 싶지 않기 때문입니다(Ruby).

물론 초점은 스크립트에 있습니다. 하나는 가져오기/내보내기 모듈 관리를 포함한 완전한 ES6 구문을 시도하는 것입니다. 두 번째는 정적 언어를 빌드하는 것과 같은 빌드 아이디어를 사용하는 것입니다. 프로젝트에서는 webpack + babel을 통해 es5 구문으로 대상 스크립트를 빌드합니다.

源(es6语法,模块化)==> 目标(es5语法,打包)
로그인 후 복사

프로젝트에서는 JQuery를 사용했는데 습관 때문에 대상 스크립트에 jQuery를 패키징하고 싶지도 않고 수동으로 다운로드하고 싶지도 않아서 그냥 Bower를 사용해 보았습니다. 수동 다운로드에 비해 Bower를 사용하는 것이 최소한 Bower Install을 통해 빌드 스크립트를 작성할 수 있다는 장점이 있습니다.

처음에는 프로젝트 디렉터리 구조를 잘 생각하지 않아서 생성된 디렉터리 구조가 사실 좀 지저분했어요. 전체 디렉토리 구조는 다음과 같습니다

[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"]
                }
            }
        ]
    }
};
로그인 후 복사

그런데 나중에 ProvidePlugin을 사용하는 것이 추천된다는 것을 보고 나중에 공부하겠습니다.

처음 코드를 완성하고 처음 실행했을 때 컴파일 이후 es6에서 에러가 발생한 소스코드 위치를 찾을 수 없어서 디버깅이 매우 번거롭다는 것을 알았습니다. 그제서야 나는 매우 중요한 소스 맵이 빠졌다는 것을 깨달았습니다. 그래서 오랫동안 인터넷을 검색하다가

externals: {
        "jquery": "jQuery"
    }
로그인 후 복사

프로그램 분석

을 추가했습니다. 이전에 작성한 적이 있기 때문에 데이터 구조에 여전히 일부 이미지가 있습니다. 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中文网!


관련 라벨:
원천:php.cn
본 웹사이트의 성명
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.
인기 튜토리얼
더>
최신 다운로드
더>
웹 효과
웹사이트 소스 코드
웹사이트 자료
프론트엔드 템플릿
회사 소개 부인 성명 Sitemap
PHP 중국어 웹사이트:공공복지 온라인 PHP 교육,PHP 학습자의 빠른 성장을 도와주세요!