使用 Canvas 实现一个【贪吃蛇】小游戏!

科技   2024-12-28 11:20   广东  

前言

大家好,我是林三心,用最通俗易懂的话讲最难的知识点是我的座右铭,基础是进阶的前提是我的初心~

又很多朋友最近说想要入门 Canvas,问我能不能出一篇讲解 Canvas 的文章,但是我感觉直接讲会比较无聊,还不如通过一些案例来让大家去手敲呢

所以我准备使用 Canvas 实现一个贪吃蛇的小游戏,希望大家能喜欢~

效果 & 实现步骤

效果是下面这样

大概的实现步骤是这样的:

  • 第一步:先把蛇画出来
  • 第二步:让蛇能动起来
  • 第三步:在图中随机找地方随机投放食物
  • 第四步:控制蛇去吃这些食物
  • 第五步:检测蛇撞到边缘

先把蛇画出来

实际上,绘制蛇的过程非常简单。蛇主要由蛇头和蛇身构成,这两部分都可以通过 正方形 格子来呈现。蛇头就是单独的一个方格,而蛇身则可以由多个方格组成

要绘制这些方格,我们可以使用 ctx.fillRect 方法。在这个过程中,我们用变量 head 来表示蛇头,而用 数组body 来存储蛇身的各个方格

让蛇能动起来

蛇的运动可以分为两种情形:

  • 第一种,当游戏开始时,蛇会自动向右方移动;
  • 第二种,玩家可以通过键盘的方向键来操控蛇,使其朝不同的方向行进。不论哪一种情况,蛇每秒钟都会前进一个方格的距离。

要让蛇动起来其实很容易理解,我就拿蛇向右移动这个情况来说明一下吧:

  • 1、蛇头首先向右移动一个方格的距离,而蛇身的其余部分保持不动。
  • 2、在蛇身的最前端添加一个新的方格。
  • 3、移除蛇身尾部的方格。
  • 4、通过使用定时器,实现蛇看起来像是在不断向右移动的效果。

实现效果如下:


在图中随机找地方随机投放食物

在画布上随机生成食物,即随机绘制一个方格,但需注意以下事项:

  • 1、确保所选坐标位于画布的有效范围内
  • 2、食物的位置不能与蛇身或蛇头重叠(否则蛇可能会被“砸晕”,哈哈)

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

控制蛇去吃这些食物

蛇吃食物的原理很容易理解,就是当蛇头的坐标与食物的坐标重合时,就算蛇吃到食物了。这里有两点需要注意:

  • 1、一旦蛇吃到食物,它的身体就要增长一个方格的长度。
  • 2、蛇吃到食物之后,之前食物的位置就需要重新随机生成。

检测蛇撞到边缘

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

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


完整代码

draw()

function draw({
    const canvas = document.getElementById('canvas')

    const ctx = canvas.getContext('2d')

    // 定义一个全局的是否吃到食物的一个变量
    let isEatFood = false

    // 小方格的构造函数
    function Rect(x, y, width, height, color{
        this.x = x
        this.y = y
        this.width = width
        this.height = height
        this.color = color
    }

    Rect.prototype.draw = function ({
        ctx.beginPath()
        ctx.fillStyle = this.color
        ctx.fillRect(this.x, this.y, this.width, this.height)
        ctx.strokeRect(this.x, this.y, this.width, this.height)
    }

    // 蛇的构造函数
    function Snake(length = 0{

        this.length = length
        // 蛇头
        this.head = new Rect(canvas.width / 2, canvas.height / 24040'red')

        // 蛇身
        this.body = []

        let x = this.head.x - 40
        let y = this.head.y

        for (let i = 0; i < this.length; i++) {
            const rect = new Rect(x, y, 4040'yellow')
            this.body.push(rect)
            x -= 40
        }
    }

    Snake.prototype.drawSnake = function ({
        // 如果碰到了
        if (isHit(this)) {
            // 清除定时器
            clearInterval(timer)
            const con = confirm(`总共吃了${this.body.length - this.length}个食物,重新开始吗`)
            // 是否重开
            if (con) {
                draw()
            }
            return
        }
        // 绘制蛇头
        this.head.draw()
        // 绘制蛇身
        for (let i = 0; i < this.body.length; i++) {
            this.body[i].draw()
        }
    }

    Snake.prototype.moveSnake = function ({
        // 将蛇头上一次状态,拼到蛇身首部
        const rect = new Rect(this.head.x, this.head.y, this.head.width, this.head.height, 'yellow')
        this.body.unshift(rect)

        // 判断蛇头是否与食物重叠,重叠就是吃到了,没重叠就是没吃到
        isEatFood = food && this.head.x === food.x && this.head.y === food.y

        // 咱们上面在蛇身首部插入方格
        if (!isEatFood) {
            // 没吃到就要去尾,相当于整条蛇没变长
            this.body.pop()
        } else {
            // 吃到了就不去尾,相当于整条蛇延长一个方格

            // 并且吃到了,就要重新生成一个随机食物
            food = randomFood(this)
            food.draw()
            isEatFood = false
        }

        // 根据方向,控制蛇头的坐标
        switch (this.direction) {
            case 0:
                this.head.x -= this.head.width
                break
            case 1:
                this.head.y -= this.head.height
                break
            case 2:
                this.head.x += this.head.width
                break
            case 3:
                this.head.y += this.head.height
                break
        }
    }

    document.onkeydown = function (e{
        // 键盘事件
        e = e || window.event
        // 左37  上38  右39  下40
        switch (e.keyCode) {
            case 37:
                console.log(37)
                // 三元表达式,防止右移动时按左,下面同理(贪吃蛇可不能直接掉头)
                snake.direction = snake.direction === 2 ? 2 : 0
                snake.moveSnake()
                break
            case 38:
                console.log(38)
                snake.direction = snake.direction === 3 ? 3 : 1
                break
            case 39:
                console.log(39)
                snake.direction = snake.direction === 0 ? 0 : 2
                break
            case 40:
                console.log(40)
                snake.direction = snake.direction === 1 ? 1 : 3
                break

        }
    }

    function randomFood(snake{
        let isInSnake = true
        let rect
        while (isInSnake) {
            const x = Math.round(Math.random() * (canvas.width - 40) / 40) * 40
            const y = Math.round(Math.random() * (canvas.height - 40) / 40) * 40
            console.log(x, y)
            // 保证是40的倍数啊
            rect = new Rect(x, y, 4040'blue')
            // 判断食物是否与蛇头蛇身重叠
            if ((snake.head.x === x && snake.head.y === y) || snake.body.find(item => item.x === x && item.y === y)) {
                isInSnake = true
                continue
            } else {
                isInSnake = false
            }
        }
        return rect
    }

    function isHit(snake{
        const head = snake.head
        // 是否碰到左右边界
        const xLimit = head.x < 0 || head.x >= canvas.width
        // 是否碰到上下边界
        const yLimit = head.y < 0 || head.y >= canvas.height
        // 是否撞到蛇身
        const hitSelf = snake.body.find(({ x, y }) => head.x === x && head.y === y)
        // 三者其中一个为true则游戏结束
        return xLimit || yLimit || hitSelf
    }

    const snake = new Snake(3)
    // 默认direction为2,也就是右
    snake.direction = 2
    snake.drawSnake()
    // 创建随机食物实例
    var food = randomFood(snake)
    // 画出食物
    food.draw()

    function animate({
        // 先清空
        ctx.clearRect(00, canvas.width, canvas.height)
        // 移动
        snake.moveSnake()
        // 再画
        snake.drawSnake()
        food.draw()
    }

    var timer = setInterval(() => {
        animate()
    }, 100)
}

结语

我是林三心,一个待过小型toG型外包公司、大型外包公司、小公司、潜力型创业公司、大公司的作死型前端选手

前端之神
一位前端小菜鸡,写过400多篇原创文章,全网有6w+个前端朋友,梦想是成为”前端之神“~
 最新文章