irpas技术客

[ HTML + CSS + Javascript ] 复盘尝试制作 2048 小游戏时遇到的问题_孜然の夏天

大大的周 1063

目录 简介CSS:遇到的问题1.伪元素设置 position:absolute 定位错误#问题描述:#解决方案: 2.使用 grid 布局绘制游戏界面3.相对长度单位 em 和 rem#问题描述:#解决方案: 4. :not 伪类选择器5. transition 添加动画效果#缩放:从中央逐渐放大至设定大小#弹出:从中央逐渐放大至超过设定大小,再缩小回设定大小#位移 JavaScript 遇到的问题1.JavaScript 设置改变元素样式,CSS transition 不生效#问题描述#解决方案 2.监听页面加载完成,进行初始化3.简化四个方向的方块移动#移动思路为:#代码如下: 源代码 & 预览

简介

使用 HTML + CSS + JavaScript 制作了 2048 小游戏,并尽可能地还原了动画效果。(虽然仍然有一些奇怪的小 bug) 源代码及预览见文末。

CSS:遇到的问题 1.伪元素设置 position:absolute 定位错误 #问题描述:

在添加分数栏的文字时使用的是伪元素,想通过给 score 的伪元素设置 position:absolute 来相对 score 定位,但是定位一直不正确。

#score::after { content: "SCORE"; position: absolute; left: 50%; transform: translateX(-50%); color: #eee4da; line-height: 3rem; font-size: 0.5rem; font-weight: bold; } #解决方案:

给 score 设置 position:relative,伪元素应当是添加在 <score> 一些 text 节点or Element 节点…</score> 的内部,作为选中元素的子节点。其中, ::before 是作为选中元素的第一个子元素, ::after 是作为选中元素的最后一个子元素。

2.使用 grid 布局绘制游戏界面 对父容器使用 grid-template-rows / grid-template-columns 进行布局行列的划分,其中可以使用 repeat(4,1fr) 来均分为 4 等份。 使用 fr 而不是具体值可以相对地按比例分配剩余空间。 其中,剩余空间 = 父容器的 width / height - 间隔大小 gap向父容器中添加16个子元素。使用 row-gap / column-gap 设置行列之间的间隙,将 16 个格子独立出来。对父容器设置 box-sizing:border-box, 使父容器的 offsetWidth / offsetHeight( = border + padding + content ) = 所设置的 width / height。 并设置 padding 使得边缘部分也有间隔。 #game { position:relative; /* 设置 grid 属性*/ display: grid; grid-template-rows: repeat(4, 1fr); grid-template-columns: repeat(4, 1fr); row-gap: 10px; column-gap: 10px; width: 450px; height: 450px; /* 设置 box-sizing */ box-sizing: border-box; padding: 10px; background-color: #bbada0; border-radius: 5px; margin: 20px 0; overflow: hidden; color: #fff; } 3.相对长度单位 em 和 rem #问题描述:

使用 em 时有时长度对不上自己的预期。

#解决方案:

查阅 MDN 可知: em

在 font-size 中使用是相对于父元素的字体大小在其他属性中使用是相对于自身的字体大小

rem 而 rem 则始终是相对于根元素的字体大小。

4. :not 伪类选择器

使用 .item:not(.item[data-value = ‘0’]) 来选择data-value 不为 ‘0’ 的所有.item元素。

5. transition 添加动画效果 #缩放:从中央逐渐放大至设定大小

在 CSS 中设置好元素最终的大小属性, 然后设置 transform:scale(0) 缩小为0, 设置 transition 的 timing-function 为 ease-in。

在 JavaScript 中将 transform 修改为 scale(1) 恢复原来的大小, 就可以激活 transition 的动画效果。 并且无需使用 JavaScript 跟踪改变元素的 left / top,将 left / top 设置为最终的 left / top 即可。 因为元素变形原点 transform-origin 默认值为 center。 ▲具体可见 MDN transform-origin

transform:scale(0); transition:transform 200ms ease-in; #弹出:从中央逐渐放大至超过设定大小,再缩小回设定大小

同样使用 transform:scale(0) 作为初始值, 同样在 JavaScript 中控制 transform 改变。 不同于上一个效果的是 超过设定大小 的效果, 可以使用自定义的 timing-function 来实现:

transform:scale(0); transition:transform 140ms cubic-bezier(0,.2,0,1.5);

其中 贝塞尔曲线 cubic-bezier(0,0.2,0,1.5) 图示如下。 ▲非常好的贝塞尔曲线网站

#位移

本例中实现方块移动的动画, 是通过在 JavaScript 中创建一个要移动元素 的 替身元素, 然后修改替身元素的 left / top 来实现移动。 因此,可以对 left / top 设置 transition。

transition:left 100ms ease-in,top 100ms ease-in; JavaScript 遇到的问题 1.JavaScript 设置改变元素样式,CSS transition 不生效 #问题描述

想生成一个位移的动画,是通过 JavaScript 获取被位移元素的属性,并在此元素的位置上生成一个替身元素(即设置其 left / top 使其与被位移元素重叠),然后操纵改变它的 left / top 使其 CSS 中的 transition 生效,但是 transition 始终不生效。

#解决方案

原因是因为在 JavaScript 同一个函数中两次修改元素的 style, 两次修改是发生在同一任务中的, 而当 JavaScript 主线程执行任务时,浏览器渲染线程是挂起的, 当任务完成时才发生 DOM 修改,因此浏览器只会进行一次渲染,即直接修改为最后的 left / top 值。

可以通过 setTimeout(()=>修改样式,0)强制将第二次修改滞后为另一次任务,使浏览器进行重绘(重排),触发 transition 动画。可以在第一次修改过后访问该元素 布局 有关的属性,如 offsetWidth / getBoundingClientRect(),强制更新 style。

此处 setTimeout 虽然为 0ms,但实际在浏览器中最小为 4ms。

▲关于css中transition, js设置两个值有时不能显示动画效果? ▲JavaScript 的单线程和异步 ▲StackOverflow 上关于此问题的详细说明

2.监听页面加载完成,进行初始化

使用 document.addEventListener(’'DOMContentLoaded",callback) 必须使用 addEventListener捕获。 DOMContentLoaded :浏览器已完全加载 HTML,并构建了 DOM 树,但像 和样式表之类的外部资源可能尚未加载完成。

3.简化四个方向的方块移动

本例中通过二维数组来存储游戏方块情况, 通过二维数组的值存储方块的数值。

因此可以通过提供一个包含遍历顺序的下标的对象 traversal 来完成遍历: 比如,向右移动时,y 轴(column)应从右向左检查空位,使方块按顺序右移,而 x 轴则无所谓,可以按照从上至下的遍历顺序,因此提供的 traversal 为:

{ x:[0,1,2,3], y:[3,2,1,0] }

向下移动时, x 轴(row)应从下向上检查空位:

{ x:[3,2,1,0], y:[0,1,2,3] }

而其余两个方向可以按照从上至下,从左至右的顺序。 因此可以提供一个初始的traversal = { x:[0,1,2,3],y:[0,1,2,3]} 然后如果方向是向右,则翻转 traversal.y.reverse(), 如果是向下,则翻转 traversal.x.reverse() 然后使用

traversal.x.forEach(x =>{ traversal.y.forEach(y => { } }

进行遍历。

#移动思路为:

1 . 按照遍历顺序进行遍历,如果遍历到的方块值(即二维数组对应元素的值)不为 0,则检查其移动方向上是否有空位,找到离该方块最远可以到达的一个位置。

2 . 检查最远可达位置 在 移动方向上的下一格(如果有)的值是否与当前方块值相同,即是否可以合并,如果可以,则最远可达位置更新为下一格。

3 . 如果最远可达位置与当前位置相同,不作修改。

4 . 如果不同,则删除当前的方块,并新增一个方块在最远可达位置,如果发生了合并,则注意更新方块的值,并注意将此格进行标记,因为一格在一次移动中最多合并一次,防止后续被再次合并。

#代码如下:

由于遍历比较耗时,因此使用 promise 包装此函数进行阻塞,返回值时相当于resolve()。

async moveTile(d, traversal) { // 检查是否发生更改,即移动过后是否要创建一个新的方块 let changed = false; // 累计本次移动的分数 let score = 0; // 调整遍历顺序 // 向下 if (Game.dir[d][0] == 1) traversal.x = traversal.x.reverse(); // 向上 if (Game.dir[d][1] == 1) traversal.y = traversal.y.reverse(); // 保存上一次被合并的方块,防止二次合并 let lastChangedItem = null; traversal.x.forEach((i) => { traversal.y.forEach((j) => { let val = this.tile[i][j]; if (val != 0) { let cur = { x: i, y: j, val: val }; // 找最远可达位置 let finalPos = this.findFinalPos(d, cur); // 最远可达位置的下一格 let next = finalPos.next; // 保存最终的位置 let newTile; // 合并的情况 if ( next.x >= 0 && next.x < 4 && next.y >= 0 && next.y < 4 && val == this.tile[next.x][next.y] && (!lastChangedItem || next.x != lastChangedItem.x || next.y != lastChangedItem.y) ) { score += val * 2; newTile = { x: next.x, y: next.y, val: val * 2 }; lastChangedItem = { x: next.x, y: next.y }; } else { newTile = { x: finalPos.x, y: finalPos.y, val: val }; } if (!changed && (newTile.x != i || newTile.y != j)) { changed = true; } // 无事发生,不修改,继续遍历 if (newTile.x == i && newTile.y == j) return; // 更新数组信息 this.tile[i][j] = 0; this.tile[newTile.x][newTile.y] = newTile.val; // 移动方块 this.move(cur, newTile); } }); }); // 更新分数 if (score) this.updateScore(score); return changed; } 源代码 & 预览

CodePen 地址:2048 game (with animation) ScauZirina - CodePen


1.本站遵循行业规范,任何转载的稿件都会明确标注作者和来源;2.本站的原创文章,会注明原创字样,如未注明都非原创,如有侵权请联系删除!;3.作者投稿可能会经我们编辑修改或补充;4.本站不提供任何储存功能只提供收集或者投稿人的网盘链接。

标签: #HTML #CSS #JavaScript #复盘尝试制作