小游戏管理平台-俄罗斯方块

978 阅读3分钟

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

线上地址:mygame.codeape.site/

源码:gitee.com/wooden-join…

开发技术

vue2 + js

需求

方块自动掉落,键盘下左右,可以控制方块移动,上键用于旋转方块。当方块固定到底部,都需要创建一个新的方块(从几个样式中随机选择一种),空格键暂停/开始游戏。

代码讲解

创建根据模型数据创建对应的方块

当页面初始化时,需要先创建方块, 该函数是每次创建方块都需调用的,所以要先判断游戏是否结束;

当游戏进行中时,需要先记录当前的模型的索引,然后初始化方块的位置数据,然后随机从数据源中深拷贝一份方块数据,再将该数据保存到一个数组中。

createModel() {
  // 判断游戏是否已经结束了
  if (this.isGameOver()) {
    this.gameOver();
    return;
  }
  // 1.当前模型索引加一
  this.currentIndex++;
  // 重新初始化 16宫格的位置
  this.moveX = 0;
  this.moveY = 0;
  // 2.确定当前使用了哪一个模型
  // 先深拷贝数据
  // 随机数 $lodash.random (从 0 到 this.MODELS.length - 1) 之间
  let obj = this.$lodash.cloneDeep(
    this.MODELS[this.$lodash.random(0, this.MODELS.length - 1)]
  );
  // es6 将对象转为数组
  this.currentModel.push(Object.values(obj));

  // 3.设置当前块元素所在的 x, y 坐标
  this.positionModel();

  // 4.让模型自动下落
  this.autoDown();
},

设置方块的位置

方块的html结构

  <div v-for="(item, index) in currentModel">
    <div
      v-for="(item2, index2) in item"
      :class="item[0].isFixed ? 'fixed_model' : 'activity_model'"
    ></div>
  </div>

紧接着需要设置当前块元素所在的 x, y 坐标;

设置该位置前需要先判断是否越界(后面方块的移动都需要调用该函数),根据当前的方块元素找到每个块应在的位置,然后设置它的 top 和 left 值。

positionModel() {
  // 设置位置前先判断是否越界
  this.checkBound();
  this.$nextTick(function () {
    let eles = document.querySelectorAll(".activity_model");
    for (let i = 0; i < eles.length; i++) {
      // 找到每个块元素对应的数据(行、列)
      let blockModel = this.currentModel[this.currentIndex][i];
      // 根据每个块元素对应的数据来指定块元素的位置
      // 每个块元素的位置由两个值确定:
      // 1、16宫格所在的位置 this.moveX。2、块元素在16宫格的位置 blockModel.row
      let top = (this.moveY + blockModel.row) * this.STEP + "px";
      let left = (this.moveX + blockModel.col) * this.STEP + "px";
      // console.log(top)
      // console.log(left)
      this.$set(eles[i].style, "top", top);
      this.$set(eles[i].style, "left", left);
      // this.$set(eles[i].style, "fontSize", "30px")
      // console.log(eles[i].style)
      // }
    }
  });
},

方块自动下落

开启定时器,然后移动方块

autoDown() {
  if (this.mInterval) {
    clearInterval(this.mInterval);
  }
  this.mInterval = setInterval(() => {
    this.move(0, 1);
  }, this.speed);
},

移动的时候需要注意是否和底下已固定的方块发生了触碰,没发生触碰则移动方块,然后调用 positionModel() 函数,使方块移动

move(x, y) {
  if (
    this.isTouch(
      this.moveX + x,
      this.moveY + y,
      this.currentModel[this.currentIndex]
    )
  ) {
    // 底部的触碰发生在移动16宫格的时候
    // 此次移动是因为 y 轴的移动而引起的
    if (y !== 0) {
      // 模型之间底部发生了触碰
      this.fixedBottomModel();
    }
    // 表示要发生触碰
    return;
  }

  // 控制16宫格移动
  this.moveX += x;
  this.moveY += y;
  // 根据16宫格的位置重新定位块元素
  this.positionModel();
},

监听用户的键盘事件

使用 document.onkeydown 方法监听键盘事件,左右下,都是调用 move 函数,上则需要调用 rotate 函数

  • 注:this.$store.commit("setCurrentKey", "left"); 只是我用来设置项目中方向键的样式的。可以不用管。
onKeyDown() {
  document.onkeydown = (e) => {
    switch (e.code) {
      // 左
      case "ArrowLeft":
        this.$store.commit("setCurrentKey", "left");
        this.move(-1, 0);
        break;
      // 上
      case "ArrowUp":
        this.$store.commit("setCurrentKey", "up");
        this.rotate();
        break;
      // 右
      case "ArrowRight":
        this.$store.commit("setCurrentKey", "right");
        this.move(1, 0);
        break;
      // 下
      case "ArrowDown":
        this.$store.commit("setCurrentKey", "down");
        this.checkBound();
        this.move(0, 1);
        break;
      // 空格键
      case "Space":
        this.pause();
        break;
    }
  };
},

方块移动时需要判断是否发生了触碰

参数中的 x,y 表示16宫格(将要/下一步)移动的位置;model 表示当前模型数据源(将要)完成的变化(旋转等)

isTouch(x, y, model) {
  // 判断触碰,就是判断活动中的模型(将要移动到的位置)是否已经存在被固定的(块元素)
  // 如果存在返回 true 表示(将要)移动的位置会发生触碰
  for (let k in model) {
    let blockModel = model[k];
    // 该位置是否已经存在块元素?
    if (this.fixedBlocks[y + blockModel.row + "_" + (x + blockModel.col)]) {
      return true;
    }
  }
  return false;
},

方块发生了触碰

将方块固定到底部,然后改变方块的样式并让其不可继续移动。每个块都需要将位置记录下来。固定的方块存放到 fixedBlocks 变量中。然后需要判断是否需要清空这一行,同时创建新的方块。

  • fixedBlocks = { 行_列: 块元素 }
fixedBottomModel() {
  // 1.改变模型中块元素的样式
  // 2.让模型不可以再进行移动
  this.currentModel[this.currentIndex][0].isFixed = true;

  this.$nextTick(function () {
    // 记录该块元素所在的位置
    let fixedClass = document.querySelectorAll(".fixed_model");
    for (let i = 0; i < this.currentModel[this.currentIndex].length; i++) {
      let blockModel = this.currentModel[this.currentIndex][i];
      // 固定的16宫格内的当前块索引
      let currentI = fixedClass.length - this.currentModel[this.currentIndex].length + i;
      this.fixedBlocks[
        this.moveY + blockModel.row + "_" + (this.moveX + blockModel.col)
      ] = fixedClass[currentI];
    }

    // 判断是否要请空该行
    this.isRemoveLine();

    // 3.创建新的模型
    this.createModel();
  });
},

判断是否要清空这一行

遍历行和列,判断每一行是否已经被铺满了,如果铺满了,则需要清除该行增加得分。

isRemoveLine() {
  // 一行中每列都存在块元素
  // 遍历所有行
  for (let i = 0; i < this.ROW_COUNT; i++) {
    // 标记符: 假设当前行已经被铺满了
    let flag = true;
    // 遍历所有列
    for (let j = 0; j < this.COL_COUNT; j++) {
      // 如果当前行中有一列没有数据,那么就说明当前行没有被铺满
      if (!this.fixedBlocks[i + "_" + j]) {
        flag = false;
        break;
      }
    }
    if (flag) {
      // 该行已经被铺满了
      this.removeLine(i);
      // 得分++
      this.score += 100;
    }
  }
},

清除铺满的那一行

遍历当前行的所有列,并删除每一个小方块,然后清除该行中所有块元素的数据源。然后让被清理行以上的所有块元素往下移动对应的行数。

removeLine(line) {
  this.$nextTick(function () {
    // 获取所有 块元素外面的 16 宫格
    let allNodes = document.querySelector("#container").childNodes;
    // 遍历该行中的所有列
    for (let i = 0; i < this.COL_COUNT; i++) {
      // 1.删除该行中所有的块元素
      allNodes.forEach((item) => {
        for (let j = 0; j < item.children.length; j++) {
          // 如果删除了一个块元素就退出循环
          if (item.children[j] === this.fixedBlocks[line + "_" + i]) {
            item.removeChild(this.fixedBlocks[line + "_" + i]);
            return;
          }
        }
      });

      // 2.删除该行中所有块元素的数据源
      this.fixedBlocks[line + "_" + i] = null;
    }
    this.downLine(line);
  });
},

让被清理行以上的方块下落

遍历被清理的行以上的所有行,如果存在数据则代表有方块,则让其数据源中的行数 +1 代表往下移动,然后操作块元素的 top 值让其下落,再清除旧的数据。

downLine(line) {
  // 遍历被清理行之上的所有行
  for (let i = line - 1; i >= 0; i--) {
    // 该行中的所有列
    for (let j = 0; j < this.COL_COUNT; j++) {
      // 如果没有数据 就跳到下一次循环
      if (!this.fixedBlocks[i + "_" + j]) continue;
      // 存在数据
      // 1.被清理行之上的 所有块元素 的数据源 的行数 + 1
      this.fixedBlocks[i + 1 + "_" + j] = this.fixedBlocks[i + "_" + j];
      // 2.让块元素在容器中的位置下落
      this.fixedBlocks[i + 1 + "_" + j].style.top =
        (i + 1) * this.STEP + "px";
      // 3.清理掉之前的块元素
      this.fixedBlocks[i + "_" + j] = null;
    }
  }
},

游戏结束

当固定的块元素到达顶部分割线区域时,则代表游戏结束。

isGameOver() {
  // 当第 0 行有元素时
  for (let i = 0; i < this.COL_COUNT; i++) {
    if (this.fixedBlocks["3_" + i]) {
      return true;
    }
  }
  return false;
},

以上就是俄罗斯方块中主要用到的函数。

收获

该俄罗斯方块,大概是一年多之前的时候开始写的。当时基础较差,看着b站的一个js版本的视频,然后自己改写的uniapp的vue版本。整个小游戏写下来对js的基本功还是挺有帮助的。像一些需要用到深拷贝的地方和一些属性的添加,就能更清晰的了解 js 的引用类型啊,vue2 的响应式监听等。