2048小游戏h5小游戏实现

pc端访问自己调整样式,没有适配,运行请再开启浏览器调试模拟移动端运行

先看一下运行效果

  1. 一开始准备做标准4x4的2048小游戏,后来突发奇想设计了自定义横纵数量的2048,让2048变得更有趣,首先定义了一个配置对象,建议空白单元格设置透明色,不然滑动动画感觉有点奇怪,其它格格块颜色可以自己加,不加就是随机颜色
    const info = {
      rows: 4, // 横轴数量
      cols: 4, // 纵轴数量
      background: "pink", // 游戏盒子背景色
      width: 80, // 矩阵宽度
      space: 16, // 格子之间的间距
      // 各个块背景颜色-没写将随机生成
      0: "transparent", // 空白格颜色
      // 生成数值概率(%)
      generate: [
        [20, 4], // 20 - 0 = 20%概率
        [100, 2], // 100 - 20 = 80%概率
      ],
      initValueNum: 2, // 初始值个数
      score: 0, // 分数
      slide: 0, // 累计滑动次数
      backNum: 3, // 可撤回次数
      slideGenerateNum: 1, // 滑动一次生成数字个数
      knockNum: 3, // 可敲击次数
    };
  2. 接下来准备游戏盒子dom、一些变量存储数据
    // 当前游戏矩阵
    let matrix = [];
    // 生成dom列表
    const contentList = [];
    // 生成随机颜色
    const getColor = () => `#${Math.random().toString(16).slice(2, 8)}`;
    // 需要+动画的单元格
    const animationList = [];
    // 历史矩阵记录列表
    const historyList = []
    // 历史分数记录列表
    const scoreList = []
    // 敲击标识
    let knockFlag = false
    
    // 游戏盒子
    const gameBox = document.querySelector("#game");
    gameBox.style.padding = info.space + "px";
    gameBox.style.width = info.width + "vw";
    gameBox.style.backgroundColor = info.background;
    // 分数盒子
    const scoreBox = document.querySelector(".score");
    scoreBox.innerHTML = `分数:${info.score}`;
    // 滑动次数盒子
    const slideBox = document.querySelector(".slide");
    slideBox.innerHTML = `滑动:${info.slide}次`;
    // 撤回次数盒子
    const backNumBox = document.querySelector(".backNum")
    backNumBox.innerHTML = `可撤回次数:${info.backNum}次`
    // 敲击次数盒子
    const knockNumBox = document.querySelector(".knockNum")
    knockNumBox.innerHTML = `可敲击次数:${info.knockNum}次`
    // 操作盒子
    const operationBox = document.querySelector(".operation")
    operationBox.style.width = info.width + "vw"
    // 提示盒子
    const tipBox = document.querySelector(".tip")
    // 撤回按钮
    const back = document.querySelector(".back")
    // 敲击按钮
    const knock = document.querySelector(".knock")
  3. 准备几个辅助方法
    // 动态计算字体大小
    const calFontSize = (parentDom, dom) => {
      const pw = parentDom.clientWidth - 16;
      const ph = parentDom.clientHeight - 16;
      const w = dom.scrollWidth;
      const h = dom.scrollHeight;
      if (w > pw || h > ph) {
        const dw = w - pw;
        const dh = h - ph;
        if (dw > dh) {
          dom.firstChild.style.transform = `scale(${(pw - 50) / w})`;
        } else {
          dom.firstChild.style.transform = `scale(${(ph - 50) / h})`;
        }
      }
    };
    
    // 空白格填入随机值
    const generateNum = () => {
      // 空白位置列表
      const zeroList = [];
      for (let i = 0; i < info.cols; i++) {
        for (let j = 0; j < info.rows; j++) {
          !matrix[i][j] && zeroList.push(i * info.rows + j);
        }
      }
      if(!zeroList.length) return
      // 随机点
      const index = Math.floor(Math.random() * zeroList.length);
      // 计算坐标
      const col = Math.floor(zeroList[index] / info.rows);
      const row = zeroList[index] % info.rows;
    
      // 随机数值并填入坐标
      const random = Math.floor(Math.random() * 100);
      for (const [probability, value] of info.generate) {
        if (random < probability) {
          if (contentList[col * info.cols + row]) {
            contentList[col * info.cols + row].style.animation = "cell 0.3s"
            setTimeout(() => {
              contentList[col * info.cols + row].style.animation = "none"
            }, 300);
          }
          matrix[col][row] = value;
          break;
        }
      }
    };
    
    // 判断游戏结束
    const checkedGameOver = () => {
      // 获取单元格相邻单元格
      const getSurround = (i, j) => [
        [i + 1, j],
        [i - 1, j],
        [i, j + 1],
        [i, j - 1],
      ];
      for (let i = 0; i < info.cols; i++) {
        for (let j = 0; j < info.rows; j++) {
          const num = matrix[i][j];
          if (num === 0) return;
          const surround = getSurround(i, j);
          for (const [col, row] of surround) {
            if (matrix[col] && matrix[col][row] != undefined) {
              if (matrix[col][row] == num) return;
            }
          }
        }
      }
      alert("游戏结束");
    };
  4. 初始化游戏,设置页面渲染函数
    
    // 初始化
    const init = () => {
      for (let i = 0; i < info.cols; i++) {
        const row = new Array(info.rows).fill(0);
        matrix.push(row);
      }
      // 初始值填入
      for (let i = 0; i < info.initValueNum; i++) {
        generateNum();
      }
      historyList.push([...matrix.map(m => [...m])])
      scoreList.push(info.score)
      // 初始化渲染dom
      for (let i = 0; i < info.cols; i++) {
        const arr = matrix[i]
        for (let j = 0; j < info.rows; j++) {
          const num = arr[j]
          const div = document.createElement("div");
          div.style.width = `calc(100% / ${info.rows})`;
          div.style.paddingBottom = `calc(100% / ${info.rows})`;
          div.style.height = "0";
          div.style.position = "relative";
          div.dataIndex = i * info.rows + j
          gameBox.appendChild(div);
    
          const content = document.createElement("div");
          content.style.padding = info.space + "px";
          content.classList.add("cell");
          div.appendChild(content);
    
          const cell = document.createElement("div");
          cell.classList.add("matrix");
          if (!info[num]) info[num] = getColor();
          cell.style.backgroundColor = info[num];
          content.appendChild(cell);
    
          const valueDom = document.createElement("div");
          valueDom.innerHTML = num === 0 ? "" : num;
          cell.appendChild(valueDom);
          contentList.push(content);
    
          // 计算字体大小
          calFontSize(content, cell);
        }
      }
    };
    
    // 渲染矩阵
    const render = (direction) => {
      // 获取dom绝对纵轴位置
      const getOffsetTop = (dom) => {
        return dom.offsetParent
          ? dom.offsetTop + getOffsetTop(dom.offsetParent)
          : dom.offsetTop;
      };
      // 获取dom绝对横轴位置
      const getOffsetLeft = (dom) => {
        return dom.offsetParent
          ? dom.offsetLeft + getOffsetLeft(dom.offsetParent)
          : dom.offsetLeft;
      };
      // 动画效果
      for (const [col, row, i, j] of animationList) {
        const start = contentList[col * info.rows + row]?.firstChild;
        const end = contentList[i * info.rows + j]?.firstChild;
        start.style.transition = "all 0.1s ease-out";
        if (["up", "down"].includes(direction)) {
          start.style.transform = `translateY(${
            getOffsetTop(end) - getOffsetTop(start)
          }px)`;
        } else {
          start.style.transform = `translateX(${
            getOffsetLeft(end) - getOffsetLeft(start)
          }px)`;
        }
      }
      setTimeout(() => {
        animationList.splice(0);
        for (let i = 0; i < info.cols; i++) {
          const arr = matrix[i];
          for (let j = 0; j < info.rows; j++) {
            const num = arr[j];
            const index = i * info.rows + j;
            const dom = contentList[index].firstChild;
            dom.style.transition = "none";
            dom.style.transform = "none";
            if (!info[num]) info[num] = getColor();
            dom.style.backgroundColor = info[num];
            dom.firstChild.innerHTML = num === 0 ? "" : num;
            calFontSize(contentList[index], dom);
          }
        }
      }, 100);
      scoreBox.innerHTML = `分数:${info.score}`;
      slideBox.innerHTML = `滑动:${info.slide}次`;
      backNumBox.innerHTML = `可撤回次数:${info.backNum}次`
      knockNumBox.innerHTML = `可敲击次数:${info.knockNum}次`
    };
  5. 判断手势滑动方向
    
    // 判断方向
    const checkedDirection = (startX, startY, endX, endY) => {
      const absX = Math.abs(startX - endX);
      const absY = Math.abs(startY - endY);
      if (absX > absY) {
        // 左右
        if (startX > endX) {
          return "left";
        } else {
          return "right";
        }
      } else {
        // 上下
        if (startY > endY) {
          return "up";
        } else {
          return "down";
        }
      }
    };
  6. 重点来了,游戏算法核心逻辑,对二位数组进行上下左右移动。
    
    // 移动
    const move = (direction) => {
      // 是否在范围内
      const isRange = (col, row) => matrix[col] && matrix[col][row] != undefined;
      // 各个方向map
      const next = {
        up: (col, row) => [col + 1, row],
        down: (col, row) => [col - 1, row],
        left: (col, row) => [col, row + 1],
        right: (col, row) => [col, row - 1],
      };
      // 获取下一个非0的值
      const getNextNonZeroValue = (col, row) => {
        let [nextX, nextY] = next[direction](col, row);
        while (isRange(nextX, nextY)) {
          const nextValue = matrix[nextX][nextY];
          if (nextValue) {
            return [nextX, nextY, nextValue];
          } else {
            [nextX, nextY] = next[direction](nextX, nextY);
          }
        }
      };
      let changeFlag;
      // 计算坐标值
      const cal = (i, j) => {
        if (!isRange(i, j)) return;
        // 计算当前坐标的值
        const result = getNextNonZeroValue(i, j);
        if (!result) return;
        const [nextX, nextY, nextValue] = result;
        if (matrix[i][j] === 0) {
          changeFlag = true;
          matrix[i][j] = nextValue;
          matrix[nextX][nextY] = 0;
          animationList.push([nextX, nextY, i, j]);
          cal(i, j);
        } else if (matrix[i][j] === nextValue) {
          animationList.push([nextX, nextY, i, j]);
          changeFlag = true;
          matrix[i][j] *= 2;
          info.score += matrix[i][j];
          matrix[nextX][nextY] = 0;
        }
        // 计算下一个坐标的值
        cal(...next[direction](i, j));
      };
      if (direction === "up") {
        for (let i = 0; i < info.rows; i++) {
          cal(0, i);
        }
      } else if (direction === "down") {
        for (let i = 0; i < info.rows; i++) {
          cal(info.cols - 1, i);
        }
      } else if (direction === "left") {
        for (let i = 0; i < info.cols; i++) {
          cal(i, 0);
        }
      } else if (direction === "right") {
        for (let i = 0; i < info.cols; i++) {
          cal(i, info.rows - 1);
        }
      }
      if (changeFlag) {
        info.slide++;
        for (let i = 0; i < info.slideGenerateNum; i++) {
          generateNum()
        }
        render(direction);
        scoreList.push(info.score)
        historyList.push([...matrix.map(m => [...m])])
        setTimeout(checkedGameOver, 300);
      }
    };
  7. 做手势事件监听,以及撤回,敲击按钮事件绑定,最后调用init(),开始游戏!!
    let startX, startY;
    // 滑动事件绑定
    gameBox.addEventListener("touchstart", (e) => {
      e.preventDefault()
      const { changedTouches: [{ clientX, clientY }] } = e
      startX = clientX;
      startY = clientY;
    });
    gameBox.addEventListener("touchend", (e) => {
      e.preventDefault()
      const { changedTouches: [{ clientX, clientY }] } = e
      if (knockFlag) {
        const x = Math.abs(startX - clientX)
        const y = Math.abs(startY - clientY)
        if (x < 10 && y < 10) {
          // 获取自定义属性索引
          const getIndex = (dom) => {
            return dom.dataIndex ?? getIndex(dom.parentNode)
          }
          const index = getIndex(e.target)
          const col = Math.floor(Number(index) / info.rows)
          const row = Number(index) % info.rows
          // 消除块儿
          matrix[col][row] = 0
          // 可敲击次数-1
          info.knockNum--
          knockFlag = false
          tipBox.style.display = "none"
          render()
        }
      } else {
        const direction = checkedDirection(startX, startY, clientX, clientY);
        move(direction);
      }
    });
    // 撤回
    back.onclick = () => {
      if (historyList.length === 1 || !info.backNum) return
      // 删除当前矩阵
      historyList.pop()
      // 上一次矩阵
      const back = historyList[historyList.length - 1]
      // 回退到上一次矩阵
      matrix = [...back.map(m => [...m])]
      // 删除当前分数
      scoreList.pop()
      // 回退到上一次分数
      info.score = scoreList[scoreList.length - 1]
      // 滑动次数-1
      info.slide--
      // 可撤回次数-1
      info.backNum--
      render()
    }
    // 敲击
    knock.onclick = () => {
      knockFlag = true
      tipBox.innerHTML = "请选择需要敲碎的块儿"
      tipBox.style.display = "block"
    }
    init();

html骨架如下

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="view" content="width=device-width, initial-scale=1.0" />
    <link rel="stylesheet" href="./index.css" />
    <title>2048</title>
  </head>
  <body>
    <noscript>
      很抱歉!当前浏览器没有启用Javascript脚本,请先开启Javascript脚本
    </noscript>
    <div class="score"></div>
    <div class="slide"></div>
    <div class="backNum"></div>
    <div class="knockNum"></div>
    <div class="tip"></div>
    <div id="game"></div>
    <div class="operation">
      <button class="operation-item back">撤回</button>
      <button class="operation-item knock">敲击</button>
    </div>
    <script src="./index.js"></script>
  </body>
</html>

样式如下

* {
  padding: 0;
  margin: 0;
  touch-action: pan-y;
}
body {
  font-size: 3em;
}
.score {
  text-align: center;
}
.slide {
  text-align: center;
}
.backNum {
  text-align: center;
}
.knockNum {
  text-align: center;
}
.tip {
  margin-top: 32px;
  text-align: center;
}
.operation {
  height: 10vw;
  position: fixed;
  bottom: 100px;
  left: 50%;
  transform: translateX(-50%);
  display: flex;
  align-items: center;
}
.operation-item {
  flex: 1;
  margin: 0 8px;
  height: 10vw;
  font-size: 1em;
}
#game {
  position: fixed;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  border-radius: 16px;
  display: flex;
  flex-wrap: wrap;
  box-sizing: border-box;
}
.cell {
  position: absolute;
  width: 100%;
  height: 100%;
  box-sizing: border-box;
  display: flex;
  justify-content: center;
  align-items: center;
}
.matrix {
  display: flex;
  justify-content: center;
  align-items: center;
  width: 100%;
  height: 100%;
  color: #ffffff;
  border-radius: 8px;
  font-size: 3.2em;
  box-sizing: border-box;
}
.animation {
  animation: cell 1s;
}
@keyframes cell {
  0% {
    transform: scale(0.8);
  }
  /*50% {
    transform: scale(1.1);
  }*/
  100% {
    transform: scale(1);
  }
}

有不足的地方或者优化建议欢迎指出来纠正,谢谢观赏

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

qw.Dong

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值