Maison > Article > interface Web > Écrire un Tetris en JavaScript
J'ai déjà écrit Tetris pour DOS en utilisant Turbo C 3.0, et peu de temps après, j'ai écrit une autre version en utilisant VB. Cette fois, j'ai décidé d'en écrire un autre en JavaScript, pas entièrement sur un coup de tête. Techniquement parlant, c'était principalement parce que je voulais essayer un projet frontal es6 pur construit avec webpack babel.
Structure du projet
Il s'agit d'un projet purement statique, et il n'y a qu'une seule page HTML, qui est index.html. Il n'y a pas beaucoup de contenu dans la feuille de style, et je suis toujours habitué à écrire en LESS. La raison pour laquelle je n'aime pas utiliser sass est en fait très simple : je ne veux pas me montrer (Ruby).
L'accent est naturellement mis sur le script. La première est d'essayer la syntaxe complète d'ES6, y compris la gestion des modules d'import/export ; la seconde est d'essayer d'utiliser l'idée de construire comme construire un langage statique ; projet, via webpack babel Créez un script cible avec la syntaxe es5.
源(es6语法,模块化)==> 目标(es5语法,打包)
JQuery a été utilisé dans le projet, mais par habitude, je ne voulais pas empaqueter jQuery dans le script cible, ni le télécharger manuellement, alors j'ai simplement essayé bower. Par rapport au téléchargement manuel, l'utilisation de Bower est avantageuse. Au moins Bower Install peut écrire le script de construction.
Je n'ai pas réfléchi très clairement à la structure des répertoires du projet au début, donc la structure des répertoires créée était en fait un peu compliquée. La structure entière du répertoire est la suivante
[root> |-- index.html : 入口 |-- js/ : 构建生成的脚本 |-- css/ : 构建生成的样式表 |-- lib/ : bower 引入的库 `-- app/ : 前端源文件 |-- less : 样式表源文件 `-- src : 脚本(es6)源文件
Configuration de construction
La partie script de construction frontale utilise webpack babel, la feuille de style en utilise moins et est ensuite organisée via gulp. Toute la configuration de build frontale et le code source sont placés dans le répertoire de l'application. Le répertoire d'applications est un projet npm avec des configurations de build telles que gulpfile.js et webpack.config.js.
Parce que j'ai déjà utilisé gulp, fulpfile.js est relativement facile à écrire, mais la configuration du webpack demande un petit effort.
J'ai d'abord copié une configuration depuis 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"] } } ] } };
Ensuite, j'ai découvert que jQuery devait être introduit pendant le processus d'écriture, j'ai donc longuement cherché en ligne et copié une phrase
externals: { "jquery": "jQuery" }
Mais plus tard j'ai vu qu'il était recommandé d'utiliser ProvidePlugin, donc je l'étudierai plus tard.
Lorsque le code a été terminé et exécuté pour la première fois, j'ai trouvé que le débogage était très gênant car après la compilation, l'emplacement du code source de l'erreur dans es6 n'a pas pu être trouvé. Ce n’est qu’à ce moment-là que j’ai réalisé qu’il manquait une carte source très importante. J'ai donc longuement cherché sur Internet et ajouté
devtool: "source-map"
Analyse du programme
Parce que je l'ai déjà écrit, il y a encore un peu d'image dans la structure des données. La zone de jeu correspond à un tableau bidimensionnel. Chaque graphique est un ensemble de coordonnées avec des relations de position relatives et bien sûr des définitions de couleurs.
Tous les comportements sont obtenus grâce à des changements de données (coordonnées). Le jugement des obstacles (petits carrés fixes) est basé sur le calcul des coordonnées de chaque petit carré en fonction de la position graphique actuelle et des positions relatives de tous les petits carrés dans la définition, puis sur la vérification s'il existe des données de petits carrés dans les coordonnées correspondantes. de la grande matrice. Cela nécessite de calculer à l'avance la liste des coordonnées qu'occupera le graphe courant dans la forme suivante.
La chute automatique des blocs est contrôlée par le cycle d'horloge. Si vous devez également gérer une animation d'élimination, vous aurez peut-être besoin de deux cycles d'horloge de contrôle. Bien sûr, le grand dénominateur commun des deux cycles d'horloge peut être fusionné en un cycle d'horloge commun, mais l'animation de Tetris est assez simple, et il semble qu'il n'y ait pas besoin d'un traitement aussi compliqué - vous pouvez envisager de suspendre le cycle d'horloge décroissant lorsque l'élimination est terminée, et l'élimination est terminée, puis redémarrez.
交互部分主要靠键盘处理,只需要给 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);
小结
俄罗斯方块的算法并不难,但这个仓促完成的小游戏中仍然存在一些问题需要将来处理掉:
没有交互方式的开始和结束,页面一旦打开就会持续运行。
还没有引入计分
每次绘制都是全部重绘,应该可以优化为局部(变化的部分)重绘
更多Écrire un Tetris en JavaScript相关文章请关注PHP中文网!