[ HTML + CSS + Javascript ] 复盘尝试制作 2048 小游戏时遇到的问题

简介

使用 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 是作为选中元素的最后一个子元素。
分数栏标识 SCORE 使用伪元素添加

2.使用 grid 布局绘制游戏界面

  1. 对父容器使用 grid-template-rows / grid-template-columns 进行布局行列的划分,其中可以使用 repeat(4,1fr) 来均分为 4 等份
    使用 fr 而不是具体值可以相对地按比例分配剩余空间。
    其中,剩余空间 = 父容器的 width / height - 间隔大小 gap
  2. 向父容器中添加16个子元素。
  3. 使用 row-gap / column-gap 设置行列之间的间隙,将 16 个格子独立出来。
  4. 对父容器设置 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

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

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

扫描二维码关注公众号,回复: 14594565 查看本文章
  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 值。

  1. 可以通过 setTimeout(()=>修改样式,0)强制将第二次修改滞后为另一次任务,使浏览器进行重绘(重排),触发 transition 动画。
  2. 可以在第一次修改过后访问该元素 布局 有关的属性,如 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

猜你喜欢

转载自blog.csdn.net/weixin_50290666/article/details/122890208