VUE+Canvas 实现桌面弹球消砖块小游戏的示例代码

 更新时间:2021年04月13日 09:13:52   作者:登楼痕  
这篇文章主要介绍了VUE+Canvas 实现桌面弹球消砖块小游戏,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下

大家都玩过弹球消砖块游戏,左右键控制最底端的一个小木板平移,接住掉落的小球,将球弹起后消除画面上方的一堆砖块。

那么用VUE+Canvas如何来实现呢?实现思路很简单,首先来拆分一下要画在画布上的内容:

(1)用键盘左右按键控制平移的木板;

(2)在画布内四处弹跳的小球;

(3)固定在画面上方,并且被球碰撞后就消失的一堆砖块。

将上述三种对象,用requestAnimationFrame()函数平移运动起来,再结合各种碰撞检查,就可以得到最终的结果。

先看看最终的效果:

一、左右平移的木板

最底部的木板是最简单的一部分,因为木板的y坐标是固定的,我们设置木板的初始参数,包括其宽度,高度,平移速度等,然后实现画木板的函数:

pannel: {
        x: 0,
        y: 0,
        height: 8,
        width: 100,
        speed: 8,
        dx: 0
},
 
....
 
drawPannel() {
      this.drawRoundRect(
        this.pannel.x,
        this.pannel.y,
        this.pannel.width,
        this.pannel.height,
        5
      );
},
drawRoundRect(x, y, width, height, radius) { // 画圆角矩形
      this.ctx.beginPath();
      this.ctx.arc(x + radius, y + radius, radius, Math.PI, (Math.PI * 3) / 2);
      this.ctx.lineTo(width - radius + x, y);
      this.ctx.arc(
        width - radius + x,
        radius + y,
        radius,
        (Math.PI * 3) / 2,
        Math.PI * 2
      );
      this.ctx.lineTo(width + x, height + y - radius);
      this.ctx.arc(
        width - radius + x,
        height - radius + y,
        radius,
        0,
        (Math.PI * 1) / 2
      );
      this.ctx.lineTo(radius + x, height + y);
      this.ctx.arc(
        radius + x,
        height - radius + y,
        radius,
        (Math.PI * 1) / 2,
        Math.PI
      );
      this.ctx.fillStyle = "#008b8b";
      this.ctx.fill();
      this.ctx.closePath();
}

程序初始化的时候,监听键盘的左右方向键,来移动木板,通过长度判断是否移动到了左右边界使其不能继续移出画面:

document.onkeydown = function(e) {
      let key = window.event.keyCode;
      if (key === 37) {
        // 左键
        _this.pannel.dx = -_this.pannel.speed;
      } else if (key === 39) {
        // 右键
        _this.pannel.dx = _this.pannel.speed;
      }
};
document.onkeyup = function(e) {
      _this.pannel.dx = 0;
};
....
 
 movePannel() {
      this.pannel.x += this.pannel.dx;
      if (this.pannel.x > this.clientWidth - this.pannel.width) {
        this.pannel.x = this.clientWidth - this.pannel.width;
      } else if (this.pannel.x < 0) {
        this.pannel.x = 0;
      }
},

二、弹跳的小球和碰撞检测

小球的运动和木板类似,只是不仅有dx的偏移,还有dy的偏移。

而且还要有碰撞检测:

(1)当碰撞的是上、右、左墙壁以及木板上的时候则反弹;

(2)当碰撞到是木板以外的下边界的时候,则输掉游戏;

(3)当碰撞的是砖块的时候,被碰的砖块消失,分数+1,小球反弹。

于是和木板一样,将小球部分分为画小球函数drawBall()和小球运动函数moveBall():

drawBall() {
      this.ctx.beginPath();
      this.ctx.arc(this.ball.x, this.ball.y, this.ball.r, 0, 2 * Math.PI);
      this.ctx.fillStyle = "#008b8b";
      this.ctx.fill();
      this.ctx.closePath();
},
moveBall() {
      this.ball.x += this.ball.dx;
      this.ball.y += this.ball.dy;
      this.breaksHandle();
      this.edgeHandle();
},
breaksHandle() {
      // 触碰砖块检测
      this.breaks.forEach(item => {
        if (item.show) {
          if (
            this.ball.x + this.ball.r > item.x &&
            this.ball.x - this.ball.r < item.x + this.breaksConfig.width &&
            this.ball.y + this.ball.r > item.y &&
            this.ball.y - this.ball.r < item.y + this.breaksConfig.height
          ) {
            item.show = false;
            this.ball.dy *= -1;
            this.score ++ ;
            if(this.showBreaksCount === 0){
              this.gameOver = true;
            }
          }
        }
      });
},
edgeHandle() {
      // 边缘检测
      // 碰到顶部反弹
      if (this.ball.y - this.ball.r < 0) {
        this.ball.dy = -this.ball.dy;
      }
      if (
        // 碰到左右墙壁
        this.ball.x - this.ball.r < 0 ||
        this.ball.x + this.ball.r > this.clientWidth
      ) {
        this.ball.dx = -this.ball.dx;
      }
      if (
        this.ball.x >= this.pannel.x &&
        this.ball.x <= this.pannel.x + this.pannel.width &&
        this.ball.y + this.ball.r >= this.clientHeight - this.pannel.height
      ) {
        // 球的x在板子范围内并触碰到了板子
        this.ball.dy *= -1;
      } else if (
        (this.ball.x < this.pannel.x ||
          this.ball.x > this.pannel.x + this.pannel.width) &&
        this.ball.y + this.ball.r >= this.clientHeight
      ) {
        // 球碰到了底边缘了
        this.gameOver = true;
        this.getCurshBreaks();
      }
}

三、砖块的生成

砖块的生成也比较简单,这里我们初始了一些数据:

breaksConfig: {
        row: 6, // 排数
        height: 25, // 砖块高度
        width: 130, // 砖块宽度
        radius: 5, // 矩形圆角
        space: 0, // 间距
        colunm: 6 // 列数
}

根据这些配置项以及画布宽度,我们可以计算出每个砖块的横向间隙是多少:

// 计算得出砖块缝隙宽度
      this.breaksConfig.space = Math.floor(
        (this.clientWidth -
          this.breaksConfig.width * this.breaksConfig.colunm) /
          (this.breaksConfig.colunm + 1)
      );

于是我们可以得到每个砖块在画布中的x,y坐标(指的砖块左上角的坐标)

for (let i = 0; i < _this.breaksConfig.row; i++) {
        for (let j = 0; j < _this.breaksConfig.colunm; j++) {
          _this.breaks.push({
            x: this.breaksConfig.space * (j + 1) + this.breaksConfig.width * j,
            y: 10 * (i + 1) + this.breaksConfig.height * i,
            show: true
          });
        }
      }

再加上绘制砖块的函数:

drawBreaks() {
      let _this = this;
      _this.breaks.forEach(item => {
        if (item.show) {
          _this.drawRoundRect(
            item.x,
            item.y,
            _this.breaksConfig.width,
            _this.breaksConfig.height,
            _this.breaksConfig.radius
          );
        }
      });
}

四、让上面三个部分动起来

(function animloop() {
      if (!_this.gameOver) {
        _this.movePannel();
        _this.moveBall();
        _this.drawAll();
      } else {
        _this.drawCrushBreaks();
      }
      window.requestAnimationFrame(animloop);
})();
....
 drawAll() {
      this.ctx.clearRect(0, 0, this.clientWidth, this.clientHeight);
      this.drawPannel();
      this.drawBall();
      this.drawScore();
      this.drawBreaks();
}

五、游戏结束后的效果

在最开始的动图里可以看到,游戏结束后,砖块粉碎成了若干的小球掉落,这个其实和画单独的小球类似,思路就是把剩余的砖块中心坐标处生产若干大小不等,运动轨迹不等,颜色不等的小球,然后继续动画。

getCurshBreaks() {
      let _this = this;
      this.breaks.forEach(item => {
        if (item.show) {
          item.show = false;
          for (let i = 0; i < 8; i++) { // 每个砖块粉碎为8个小球
            this.crushBalls.push({
              x: item.x + this.breaksConfig.width / 2,
              y: item.y + this.breaksConfig.height / 2,
              dx: _this.getRandomArbitrary(-6, 6),
              dy: _this.getRandomArbitrary(-6, 6),
              r: _this.getRandomArbitrary(1, 4),
              color: _this.getRandomColor()
            });
          }
        }
      });
},
drawCrushBreaks() {
      this.ctx.clearRect(0, 0, this.clientWidth, this.clientHeight);
      this.crushBalls.forEach(item => {
        this.ctx.beginPath();
        this.ctx.arc(item.x, item.y, item.r, 0, 2 * Math.PI);
        this.ctx.fillStyle = item.color;
        this.ctx.fill();
        this.ctx.closePath();
        item.x += item.dx;
        item.y += item.dy;
        if (
          // 碰到左右墙壁
          item.x - item.r < 0 ||
          item.x + item.r > this.clientWidth
        ) {
          item.dx = -item.dx;
        }
        if (
          // 碰到上下墙壁
          item.y - item.r < 0 ||
          item.y + item.r > this.clientHeight
        ) {
          item.dy = -item.dy;
        }
      });
},

以上就是桌面弹球消砖块小游戏的实现思路和部分代码,实现起来很简单,两三百行代码就可以实现这个小游戏。在小球的运动上可以进行持续优化,并且也可以增加难度选项操作。

最后附上全部的vue文件代码,供大家参考学习:

<template>
  <div class="break-ball">
    <canvas id="breakBall" width="900" height="600"></canvas>
    <div class="container" v-if="gameOver">
      <div class="dialog">
        <p class="once-again">本轮分数:{{score}}分</p>
        <p class="once-again">真好玩!</p>
        <p class="once-again">再来一次~~</p>
        <el-button class="once-again-btn" @click="init">开始</el-button>
      </div>
    </div>
  </div>
</template>
 
<script>
const randomColor = [
  "#FF95CA",
  "#00E3E3",
  "#00E3E3",
  "#6F00D2",
  "#6F00D2",
  "#C2C287",
  "#ECFFFF",
  "#FFDC35",
  "#93FF93",
  "#d0d0d0"
];
export default {
  name: "BreakBall",
  data() {
    return {
      clientWidth: 0,
      clientHeight: 0,
      ctx: null,
      crushBalls: [],
      pannel: {
        x: 0,
        y: 0,
        height: 8,
        width: 100,
        speed: 8,
        dx: 0
      },
      ball: {
        x: 0,
        y: 0,
        r: 8,
        dx: -4,
        dy: -4
      },
      score: 0,
      gameOver: false,
      breaks: [],
      breaksConfig: {
        row: 6, // 排数
        height: 25, // 砖块高度
        width: 130, // 砖块宽度
        radius: 5, // 矩形圆角
        space: 0, // 间距
        colunm: 6 // 列数
      }
    };
  },
  mounted() {
    let _this = this;
    let container = document.getElementById("breakBall");
    this.ctx = container.getContext("2d");
    this.clientHeight = container.height;
    this.clientWidth = container.width;
    _this.init();
    document.onkeydown = function(e) {
      let key = window.event.keyCode;
      if (key === 37) {
        // 左键
        _this.pannel.dx = -_this.pannel.speed;
      } else if (key === 39) {
        // 右键
        _this.pannel.dx = _this.pannel.speed;
      }
    };
    document.onkeyup = function(e) {
      _this.pannel.dx = 0;
    };
    (function animloop() {
      if (!_this.gameOver) {
        _this.movePannel();
        _this.moveBall();
        _this.drawAll();
      } else {
        _this.drawCrushBreaks();
      }
      window.requestAnimationFrame(animloop);
    })();
  },
  computed:{
    showBreaksCount(){
      return this.breaks.filter(item=>{
        return item.show;
      }).length;
    }
  },
  methods: {
    init() {
      let _this = this;
      _this.gameOver = false;
      this.pannel.y = this.clientHeight - this.pannel.height;
      this.pannel.x = this.clientWidth / 2 - this.pannel.width / 2;
      this.ball.y = this.clientHeight / 2;
      this.ball.x = this.clientWidth / 2;
      this.score = 0;
      this.ball.dx = [-1,1][Math.floor(Math.random() * 2)]*4;
      this.ball.dy = [-1,1][Math.floor(Math.random() * 2)]*4;
      this.crushBalls = [];
      this.breaks = [];
      // 计算得出砖块缝隙宽度
      this.breaksConfig.space = Math.floor(
        (this.clientWidth -
          this.breaksConfig.width * this.breaksConfig.colunm) /
          (this.breaksConfig.colunm + 1)
      );
 
      for (let i = 0; i < _this.breaksConfig.row; i++) {
        for (let j = 0; j < _this.breaksConfig.colunm; j++) {
          _this.breaks.push({
            x: this.breaksConfig.space * (j + 1) + this.breaksConfig.width * j,
            y: 10 * (i + 1) + this.breaksConfig.height * i,
            show: true
          });
        }
      }
    },
    drawAll() {
      this.ctx.clearRect(0, 0, this.clientWidth, this.clientHeight);
      this.drawPannel();
      this.drawBall();
      this.drawScore();
      this.drawBreaks();
    },
    movePannel() {
      this.pannel.x += this.pannel.dx;
      if (this.pannel.x > this.clientWidth - this.pannel.width) {
        this.pannel.x = this.clientWidth - this.pannel.width;
      } else if (this.pannel.x < 0) {
        this.pannel.x = 0;
      }
    },
    moveBall() {
      this.ball.x += this.ball.dx;
      this.ball.y += this.ball.dy;
      this.breaksHandle();
      this.edgeHandle();
    },
    breaksHandle() {
      // 触碰砖块检测
      this.breaks.forEach(item => {
        if (item.show) {
          if (
            this.ball.x + this.ball.r > item.x &&
            this.ball.x - this.ball.r < item.x + this.breaksConfig.width &&
            this.ball.y + this.ball.r > item.y &&
            this.ball.y - this.ball.r < item.y + this.breaksConfig.height
          ) {
            item.show = false;
            this.ball.dy *= -1;
            this.score ++ ;
            if(this.showBreaksCount === 0){
              this.gameOver = true;
            }
          }
        }
      });
    },
    edgeHandle() {
      // 边缘检测
      // 碰到顶部反弹
      if (this.ball.y - this.ball.r < 0) {
        this.ball.dy = -this.ball.dy;
      }
      if (
        // 碰到左右墙壁
        this.ball.x - this.ball.r < 0 ||
        this.ball.x + this.ball.r > this.clientWidth
      ) {
        this.ball.dx = -this.ball.dx;
      }
      if (
        this.ball.x >= this.pannel.x &&
        this.ball.x <= this.pannel.x + this.pannel.width &&
        this.ball.y + this.ball.r >= this.clientHeight - this.pannel.height
      ) {
        // 球的x在板子范围内并触碰到了板子
        this.ball.dy *= -1;
      } else if (
        (this.ball.x < this.pannel.x ||
          this.ball.x > this.pannel.x + this.pannel.width) &&
        this.ball.y + this.ball.r >= this.clientHeight
      ) {
        // 球碰到了底边缘了
        this.gameOver = true;
        this.getCurshBreaks();
      }
    },
    drawScore(){
      this.ctx.beginPath();
      this.ctx.font="14px Arial";
      this.ctx.fillStyle = "#FFF";
      this.ctx.fillText("分数:"+this.score,10,this.clientHeight-14);
      this.ctx.closePath();
    },
    drawCrushBreaks() {
      this.ctx.clearRect(0, 0, this.clientWidth, this.clientHeight);
      this.crushBalls.forEach(item => {
        this.ctx.beginPath();
        this.ctx.arc(item.x, item.y, item.r, 0, 2 * Math.PI);
        this.ctx.fillStyle = item.color;
        this.ctx.fill();
        this.ctx.closePath();
        item.x += item.dx;
        item.y += item.dy;
        if (
          // 碰到左右墙壁
          item.x - item.r < 0 ||
          item.x + item.r > this.clientWidth
        ) {
          item.dx = -item.dx;
        }
        if (
          // 碰到上下墙壁
          item.y - item.r < 0 ||
          item.y + item.r > this.clientHeight
        ) {
          item.dy = -item.dy;
        }
      });
    },
    getRandomColor() {
      return randomColor[Math.floor(Math.random() * randomColor.length)];
    },
    getRandomArbitrary(min, max) {
      return Math.random() * (max - min) + min;
    },
    getCurshBreaks() {
      let _this = this;
      this.breaks.forEach(item => {
        if (item.show) {
          item.show = false;
          for (let i = 0; i < 8; i++) {
            this.crushBalls.push({
              x: item.x + this.breaksConfig.width / 2,
              y: item.y + this.breaksConfig.height / 2,
              dx: _this.getRandomArbitrary(-6, 6),
              dy: _this.getRandomArbitrary(-6, 6),
              r: _this.getRandomArbitrary(1, 4),
              color: _this.getRandomColor()
            });
          }
        }
      });
    },
    drawBall() {
      this.ctx.beginPath();
      this.ctx.arc(this.ball.x, this.ball.y, this.ball.r, 0, 2 * Math.PI);
      this.ctx.fillStyle = "#008b8b";
      this.ctx.fill();
      this.ctx.closePath();
    },
    drawPannel() {
      this.drawRoundRect(
        this.pannel.x,
        this.pannel.y,
        this.pannel.width,
        this.pannel.height,
        5
      );
    },
    drawRoundRect(x, y, width, height, radius) {
      this.ctx.beginPath();
      this.ctx.arc(x + radius, y + radius, radius, Math.PI, (Math.PI * 3) / 2);
      this.ctx.lineTo(width - radius + x, y);
      this.ctx.arc(
        width - radius + x,
        radius + y,
        radius,
        (Math.PI * 3) / 2,
        Math.PI * 2
      );
      this.ctx.lineTo(width + x, height + y - radius);
      this.ctx.arc(
        width - radius + x,
        height - radius + y,
        radius,
        0,
        (Math.PI * 1) / 2
      );
      this.ctx.lineTo(radius + x, height + y);
      this.ctx.arc(
        radius + x,
        height - radius + y,
        radius,
        (Math.PI * 1) / 2,
        Math.PI
      );
      this.ctx.fillStyle = "#008b8b";
      this.ctx.fill();
      this.ctx.closePath();
    },
    drawBreaks() {
      let _this = this;
      _this.breaks.forEach(item => {
        if (item.show) {
          _this.drawRoundRect(
            item.x,
            item.y,
            _this.breaksConfig.width,
            _this.breaksConfig.height,
            _this.breaksConfig.radius
          );
        }
      });
    }
  }
};
</script>
 
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped lang="scss">
.break-ball {
  width: 900px;
  height: 600px;
  position: relative;
  #breakBall {
    background: #2a4546;
  }
  .container {
    position: absolute;
    top: 0;
    right: 0;
    bottom: 0;
    left: 0;
    background-color: rgba(0, 0, 0, 0.3);
    text-align: center;
    font-size: 0;
    white-space: nowrap;
    overflow: auto;
  }
  .container:after {
    content: "";
    display: inline-block;
    height: 100%;
    vertical-align: middle;
  }
  .dialog {
    width: 400px;
    height: 300px;
    background: rgba(255, 255, 255, 0.5);
    box-shadow: 3px 3px 6px 3px rgba(0, 0, 0, 0.3);
    display: inline-block;
    vertical-align: middle;
    text-align: left;
    font-size: 28px;
    color: #fff;
    font-weight: 600;
    border-radius: 10px;
    white-space: normal;
    text-align: center;
    .once-again-btn {
      background: #1f9a9a;
      border: none;
      color: #fff;
    }
  }
}
</style>

到此这篇关于VUE+Canvas 实现桌面弹球消砖块小游戏的文章就介绍到这了,更多相关vue弹球消砖块小游戏内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • vuex mutations的两种调用方法小结

    vuex mutations的两种调用方法小结

    这篇文章主要介绍了vuex mutations的两种调用方法小结,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-03-03
  • vue mint-ui学习笔记之picker的使用

    vue mint-ui学习笔记之picker的使用

    本篇文章主要介绍了vue mint-ui学习笔记之picker的使用,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2017-10-10
  • Vue实现virtual-dom的原理简析

    Vue实现virtual-dom的原理简析

    这篇文章主要介绍了Vue实现virtual-dom的原理简析,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2017-07-07
  • 在Vue中使用Echarts+封装

    在Vue中使用Echarts+封装

    这篇文章主要介绍了在Vue中使用Echarts++封装,需要的朋友可以参考下
    2023-11-11
  • vue登录注册及token验证实现代码

    vue登录注册及token验证实现代码

    在vue单页中,我们可以通过监控route对象,从中匹配信息去决定是否验证token,然后定义后续行为。下面通过实例代码给大家分享vue登录注册及token验证功能,需要的朋友参考下吧
    2017-12-12
  • Vue3中Composition API和Options API的区别

    Vue3中Composition API和Options API的区别

    Vue3的Composition API和Options API是Vue.js框架中的两种不同的API,本文主要介绍了Vue3中Composition API和Options API的区别,文中通过示例代码介绍的非常详细,需要的朋友们下面随着小编来一起学习学习吧
    2023-06-06
  • Vue3使用Echarts导致tooltip失效问题及解决方法

    Vue3使用Echarts导致tooltip失效问题及解决方法

    Vue3 使用 proxy 对象代理,而 echarts 则使用了大量的全等(===), 对比失败从而导致了bug,这篇文章主要介绍了Vue3使用Echarts导致tooltip失效问题及解决方法,需要的朋友可以参考下
    2023-08-08
  • vue-cli 3.0 引入mint-ui报错问题及解决

    vue-cli 3.0 引入mint-ui报错问题及解决

    这篇文章主要介绍了vue-cli 3.0 引入mint-ui报错问题及解决方案,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-05-05
  • 解决vue-cli3创建使用iview定制主题javascriptEnabled找不到该配置项

    解决vue-cli3创建使用iview定制主题javascriptEnabled找不到该配置项

    这篇文章主要介绍了解决vue-cli3创建使用iview定制主题javascriptEnabled找不到该配置项问题,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2023-04-04
  • vue中前进刷新、后退缓存用户浏览数据和浏览位置的实例讲解

    vue中前进刷新、后退缓存用户浏览数据和浏览位置的实例讲解

    今天小编就为大家分享一篇vue中前进刷新、后退缓存用户浏览数据和浏览位置的实例讲解,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2018-09-09

最新评论