飞道的博客

VUE+Canvas 实现桌面弹球消砖块小游戏

298人阅读  评论(0)

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

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

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

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

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

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

先看看最终的效果:

一、左右平移的木板

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


  
  1. pannel: {
  2. x: 0,
  3. y: 0,
  4. height: 8,
  5. width: 100,
  6. speed: 8,
  7. dx: 0
  8. },
  9. ....
  10. drawPannel() {
  11. this.drawRoundRect(
  12. this.pannel.x,
  13. this.pannel.y,
  14. this.pannel.width,
  15. this.pannel.height,
  16. 5
  17. );
  18. },
  19. drawRoundRect(x, y, width, height, radius) { // 画圆角矩形
  20. this.ctx.beginPath();
  21. this.ctx.arc(x + radius, y + radius, radius, Math.PI, ( Math.PI * 3) / 2);
  22. this.ctx.lineTo(width - radius + x, y);
  23. this.ctx.arc(
  24. width - radius + x,
  25. radius + y,
  26. radius,
  27. ( Math.PI * 3) / 2,
  28. Math.PI * 2
  29. );
  30. this.ctx.lineTo(width + x, height + y - radius);
  31. this.ctx.arc(
  32. width - radius + x,
  33. height - radius + y,
  34. radius,
  35. 0,
  36. ( Math.PI * 1) / 2
  37. );
  38. this.ctx.lineTo(radius + x, height + y);
  39. this.ctx.arc(
  40. radius + x,
  41. height - radius + y,
  42. radius,
  43. ( Math.PI * 1) / 2,
  44. Math.PI
  45. );
  46. this.ctx.fillStyle = "#008b8b";
  47. this.ctx.fill();
  48. this.ctx.closePath();
  49. }

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


  
  1. document.onkeydown = function(e) {
  2. let key = window.event.keyCode;
  3. if (key === 37) {
  4. // 左键
  5. _this.pannel.dx = -_this.pannel.speed;
  6. } else if (key === 39) {
  7. // 右键
  8. _this.pannel.dx = _this.pannel.speed;
  9. }
  10. };
  11. document.onkeyup = function(e) {
  12. _this.pannel.dx = 0;
  13. };
  14. ....
  15. movePannel() {
  16. this.pannel.x += this.pannel.dx;
  17. if ( this.pannel.x > this.clientWidth - this.pannel.width) {
  18. this.pannel.x = this.clientWidth - this.pannel.width;
  19. } else if ( this.pannel.x < 0) {
  20. this.pannel.x = 0;
  21. }
  22. },

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

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

而且还要有碰撞检测:

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

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

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

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


  
  1. drawBall() {
  2. this.ctx.beginPath();
  3. this.ctx.arc( this.ball.x, this.ball.y, this.ball.r, 0, 2 * Math.PI);
  4. this.ctx.fillStyle = "#008b8b";
  5. this.ctx.fill();
  6. this.ctx.closePath();
  7. },
  8. moveBall() {
  9. this.ball.x += this.ball.dx;
  10. this.ball.y += this.ball.dy;
  11. this.breaksHandle();
  12. this.edgeHandle();
  13. },
  14. breaksHandle() {
  15. // 触碰砖块检测
  16. this.breaks.forEach( item => {
  17. if (item.show) {
  18. if (
  19. this.ball.x + this.ball.r > item.x &&
  20. this.ball.x - this.ball.r < item.x + this.breaksConfig.width &&
  21. this.ball.y + this.ball.r > item.y &&
  22. this.ball.y - this.ball.r < item.y + this.breaksConfig.height
  23. ) {
  24. item.show = false;
  25. this.ball.dy *= -1;
  26. this.score ++ ;
  27. if( this.showBreaksCount === 0){
  28. this.gameOver = true;
  29. }
  30. }
  31. }
  32. });
  33. },
  34. edgeHandle() {
  35. // 边缘检测
  36. // 碰到顶部反弹
  37. if ( this.ball.y - this.ball.r < 0) {
  38. this.ball.dy = - this.ball.dy;
  39. }
  40. if (
  41. // 碰到左右墙壁
  42. this.ball.x - this.ball.r < 0 ||
  43. this.ball.x + this.ball.r > this.clientWidth
  44. ) {
  45. this.ball.dx = - this.ball.dx;
  46. }
  47. if (
  48. this.ball.x >= this.pannel.x &&
  49. this.ball.x <= this.pannel.x + this.pannel.width &&
  50. this.ball.y + this.ball.r >= this.clientHeight - this.pannel.height
  51. ) {
  52. // 球的x在板子范围内并触碰到了板子
  53. this.ball.dy *= -1;
  54. } else if (
  55. ( this.ball.x < this.pannel.x ||
  56. this.ball.x > this.pannel.x + this.pannel.width) &&
  57. this.ball.y + this.ball.r >= this.clientHeight
  58. ) {
  59. // 球碰到了底边缘了
  60. this.gameOver = true;
  61. this.getCurshBreaks();
  62. }
  63. }

三、砖块的生成

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


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

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


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

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


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

再加上绘制砖块的函数:


  
  1. drawBreaks() {
  2. let _this = this;
  3. _this.breaks.forEach( item => {
  4. if (item.show) {
  5. _this.drawRoundRect(
  6. item.x,
  7. item.y,
  8. _this.breaksConfig.width,
  9. _this.breaksConfig.height,
  10. _this.breaksConfig.radius
  11. );
  12. }
  13. });
  14. }

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


  
  1. ( function animloop() {
  2. if (!_this.gameOver) {
  3. _this.movePannel();
  4. _this.moveBall();
  5. _this.drawAll();
  6. } else {
  7. _this.drawCrushBreaks();
  8. }
  9. window.requestAnimationFrame(animloop);
  10. })();
  11. ....
  12. drawAll() {
  13. this.ctx.clearRect( 0, 0, this.clientWidth, this.clientHeight);
  14. this.drawPannel();
  15. this.drawBall();
  16. this.drawScore();
  17. this.drawBreaks();
  18. }

五、游戏结束后的效果

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


  
  1. getCurshBreaks() {
  2. let _this = this;
  3. this.breaks.forEach( item => {
  4. if (item.show) {
  5. item.show = false;
  6. for ( let i = 0; i < 8; i++) { // 每个砖块粉碎为8个小球
  7. this.crushBalls.push({
  8. x: item.x + this.breaksConfig.width / 2,
  9. y: item.y + this.breaksConfig.height / 2,
  10. dx: _this.getRandomArbitrary( -6, 6),
  11. dy: _this.getRandomArbitrary( -6, 6),
  12. r: _this.getRandomArbitrary( 1, 4),
  13. color: _this.getRandomColor()
  14. });
  15. }
  16. }
  17. });
  18. },
  19. drawCrushBreaks() {
  20. this.ctx.clearRect( 0, 0, this.clientWidth, this.clientHeight);
  21. this.crushBalls.forEach( item => {
  22. this.ctx.beginPath();
  23. this.ctx.arc(item.x, item.y, item.r, 0, 2 * Math.PI);
  24. this.ctx.fillStyle = item.color;
  25. this.ctx.fill();
  26. this.ctx.closePath();
  27. item.x += item.dx;
  28. item.y += item.dy;
  29. if (
  30. // 碰到左右墙壁
  31. item.x - item.r < 0 ||
  32. item.x + item.r > this.clientWidth
  33. ) {
  34. item.dx = -item.dx;
  35. }
  36. if (
  37. // 碰到上下墙壁
  38. item.y - item.r < 0 ||
  39. item.y + item.r > this.clientHeight
  40. ) {
  41. item.dy = -item.dy;
  42. }
  43. });
  44. },

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

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


  
  1. <template>
  2. <div class="break-ball">
  3. <canvas id="breakBall" width="900" height="600"> </canvas>
  4. <div class="container" v-if="gameOver">
  5. <div class="dialog">
  6. <p class="once-again">本轮分数:{{score}}分 </p>
  7. <p class="once-again">真好玩! </p>
  8. <p class="once-again">再来一次~~ </p>
  9. <el-button class="once-again-btn" @click="init">开始 </el-button>
  10. </div>
  11. </div>
  12. </div>
  13. </template>
  14. <script>
  15. const randomColor = [
  16. "#FF95CA",
  17. "#00E3E3",
  18. "#00E3E3",
  19. "#6F00D2",
  20. "#6F00D2",
  21. "#C2C287",
  22. "#ECFFFF",
  23. "#FFDC35",
  24. "#93FF93",
  25. "#d0d0d0"
  26. ];
  27. export default {
  28. name: "BreakBall",
  29. data() {
  30. return {
  31. clientWidth: 0,
  32. clientHeight: 0,
  33. ctx: null,
  34. crushBalls: [],
  35. pannel: {
  36. x: 0,
  37. y: 0,
  38. height: 8,
  39. width: 100,
  40. speed: 8,
  41. dx: 0
  42. },
  43. ball: {
  44. x: 0,
  45. y: 0,
  46. r: 8,
  47. dx: -4,
  48. dy: -4
  49. },
  50. score: 0,
  51. gameOver: false,
  52. breaks: [],
  53. breaksConfig: {
  54. row: 6, // 排数
  55. height: 25, // 砖块高度
  56. width: 130, // 砖块宽度
  57. radius: 5, // 矩形圆角
  58. space: 0, // 间距
  59. colunm: 6 // 列数
  60. }
  61. };
  62. },
  63. mounted() {
  64. let _this = this;
  65. let container = document.getElementById( "breakBall");
  66. this.ctx = container.getContext( "2d");
  67. this.clientHeight = container.height;
  68. this.clientWidth = container.width;
  69. _this.init();
  70. document.onkeydown = function(e) {
  71. let key = window.event.keyCode;
  72. if (key === 37) {
  73. // 左键
  74. _this.pannel.dx = -_this.pannel.speed;
  75. } else if (key === 39) {
  76. // 右键
  77. _this.pannel.dx = _this.pannel.speed;
  78. }
  79. };
  80. document.onkeyup = function(e) {
  81. _this.pannel.dx = 0;
  82. };
  83. ( function animloop() {
  84. if (!_this.gameOver) {
  85. _this.movePannel();
  86. _this.moveBall();
  87. _this.drawAll();
  88. } else {
  89. _this.drawCrushBreaks();
  90. }
  91. window.requestAnimationFrame(animloop);
  92. })();
  93. },
  94. computed:{
  95. showBreaksCount(){
  96. return this.breaks.filter( item=>{
  97. return item.show;
  98. }).length;
  99. }
  100. },
  101. methods: {
  102. init() {
  103. let _this = this;
  104. _this.gameOver = false;
  105. this.pannel.y = this.clientHeight - this.pannel.height;
  106. this.pannel.x = this.clientWidth / 2 - this.pannel.width / 2;
  107. this.ball.y = this.clientHeight / 2;
  108. this.ball.x = this.clientWidth / 2;
  109. this.score = 0;
  110. this.ball.dx = [ -1, 1][ Math.floor( Math.random() * 2)]* 4;
  111. this.ball.dy = [ -1, 1][ Math.floor( Math.random() * 2)]* 4;
  112. this.crushBalls = [];
  113. this.breaks = [];
  114. // 计算得出砖块缝隙宽度
  115. this.breaksConfig.space = Math.floor(
  116. ( this.clientWidth -
  117. this.breaksConfig.width * this.breaksConfig.colunm) /
  118. ( this.breaksConfig.colunm + 1)
  119. );
  120. for ( let i = 0; i < _this.breaksConfig.row; i++) {
  121. for ( let j = 0; j < _this.breaksConfig.colunm; j++) {
  122. _this.breaks.push({
  123. x: this.breaksConfig.space * (j + 1) + this.breaksConfig.width * j,
  124. y: 10 * (i + 1) + this.breaksConfig.height * i,
  125. show: true
  126. });
  127. }
  128. }
  129. },
  130. drawAll() {
  131. this.ctx.clearRect( 0, 0, this.clientWidth, this.clientHeight);
  132. this.drawPannel();
  133. this.drawBall();
  134. this.drawScore();
  135. this.drawBreaks();
  136. },
  137. movePannel() {
  138. this.pannel.x += this.pannel.dx;
  139. if ( this.pannel.x > this.clientWidth - this.pannel.width) {
  140. this.pannel.x = this.clientWidth - this.pannel.width;
  141. } else if ( this.pannel.x < 0) {
  142. this.pannel.x = 0;
  143. }
  144. },
  145. moveBall() {
  146. this.ball.x += this.ball.dx;
  147. this.ball.y += this.ball.dy;
  148. this.breaksHandle();
  149. this.edgeHandle();
  150. },
  151. breaksHandle() {
  152. // 触碰砖块检测
  153. this.breaks.forEach( item => {
  154. if (item.show) {
  155. if (
  156. this.ball.x + this.ball.r > item.x &&
  157. this.ball.x - this.ball.r < item.x + this.breaksConfig.width &&
  158. this.ball.y + this.ball.r > item.y &&
  159. this.ball.y - this.ball.r < item.y + this.breaksConfig.height
  160. ) {
  161. item.show = false;
  162. this.ball.dy *= -1;
  163. this.score ++ ;
  164. if( this.showBreaksCount === 0){
  165. this.gameOver = true;
  166. }
  167. }
  168. }
  169. });
  170. },
  171. edgeHandle() {
  172. // 边缘检测
  173. // 碰到顶部反弹
  174. if ( this.ball.y - this.ball.r < 0) {
  175. this.ball.dy = - this.ball.dy;
  176. }
  177. if (
  178. // 碰到左右墙壁
  179. this.ball.x - this.ball.r < 0 ||
  180. this.ball.x + this.ball.r > this.clientWidth
  181. ) {
  182. this.ball.dx = - this.ball.dx;
  183. }
  184. if (
  185. this.ball.x >= this.pannel.x &&
  186. this.ball.x <= this.pannel.x + this.pannel.width &&
  187. this.ball.y + this.ball.r >= this.clientHeight - this.pannel.height
  188. ) {
  189. // 球的x在板子范围内并触碰到了板子
  190. this.ball.dy *= -1;
  191. } else if (
  192. ( this.ball.x < this.pannel.x ||
  193. this.ball.x > this.pannel.x + this.pannel.width) &&
  194. this.ball.y + this.ball.r >= this.clientHeight
  195. ) {
  196. // 球碰到了底边缘了
  197. this.gameOver = true;
  198. this.getCurshBreaks();
  199. }
  200. },
  201. drawScore(){
  202. this.ctx.beginPath();
  203. this.ctx.font= "14px Arial";
  204. this.ctx.fillStyle = "#FFF";
  205. this.ctx.fillText( "分数:"+ this.score, 10, this.clientHeight -14);
  206. this.ctx.closePath();
  207. },
  208. drawCrushBreaks() {
  209. this.ctx.clearRect( 0, 0, this.clientWidth, this.clientHeight);
  210. this.crushBalls.forEach( item => {
  211. this.ctx.beginPath();
  212. this.ctx.arc(item.x, item.y, item.r, 0, 2 * Math.PI);
  213. this.ctx.fillStyle = item.color;
  214. this.ctx.fill();
  215. this.ctx.closePath();
  216. item.x += item.dx;
  217. item.y += item.dy;
  218. if (
  219. // 碰到左右墙壁
  220. item.x - item.r < 0 ||
  221. item.x + item.r > this.clientWidth
  222. ) {
  223. item.dx = -item.dx;
  224. }
  225. if (
  226. // 碰到上下墙壁
  227. item.y - item.r < 0 ||
  228. item.y + item.r > this.clientHeight
  229. ) {
  230. item.dy = -item.dy;
  231. }
  232. });
  233. },
  234. getRandomColor() {
  235. return randomColor[ Math.floor( Math.random() * randomColor.length)];
  236. },
  237. getRandomArbitrary(min, max) {
  238. return Math.random() * (max - min) + min;
  239. },
  240. getCurshBreaks() {
  241. let _this = this;
  242. this.breaks.forEach( item => {
  243. if (item.show) {
  244. item.show = false;
  245. for ( let i = 0; i < 8; i++) {
  246. this.crushBalls.push({
  247. x: item.x + this.breaksConfig.width / 2,
  248. y: item.y + this.breaksConfig.height / 2,
  249. dx: _this.getRandomArbitrary( -6, 6),
  250. dy: _this.getRandomArbitrary( -6, 6),
  251. r: _this.getRandomArbitrary( 1, 4),
  252. color: _this.getRandomColor()
  253. });
  254. }
  255. }
  256. });
  257. },
  258. drawBall() {
  259. this.ctx.beginPath();
  260. this.ctx.arc( this.ball.x, this.ball.y, this.ball.r, 0, 2 * Math.PI);
  261. this.ctx.fillStyle = "#008b8b";
  262. this.ctx.fill();
  263. this.ctx.closePath();
  264. },
  265. drawPannel() {
  266. this.drawRoundRect(
  267. this.pannel.x,
  268. this.pannel.y,
  269. this.pannel.width,
  270. this.pannel.height,
  271. 5
  272. );
  273. },
  274. drawRoundRect(x, y, width, height, radius) {
  275. this.ctx.beginPath();
  276. this.ctx.arc(x + radius, y + radius, radius, Math.PI, ( Math.PI * 3) / 2);
  277. this.ctx.lineTo(width - radius + x, y);
  278. this.ctx.arc(
  279. width - radius + x,
  280. radius + y,
  281. radius,
  282. ( Math.PI * 3) / 2,
  283. Math.PI * 2
  284. );
  285. this.ctx.lineTo(width + x, height + y - radius);
  286. this.ctx.arc(
  287. width - radius + x,
  288. height - radius + y,
  289. radius,
  290. 0,
  291. ( Math.PI * 1) / 2
  292. );
  293. this.ctx.lineTo(radius + x, height + y);
  294. this.ctx.arc(
  295. radius + x,
  296. height - radius + y,
  297. radius,
  298. ( Math.PI * 1) / 2,
  299. Math.PI
  300. );
  301. this.ctx.fillStyle = "#008b8b";
  302. this.ctx.fill();
  303. this.ctx.closePath();
  304. },
  305. drawBreaks() {
  306. let _this = this;
  307. _this.breaks.forEach( item => {
  308. if (item.show) {
  309. _this.drawRoundRect(
  310. item.x,
  311. item.y,
  312. _this.breaksConfig.width,
  313. _this.breaksConfig.height,
  314. _this.breaksConfig.radius
  315. );
  316. }
  317. });
  318. }
  319. }
  320. };
  321. </script>
  322. <!-- Add "scoped" attribute to limit CSS to this component only -->
  323. <style scoped lang="scss">
  324. .break-ball {
  325. width: 900px;
  326. height: 600px;
  327. position: relative;
  328. #breakBall {
  329. background: #2a4546;
  330. }
  331. .container {
  332. position: absolute;
  333. top: 0;
  334. right: 0;
  335. bottom: 0;
  336. left: 0;
  337. background-color: rgba(0, 0, 0, 0.3);
  338. text-align: center;
  339. font-size: 0;
  340. white-space: nowrap;
  341. overflow: auto;
  342. }
  343. .container:after {
  344. content: "";
  345. display: inline-block;
  346. height: 100%;
  347. vertical-align: middle;
  348. }
  349. .dialog {
  350. width: 400px;
  351. height: 300px;
  352. background: rgba(255, 255, 255, 0.5);
  353. box-shadow: 3px 3px 6px 3px rgba(0, 0, 0, 0.3);
  354. display: inline-block;
  355. vertical-align: middle;
  356. text-align: left;
  357. font-size: 28px;
  358. color: #fff;
  359. font-weight: 600;
  360. border-radius: 10px;
  361. white-space: normal;
  362. text-align: center;
  363. .once-again-btn {
  364. background: #1f9a9a;
  365. border: none;
  366. color: #fff;
  367. }
  368. }
  369. }
  370. </style>

 


转载:https://blog.csdn.net/denglouhen/article/details/115487802
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场