小游戏管理平台-2048

306 阅读4分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第五天,点击查看活动详情

线上地址:mygame.codeape.site

源码:gitee.com/wooden-join…

小游戏管理平台:juejin.cn/post/714916…

开发技术

vue2 + js + canvas

需求

方向键上下左右可以控制数字往一个方向移动,当数字两两相同可以进行合并,例:2+2=4。当棋盘内无法继续移动时,则游戏结束。

代码讲解

初始化数据

获取 canvas 上下文,定义棋盘内盒子的宽高(4*4),紧接着为棋盘定义一个二维数组并赋值为0,然后是创建出两个随机数字,然后是绘画界面和绘画出这两个数字。最后监听键盘事件。

init() {
  setTimeout(() => {
    this.canvas = this.$refs.canvas;
    this.ctx = this.canvas.getContext("2d");
    // 获取画布内盒子的大小和间距
    this.boxWidth = (this.canvas.width * 0.8) / 4;
    this.boxMargin = (this.canvas.width * 0.2) / 5;
    // 初始化二维数组并赋值为 0
    for (let i = 0; i < 4; i++) {
      this.boxArr.push([]);
      for (let j = 0; j < 4; j++) {
        this.boxArr[i][j] = 0;
      }
    }
    this.createRandom();
    this.createRandom();
    this.drawInterface();
    this.drawText();
    this.onKeyDown();
  }, 100);
},

创建数字2

在4*4方格的随机位置创建一个数字2,给x和y一个0-3的随机整数即可,然后如果创建出来的位置没有值,则代表该位置可以创建数字2,否则递归调用即可。

createRandom() {
  let x = Math.round(Math.random() * 3);
  let y = Math.round(Math.random() * 3);
  this.boxArr[x][y] == 0
    ? ((this.boxArr[x][y] = 2), (this.score += 2))
    : this.createRandom();
},

画界面

canvas 画板中填充上一层背景色,紧接着遍历数组,计算出每个需要绘画的格子的位置和对于的颜色,紧接着级就可以绘画格子了。

drawInterface() {
  this.ctx.beginPath();
  this.ctx.fillStyle = "#576eb9";
  this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
  let color, x, y;
  // 遍历数组 辨认数组的当前值 并赋上颜色
  for (let i = 0; i < 4; i++) {
    for (let j = 0; j < 4; j++) {
      color = this.boxColor[this.boxArr[i][j]];
      x = this.boxMargin + j * (this.boxWidth + this.boxMargin);
      y = this.boxMargin + i * (this.boxWidth + this.boxMargin);
      this.drawRect(x, y, color);
    }
  }
},

画格子

绘画格子,我这里画的不是一个简单的正方形格子,而是一个带圆角的格子;我这里用到了路径的画法来实现。首先是创建一个路径 beginPath,然后确定一个初始的绘制点 moveTo,最后就是四个角的绘制,通过 arcTo 来绘制4个弧度,然后这一个路径就完成了,即完成了一个带圆角的矩形。

drawRect(x, y, color) {
  this.ctx.beginPath(); // 1.创建路径
  this.ctx.fillStyle = color;
  this.ctx.moveTo(x, y); // 2.移动绘制点
  let w = this.boxWidth;
  let m = this.boxMargin;
  // 这是画一个矩形的四个圆角
  // arcTo(起点x, y, 终点x, y, 半径) 分别是 右上 右下 左下 左上
  this.ctx.arcTo(x + w, y, x + w, y + 1, m * 0.7);
  this.ctx.arcTo(x + w, y + w, x + w - 1, y + w, m * 0.7);
  this.ctx.arcTo(x, y + w, x, y + w - 1, m * 0.7);
  this.ctx.arcTo(x, y, x + 1, y, m * 0.7);
  this.ctx.fill();
},

画文字

遍历每个格子,根据里面的值来绘画对应的数字,计算出该文字应在方格中的位置,然后通过 fillText 方法来给画布的该位置绘制一个数字即可。

drawText() {
  let x, y;
  for (let i = 0; i < 4; i++) {
    for (let j = 0; j < 4; j++) {
      if (this.boxArr[i][j] > 0) {
        this.ctx.beginPath();
        this.ctx.textAlign = "center";
        this.ctx.textBaseline = "middle";
        this.ctx.fillStyle = "#3a5075";
        this.ctx.font = "40px Arial";
        x =
          this.boxMargin +
          j * (this.boxWidth + this.boxMargin) +
          this.boxWidth / 2;
        y =
          this.boxMargin +
          i * (this.boxWidth + this.boxMargin) +
          this.boxWidth / 2;
        // 在画布上对应的位置写上二维数组中的值
        this.ctx.fillText(this.boxArr[i][j], x, y);
      }
    }
  }
},

监听用户键盘事件

onKeyDown() {
  document.onkeydown = (e) => {
    switch (e.code) {
      case "ArrowLeft":
        this.accordingKey("left");
        break;
      case "ArrowUp":
        this.accordingKey("up");
        break;
      case "ArrowRight":
        this.accordingKey("right");
        break;
      case "ArrowDown":
        this.accordingKey("down");
        break;
    }
  };
},

代码中的复杂点

根据按键,处理方格中的数字

这里当时水平有限,我记得处理了很久😂。

首先是一个判断,防止一直重复按一个方向,我这里的最大次数限制为5;

然后就是循环4次,代表当前方向上的的4行格子,再遍历每一行的4列格子,根据上下左右的操作来区分出对数组索引的操作。

这里需要调用 checkSorted 判断是否已经排列好了,没排列好就调用 moveBoxArr 进行排列,如果排列好了,即可进行给当前的方格二维数组赋值。

紧接着就是调用 isGameOver 判断游戏是否结束,未结束就清空画板,再创建一个数字2,再进行界面和数字的绘画。

accordingKey(key) {
  this.$store.commit("setCurrentKey", key);
  // 处理按键,防止一直按一个键位
  if (this.currentKey.key == key) {
    this.currentKey.num++;
  } else {
    this.currentKey.key = key;
    this.currentKey.num = 0;
  }
  if (this.currentKey.num > 5) {
    this.$message({
      showClose: true,
      message: "该方向按了很多次了,试试别的方向吧",
      duration: "1500",
      offset: 10,
    });
  } else {
    for (let i = 0; i < 4; i++) {
      var arr = new Array();
      let x, y;
      key == "left" || key == "right" ? (x = i) : "";
      key == "up" || key == "down" ? (y = i) : "";
      // 由于是向左操作,将每行的数字都给一个数组,用于操作判断
      for (let j = 0; j < 4; j++) {
        key == "left" || key == "right" ? (y = j) : "";
        key == "up" || key == "down" ? (x = j) : "";
        arr[j] = this.boxArr[x][y];
      }
      // 如果点击的是右或下就 将数组反序排列
      key == "right" || key == "down" ? arr.reverse() : "";
      // 数字还没操作好就去操作
      if (!this.checkSorted(arr)) arr = this.moveBoxArr(arr);
      key == "right" || key == "down" ? arr.reverse() : "";
      // 重新赋值
      for (let j = 0; j < 4; j++) {
        key == "left" || key == "right" ? (y = j) : "";
        key == "up" || key == "down" ? (x = j) : "";
        this.boxArr[x][y] = arr[j];
      }
    }
    if (this.isGameOver() == 1) {
      this.$message({
        showClose: true,
        message: "该方向已经无法移动了,试试别的方向吧",
        duration: "1500",
        // Message 距离窗口顶部的偏移量
        offset: 10,
      });
    } else if (this.isGameOver() == 0) {
      this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
      this.createRandom();
      this.drawInterface();
      this.drawText();
    } else {
      this.gameOver = true;
    }
  }
},

checkSorted 判断当前行或列是否已经排列好

我这里用了个笨方法,就是根据当前传入的数组来判断该列是否排列好,一列一列(or一行一行)的比较值,然后先处理0的情况,当该列(or行)存在0时,其余条件是否满足。

紧接着就是判断相邻的数字是否相等,相等则代表还未排列好。

这里当时好像借鉴了网上的一些代码,由于时间是一年多之前的了,我也忘了是哪找的了。😂

checkSorted() {
  return (arr) => {
    let flag = false;
    // 先处理 0 的情况
    if (
      (arr[0] == 0 && arr[1] == 0 && arr[2] == 0 && arr[3] == 0) ||
      (arr[0] > 0 && arr[1] == 0 && arr[2] == 0 && arr[3] == 0) ||
      (arr[0] > 0 && arr[1] > 0 && arr[2] == 0 && arr[3] == 0) ||
      (arr[0] > 0 && arr[1] > 0 && arr[2] > 0 && arr[3] == 0) ||
      (arr[0] > 0 && arr[1] > 0 && arr[2] > 0 && arr[3] > 0)
    ) {
      flag = true;
    }
    // 如果有一个相邻数字是相等的就是还没排列好
    if (
      (arr[0] == arr[1] && arr[0] != 0) ||
      (arr[1] == arr[2] && arr[1] != 0) ||
      (arr[2] == arr[3] && arr[2] != 0)
    ) {
      flag = false;
    }
    return flag;
  };
},

moveBoxArr 移动数字并合并

根据传入的数组,遍历当前的列(or行),如果该值为0,则需要和另一个值交换位置(即可实现值移动了的效果)。

如果相邻的两个数字相同,就让后一个的值加到前一个值(即可实现合并的效果)。

最后还需判断当前的移动是否已经排列好值了,如果没排列好就继续将当前数组传入递归调用,排列好了就返回数组。

moveBoxArr(arr) {
  for (let i = 0; i < 3; i++) {
    // 0和其他数字交换位置
    if (arr[i] == 0) {
      let temp = arr[i];
      arr[i] = arr[i + 1];
      arr[i + 1] = temp;
    }
    // 如果两个数字相同就相加
    if (arr[i] != 0 && arr[i] == arr[i + 1]) {
      arr[i] = arr[i] + arr[i + 1];
      this.score += arr[i];
      // 合出128就有奖励分
      arr[i] == 128 ? (this.reward += arr[i]) : "";
      arr[i + 1] = 0;
    }
  }
  // 如果当前已经排好序就返回数组,否处继续排序
  if (this.checkSorted(arr)) {
    return arr;
  } else {
    return this.moveBoxArr(arr);
  }
},

isGameOver 判断游戏是否已经结束

首先是判断当前页面是否已经铺满数字了,如果未铺满,则返回0,代表游戏还能继续。

isFullNum() {
  for (let i = 0; i < 4; i++) {
    if (this.boxArr[i].indexOf(0) != -1) {
      return false;
    }
  }
  return true;
},

如果页面已经铺满数字的话,还需判断是否存在相邻的数字还有相等的,就返回1,代表某个方向还能进行移动操作。游戏还未结束。

如果以上条件都不满足则返回2,代表游戏结束。

// 0:数字还没铺满。 1:铺满了但相邻数还有相等的。 2:无法移动了游戏结束。
isGameOver() {
  // 页面铺满数字的前提
  if (this.isFullNum()) {
    for (let i = 0; i < 3; i++) {
      for (let j = 0; j < 3; j++) {
        // 如果相邻的数(每个数的右和下数字)还相等就表示游戏还没结束
        if (
          this.boxArr[i][j] == this.boxArr[i][j + 1] ||
          this.boxArr[i][j] == this.boxArr[i + 1][j]
        ) {
          return 1;
        }
      }
    }
    // 最后一行和最后一列的判断
    for (let i = 0; i < 3; i++) {
      if (
        this.boxArr[3][i] == this.boxArr[3][i + 1] ||
        this.boxArr[i][3] == this.boxArr[i + 1][3]
      ) {
        return 1;
      }
    }
    // 如果以上条件都满足了,游戏就结束了
    return 2;
  }
  return 0;
},

感悟

2048 这一个 canvas 小游戏,我记得当时折磨了我很久,对数字的移动这块我想了很久,后来有个别地方也借鉴了一下网上的方法,由于已经过去一年多了,我也忘了是从哪看的了。最终整个实现下来,对js基本功的提升肯定是很大的,使我对于数组的处理,还有递归的处理也越来越熟悉了。