Ich habe einmal Tetris für DOS mit Turbo C++ 3.0 geschrieben und kurz darauf eine weitere Version mit VB geschrieben. Diesmal habe ich beschlossen, ein weiteres Projekt in JavaScript zu schreiben, und zwar nicht ganz aus einer Laune heraus. Technisch gesehen lag es hauptsächlich daran, dass ich ein reines ES6-Frontend-Projekt ausprobieren wollte, das mit Webpack + Babel erstellt wurde.
Projektstruktur
Dies ist ein rein statisches Projekt und es gibt nur eine HTML-Seite, nämlich index.html. Das Stylesheet enthält nicht viel Inhalt und ich bin es immer noch gewohnt, es in WENIGER zu schreiben. Der Grund, warum ich Sass nicht gerne verwende, ist eigentlich ganz einfach: Ich möchte nicht angeben (Ruby).
Der Schwerpunkt liegt natürlich auf dem Skript. Zum einen soll die vollständige ES6-Syntax ausprobiert werden, einschließlich der Verwaltung von Import-/Exportmodulen Projekt erstellt über Webpack + Babel ein Zielskript mit ES5-Syntax.
源(es6语法,模块化)==> 目标(es5语法,打包)
JQuery wurde im Projekt verwendet, aber aus Gewohnheit wollte ich jQuery nicht in das Zielskript packen und es auch nicht manuell herunterladen, also habe ich es einfach mit Bower versucht. Im Vergleich zum manuellen Herunterladen ist die Verwendung von Bower von Vorteil. Zumindest kann die Bower-Installation das Build-Skript schreiben.
Ich habe mir am Anfang keine klaren Gedanken über die Projektverzeichnisstruktur gemacht, daher war die erstellte Verzeichnisstruktur tatsächlich etwas chaotisch. Die gesamte Verzeichnisstruktur ist wie folgt:
[root> |-- index.html : 入口 |-- js/ : 构建生成的脚本 |-- css/ : 构建生成的样式表 |-- lib/ : bower 引入的库 `-- app/ : 前端源文件 |-- less : 样式表源文件 `-- src : 脚本(es6)源文件
Build-Konfiguration
Der Front-End-Build-Skriptteil verwendet Webpack + Babel, das Stylesheet verwendet weniger und ist nach gulp organisiert. Die gesamte Front-End-Build-Konfiguration und der Quellcode werden im App-Verzeichnis abgelegt. Das App-Verzeichnis ist ein npm-Projekt mit Build-Konfigurationen wie gulpfile.js und webpack.config.js.
Da ich gulp schon einmal verwendet habe, ist fulpfile.js relativ einfach zu schreiben, aber die Konfiguration des Webpacks erfordert ein wenig Aufwand.
Zuerst habe ich eine Konfiguration aus dem Internet kopiert
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"] } } ] } };
Dann stellte ich fest, dass jQuery während des Schreibprozesses eingeführt werden musste, also habe ich lange online gesucht und einen Satz kopiert
externals: { "jquery": "jQuery" }
Aber später habe ich gesehen, dass die Verwendung von ProvidePlugin empfohlen wurde, also werde ich es später studieren.
Als der Code zum ersten Mal fertiggestellt und ausgeführt wurde, stellte ich fest, dass das Debuggen sehr mühsam war, da nach der Kompilierung der Quellcode-Speicherort des Fehlers in es6 nicht gefunden werden konnte. Erst dann wurde mir klar, dass eine sehr wichtige Quellkarte fehlte. Also habe ich lange im Internet gesucht und
devtool: "source-map"
Programmanalyse
Da ich es schon einmal geschrieben habe, gibt es immer noch ein kleines Bild in der Datenstruktur Der Spielbereich entspricht einem zweidimensionalen Array. Jede Grafik besteht aus einer Reihe von Koordinaten mit relativen Positionsbeziehungen und natürlich Farbdefinitionen.
Alle Verhaltensweisen werden durch Änderungen in Daten (Koordinaten) erreicht. Die Beurteilung von Hindernissen (feste kleine Quadrate) basiert auf der Berechnung der Koordinaten jedes kleinen Quadrats basierend auf der aktuellen Grafikposition und den relativen Positionen aller kleinen Quadrate in der Definition und anschließender Prüfung, ob in den entsprechenden Koordinaten kleine Quadratdaten vorhanden sind der großen Matrix. Dazu muss im Voraus die Liste der Koordinaten berechnet werden, die der aktuelle Graph im nächsten Formular einnehmen wird.
Das automatische Abfallen der Blöcke wird durch den Taktzyklus gesteuert. Wenn Sie sich auch mit Eliminierungsanimationen befassen müssen, benötigen Sie möglicherweise zwei Steuertaktzyklen. Natürlich kann der große gemeinsame Nenner der beiden Taktzyklen zu einem gemeinsamen Taktzyklus zusammengeführt werden, aber die Animation von Tetris ist recht einfach und es scheint, dass eine so komplizierte Verarbeitung nicht erforderlich ist – Sie können darüber nachdenken, sie anzuhalten Wenn die Eliminierung abgeschlossen ist, fällt der Taktzyklus und die Eliminierung ist abgeschlossen. Starten Sie dann neu.
交互部分主要靠键盘处理,只需要给 document 绑定 keydown 事件处理就好。
方块模型
传统的俄罗斯方块只有 7 种图形,加上旋转变形一共也才 19 个图形。所以需要定义的图形不多,懒得去写旋转算法,直接用坐标来定义了。于是先用WPS表格把图形画出来了:
然后照此图形,在 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。这样就完成了绘制。
边界和障碍判断
之前提到的 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);
小结
俄罗斯方块的算法并不难,但这个仓促完成的小游戏中仍然存在一些问题需要将来处理掉:
没有交互方式的开始和结束,页面一旦打开就会持续运行。
还没有引入计分
每次绘制都是全部重绘,应该可以优化为局部(变化的部分)重绘
更多Schreiben Sie ein Tetris in JavaScript相关文章请关注PHP中文网!