信息发布→ 登录 注册 退出

如何利用vue3实现一个俄罗斯方块

发布时间:2026-01-11

点击量:
目录
  • 前言
  • 游戏相关设置
    • 游戏界面设置
    • 存储还在移动的俄罗斯方块信息
    • 存储已经不能移动的俄罗斯方块信息
    • 使用之前在贪吃蛇中使用的颜色渲染工具
  • 让方块移动起来(不考虑切换方块的形态切换)
    • 检测移动的俄罗斯方块二维数组在移动后它的每个坐标点数组是否有超出范围的
    • 检测移动的俄罗斯方块二维数组在移动后它的每个坐标点数组是否会触碰到不可移动的俄罗斯方块上
    • 特殊情况(得分)
    • 移动方法
  • 切换操作书写
    • 设计检测该形状形态的方法
    • 检测长方形形态的方法
    • 检测z形和反z形形态的方法
    • 检测L形、反L形、三角形形态的方法
    • 设计切换方法
    • 切换L形形态(代码过多只解释一个,其余的可以看我仓库代码)
  • 单个方格组件展示
    • 总结

      前言

      之前写了一个贪吃蛇的demo,但是公司最近没业务还得继续摸鱼。所以趁热打铁,在下又去攒了一个俄罗斯方块。代码地址

      游戏相关设置

      游戏界面设置

      构建一个16×25的游戏界面,使用响应式数据存储一个二维数组,该二维数据中存储着每一个有关该方格信息的数组,该数组格式为[从上到下的坐标位置,从左到右的坐标位置, 该方格颜色(0为白色,1为红色,2为绿色)]

      //游戏界面(作为俄罗斯方块在各个格子上渲染颜色的依据)
      let checkerboardInfo = reactive([]);
      
      //设置游戏界面
      const setCheckerboard = () =>{
        for (let i = 0; i < 25; i++) {
          for (let j = 0; j < 16; j++) {
            checkerboardInfo.push([i, j, 0])
          }
        }
      }

      存储还在移动的俄罗斯方块信息

      可移动的俄罗斯方块一共分为7种:正方形、长方形、L形、反L形、z形、反z形、三角形,使用随机数选取每个可移动的俄罗斯方块,使用二维数组存储可移动的俄罗斯方块四个坐标点,并确保其从游戏界面中间落下(实力有限,直接将七种形态用代码穷举了(●'◡'●))

      /存储当前还在坠落的方格坐标
      let moveSquareCoordinate = null;
      
      /方块类型(长方形、三角形、正方形,z字型, 反z字型,l型, 反l型)
      let squareType = [0, 1, 2, 3, 4, 5, 6];
      
      //方块类型下标
      let squareTypeIndex = -1;
      
      //随机选取方块
      const randomSquareType = ()=>{
        squareTypeIndex = Math.floor(Math.random()*(7));
      };
      
      //构造方块
      const setSquare = () =>{
        randomSquareType();
        switch(squareTypeIndex) {
      
          //长方形
          case 0:
            moveSquareCoordinate = [[0, 6], [0, 7], [0, 8], [0, 9]]
            break;
      
          //三角形
          case 1:
            moveSquareCoordinate = [[0, 8], [0, 9], [0, 10], [1, 9]]
            break;
          
          //正方形
          case 2:
            moveSquareCoordinate = [[0, 7], [0, 8], [1, 7], [1, 8]]
            break;
      
          //z字型
          case 3:
            moveSquareCoordinate = [[0, 7], [0, 8], [1, 8], [1, 9]]
            break;
      
          //反z字型
          case 4:
            moveSquareCoordinate = [[0, 8], [0, 7], [1, 7], [1, 6]]
            break;
      
          //l型
          case 5:
            moveSquareCoordinate = [[0, 8], [0, 9], [0, 10], [1, 8]]
            break;
      
          //反l型
          case 6:
            moveSquareCoordinate = [[0, 8], [0, 9], [0, 10], [1, 10]]
            break;
        }
      }

      存储已经不能移动的俄罗斯方块信息

      当俄罗斯方块触碰到底部边界或者,触碰到不能移动的俄罗斯方块时,可移动的俄罗斯方块就会变成不可移动的方块,所以需要也需要用一个专门的数组存储

      //存储当前已经稳定坠落的方块的坐标
      let stabilitySquareCoordinate = [];

      使用之前在贪吃蛇中使用的颜色渲染工具

      //改变棋盘格子颜色([A, B]为坐标,color是需要渲染为什么颜色(0为白色,1为红色,2为绿色))
      const changeCheckerboard = ([A, B], color) => {
        for (let i = 0; i < checkerboardInfo.length; i++) {
            let [x, y, num] = checkerboardInfo[i];
            if( A===x && B===y ){
              checkerboardInfo[i][2]=color;
              break
            }
          }
      };
      
      //清空棋盘颜色
      const clearCheckerboard = () => {
        for (let index = 0; index < checkerboardInfo.length; index++) {
          checkerboardInfo[index][2] = 0
        }
      };

      让方块移动起来(不考虑切换方块的形态切换)

      可移动的俄罗斯方块会一直向下移动,能通过键盘左右键控制其向左右移动,通过键盘下键加速向下移动,不能向上移动(键盘上键控制该方块形态的切换),移动时不能超过该游戏界面的范围,如果移动的俄罗斯方块触碰到不可移动的俄罗斯方块,则停止移动,并且会积累在不可移动的俄罗斯方块上

      在移动之前得先设置工具函数,用于检测移动后的俄罗斯方块二维数组中存储的坐标点数组是否会超出游戏界面的范围,以及是否会触碰到不可移动的俄罗斯方块上

      检测移动的俄罗斯方块二维数组在移动后它的每个坐标点数组是否有超出范围的

      在范围之中则不管,不在范围之中则不允许移动

      //判断是否碰到边界(arr为可移动俄罗斯方块上单个的坐标点)
      const judgeBoundary = (arr) => {
        if((arr[0]<0||arr[0]>24)||(arr[1]<0||arr[1]>15)){
          return true
        };
        return false
      }

      检测移动的俄罗斯方块二维数组在移动后它的每个坐标点数组是否会触碰到不可移动的俄罗斯方块上

      观察可知,移动的俄罗斯方块和不可移动的俄罗斯方块触碰到一起就是在它们存储的坐标点数组下标为0的值相差一,而下标为1的值相等时,该移动的俄罗斯方块变成不可移动的俄罗斯方块

      特殊情况:当移动方块没碰到不可移动的方块,但是触碰到游戏界面的最底部,即存储的坐标点数组下标为0的值为24时该移动方块亦会变成不可移动的方块

      //查看方块是否碰到已经存在的方格中(arr为可移动俄罗斯方块上单个的坐标点)
      const judgeStabilitySquareCoordinate = (arr) =>{
        //移动到最后一格时
        if( arr[0] === 24 ){
          return true
        }
        
        //遍历不可移动的俄罗斯方块与该点做比较
        for (let index = 0; index < stabilitySquareCoordinate.length; index++) {
          if(stabilitySquareCoordinate[index][0]-1 === arr[0] && stabilitySquareCoordinate[index][1] === arr[1]){
            return true
          }
        };
        return false;
      }

      特殊情况(得分)

      得分是在可移动的俄罗斯变为不可移动的俄罗斯方块发生的一个判断

      如果在不可移动的俄罗斯方块二维数组中,有坐标点数组下标为0的值相同且存在16个,那么这16个点应该被删除出不可移动的俄罗斯方块二维数组,这16个点被删除后,在这些点上的坐标得向下移动一格(如下图所示)

      //得分(arr为可移动俄罗斯方块上单个的坐标点)
      const score = (arr)=>{
        let num = 0;
        for (let index = 0; index < stabilitySquareCoordinate.length; index++) {
          if(arr[0] === stabilitySquareCoordinate[index][0]){
            num++;
            //等于16后满足当前销毁需求(直接跳出循环减少性能消耗)
            if(num === 16){
              break;
            }
          }    
        };
        if(num === 16){
        
          //删除已经在该数组中凑齐能得分的行数(小技巧,倒着循环数组能保证每个符合删除条件的数据都能被删除)
          for (let index = stabilitySquareCoordinate.length-1; index >-1; index--) {
            if(arr[0] === stabilitySquareCoordinate[index][0]){
              stabilitySquareCoordinate.splice(index, 1)
            }
          }
          
          //将所有在销毁行上面的稳定方块移动至下一行去
          for (let index = 0; index < stabilitySquareCoordinate.length; index++) {
            if(arr[0] > stabilitySquareCoordinate[index][0]){
              stabilitySquareCoordinate[index][0]++
            }
          }
        }
      }

      移动方法

      移动就是通过监听键盘弹起事件,通过对应的keycode更改可移动的俄罗斯方块中每个坐标点的值,然后再将获取到的新的二维数组去执行之前的三个方法依次判断

      //事件(方便后期摘除事件)
      const listener = (event)=>{
        //只监听上下左右四个按键
        const keyCodeArr =  [37, 39, 40];
        if(keyCodeArr.includes(event.keyCode)){
          //监听方向键变化(执行方块移动方向)
          moveSquare(event.keyCode);
        }
      }
      
      //定时器(一直会有一个下移方块指令执行)
      let timer = setInterval(()=>{
        moveSquare(40)
      },500);
      
      //在window上挂载事件监听器
      window.addEventListener("keydown", listener);
      //移动方块的指令
      const moveSquare = (num) => {
        if( !isShowSquare.value ){
          return
        };
      
        //移动
        for (let index = 0; index < moveSquareCoordinate.length; index++) {
          switch (num) {
            //键盘对应数字如下
            //40:下;37:左;39:右;
            case 37:
              moveSquareCoordinate[index][1] = moveSquareCoordinate[index][1]-1
              break;
            case 39:
              moveSquareCoordinate[index][1] = moveSquareCoordinate[index][1]+1
              break;
            case 40:
              moveSquareCoordinate[index][0] = moveSquareCoordinate[index][0]+1
              break;
          };
        };
      
        //是否超过边界的标杆
        let flag1 = false;
        for (let index = 0; index < moveSquareCoordinate.length; index++) {
          if(judgeBoundary(moveSquareCoordinate[index])){
            flag1 = true;
          }
        }
      
        //标杆满足后方块复位
        if(flag1){
          for (let index = 0; index < moveSquareCoordinate.length; index++) {
            switch (num) {
              case 37:
                moveSquareCoordinate[index][1] = moveSquareCoordinate[index][1]+1
                break;
              case 39:
                moveSquareCoordinate[index][1] = moveSquareCoordinate[index][1]-1
                break;
              case 40:
                moveSquareCoordinate[index][0] = moveSquareCoordinate[index][0]-1
                break;
            };
          };
        }
      
        //能否触碰到已稳定方块的标杆
        let flag2 = false;
        let coordinate = [];
        for (let index = 0; index < moveSquareCoordinate.length; index++) {
          if(judgeStabilitySquareCoordinate(moveSquareCoordinate[index])){
            flag2 = true;
          }
        };
      
        //只要碰到了
        if (flag2) {
      
          //添加进入不移动的方块坐标数组
          for (let index = 0; index < moveSquareCoordinate.length; index++) {
            stabilitySquareCoordinate.push(moveSquareCoordinate[index]);
            if(moveSquareCoordinate[index][0]-1 === 0){
              //输了就跳出循环,不必再给已稳定的方块坐标添加新坐标了
              isFail.value = true;
              break
            }
          }
      
          //如果已经失败则停止移动
          if (isFail.value) {
            return
          }
          isShowSquare.value = false;
      
          //将移动方块中每个点坐标去做得分判断
          for (let index = 0; index < moveSquareCoordinate.length; index++) {
            score(moveSquareCoordinate[index]);
          }
        }
        
        //重新渲染游戏界面颜色
        clearCheckerboard();
        for (let index = 0; index < moveSquareCoordinate.length; index++) {
          changeCheckerboard(moveSquareCoordinate[index], 2)
        }
        for (let index = 0; index < stabilitySquareCoordinate.length; index++) {
          changeCheckerboard(stabilitySquareCoordinate[index], 1)
        }
      };

      经过一系列的操作,我们已经得到了一个可以玩的俄罗斯方块,它能够移动方块,也能得分,但是不能将移动的俄罗斯方块切换形态,十分没有游戏体验感,那就再加切换形态的操作(这个操作把我差点带走,看到这里多少给个赞呗o( ̄▽ ̄)ブ)

      切换操作书写

      先穷举七种形状的不同形态(如下图)

      形态变化 
      长方形2种
      正方形1种
      z形2种
      反z形2种
      三角形4种
      L形4种
      反L形4种

      设计检测该形状形态的方法

      检测长方形形态的方法

      通过判断A、B两点的下标为0是的值是否相同来返回形态

      const detectionToolAboutRectangle = () => {
        if (moveSquareCoordinate[0][0] !== moveSquareCoordinate[1][0]) {
          //竖直的方块
          return 0
        }
      
        //横着的长方形方块
        return 1
      };

      检测z形和反z形形态的方法

      通过判断A、B两点的下标为0是的值是否相同来返回形态

      //检验工具(z字形和反z字形)
      const detectionToolAboutZOr_Z = () => {
        if(moveSquareCoordinate[0][0] === moveSquareCoordinate[1][0]){
          //z字形
          return 0
        }
        //n字形
        return 1
      };

      检测L形、反L形、三角形形态的方法

      通过判断判断A、B、C三点构成的线与D点的相对位置来判断形态,第一种形态为D点在线段ABC下方,第二种形态为D点在线段ABC左边,第三种形态为D点在线段ABC上方,第四种形态为D点在线段ABC右边,

      //检验工具(三角形、l型、反l型)
      const detectionToolAboutTriangle = () => {
        
        //判断四种形态
        if ((moveSquareCoordinate[0][0] === moveSquareCoordinate[1][0] && moveSquareCoordinate[1][0] === moveSquareCoordinate[2][0]) && moveSquareCoordinate[1][0]<moveSquareCoordinate[3][0]) {
          return 0
        }
        if ((moveSquareCoordinate[0][1] === moveSquareCoordinate[1][1] && moveSquareCoordinate[1][1] === moveSquareCoordinate[2][1]) && moveSquareCoordinate[2][1]>moveSquareCoordinate[3][1]) {
          return 1
        }
        if ((moveSquareCoordinate[0][0] === moveSquareCoordinate[1][0] && moveSquareCoordinate[1][0] === moveSquareCoordinate[2][0]) && moveSquareCoordinate[1][0]>moveSquareCoordinate[3][0]) {
          return 2
        }
        if ((moveSquareCoordinate[0][1] === moveSquareCoordinate[1][1] && moveSquareCoordinate[1][1] === moveSquareCoordinate[2][1]) && moveSquareCoordinate[2][1]<moveSquareCoordinate[3][1]) {
          return 3
        }
      };

      设计切换方法

      需要判断其现有形态,然后判断切换后是否会超出游戏范围界面,或者触碰到不能移动,如果不超出且不触碰则切换状态

      切换L形形态(代码过多只解释一个,其余的可以看我仓库代码)

      由于业务水平缘故,在下直接保证每次切换形态都是向右旋转90度(如图旋转)

      在每一次旋转后,都以之前的B点坐标重构方块,(没错在下直接穷举了7种方块的18个类型变化,如果有其他好方法,欢迎评论区留言,谢谢大哥)

      const toggleSquareShapeAboutL= () => {
        
        //改变后的俄罗斯方块数组
        let arr = null;
        
        //满足哪种形态则将后一种形态的坐标点存储进该数组中
        switch (detectionToolAboutTriangle()) {
          case 0:
            arr = [[moveSquareCoordinate[1][0]-1,moveSquareCoordinate[1][1]],[moveSquareCoordinate[1][0],moveSquareCoordinate[1][1]],[moveSquareCoordinate[1][0]+1,moveSquareCoordinate[1][1]],[moveSquareCoordinate[1][0]-1,moveSquareCoordinate[1][1]-1]];
            
            //判断每个点会不会超出游戏界面或者触碰到不可移动的俄罗斯方块上
            for (let index = 0; index < arr.length; index++) {
              if(judgeBoundary(arr[index]) || judgeStabilitySquareCoordinate(arr[index])){
                return
              }
            }
            moveSquareCoordinate = arr;
            clearCheckerboard();
            for (let index = 0; index < moveSquareCoordinate.length; index++) {
              changeCheckerboard(moveSquareCoordinate[index], 2)
            }
            for (let index = 0; index < stabilitySquareCoordinate.length; index++) {
              changeCheckerboard(stabilitySquareCoordinate[index], 1)
            }
            break;
          case 1:
            arr = [[moveSquareCoordinate[1][0],moveSquareCoordinate[1][1]+1],[moveSquareCoordinate[1][0],moveSquareCoordinate[1][1]],[moveSquareCoordinate[1][0],moveSquareCoordinate[1][1]-1],[moveSquareCoordinate[1][0]-1,moveSquareCoordinate[1][1]+1]];
            for (let index = 0; index < arr.length; index++) {
              if(judgeBoundary(arr[index]) || judgeStabilitySquareCoordinate(arr[index])){
                return
              }
            }
            moveSquareCoordinate = arr;
            clearCheckerboard();
            for (let index = 0; index < moveSquareCoordinate.length; index++) {
              changeCheckerboard(moveSquareCoordinate[index], 2)
            }
            for (let index = 0; index < stabilitySquareCoordinate.length; index++) {
              changeCheckerboard(stabilitySquareCoordinate[index], 1)
            }
            break;
          case 2:
            arr = [[moveSquareCoordinate[1][0]+1,moveSquareCoordinate[1][1]],[moveSquareCoordinate[1][0],moveSquareCoordinate[1][1]],[moveSquareCoordinate[1][0]-1,moveSquareCoordinate[1][1]],[moveSquareCoordinate[1][0]+1,moveSquareCoordinate[1][1]+1]];
            for (let index = 0; index < arr.length; index++) {
              if(judgeBoundary(arr[index]) || judgeStabilitySquareCoordinate(arr[index])){
                return
              }
            }
            moveSquareCoordinate = arr;
            clearCheckerboard();
            for (let index = 0; index < moveSquareCoordinate.length; index++) {
              changeCheckerboard(moveSquareCoordinate[index], 2)
            }
            for (let index = 0; index < stabilitySquareCoordinate.length; index++) {
              changeCheckerboard(stabilitySquareCoordinate[index], 1)
            }
            break;
          case 3:
            arr = [[moveSquareCoordinate[1][0],moveSquareCoordinate[1][1]-1],[moveSquareCoordinate[1][0],moveSquareCoordinate[1][1]],[moveSquareCoordinate[1][0],moveSquareCoordinate[1][1]+1],[moveSquareCoordinate[1][0]+1,moveSquareCoordinate[1][1]-1]];
            for (let index = 0; index < arr.length; index++) {
              if(judgeBoundary(arr[index]) || judgeStabilitySquareCoordinate(arr[index])){
                return
              }
            }
            
            //给移动的俄罗斯方块数组重新赋值
            moveSquareCoordinate = arr;
            
            //重新渲染游戏界面
            clearCheckerboard();
            for (let index = 0; index < moveSquareCoordinate.length; index++) {
              changeCheckerboard(moveSquareCoordinate[index], 2)
            }
            for (let index = 0; index < stabilitySquareCoordinate.length; index++) {
              changeCheckerboard(stabilitySquareCoordinate[index], 1)
            }
            break;
        }
      };

      单个方格组件展示

      CheckerboardItem.vue

      只通过存入的方格颜色信息(方格信息数组中的第三个值)来判断当前方格显示的颜色

      const props = defineProps({
        checkerboardItemInfo:Array,
      });
      
      //通过toRefs解构方格信息数组中的第三个值(只有使用toRefs才能保持该引用数据解构后的数据依然保持响应式)
      let [ x, y, num] = toRefs(props.checkerboardItemInfo)
      
      let color = ref('');
      
      //使用监听器完成数据监听,给背景色设置不同值
      watchEffect(()=>{
        switch (num.value) {
          case 0:
            color.value = 'while'
            break;
          case 1:
            color.value = 'red'
            break;
          case 2:
            color.value = 'green'
            break;
        }
      })
      <style lang="less" scoped>
      .checkerboardItem{
        //vue3.2能在css中使用v-bind绑定响应式数据
        background-color: v-bind(color);
      }
      </style>

      好了,俄罗斯方块搞定

      总结

      在线客服
      服务热线

      服务热线

      4008888355

      微信咨询
      二维码
      返回顶部
      ×二维码

      截屏,微信识别二维码

      打开微信

      微信号已复制,请打开微信添加咨询详情!