当前位置: 首页 > news >正文

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

目录

    • 简介
    • 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 是作为选中元素的最后一个子元素。
分数栏标识 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

  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

相关文章:

  • [Symbol.toPrimitive](hint) hint 什么时候为 default?
  • JavaScript 对象遍历方法及其遍历顺序的总结
  • vue 实现根据窗口大小自适应图片预览
  • 《计算机网络 自顶向下方法》笔记 - 第二章 应用层
  • 使用 BrowserRouter 报错 invaild hook call 解决方案
  • python中assert关键字的作用
  • CSS3 :nth-child(n)用法
  • CSS3中的transition属性详解
  • HTML中导航栏title前面小图标的实现
  • mysql区分大小写
  • SpringMvc中/和/*的区别
  • varchar 和 varchar2的区别
  • IntelliJ IDEA 各种搜索功能
  • HashMap中的tableSizeFor(int cap)
  • Jdk1.8-HashMap put() 方法tab[i = (n - 1) hash] 解惑
  • ----------
  • 230. Kth Smallest Element in a BST
  • Docker 笔记(1):介绍、镜像、容器及其基本操作
  • ECS应用管理最佳实践
  • ES6简单总结(搭配简单的讲解和小案例)
  • javascript 总结(常用工具类的封装)
  • JavaScript工作原理(五):深入了解WebSockets,HTTP/2和SSE,以及如何选择
  • Java知识点总结(JDBC-连接步骤及CRUD)
  • Netty 4.1 源代码学习:线程模型
  • RxJS 实现摩斯密码(Morse) 【内附脑图】
  • Solarized Scheme
  • 等保2.0 | 几维安全发布等保检测、等保加固专版 加速企业等保合规
  • 类orAPI - 收藏集 - 掘金
  • 猫头鹰的深夜翻译:JDK9 NotNullOrElse方法
  • 前端技术周刊 2018-12-10:前端自动化测试
  • 如何将自己的网站分享到QQ空间,微信,微博等等
  • 如何使用 JavaScript 解析 URL
  • 新海诚画集[秒速5センチメートル:樱花抄·春]
  • ​​​​​​​​​​​​​​汽车网络信息安全分析方法论
  • ​2021半年盘点,不想你错过的重磅新书
  • ​ssh-keyscan命令--Linux命令应用大词典729个命令解读
  • ​虚拟化系列介绍(十)
  • ​学习一下,什么是预包装食品?​
  • #微信小程序:微信小程序常见的配置传值
  • (02)Hive SQL编译成MapReduce任务的过程
  • (04)Hive的相关概念——order by 、sort by、distribute by 、cluster by
  • (30)数组元素和与数字和的绝对差
  • (4) openssl rsa/pkey(查看私钥、从私钥中提取公钥、查看公钥)
  • (C语言)fgets与fputs函数详解
  • (二)JAVA使用POI操作excel
  • (力扣记录)1448. 统计二叉树中好节点的数目
  • (转)jQuery 基础
  • (最全解法)输入一个整数,输出该数二进制表示中1的个数。
  • .apk文件,IIS不支持下载解决
  • .bashrc在哪里,alias妙用
  • .NET Core WebAPI中封装Swagger配置
  • .Net MVC4 上传大文件,并保存表单
  • .NET 中使用 Mutex 进行跨越进程边界的同步
  • .NET6 命令行启动及发布单个Exe文件
  • .NET设计模式(8):适配器模式(Adapter Pattern)