Vue2模拟俄罗斯方块小游戏

目录

一、效果展示

二、代码分享

三、原理分析

3.1、界面搭建

3.2、方块创建

3.3、方块旋转

3.4、方块移动

3.5、移动判断

3.6、下落判断与清除

3.7、得分计算

一、效果展示

二、代码分享

<template>
  <div class="game">
    <div class="game-div">
      <div class="game-min">
        <div class="row" v-for="(row, i) in frame" :key="i">
          <p
            class="element"
            v-for="(col, j) in row"
            :key="j"
            :style="{ background: col.bg }"
          ></p>
        </div>
      </div>
      <!-- 小屏幕 -->
      <div class="right-div">
        <div class="ass">
          <div class="row" v-for="(row, i) in ass" :key="i">
            <p
              class="element"
              v-for="(col, j) in row"
              :key="j"
              :style="{ background: col.bg }"
            ></p>
          </div>
        </div>
        <!-- 分数计算 -->
        <div class="score-div">
          <div>
            <p>
              <span>得分:</span>&emsp;<span style="color: red">{
   
   {
                score
              }}</span>
            </p>
          </div>
          <div>
            <p>
              <span>等级:</span>&emsp;<span style="color: red">{
   
   {
                level
              }}</span>
            </p>
          </div>
          <div>
            <p>
              <span>消除:</span>&emsp;<span style="color: red">{
   
   {
                times
              }}</span>
            </p>
          </div>
          <div class="ztks" @click="stopGame">暂停/开始</div>
        </div>
      </div>
    </div>
    <!-- 控制台 -->
    <div class="control">
      <p @click="change1">变换</p>
      <div class="control-center">
        <div @click="moveLeft">向左</div>
        <div @click="moveRight">向右</div>
      </div>
      <p @click="moveDown">向下</p>
    </div>
  </div>
</template>

<script>
import { color, blockMod, transition } from "@/utils/ykdata.js";
export default {
  data() {
    return {
      row: 20, //行
      col: 10, //列
      frame: [], //界面
      ass: [], //副屏幕
      bg: "#eee",
      block: [], //基本方块集合
      now: { b: 0, c: 0 }, //当前方块以及其旋转角度
      next: { b: 0, c: 0 }, //下一个方块以及其旋转角度
      nowBlock: [], //当前形状数据
      nextBlock: [], //下一个形状数据
      xz: 0, //当前旋转角度
      timer: "", //自动下落
      speed: 800, //速度
      score: 0, //得分
      level: 1, //等级
      times: 0, //消除次数
      stop: true, //是否停止
      removeRow: [], //消除的行记录
    };
  },
  mounted() {
    this.gameFrame();
    this.getBlock(0);
    this.getNext();
    this.init();
  },
  methods: {
    // 游戏框架
    gameFrame() {
      //主屏幕
      for (let i = 0; i < this.row; i++) {
        let a = [];
        for (let j = 0; j < this.col; j++) {
          let b = {
            data: 0,
            bg: this.bg,
          };
          a.push(b);
        }
        this.frame.push(a);
      }
      //副屏幕
      for (let i = 0; i < 4; i++) {
        let a = [];
        for (let j = 0; j < 4; j++) {
          let b = {
            data: 0,
            bg: this.bg,
          };
          a.push(b);
        }
        this.ass.push(a);
      }
      // 模拟格子被占用
      // this.frame[4][4].bg = "#00aaee";
      // this.frame[4][4].data = 1;
    },
    // 渲染方块
    getBlock(e) {
      this.block = blockMod(color[e]);
    },
    // 下一个形状
    async getNext() {
      // 随机获取形状
      this.next.b = Math.floor(Math.random() * this.block.length);
      this.next.c = Math.floor(Math.random() * 4);
    },
    // 渲染当前形状
    init() {
      // 获取到下一个形状数据
      this.now = JSON.parse(JSON.stringify(this.next));
      this.xz = this.now.c;
      // 当前形状数据
      this.nowBlock = JSON.parse(JSON.stringify(this.block[this.now.b]));
      //   渲染形状数据
      //   let c = this.nowBlock.site;
      //   for (let i = 0; i < c.length; i += 2) {
      //     this.frame[c[i]][c[i + 1]].bg = this.nowBlock.color;
      //   }
      this.renderBlock(this.nowBlock, this.frame, 1);
      // 旋转
      if (this.now.c > 0) {
        for (let i = 0; i < this.now.c; i++) {
          this.change(this.nowBlock, this.frame, this.now, i);
        }
      }
      this.getNext().then(() => {
        if (this.nextBlock.site) {
          this.renderBlock(this.nextBlock, this.ass, 0);
        }
        //   下一个形状
        this.nextBlock = JSON.parse(JSON.stringify(this.block[this.next.b]));
        this.renderBlock(this.nextBlock, this.ass, 1);
        // 旋转
        if (this.next.c > 0) {
          for (let i = 0; i < this.next.c; i++) {
            this.change(this.nextBlock, this.ass, this.next, i);
          }
        }
      });
    },
    // 渲染形状
    // b:方块,d:位置,n:0擦除,1生成,2确定落到最下层
    renderBlock(b, d, n) {
      let c = b.site;
      if (n == 0) {
        for (let i = 0; i < c.length; i += 2) {
          d[c[i]][c[i + 1]].bg = this.bg; //0擦除
        }
      } else if (n == 1) {
        for (let i = 0; i < c.length; i += 2) {
          d[c[i]][c[i + 1]].bg = b.color; //1生成
        }
      } else if (n == 2) {
        for (let i = 0; i < c.length; i += 2) {
          d[c[i]][c[i + 1]].data = 1; //2确定落到最下层
        }
      }
    },
    // 旋转 b:当前方块   d:渲染的位置   z:渲染的对象现在还是下一个  xz:当前旋转角度
    change(b, d, z, xz) {
      this.renderBlock(b, d, 0); //先清空再旋转
      // 记录第一块位置
      const x = b.site[0];
      const y = b.site[1];
      for (let i = 0; i < b.site.length; i += 2) {
        let a = b.site[i];
        b.site[i] = b.site[i + 1] - y + x + transition[z.b][xz].x;
        b.site[i + 1] = -(a - x) + y + transition[z.b][xz].y;
      }
      xz++;
      if (xz == 4) {
        xz = 0;
      }
      this.renderBlock(b, d, 1);
    },
    // 向下移动
    moveDown() {
      if (this.canMove(3)) {
        // 先清空
        this.renderBlock(this.nowBlock, this.frame, 0);
        for (let i = 0; i < this.nowBlock.site.length; i += 2) {
          // 向下移动一位
          this.nowBlock.site[i]++;
        }
        this.renderBlock(this.nowBlock, this.frame, 1); //再渲染数据
      } else {
        // 已经不能下落了
        this.renderBlock(this.nowBlock, this.frame, 2);
        // 判断是否可以消除
        this.removeBlock();
      }
    },
    // 向左移动
    moveLeft() {
      if (this.canMove(2)) {
        // 先清空
        this.renderBlock(this.nowBlock, this.frame, 0);
        for (let i = 0; i < this.nowBlock.site.length; i += 2) {
          // 向左移动一位
          this.nowBlock.site[i + 1]--;
        }
        this.renderBlock(this.nowBlock, this.frame, 1); //再渲染数据
      }
    },
    // 向右移动
    moveRight() {
      if (this.canMove(1)) {
        this.renderBlock(this.nowBlock, this.frame, 0);
        for (let i = 0; i < this.nowBlock.site.length; i += 2) {
          this.nowBlock.site[i + 1]++;
        }
        this.renderBlock(this.nowBlock, this.frame, 1); //再渲染数据
      }
    },
    // 是否可移动判断
    // 预判能否移动或变化   e:1右移  2左移  3下移  4变化
    canMove(e) {
      let c = this.nowBlock.site;
      let d = 0;
      switch (e) {
        case 1:
          for (let i = 0; i < c.length; i += 2) {
            if (c[i + 1] >= this.col - 1) {
              return false;
            }
            // 判断下一个位置是否被占用
            d += this.frame[c[i]][c[i + 1] + 1].data;
          }
          if (d > 0) {
            return false;
          }
          break;
        case 2:
          for (let i = 0; i < c.length; i += 2) {
            if (c[i + 1] <= 0) {
              return false;
            }
            // 判断下一个位置是否被占用
            d += this.frame[c[i]][c[i + 1] - 1].data;
          }
          if (d > 0) {
            return false;
          }
          break;
        case 3:
          for (let i = 0; i < c.length; i += 2) {
            if (c[i] >= this.row - 1) {
              return false;
            }
            // 判断下一个位置是否被占用(防穿透)
            d += this.frame[c[i] + 1][c[i + 1]].data;
          }
          if (d > 0) {
            return false;
          }
          break;
        // case 4:
        //   for (let i = 0; i < c.length; i += 2) {
        //     if (c[i + 1] >= this.col - 1) {
        //       return false;
        //     }
        //   }
        //   break;
      }
      return true;
    },
    // 下落时旋转
    // 旋转 b:当前方块  xz:当前旋转角度
    change1() {
      const b = JSON.parse(JSON.stringify(this.nowBlock));
      // 记录第一块位置
      const x = b.site[0];
      const y = b.site[1];
      let n = true;
      for (let i = 0; i < b.site.length; i += 2) {
        let a = b.site[i];
        b.site[i] = b.site[i + 1] - y + x + transition[this.now.b][this.xz].x;
        b.site[i + 1] = -(a - x) + y + transition[this.now.b][this.xz].y;
        // 判断旋转后该点是否合理
        if (
          b.site[i + 1] < 0 ||
          b.site[i + 1] >= this.col ||
          b.site[i] >= this.row ||
          this.frame[b.site[i]][b.site[i + 1]].data > 0
        ) {
          n = false;
        }
      }
      // 符合条件
      if (n) {
        this.renderBlock(this.nowBlock, this.frame, 0); //先清空
        this.xz++;
        if (this.xz == 4) {
          this.xz = 0;
        }
        this.nowBlock = b;
        this.renderBlock(this.nowBlock, this.frame, 1); //再旋转
      }
    },
    // 到底部:确定位置不能再动,保证上面其他方块下落时不会将它穿透
    // 自动下落
    autoMoveDown() {
      this.timer = setInterval(() => {
        this.moveDown();
      }, this.speed);
    },
    // 开始与暂停
    stopGame() {
      this.stop = !this.stop;
      if (this.stop) {
        clearInterval(this.timer);
      } else {
        this.autoMoveDown();
      }
    },
    // 判断是否可以消除
    removeBlock() {
      // 遍历整个界面
      for (let i = 0; i < this.row; i++) {
        let a = 0;
        for (let j = 0; j < this.col; j++) {
          if (this.frame[i][j].data == 1) {
            a++;
          }
        }
        if (a == this.col) {
          // 说明该行已经占满可以消除
          this.removeRow.push(i);
        }
      }
      // 获取是否可以消除行
      if (this.removeRow.length > 0) {
        let l = this.removeRow;
        for (let i = 0; i < l.length; i++) {
          let j = 0;
          let timer = setInterval(() => {
            this.frame[l[i]][j] = { bg: this.bg, data: 0 };
            j++;
            if (j == this.col) {
              clearInterval(timer);
            }
          }, 20);
        }
        setTimeout(() => {
          // 上面方块下移,从下往上判断
          for (let i = this.row - 1; i >= 0; i--) {
            let a = 0;
            for (let j = 0; j < l.length; j++) {
              if (l[j] > i) {
                a++;
              }
            }
            if (a > 0) {
              for (let k = 0; k < this.col; k++) {
                if (this.frame[i][k].data == 1) {
                  // 先向下移动
                  this.frame[i + a][k] = this.frame[i][k];
                  // 再清除当前
                  this.frame[i][k] = { bg: this.bg, data: 0 };
                }
              }
            }
          }
          this.removeRow = []; //清除行记录
          // 生成下一个
          this.init();
        }, 20 * this.col);
        // 数据处理
        // 消除次数+1
        this.times++;
        // 等级向下取整+1
        let lev =  Math.floor(this.times / 10) + 1;
        if (lev > this.level) {
          this.level = lev;
          // 速度
          if (this.level < 21) {
            // 20级以内做减法
            this.speed = 800 - (this.level - 1) * 40;
          } else {
            this.speed = 30;
          }
          // 清除当前下落
          clearInterval(this.timer);
          // 加速
          this.autoMoveDown();
        }
        this.level = this.times;
        // 分数消除一行100,两行300,三行600,四行1000
        this.score += ((l.length * (l.length + 1)) / 2) * 100 * this.level;
      } else {
        this.init();
      }
    },
  },
};
</script>

<style lang='less' scoped>
.game {
  .game-div {
    display: flex;
    .game-min {
      .row {
        display: flex;
        padding-top: 2px;
        .element {
          width: 20px;
          height: 20px;
          padding: 0;
          margin: 0 2px 0 0;
        }
      }
    }
    .right-div {
      padding-left: 20px;
      .ass {
        .row {
          display: flex;
          padding-top: 2px;
          .element {
            width: 20px;
            height: 20px;
            padding: 0;
            margin: 0 2px 0 0;
          }
        }
      }
      .score-div {
        div {
          height: 20px;
          line-height: 20px;
        }
        .ztks {
          width: 100px;
          height: 40px;
          margin-bottom: 10px;
          background-color: palevioletred;
          text-align: center;
          line-height: 40px;
        }
      }
    }
  }
  .control {
    width: 220px;
    p {
      width: 220px;
      height: 40px;
      text-align: center;
      line-height: 40px;
      background-color: #B940EF;
      margin-bottom: 20px;
    }
    .control-center {
      align-items: center;
      display: flex;
      justify-content: space-between;
      margin-bottom: 20px;
      div {
        width: 90px;
        height: 40px;
        text-align: center;
        line-height: 40px;
        background-color: #B940EF;
      }
    }
  }
}
</style>

 工具函数:

//渐变色
export const color = [
  [
    'linear-gradient(180deg, #FFA7EB 0%, #F026A8 100%)',
    'linear-gradient(180deg, #DFA1FF 0%, #9A36F0 100%)',
    'linear-gradient(180deg, #9EAAFF 0%, #3846F4 100%)',
    'linear-gradient(180deg, #7BE7FF 2%, #1E85E2 100%)',
    'linear-gradient(180deg, #89FED8 0%, #18C997 100%)',
    'linear-gradient(180deg, #FFED48 0%, #FD9E16 100%)',
    'linear-gradient(180deg, #FFBA8D 1%, #EB6423 100%)',
  ],
  [
    '#2B7AF5','#2B9DF5','#79CFFF','#1B67DD','#4F94FF','#2180F2','#3FD0FF',
  ],
];

//7种方块元素
export const blockMod = (color) => {
  let a = {
    site: [0, 1, 0, 2, 1, 2, 2, 2],
    color: color[0],
  };
  let b = {
    site: [0, 1, 1, 1, 1, 2, 2, 2],
    color: color[1],
  };
  let c = {
    site: [1, 1, 1, 2, 2, 1, 2, 2],
    color: color[2],
  };
  let d = {
    site: [1, 0, 1, 1, 1, 2, 1, 3],
    color: color[3],
  };
  let e = {
    site: [0, 2, 1, 1, 1, 2, 2, 1],
    color: color[4],
  };
  let f = {
    site: [0, 1, 0, 2, 1, 1, 2, 1],
    color: color[5],
  };
  let g = {
    site: [1, 1, 2, 0, 2, 1, 2, 2],
    color: color[6],
  };
  return ([a, b, c, d, e, f, g]);
};

//旋转规则
export const transition = [
  [
    {
      x: 1, y: 1,
    }, {
      x: 1, y: 0,
    }, {
      x: 0, y: -2,
    }, {
      x: -2, y: 1,
    }
  ],
  [
    {
      x: 1, y: 1,
    }, {
      x: 1, y: 0,
    }, {
      x: 0, y: -2,
    }, {
      x: -2, y: 1,
    }
  ],
  [
    {
      x: 0, y: 1,
    }, {
      x: 1, y: 0,
    }, {
      x: 0, y: -1,
    }, {
      x: -1, y: 0,
    }
  ],
  [
    {
      x: -1, y: 2,
    }, {
      x: 1, y: 1,
    }, {
      x: 2, y: -1,
    }, {
      x: -2, y: -2,
    }
  ],
  [
    {
      x: 2, y: 0,
    }, {
      x: 0, y: -1,
    }, {
      x: -1, y: -1,
    }, {
      x: -1, y: 2,
    }
  ],
  [
    {
      x: 1, y: 1,
    }, {
      x: 1, y: 0,
    }, {
      x: 0, y: -2,
    }, {
      x: -2, y: 1,
    }
  ],
  [
    {
      x: 0, y: 0,
    }, {
      x: 1, y: 0,
    }, {
      x: -1, y: 0,
    }, {
      x: 0, y: 0,
    }
  ],
]

三、原理分析

3.1、界面搭建

  主界面的20X10,类似贪吃蛇,副界面的随机方块,则是4x4,都是双重for循环。初始化的时候调用gameFrame()即可。

3.2、方块创建

  主要说明一下随机生成的方块,每个都是有4个小方格组成,组成了7种基本样式,在自身基础上进行四个方向的旋转,就是下落的所有可能性。参考坐标如右图所示:

3.3、方块旋转

  旋转 b:当前方块   d:渲染的位置   z:渲染的对象现在还是下一个  xz:当前旋转角度,在change(b, d, z, xz)里进行旋转,这里用到了工具函数里的transition,旋转核心就是找到一个固定的点,看出x和Y坐标的变化,即(x=y,y=-x),加上工具函数里的blockMod,就可以依次生成对应的下落方块。

3.4、方块移动

  在生成一个的同时,要考虑的下一个方块的生成与随机旋转,所以 b:方块,d:位置,n:0擦除,1生成,2确定落到最下层,在renderBlock(b, d, n)方法里进行形状的渲染。

  我们有三个方向:moveDown()、moveLeft()、moveRight(),这里的原理和贪吃蛇基本类似,不过在向下移动时考虑因素较多,下落时旋转要考虑 b:当前方块  xz:当前旋转角度change1()方法,为了保持原来的形状,所以多处用到了深拷贝。

3.5、移动判断

  预判能否移动或变化   e:1右移  2左移  3下移  4变化,在 canMove(e)方法里实现。主要是判断下一个位置是否被占用,到了底部:确定位置不能再动,保证上面其他方块下落时不会将它穿透,是要考虑的问题。

3.6、下落判断与清除

  判断是否可以消除:遍历整个界面,当一行被占满,将该行的方块依次删去,并保留上面附着的方块位置不变,形成“被吞噬”的现象。在消掉满了的一行时,要让上面方块下移,从下往上判断,再清除当前方块,避免冲突。

3.7、得分计算


  分数消除一行100,两行300,三行600,四行1000,消掉一行,带分数增加,以此同时,随着等级的增加,速度也会越来越快,难度增加。

猜你喜欢

转载自blog.csdn.net/qq_44930306/article/details/131132997