当前位置:   article > 正文

为了让你们进阶 Canvas,我花 7 小时写了 3 个有趣的小游戏!!!

canvas小游戏

点击上方 前端瓶子君,关注公众号

回复算法,加入前端编程面试算法每日一题群

前言

大家好,我是林三心,相信大家看了我前一篇canvas入门文章为了让她10分钟入门canvas,我熬夜写了3个小项目和这篇文章,对canvas已经有了入门级的了解。今天,我又用canvas写了三个有趣的小游戏,来哄你们开心,没错,我的心里只有你们,没有她。

现在是凌晨0点15分,咱们开搞????????????????????????????????????????,一边调试一边把这篇文章写了!!!

贪吃蛇????

最终效果如下: 实现步骤分为以下几步:

  • 1、把蛇画出来

  • 2、让蛇动起来

  • 3、随机投放食物

  • 4、蛇吃食物

  • 5、边缘检测与撞自己检测

1. 把蛇画出来

其实画蛇很简单,蛇就是由蛇头和蛇身组成,而其实都可以用正方格来表示,蛇头就是一个方格,而蛇身可以是很多个方格

画方格可以用ctx.fillRect来画,蛇头使用head表示,而蛇身使用数组body来表示

  1. // html
  2. <canvas id="canvas" width="800" height="800"></canvas>
  3. // js
  4. draw()
  5. function draw() {
  6.     const canvas = document.getElementById('canvas')
  7.     const ctx = canvas.getContext('2d')
  8.     // 小方格的构造函数
  9.     function Rect(x, y, width, height, color) {
  10.         this.x = x
  11.         this.y = y
  12.         this.width = width
  13.         this.height = height
  14.         this.color = color
  15.     }
  16.     Rect.prototype.draw = function () {
  17.         ctx.beginPath()
  18.         ctx.fillStyle = this.color
  19.         ctx.fillRect(this.x, this.y, this.width, this.height)
  20.         ctx.strokeRect(this.x, this.y, this.width, this.height)
  21.     }
  22.     // 蛇的构造函数
  23.     function Snake(length = 0) {
  24.         this.length = length
  25.         // 蛇头
  26.         this.head = new Rect(canvas.width / 2, canvas.height / 24040'red')
  27.         // 蛇身
  28.         this.body = []
  29.         let x = this.head.x - 40
  30.         let y = this.head.y
  31.         for (let i = 0; i < this.length; i++) {
  32.             const rect = new Rect(x, y, 4040'yellow')
  33.             this.body.push(rect)
  34.             x -= 40
  35.         }
  36.     }
  37.     Snake.prototype.drawSnake = function () {
  38.         // 绘制蛇头
  39.         this.head.draw()
  40.         // 绘制蛇身
  41.         for (let i = 0; i < this.body.length; i++) {
  42.             this.body[i].draw()
  43.         }
  44.     }
  45.     const snake = new Snake(3)
  46.     snake.drawSnake()
  47. }
  48. 复制代码

2. 让蛇动起来

蛇动起来有两种情况:

  • 1、蛇一开始就会默认向右移动

  • 2、通过方向键控制,往不同方向移动

这两种情况每秒都是移动一个方格的位置

让蛇动起来,其实原理很简单,我就以蛇向右移动来举例子吧:

  • 1、蛇头先右移一个方格距离,蛇身不动

  • 2、蛇身首部加一个方格

  • 3、蛇身尾部的方格去除

  • 4、利用定时器,造成蛇不断向右移动的视觉

  1.     Snake.prototype.moveSnake = function () {
  2.         // 将蛇头上一次状态,拼到蛇身首部
  3.         const rect = new Rect(this.head.x, this.head.y, this.head.width, this.head.height, 'yellow')
  4.         this.body.unshift(rect)
  5.         this.body.pop()
  6.         // 根据方向,控制蛇头的坐标
  7.         switch (this.direction) {
  8.             case 0:
  9.                 this.head.x -= this.head.width
  10.                 break
  11.             case 1:
  12.                 this.head.y -= this.head.height
  13.                 break
  14.             case 2:
  15.                 this.head.x += this.head.width
  16.                 break
  17.             case 3:
  18.                 this.head.y += this.head.height
  19.                 break
  20.         }
  21.     }
  22.     document.onkeydown = function (e) {
  23.         // 键盘事件
  24.         e = e || window.event
  25.         // 左37  上38  右39  下40
  26.         switch (e.keyCode) {
  27.             case 37:
  28.                 console.log(37)
  29.                 // 三元表达式,防止右移动时按左,下面同理(贪吃蛇可不能直接掉头)
  30.                 snake.direction = snake.direction === 2 ? 2 : 0
  31.                 snake.moveSnake()
  32.                 break
  33.             case 38:
  34.                 console.log(38)
  35.                 snake.direction = snake.direction === 3 ? 3 : 1
  36.                 break
  37.             case 39:
  38.                 console.log(39)
  39.                 snake.direction = snake.direction === 0 ? 0 : 2
  40.                 break
  41.             case 40:
  42.                 console.log(40)
  43.                 snake.direction = snake.direction === 1 ? 1 : 3
  44.                 break
  45.         }
  46.     }
  47.     const snake = new Snake(3)
  48.     // 默认direction为2,也就是右
  49.     snake.direction = 2
  50.     snake.drawSnake()
  51.     function animate() {
  52.         // 先清空
  53.         ctx.clearRect(00, canvas.width, canvas.height)
  54.         // 移动
  55.         snake.moveSnake()
  56.         // 再画
  57.         snake.drawSnake()
  58.     }
  59.     var timer = setInterval(() => {
  60.         animate()
  61.     }, 100)
  62. }
  63. 复制代码

实现效果如下:

蛇动起来.gif

3. 随机投放食物

随机投放食物,也就是在画布中随机画一个方格,要注意以下两点:

  • 1、坐标要在画布范围内

  • 2、食物不能投到蛇身或者蛇头上(这样会把蛇砸晕的嘿嘿)

  1.     function randomFood(snake) {
  2.         let isInSnake = true
  3.         let rect
  4.         while (isInSnake) {
  5.             const x = Math.round(Math.random() * (canvas.width - 40) / 40) * 40
  6.             const y = Math.round(Math.random() * (canvas.height - 40) / 40) * 40
  7.             console.log(x, y)
  8.             // 保证是40的倍数啊
  9.             rect = new Rect(x, y, 4040'blue')
  10.             // 判断食物是否与蛇头蛇身重叠
  11.             if ((snake.head.x === x && snake.head.y === y) || snake.body.find(item => item.x === x && item.y === y)) {
  12.                 isInSnake = true
  13.                 continue
  14.             } else {
  15.                 isInSnake = false
  16.             }
  17.         }
  18.         return rect
  19.     }
  20.     const snake = new Snake(3)
  21.     // 默认direction为2,也就是右
  22.     snake.direction = 2
  23.     snake.drawSnake()
  24.     // 创建随机食物实例
  25.     var food = randomFood(snake)
  26.     // 画出食物
  27.     food.draw()
  28.     function animate() {
  29.         // 先清空
  30.         ctx.clearRect(00, canvas.width, canvas.height)
  31.         // 移动
  32.         snake.moveSnake()
  33.         // 再画
  34.         snake.drawSnake()
  35.         food.draw()
  36.     }
  37. 复制代码

效果如下,随机食物画出来了:

4. 蛇吃食物

其实蛇吃食物,很简单理解,也就是蛇头移动到跟食物的坐标重叠时,就算是吃到食物了,注意两点:

  • 1、吃到食物后,蛇身要延长一个空格

  • 2、吃到食物后,随机食物要变换位置

  1. const canvas = document.getElementById('canvas')
  2. const ctx = canvas.getContext('2d')
  3. // 定义一个全局的是否吃到食物的一个变量
  4. let isEatFood = false
  5.     
  6.     Snake.prototype.moveSnake = function () {
  7.         // 将蛇头上一次状态,拼到蛇身首部
  8.         const rect = new Rect(this.head.x, this.head.y, this.head.width, this.head.height, 'yellow')
  9.         this.body.unshift(rect)
  10.         // 判断蛇头是否与食物重叠,重叠就是吃到了,没重叠就是没吃到
  11.         isEatFood = food && this.head.x === food.x && this.head.y === food.y
  12.         // 咱们上面在蛇身首部插入方格
  13.         if (!isEatFood) {
  14.             // 没吃到就要去尾,相当于整条蛇没变长
  15.             this.body.pop()
  16.         } else {
  17.             // 吃到了就不去尾,相当于整条蛇延长一个方格
  18.             // 并且吃到了,就要重新生成一个随机食物
  19.             food = randomFood(this)
  20.             food.draw()
  21.             isEatFood = false
  22.         }
  23.         // 根据方向,控制蛇头的坐标
  24.         switch (this.direction) {
  25.             case 0:
  26.                 this.head.x -= this.head.width
  27.                 break
  28.             case 1:
  29.                 this.head.y -= this.head.height
  30.                 break
  31.             case 2:
  32.                 this.head.x += this.head.width
  33.                 break
  34.             case 3:
  35.                 this.head.y += this.head.height
  36.                 break
  37.         }
  38.     }
  39. 复制代码

5. 碰边界与碰自己

众所周知,蛇头碰到边界,或者碰到蛇身,都会终止游戏

  1.     Snake.prototype.drawSnake = function () {
  2.         // 如果碰到了
  3.         if (isHit(this)) {
  4.             // 清除定时器
  5.             clearInterval(timer)
  6.             const con = confirm(`总共吃了${this.body.length - this.length}个食物,重新开始吗`)
  7.             // 是否重开
  8.             if (con) {
  9.                 draw()
  10.             }
  11.             return
  12.         }
  13.         // 绘制蛇头
  14.         this.head.draw()
  15.         // 绘制蛇身
  16.         for (let i = 0; i < this.body.length; i++) {
  17.             this.body[i].draw()
  18.         }
  19.     }
  20.     
  21.     
  22.     function isHit(snake) {
  23.         const head = snake.head
  24.         // 是否碰到左右边界
  25.         const xLimit = head.x < 0 || head.x >= canvas.width
  26.         // 是否碰到上下边界
  27.         const yLimit = head.y < 0 || head.y >= canvas.height
  28.         // 是否撞到蛇身
  29.         const hitSelf = snake.body.find(({ x, y }) => head.x === x && head.y === y)
  30.         // 三者其中一个为true则游戏结束
  31.         return xLimit || yLimit || hitSelf
  32.     }
  33. 复制代码

自此,贪吃蛇????小游戏完成喽:

6. 全部代码:

  1. draw()
  2. function draw() {
  3.     const canvas = document.getElementById('canvas')
  4.     const ctx = canvas.getContext('2d')
  5.     // 定义一个全局的是否吃到食物的一个变量
  6.     let isEatFood = false
  7.     // 小方格的构造函数
  8.     function Rect(x, y, width, height, color) {
  9.         this.x = x
  10.         this.y = y
  11.         this.width = width
  12.         this.height = height
  13.         this.color = color
  14.     }
  15.     Rect.prototype.draw = function () {
  16.         ctx.beginPath()
  17.         ctx.fillStyle = this.color
  18.         ctx.fillRect(this.x, this.y, this.width, this.height)
  19.         ctx.strokeRect(this.x, this.y, this.width, this.height)
  20.     }
  21.     // 蛇的构造函数
  22.     function Snake(length = 0) {
  23.         this.length = length
  24.         // 蛇头
  25.         this.head = new Rect(canvas.width / 2, canvas.height / 24040'red')
  26.         // 蛇身
  27.         this.body = []
  28.         let x = this.head.x - 40
  29.         let y = this.head.y
  30.         for (let i = 0; i < this.length; i++) {
  31.             const rect = new Rect(x, y, 4040'yellow')
  32.             this.body.push(rect)
  33.             x -= 40
  34.         }
  35.     }
  36.     Snake.prototype.drawSnake = function () {
  37.         // 如果碰到了
  38.         if (isHit(this)) {
  39.             // 清除定时器
  40.             clearInterval(timer)
  41.             const con = confirm(`总共吃了${this.body.length - this.length}个食物,重新开始吗`)
  42.             // 是否重开
  43.             if (con) {
  44.                 draw()
  45.             }
  46.             return
  47.         }
  48.         // 绘制蛇头
  49.         this.head.draw()
  50.         // 绘制蛇身
  51.         for (let i = 0; i < this.body.length; i++) {
  52.             this.body[i].draw()
  53.         }
  54.     }
  55.     Snake.prototype.moveSnake = function () {
  56.         // 将蛇头上一次状态,拼到蛇身首部
  57.         const rect = new Rect(this.head.x, this.head.y, this.head.width, this.head.height, 'yellow')
  58.         this.body.unshift(rect)
  59.         // 判断蛇头是否与食物重叠,重叠就是吃到了,没重叠就是没吃到
  60.         isEatFood = food && this.head.x === food.x && this.head.y === food.y
  61.         // 咱们上面在蛇身首部插入方格
  62.         if (!isEatFood) {
  63.             // 没吃到就要去尾,相当于整条蛇没变长
  64.             this.body.pop()
  65.         } else {
  66.             // 吃到了就不去尾,相当于整条蛇延长一个方格
  67.             // 并且吃到了,就要重新生成一个随机食物
  68.             food = randomFood(this)
  69.             food.draw()
  70.             isEatFood = false
  71.         }
  72.         // 根据方向,控制蛇头的坐标
  73.         switch (this.direction) {
  74.             case 0:
  75.                 this.head.x -= this.head.width
  76.                 break
  77.             case 1:
  78.                 this.head.y -= this.head.height
  79.                 break
  80.             case 2:
  81.                 this.head.x += this.head.width
  82.                 break
  83.             case 3:
  84.                 this.head.y += this.head.height
  85.                 break
  86.         }
  87.     }
  88.     document.onkeydown = function (e) {
  89.         // 键盘事件
  90.         e = e || window.event
  91.         // 左37  上38  右39  下40
  92.         switch (e.keyCode) {
  93.             case 37:
  94.                 console.log(37)
  95.                 // 三元表达式,防止右移动时按左,下面同理(贪吃蛇可不能直接掉头)
  96.                 snake.direction = snake.direction === 2 ? 2 : 0
  97.                 snake.moveSnake()
  98.                 break
  99.             case 38:
  100.                 console.log(38)
  101.                 snake.direction = snake.direction === 3 ? 3 : 1
  102.                 break
  103.             case 39:
  104.                 console.log(39)
  105.                 snake.direction = snake.direction === 0 ? 0 : 2
  106.                 break
  107.             case 40:
  108.                 console.log(40)
  109.                 snake.direction = snake.direction === 1 ? 1 : 3
  110.                 break
  111.         }
  112.     }
  113.     function randomFood(snake) {
  114.         let isInSnake = true
  115.         let rect
  116.         while (isInSnake) {
  117.             const x = Math.round(Math.random() * (canvas.width - 40) / 40) * 40
  118.             const y = Math.round(Math.random() * (canvas.height - 40) / 40) * 40
  119.             console.log(x, y)
  120.             // 保证是40的倍数啊
  121.             rect = new Rect(x, y, 4040'blue')
  122.             // 判断食物是否与蛇头蛇身重叠
  123.             if ((snake.head.x === x && snake.head.y === y) || snake.body.find(item => item.x === x && item.y === y)) {
  124.                 isInSnake = true
  125.                 continue
  126.             } else {
  127.                 isInSnake = false
  128.             }
  129.         }
  130.         return rect
  131.     }
  132.     function isHit(snake) {
  133.         const head = snake.head
  134.         // 是否碰到左右边界
  135.         const xLimit = head.x < 0 || head.x >= canvas.width
  136.         // 是否碰到上下边界
  137.         const yLimit = head.y < 0 || head.y >= canvas.height
  138.         // 是否撞到蛇身
  139.         const hitSelf = snake.body.find(({ x, y }) => head.x === x && head.y === y)
  140.         // 三者其中一个为true则游戏结束
  141.         return xLimit || yLimit || hitSelf
  142.     }
  143.     const snake = new Snake(3)
  144.     // 默认direction为2,也就是右
  145.     snake.direction = 2
  146.     snake.drawSnake()
  147.     // 创建随机食物实例
  148.     var food = randomFood(snake)
  149.     // 画出食物
  150.     food.draw()
  151.     function animate() {
  152.         // 先清空
  153.         ctx.clearRect(00, canvas.width, canvas.height)
  154.         // 移动
  155.         snake.moveSnake()
  156.         // 再画
  157.         snake.drawSnake()
  158.         food.draw()
  159.     }
  160.     var timer = setInterval(() => {
  161.         animate()
  162.     }, 100)
  163. }
  164. 复制代码

星星连线

效果如下,是不是很酷炫呢,兄弟们(背景图片[3] 可以自己去下载一下):

星星连线.gif

这个小游戏可分为以下几步:

  • 1、画出单个小星星并使他移动

  • 2、造出一百个小星星

  • 3、星星之间靠近时,进行连线

  • 4、鼠标移动生成小星星

  • 5、鼠标点击产生5个小星星

1. 画出单个小星星,并使他移动

其实移动星星很简单,就是清除后重新绘制星星,并利用定时器,就会有移动的视觉了。注意点在于:碰到边界要反弹

  1. // html
  2. <style>
  3.     #canvas {
  4.             background: url(./光能使者.jpg) 0 0/cover no-repeat;
  5.         }
  6. </style>
  7. <canvas id="canvas"></canvas>
  8. // js
  9. const canvas = document.getElementById('canvas')
  10. const ctx = canvas.getContext('2d')
  11. // 获取当前视图的宽度和高度
  12. let aw = document.documentElement.clientWidth || document.body.clientWidth
  13. let ah = document.documentElement.clientHeight || document.body.clientHeight
  14. // 赋值给canvas
  15. canvas.width = aw
  16. canvas.height = ah
  17. // 屏幕变动时也要监听实时宽高
  18. window.onresize = function () {
  19.     aw = document.documentElement.clientWidth || document.body.clientWidth
  20.     ah = document.documentElement.clientHeight || document.body.clientHeight
  21.     // 赋值给canvas
  22.     canvas.width = aw
  23.     canvas.height = ah
  24. }
  25. // 本游戏无论是实心,还是线条,色调都是白色
  26. ctx.fillStyle = 'white'
  27. ctx.strokeStyle = 'white'
  28. function Star(x, y, r) {
  29.     // x,y是坐标,r是半径
  30.     this.x = x
  31.     this.y = y
  32.     this.r = r
  33.     // speed参数,在  -3 ~ 3 之间取值
  34.     this.speedX = (Math.random() * 3) * Math.pow(-1, Math.round(Math.random()))
  35.     this.speedY = (Math.random() * 3) * Math.pow(-1, Math.round(Math.random()))
  36. }
  37. Star.prototype.draw = function () {
  38.     ctx.beginPath()
  39.     ctx.arc(this.x, this.y, this.r, 0, Math.PI * 2)
  40.     ctx.fill()
  41.     ctx.closePath()
  42. }
  43. Star.prototype.move = function () {
  44.     this.x -= this.speedX
  45.     this.y -= this.speedY
  46.     // 碰到边界时,反弹,只需要把speed取反就行
  47.     if (this.x < 0 || this.x > aw) this.speedX *= -1
  48.     if (this.y < 0 || this.y > ah) this.speedY *= -1
  49. }
  50. // 随机在canvas范围内找一个坐标画星星
  51. const star = new Star(Math.random() * aw, Math.random() * ah, 3)
  52. star
  53. // 星星的移动
  54. setInterval(() => {
  55.     ctx.clearRect(00, aw, ah)
  56.     star.move()
  57.     star.draw()
  58. }, 50)
  59. 复制代码

达到以下移动以及反弹的效果:

星星反弹.gif

2、画100个小星星

创建一个数组stars来存储这些星星

  1. const stars = []
  2. for (let i = 0; i < 100; i++) {
  3.     // 随机在canvas范围内找一个坐标画星星
  4.     stars.push(new Star(Math.random() * aw, Math.random() * ah, 3))
  5. }
  6. // 星星的移动
  7. setInterval(() => {
  8.     ctx.clearRect(00, aw, ah)
  9.     // 遍历移动渲染
  10.     stars.forEach(star => {
  11.         star.move()
  12.         star.draw()
  13.     })
  14. }, 50)
  15. 复制代码

效果如下:

100个星星.gif

3. 星星之间靠近时,进行连线

当两个星星的x和y相差都小于50时,就进行连线,连线只需要使用ctx.moveTo和ctx.lineTo就可以了

  1. function drawLine(startX, startY, endX, endY) {
  2.     ctx.beginPath()
  3.     ctx.moveTo(startX, startY)
  4.     ctx.lineTo(endX, endY)
  5.     ctx.stroke()
  6.     ctx.closePath()
  7. }
  8. // 星星的移动
  9. setInterval(() => {
  10.     ctx.clearRect(00, aw, ah)
  11.     // 遍历移动渲染
  12.     stars.forEach(star => {
  13.         star.move()
  14.         star.draw()
  15.     })
  16.     stars.forEach((star, index) => {
  17.         // 类似于冒泡排序那样,去比较,确保所有星星两两之间都比较到
  18.         for (let i = index + 1; i < stars.length; i++) {
  19.             if (Math.abs(star.x - stars[i].x) < 50 && Math.abs(star.y - stars[i].y) < 50) {
  20.                 drawLine(star.x, star.y, stars[i].x, stars[i].y)
  21.             }
  22.         }
  23.     })
  24. }, 50)
  25. 复制代码

大家可以想一想,为什么两个forEach不能何在一起去执行。这是个值得思考的问题,或者大家可以合并在一起执行,试试效果,获取就懂了。算是给大家留的一个作业哈!

效果如下:

连线星星.gif

4.鼠标移动时带着小星星

也就是鼠标到哪,那个小星星就到哪,并且这个小星星走到哪都会跟距离近的小星星连线

  1. const mouseStar = new Star(003)
  2. canvas.onmousemove = function (e) {
  3.     mouseStar.x = e.clientX
  4.     mouseStar.y = e.clientY
  5. }
  6. // 星星的移动
  7. setInterval(() => {
  8.     ctx.clearRect(00, aw, ah)
  9.     // 鼠标星星渲染
  10.     mouseStar.draw()
  11.     // 遍历移动渲染
  12.     stars.forEach(star => {
  13.         star.move()
  14.         star.draw()
  15.     })
  16.     stars.forEach((star, index) => {
  17.         // 类似于冒泡排序那样,去比较,确保所有星星两两之间都比较到
  18.         for (let i = index + 1; i < stars.length; i++) {
  19.             if (Math.abs(star.x - stars[i].x) < 50 && Math.abs(star.y - stars[i].y) < 50) {
  20.                 drawLine(star.x, star.y, stars[i].x, stars[i].y)
  21.             }
  22.         }
  23.         // 判断鼠标星星连线
  24.         if (Math.abs(mouseStar.x - star.x) < 50 && Math.abs(mouseStar.y - star.y) < 50) {
  25.             drawLine(mouseStar.x, mouseStar.y, star.x, star.y)
  26.         }
  27.     })
  28. }, 50)
  29. 复制代码

效果如下:

鼠标星星.gif

5. 鼠标点击生成五个小星星

思路就是,鼠标点击,生成5个小星星,并加到数组stars

  1. window.onclick = function (e) {
  2.     for (let i = 0; i < 5; i++) {
  3.         stars.push(new Star(e.clientX, e.clientY, 3))
  4.     }
  5. }
  6. 复制代码

效果如下:

点击生成星星.gif

最终效果:TODO: 图片待插入

6. 全部代码

  1. const canvas = document.getElementById('canvas')
  2. const ctx = canvas.getContext('2d')
  3. // 获取当前视图的宽度和高度
  4. let aw = document.documentElement.clientWidth || document.body.clientWidth
  5. let ah = document.documentElement.clientHeight || document.body.clientHeight
  6. // 赋值给canvas
  7. canvas.width = aw
  8. canvas.height = ah
  9. // 屏幕变动时也要监听实时宽高
  10. window.onresize = function () {
  11.     aw = document.documentElement.clientWidth || document.body.clientWidth
  12.     ah = document.documentElement.clientHeight || document.body.clientHeight
  13.     // 赋值给canvas
  14.     canvas.width = aw
  15.     canvas.height = ah
  16. }
  17. // 本游戏无论是实心,还是线条,色调都是白色
  18. ctx.fillStyle = 'white'
  19. ctx.strokeStyle = 'white'
  20. function Star(x, y, r) {
  21.     // x,y是坐标,r是半径
  22.     this.x = x
  23.     this.y = y
  24.     this.r = r
  25.     // speed参数,在  -3 ~ 3 之间取值
  26.     this.speedX = (Math.random() * 3) * Math.pow(-1, Math.round(Math.random()))
  27.     this.speedY = (Math.random() * 3) * Math.pow(-1, Math.round(Math.random()))
  28. }
  29. Star.prototype.draw = function () {
  30.     ctx.beginPath()
  31.     ctx.arc(this.x, this.y, this.r, 0, Math.PI * 2)
  32.     ctx.fill()
  33.     ctx.closePath()
  34. }
  35. Star.prototype.move = function () {
  36.     this.x -= this.speedX
  37.     this.y -= this.speedY
  38.     // 碰到边界时,反弹,只需要把speed取反就行
  39.     if (this.x < 0 || this.x > aw) this.speedX *= -1
  40.     if (this.y < 0 || this.y > ah) this.speedY *= -1
  41. }
  42. function drawLine(startX, startY, endX, endY) {
  43.     ctx.beginPath()
  44.     ctx.moveTo(startX, startY)
  45.     ctx.lineTo(endX, endY)
  46.     ctx.stroke()
  47.     ctx.closePath()
  48. }
  49. const stars = []
  50. for (let i = 0; i < 100; i++) {
  51.     // 随机在canvas范围内找一个坐标画星星
  52.     stars.push(new Star(Math.random() * aw, Math.random() * ah, 3))
  53. }
  54. const mouseStar = new Star(003)
  55. canvas.onmousemove = function (e) {
  56.     mouseStar.x = e.clientX
  57.     mouseStar.y = e.clientY
  58. }
  59. window.onclick = function (e) {
  60.     for (let i = 0; i < 5; i++) {
  61.         stars.push(new Star(e.clientX, e.clientY, 3))
  62.     }
  63. }
  64. // 星星的移动
  65. setInterval(() => {
  66.     ctx.clearRect(00, aw, ah)
  67.     // 鼠标星星渲染
  68.     mouseStar.draw()
  69.     // 遍历移动渲染
  70.     stars.forEach(star => {
  71.         star.move()
  72.         star.draw()
  73.     })
  74.     stars.forEach((star, index) => {
  75.         // 类似于冒泡排序那样,去比较,确保所有星星两两之间都比较到
  76.         for (let i = index + 1; i < stars.length; i++) {
  77.             if (Math.abs(star.x - stars[i].x) < 50 && Math.abs(star.y - stars[i].y) < 50) {
  78.                 drawLine(star.x, star.y, stars[i].x, stars[i].y)
  79.             }
  80.         }
  81.         if (Math.abs(mouseStar.x - star.x) < 50 && Math.abs(mouseStar.y - star.y) < 50) {
  82.             drawLine(mouseStar.x, mouseStar.y, star.x, star.y)
  83.         }
  84.     })
  85. }, 50)
  86. 复制代码

3. 五子棋

看看将实现的效果:

五子棋分为以下步骤:

  • 1、画出棋盘

  • 2、黑白棋切换着下,不能覆盖已下的坑位

  • 3、判断是否五连子,是的话就赢了

  • 4、彩蛋:跟AI下棋(实现单人玩游戏)

1. 画出棋盘

其实很简单,利用ctx.moveTo和ctx.lineTo,横着画15条线,竖着画15条线,就OK了。

  1. // html
  2. #canvas {
  3.             background: #e3cdb0;
  4.         }
  5. <canvas id="canvas" width="600" height="600"></canvas>
  6. // js
  7. play()
  8. function play() {
  9.     const canvas = document.getElementById('canvas')
  10.     const ctx = canvas.getContext('2d')
  11.     // 绘制棋盘
  12.     // 水平,总共15条线
  13.     for (let i = 0; i < 15; i++) {
  14.         ctx.beginPath()
  15.         ctx.moveTo(2020 + i * 40)
  16.         ctx.lineTo(58020 + i * 40)
  17.         ctx.stroke()
  18.         ctx.closePath()
  19.     }
  20.     // 垂直,总共15条线
  21.     for (let i = 0; i < 15; i++) {
  22.         ctx.beginPath()
  23.         ctx.moveTo(20 + i * 4020)
  24.         ctx.lineTo(20 + i * 40580)
  25.         ctx.stroke()
  26.         ctx.closePath()
  27.     }
  28. }
  29. 复制代码

这样就画出了棋盘:

截屏2021-07-25 下午12.25.09.png

2. 黑白棋切换着下

  • 1、鼠标点击事件,获取坐标,将棋画出来(ctx.arc

  • 2、确保已下的棋位不能重复下

第一步,获取鼠标坐标,但是我们要注意一件事,棋子只能下在线的交叉处,所以拿到鼠标坐标后,要做一下处理,四舍五入,以最近的一个线交叉点为圆的圆心

第二步,如何确保棋位不重复下呢?咱们可以使用一个二维数组来记录,初始是0,下过黑棋就变为1,下过白棋就变为2,但是这里要注意一点,数组索引的x,y跟画布坐标的x,y是相反的,所以后面代码里坐标反过来,希望大家能思考一下为啥。

截屏2021-07-25 下午12.33.29.png
  1. // 是否下黑棋
  2.     // 黑棋先走
  3.     let isBlack = true
  4.     // 棋盘二维数组
  5.     let cheeks = []
  6.     for (let i = 0; i < 15; i++) {
  7.         cheeks[i] = new Array(15).fill(0)
  8.     }
  9.     canvas.onclick = function (e) {
  10.         const clientX = e.clientX
  11.         const clientY = e.clientY
  12.         // 对40进行取整,确保棋子落在交叉处
  13.         const x = Math.round((clientX - 20) / 40) * 40 + 20
  14.         const y = Math.round((clientY - 20) / 40) * 40 + 20
  15.         // cheeks二维数组的索引
  16.         // 这么写有点冗余,这么写你们好理解一点
  17.         const cheeksX = (x - 20) / 40
  18.         const cheeksY = (y - 20) / 40
  19.         // 对应元素不为0说明此地方已有棋,返回
  20.         if (cheeks[cheeksY][cheeksX]) return
  21.         // 黑棋为1,白棋为2
  22.         cheeks[cheeksY][cheeksX] = isBlack ? 1 : 2
  23.         ctx.beginPath()
  24.         // 画圆
  25.         ctx.arc(x, y, 2002 * Math.PI)
  26.         // 判断走黑还是白
  27.         ctx.fillStyle = isBlack ? 'black' : 'white'
  28.         ctx.fill()
  29.         ctx.closePath()
  30.         // 切换黑白
  31.         isBlack = !isBlack
  32.     }
  33. 复制代码

效果如下:

3. 判断是否五连子

如何判断呢?有四种情况:上下五连子,左右吴连子,左上右下五连子,右上左下五连子,只要咱们每次落子的时候全部判断一次就好了。

截屏2021-07-25 下午12.55.53.png

顺便附上所有代码

  1. play()
  2. function play() {
  3.     const canvas = document.getElementById('canvas')
  4.     const ctx = canvas.getContext('2d')
  5.     // 绘制棋盘
  6.     // 水平,总共15条线
  7.     for (let i = 0; i < 15; i++) {
  8.         ctx.beginPath()
  9.         ctx.moveTo(2020 + i * 40)
  10.         ctx.lineTo(58020 + i * 40)
  11.         ctx.stroke()
  12.         ctx.closePath()
  13.     }
  14.     // 垂直,总共15条线
  15.     for (let i = 0; i < 15; i++) {
  16.         ctx.beginPath()
  17.         ctx.moveTo(20 + i * 4020)
  18.         ctx.lineTo(20 + i * 40580)
  19.         ctx.stroke()
  20.         ctx.closePath()
  21.     }
  22.     // 是否下黑棋
  23.     // 黑棋先走
  24.     let isBlack = true
  25.     // 棋盘二维数组
  26.     let cheeks = []
  27.     for (let i = 0; i < 15; i++) {
  28.         cheeks[i] = new Array(15).fill(0)
  29.     }
  30.     canvas.onclick = function (e) {
  31.         const clientX = e.clientX
  32.         const clientY = e.clientY
  33.         // 对40进行取整,确保棋子落在交叉处
  34.         const x = Math.round((clientX - 20) / 40) * 40 + 20
  35.         const y = Math.round((clientY - 20) / 40) * 40 + 20
  36.         // cheeks二维数组的索引
  37.         // 这么写有点冗余,这么写你们好理解一点
  38.         const cheeksX = (x - 20) / 40
  39.         const cheeksY = (y - 20) / 40
  40.         // 对应元素不为0说明此地方已有棋,返回
  41.         if (cheeks[cheeksY][cheeksX]) return
  42.         // 黑棋为1,白棋为2
  43.         cheeks[cheeksY][cheeksX] = isBlack ? 1 : 2
  44.         ctx.beginPath()
  45.         // 画圆
  46.         ctx.arc(x, y, 2002 * Math.PI)
  47.         // 判断走黑还是白
  48.         ctx.fillStyle = isBlack ? 'black' : 'white'
  49.         ctx.fill()
  50.         ctx.closePath()
  51.         // canvas画图是异步的,保证画出来再去检测输赢
  52.         setTimeout(() => {
  53.             if (isWin(cheeksX, cheeksY)) {
  54.                 const con = confirm(`${isBlack ? '黑棋' : '白棋'}赢了!是否重新开局?`)
  55.                 // 重新开局
  56.                 ctx.clearRect(00600600)
  57.                 con && play()
  58.             }
  59.             // 切换黑白
  60.             isBlack = !isBlack
  61.         }, 0)
  62.     }
  63.     // 判断是否五连子
  64.     function isWin(x, y) {
  65.         const flag = isBlack ? 1 : 2
  66.         // 上和下
  67.         if (up_down(x, y, flag)) {
  68.             return true
  69.         }
  70.         // 左和右
  71.         if (left_right(x, y, flag)) {
  72.             return true
  73.         }
  74.         // 左上和右下
  75.         if (lu_rd(x, y, flag)) {
  76.             return true
  77.         }
  78.         // 右上和左下
  79.         if (ru_ld(x, y, flag)) {
  80.             return true
  81.         }
  82.         return false
  83.     }
  84.     function up_down(x, y, flag) {
  85.         let num = 1
  86.         // 向上找
  87.         for (let i = 1; i < 5; i++) {
  88.             let tempY = y - i
  89.             console.log(x, tempY)
  90.             if (tempY < 0 || cheeks[tempY][x] !== flag) break
  91.             if (cheeks[tempY][x] === flag) num += 1
  92.         }
  93.         // 向下找
  94.         for (let i = 1; i < 5; i++) {
  95.             let tempY = y + i
  96.             console.log(x, tempY)
  97.             if (tempY > 14 || cheeks[tempY][x] !== flag) break
  98.             if (cheeks[tempY][x] === flag) num += 1
  99.         }
  100.         return num >= 5
  101.     }
  102.     function left_right(x, y, flag) {
  103.         let num = 1
  104.         // 向左找
  105.         for (let i = 1; i < 5; i++) {
  106.             let tempX = x - i
  107.             if (tempX < 0 || cheeks[y][tempX] !== flag) break
  108.             if (cheeks[y][tempX] === flag) num += 1
  109.         }
  110.         // 向右找
  111.         for (let i = 1; i < 5; i++) {
  112.             let tempX = x + i
  113.             if (tempX > 14 || cheeks[y][tempX] !== flag) break
  114.             if (cheeks[y][tempX] === flag) num += 1
  115.         }
  116.         return num >= 5
  117.     }
  118.     function lu_rd(x, y, flag) {
  119.         let num = 1
  120.         // 向左上找
  121.         for (let i = 1; i < 5; i++) {
  122.             let tempX = x - i
  123.             let tempY = y - i
  124.             if (tempX < 0 || tempY < 0 || cheeks[tempY][tempX] !== flag) break
  125.             if (cheeks[tempY][tempX] === flag) num += 1
  126.         }
  127.         // 向右下找
  128.         for (let i = 1; i < 5; i++) {
  129.             let tempX = x + i
  130.             let tempY = y + i
  131.             if (tempX > 14 || tempY > 14 || cheeks[tempY][tempX] !== flag) break
  132.             if (cheeks[tempY][tempX] === flag) num += 1
  133.         }
  134.         return num >= 5
  135.     }
  136.     function ru_ld(x, y, flag) {
  137.         let num = 1
  138.         // 向右上找
  139.         for (let i = 1; i < 5; i++) {
  140.             let tempX = x - i
  141.             let tempY = y + i
  142.             if (tempX < 0 || tempY > 14 || cheeks[tempY][tempX] !== flag) break
  143.             if (cheeks[tempY][tempX] === flag) num += 1
  144.         }
  145.         // 向左下找
  146.         for (let i = 1; i < 5; i++) {
  147.             let tempX = x + i
  148.             let tempY = y - i
  149.             if (tempX > 14 || tempY < 0 || cheeks[tempY][tempX] !== flag) break
  150.             if (cheeks[tempY][tempX] === flag) num += 1
  151.         }
  152.         return num >= 5
  153.     }
  154. }
  155. 复制代码

4. 彩蛋:与AI下棋

其实很简单,每次下完棋,设置一个函数:随机找位置下棋。这样就实现了和电脑下棋,单人游戏的功能了,这个功能我已经实现,但是我就不写出来了,交给大家吧,当做是大家巩固这篇文章的作业。哈哈哈哈

结语

睡了睡了,这篇文章连续写了我7个小时,其实这三个小游戏还有很多可以优化的地方,大家可以提出来,互相学习。喜欢的兄弟姐妹点点赞哈,谢谢大家!!学习群请点这里[4]

image.png

关于本文

来源:Sunshine_Lin

https://juejin.cn/post/6989003710030413838

最后

欢迎关注【前端瓶子君】✿✿ヽ(°▽°)ノ✿

回复「算法」,加入前端编程源码算法群,每日一道面试题(工作日),第二天瓶子君都会很认真的解答哟!

回复「交流」,吹吹水、聊聊技术、吐吐槽!

回复「阅读」,每日刷刷高质量好文!

如果这篇文章对你有帮助,「在看」是最大的支持

 》》面试官也在看的算法资料《《

“在看和转发”就是最大的支持

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/我家自动化/article/detail/323572
推荐阅读
相关标签
  

闽ICP备14008679号