【JS】原生js实现极简版贪吃蛇,附全部代码

需求分析:

1.小蛇朝着某个方向不断运动 (头部运动 身体也动 每节身体运动的位置是下一节的位置)

2.上下左右能控制小蛇的运动方向

3.随机生成食物

4.碰到食物会增大

5.碰到四周或自己 游戏结束

实现思路:

最关键的就是利用Vue操作数据来改变视图的MVVM思想,我们设定一个数组,里面存放着小蛇每一个节点的全部信息,先改变数组内的数据,再根据数组数据进行dom操作。

1. 小蛇的渲染

设定一个数组,里面存放的是小蛇每节身体的数据,包括id和位置信息,根据这些信息渲染小蛇的dom节点(我这里是一个li作为一节身体),小蛇头部有一个移动方向的额外属性。

       let snake_arr = [
            { id: 0, left: 100, top: 100, move_direction: 'r' },
            { id: 1, left: 80, top: 100 },
            { id: 2, left: 60, top: 100 }]

        // 1.根据数组渲染小蛇  为了让小蛇头部在右端,所以最后渲染
        function render_snake(flag = false) {
            if (flag) {
                // 当添加身体时,先把ul里面的li全部清空再进行渲染
                // 缺点是这种做法会让transition的效果失效
                ul_body.innerHTML = ''
            }

            // 从后往前 向ul里面添加li标签
            for (let i = snake_arr.length - 1; i >= 0; i--) {

                let li = document.createElement('li')
                // 第0个是小蛇的头部 多一个属性控制移动方向
                if (snake_arr[i].id == 0) {
                    li.classList.add('cube', 'head')
                } else if (snake_arr[i].id == snake_arr.length - 1) {
                    li.innerText = i
                    li.classList.add('cube')
                }
                else {
                    li.innerText = ''
                    li.classList.add('cube')
                }
                li.style.left = snake_arr[i].left + 'px'
                li.style.top = snake_arr[i].top + 'px'

                ul_body.append(li)
            }
        }
       render_snake()

2.小蛇的移动

其实是改变小蛇头部的一个属性而已,switchcase获取按键,并将该属性改为对应的值。然后根据小蛇头部位置进行dom操作来移动小蛇的头部。

而身体的移动其实是 上一节的身体位置和下一节身体的位置信息进行了交换,然后重新渲染小蛇身体,因为浏览器执行速度很快,所以看起来就像是小蛇在移动,其实是小蛇先消失,然后重新出现的。

//根据上下左右改变小蛇头部的属性的代码略,就是先获取用户按的是上还是下,是左还是右,然后赋值给一个变量。不过需要注意:当小蛇在水平移动时,再按←或→不进行反应,上下也是同理,防止小蛇移动时原地掉头直接死亡了。

// 先改变数组数据,然后重新渲染整条小蛇,渲染的函数可以复用刚才的函数
function snake_move() {
            let d = snake_arr[0].move_direction

            /* lis:    [尾 5 4 3 2 1 0 头] 
           snake_arr:  [头 0 1 2 3 4 5 尾]*/

            // 如果是操控dom元素的位置的话会比较难 所以直接改变存放小蛇数据的数组
            // 让每一节小蛇的身体都获取下一节的位置 
            for (let i = snake_arr.length - 1; i >= 1; i--) {
                snake_arr[i].left = snake_arr[i - 1].left
                snake_arr[i].top = snake_arr[i - 1].top
            }

            // 头部单独处理 根据移动方向进行位置变化  这里取到20是因为小蛇每一节身体都是20*20px
            switch (d) {
                case 'l':
                    snake_arr[0].left -= 20
                    break
                case 'r':
                    snake_arr[0].left += 20
                    break
                case 'u':
                    snake_arr[0].top -= 20
                    break
                case 'd':
                    snake_arr[0].top += 20
                    break
            }

            // 数组变动之后,再重新进行渲染 这种方法的优点是代码量比较少 缺点是无法使用transition效果,但是使用过渡效果也会有bug,主要是拐弯时过渡效果会使得小蛇身体出现重影
            render_snake(true)
        }

        let auto_move = setInterval(snake_move, 100)

3. 随机生成食物

重点是生成的食物应该和小蛇可以完全重合,所以食物所在的位置和大小必须和小蛇的身体成倍数关系才行。

// 随机在地图上生成食物
        function random_food() {
            // 0 - width/height 还得是20的倍数 这样20px * 20px的小蛇才能和食物完全重合
            let random_left = Math.floor(Math.random() * container.clientWidth / 20)
            let random_top = Math.floor(Math.random() * container.clientHeight / 20)

            let food = document.createElement('div')
            food.classList.add('food')
            food.style.left = random_left * 20 + 'px'
            food.style.top = random_top * 20 + 'px'

            container.append(food)
        }

        random_food()

效果如下:食物就像是在一个个的网格上面,不会出现错位的情况

4.吃到食物身体增加一节

需要使用定时器不断检测小蛇头部的位置,判断其是否与食物重合,重合的话才会执行下一步操作:即增加数组内一个元素,并将其渲染到dom节点中。

 // 吃到食物身体增加1节
        // 获取到指定的某个元素的相对位置
        function get_position(domEle) {
            return [domEle.offsetLeft, domEle.offsetTop]
        }

        // 1.吃食物的检测与后续操作
        function eat_food() {
            let test_head_position
            // 获得食物的位置 因为食物是不会动的,所以获取食物的位置只需要获取一次即可
            let food = document.querySelector('.food')
            let [fl, ft] = get_position(food)

            // 检测小蛇头部是否与食物的位置重合
            test_head_position = setInterval(() => {
                // console.log(test_head_position); //用log的频率来检测定时器是否被清除了

                // 小蛇是在不断移动的,所以需要不断重新获取小蛇的位置才行
                let head = document.querySelector('.snake_body .head')
                let [hl, ht] = get_position(head)

                if (hl == fl && ht == ft) {
                    // 吃到了食物的时候再清除定时器
                    clearInterval(test_head_position)
                    // console.log('吃到了食物');
                    // 删除被吃掉的食物
                    food.parentElement.removeChild(food)
                    // 随机生成一个新的食物 
                    random_food()
                    // 执行这个函数 以便获取新食物的位置
                    eat_food()
                    // 增加n节身体长度
                    add_body(1)
                }
            }, 100)
        }

        // 2.增加1节身体
        function add_body(n = 1) {
            for (let i = 0; i < n; i++) {
                let length = snake_arr.length
                // 直接加在最后一节身体的上面  虽然当时重合在了一起 但是下次移动时,倒数第二个位置会变到倒数第三个cube的位置,倒数第一个位置相当于没变动,就露出来了
                snake_arr.push({ id: length, left: snake_arr[length - 1].left, top: snake_arr[length - 1].top })
            }
            render_snake(true)
            // console.log(snake_arr)
        }

5.检测是否撞到了自己

设定一个定时器,不断执行下面函数,因为数组内存放的有小蛇每一节身体的位置数据,所以只需要遍历数组进行判断即可。

// 检测是否撞到了自己
        function test_body() {
            // 通过小蛇存放的位置来判断头部是否和某一个身体部位重合
            console.log(snake_arr[0].move_direction);
            for (let i = snake_arr.length - 1; i > 0; i--) {
                if (snake_arr[0].left == snake_arr[i].left && snake_arr[0].top == snake_arr[i].top) {
                    return true
                }
            }
            return false
        }

额外注意:

浏览器在监听键盘按压事件时,灵敏度是很高的,很可能出现同时或几乎同时按下两个按键时,小蛇移动出现原地掉头情况,然后直接死亡,玩家体验非常不好,这个问题我试验了很多次,但是都没有很好的解决,希望评论区有大神可以解决这个问题。以下是我尝试解决的方法:

   // 贪吃蛇要用keyup事件,否则 按住 ← 再按其他的按键,会同时执行这两个按键的事件
        window.addEventListener('keyup', (e) => {
            // console.log(e.key);
            // 1.使用防抖函数来 减少 快速按压两个按键时咬到自己的bug 的发生频率 (改成100ms几乎完全不会触发这个bug)
            // 在小蛇向下移动时,同时按下 → ↑ (先随便按一个 再同时按下相反的按键)就会触发这个bug,其他方向也有这个bug   
            // (bug原因可能是因为在按下→键时,js代码已经改变了小蛇移动的方向 从d变成了r,但是100ms才移动一次,这时候
            // 小蛇还未向右移动,恰巧用户又按下了↑,导致snake_move执行时,小蛇移动方向变成了u,是向上移动的 就触发了这个bug)
            // debounce(e.key)

            // 重点是保证永远只有一个事件执行(防抖是只允许最后一个执行,节流是只允许第一个事件执行)

            // 2.如果不用防抖 小蛇虽然转向没延迟了 但是这个bug更容易触发了
            test_key(e.key)

            // 3.使用节流阀也行,但是其实本质是让节流阀来限制用户的操作频率 (如果在100ms之内按下了两次按键,只有第一次会被执行 第二次无效)
            // test_key(e.key)

        })

代码附录:

因为是极简版,所以没有素材图片,直接复制粘贴即可运行

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>贪吃蛇-极简版</title>
    <style>
        * {
            margin: 0;
            padding: 0;
        }

        ul {
            list-style: none;
        }

        .container {
            width: 1000px;
            height: 800px;
            margin: 10px auto;
            background-color: antiquewhite;
            position: relative;
        }

        .food {
            width: 20px;
            height: 20px;
            background-color: lightcoral;
            position: absolute;
        }

        /* .snake_body {
            position: relative;
        } */

        .snake_body .cube {
            position: absolute;
            width: 20px;
            height: 20px;
            background-color: #bff;
        }

        .cube.head {
            background-color: rgb(4, 147, 250);
        }
    </style>
</head>

<body>
    <div class="container">

        <ul class="snake_body">
            <!-- <li class="cube"></li>
            <li class="cube"></li>
            <li class="cube head"></li> -->
        </ul>

    </div>


    <script>
        const container = document.querySelector('.container')

        const ul_body = document.querySelector('.snake_body')

        /*需求:
       1.小蛇朝着某个方向不断运动 (头部运动 身体也动 每节身体运动的位置是下一节的位置)
       2.上下左右能控制小蛇的运动方向
       3.随机生成食物
       4.碰到食物会增大
       5.碰到四周或自己 游戏结束
       6.采用vue 操作数据的思想 */

        let snake_arr = [
            { id: 0, left: 100, top: 100, move_direction: 'r' },
            { id: 1, left: 80, top: 100 },
            { id: 2, left: 60, top: 100 }]

        render_snake()
        // 1.根据数组渲染小蛇  为了让小蛇头部在右,所以最后渲染
        function render_snake(flag = false) {
            if (flag) {
                // 当添加身体时,先把ul里面的li全部清空再进行渲染
                // 这种做法会让transition的效果失效
                ul_body.innerHTML = ''
            }

            // 从后往前 向ul里面添加li标签
            for (let i = snake_arr.length - 1; i >= 0; i--) {

                let li = document.createElement('li')
                // 第0个是小蛇的头部 多一个属性控制移动方向
                if (snake_arr[i].id == 0) {
                    li.classList.add('cube', 'head')
                } else if (snake_arr[i].id == snake_arr.length - 1) {
                    li.innerText = i
                    li.classList.add('cube')
                }
                else {
                    li.innerText = ''
                    li.classList.add('cube')
                }
                li.style.left = snake_arr[i].left + 'px'
                li.style.top = snake_arr[i].top + 'px'

                ul_body.append(li)
            }
        }

        // 2.增加1节身体
        function add_body(n = 1) {
            for (let i = 0; i < n; i++) {
                let length = snake_arr.length
                // 直接加在最后一节身体的上面  虽然当时重合在了一起 但是下次移动时,倒数第二个位置会变到倒数第三个cube的位置,倒数第一个位置相当于没变动,就露出来了
                snake_arr.push({ id: length, left: snake_arr[length - 1].left, top: snake_arr[length - 1].top })
            }
            render_snake(true)
            // console.log(snake_arr)
        }

        // 3. 根据头部的方向移动
        function snake_move() {
            let d = snake_arr[0].move_direction

            /* lis:    [尾 5 4 3 2 1 0 头] 
           snake_arr:  [头 0 1 2 3 4 5 尾]*/

            // 如果是操控dom元素的位置的话会比较难 所以直接改变存放小蛇数据的数组
            // 让每一节小蛇的身体都获取下一节的位置 
            for (let i = snake_arr.length - 1; i >= 1; i--) {
                snake_arr[i].left = snake_arr[i - 1].left
                snake_arr[i].top = snake_arr[i - 1].top
            }

            // 头部单独处理 根据移动方向进行位置变化  这里取到20是因为小蛇每一节身体都是20*20px
            switch (d) {
                case 'l':
                    snake_arr[0].left -= 20
                    break
                case 'r':
                    snake_arr[0].left += 20
                    break
                case 'u':
                    snake_arr[0].top -= 20
                    break
                case 'd':
                    snake_arr[0].top += 20
                    break
            }

            // 数组变动之后,再重新进行渲染 这种方法的优点是代码量比较少 缺点是无法使用transition效果,但是使用过渡效果也会有bug,主要是拐弯时过渡效果会使得小蛇身体出现重影
            render_snake(true)
        }

        let auto_move = setInterval(snake_move, 100)

        // 4.按键改变方向 其实是改变数组中head的属性值,然后移动时是看属性值进行移动的

        // 节流阀的做法:定义lock
        let lock = false
        // 判断一下是否和正在移动的方向相反 如果是的话不对用户的操作作出反应
        function test_key(key) {
            if (!lock) {
                lock = true
                // console.log('我执行了');
                let d = snake_arr[0].move_direction
                // 判断一下按压的key是水平的还是垂直的
                let key_type = 'vertical'
                if (key == 'ArrowLeft' || key == 'ArrowRight') {
                    key_type = 'level'
                } else {
                    key_type = 'vertical'
                }

                // 如果用户想让小蛇水平移动 且此时小蛇已经是水平移动状态了 那么就不做操作
                if (key_type == 'level' && (d == 'r' || d == 'l')) {
                    // console.log('小蛇正在水平移动');
                } else if (key_type == 'vertical' && (d == 'u' || d == 'd')) {
                    // console.log('小蛇正在垂直移动');
                } else {
                    key_change_direction(key)
                }
                setTimeout(() => {
                    lock = false
                }, 50)
            }
        }
        // 根据按键改变小蛇的头部方向
        function key_change_direction(key) {

            switch (key) {
                case 'ArrowLeft':
                    snake_arr[0].move_direction = 'l'
                    break
                case 'ArrowRight':
                    snake_arr[0].move_direction = 'r'
                    break
                case 'ArrowUp':
                    snake_arr[0].move_direction = 'u'
                    break
                case 'ArrowDown':
                    snake_arr[0].move_direction = 'd'
                    break
                default:
                    console.log(key);
            }


        }

        let timer = null
        function debounce(key) {
            if (timer) {
                // console.log('已经有了一个定时器:' + timer);
                clearTimeout(timer)
            }
            timer = setTimeout(() => {
                test_key(key)
            }, 50)
        }

        // 贪吃蛇要用keyup事件,否则 按住 ← 再按其他的按键,会同时执行这两个按键的事件
        window.addEventListener('keyup', (e) => {
            // console.log(e.key);
            // 1.使用防抖函数来 减少 快速按压两个按键时咬到自己的bug 的发生频率 (改成100ms几乎完全不会触发这个bug)
            // 如果把211行代码改成0,在 小蛇向下移动时,同时按下 → ↑ (先随便按一个 再同时按下相反的按键)就会触发这个bug,其他方向也有这个bug   
            // (bug原因可能是因为在按下→键时,js代码已经改变了小蛇移动的方向 从d变成了r,但是100ms才移动一次,这时候
            // 小蛇还未向右移动,恰巧用户又按下了↑,导致snake_move执行时,小蛇移动方向变成了u,是向上移动的 就触发了这个bug)
            // debounce(e.key)

            // 重点是保证永远只有一个事件执行(防抖是只允许最后一个执行,节流是只允许第一个事件执行)

            // 2.如果不用防抖 小蛇虽然转向没延迟了 但是这个bug更容易触发了
            test_key(e.key)

            // 3.使用节流阀也行,但是其实本质是让节流阀来限制用户的操作频率 (如果在100ms之内按下了两次按键,只有第一次会被执行 第二次无效)
            // test_key(e.key)

        })

        // 5.随机在地图上生成食物
        function random_food() {
            // 0 - width/height 还得是20的倍数 这样20px * 20px的小蛇才能和食物完全重合
            let random_left = Math.floor(Math.random() * container.clientWidth / 20)
            let random_top = Math.floor(Math.random() * container.clientHeight / 20)

            let food = document.createElement('div')
            food.classList.add('food')
            food.style.left = random_left * 20 + 'px'
            food.style.top = random_top * 20 + 'px'

            container.append(food)
        }

        random_food()
        // 6.吃到食物身体增加1节

        // 获取到指定元素的相对位置
        function get_position(domEle) {
            return [domEle.offsetLeft, domEle.offsetTop]
        }

        // 吃食物的检测与后续操作
        function eat_food() {
            let test_head_position
            // 获得食物的位置 因为食物是不会动的,所以获取食物的位置只需要获取一次即可
            let food = document.querySelector('.food')
            let [fl, ft] = get_position(food)

            // 检测小蛇头部是否与食物的位置重合
            test_head_position = setInterval(() => {
                // console.log(test_head_position); //用log的频率来检测定时器是否被清除了

                // 小蛇是在不断移动的,所以需要不断重新获取小蛇的位置才行
                let head = document.querySelector('.snake_body .head')
                let [hl, ht] = get_position(head)

                if (hl == fl && ht == ft) {
                    // 吃到了食物的时候再清除定时器
                    clearInterval(test_head_position)
                    // console.log('吃到了食物');
                    // 删除被吃掉的食物
                    food.parentElement.removeChild(food)
                    // 随机生成一个新的食物 
                    random_food()
                    // 执行这个函数 以便获取新食物的位置
                    eat_food()
                    // 增加n节身体长度
                    add_body(1)
                }
            }, 100)
        }

        eat_food()

        // 7.增加失败条件
        let container_w = container.offsetWidth
        let container_h = container.offsetHeight

        let test_failue = setInterval(() => {

            // 跑出圈外判负
            if (snake_arr[0].left >= container_w || snake_arr[0].left < 0 || snake_arr[0].top >= container_h || snake_arr[0].top < 0) {
                // location.reload()
                // alert('碰壁了!')
                // 不清除定时器的话会一直alert很多遍 但是如果把reload放到alert上面就不用清除定时器了
                clearInterval(test_failue)
                clearInterval(auto_move)
            }
            // 碰到自己的身体也判负
            else if (test_body()) {
                // location.reload()
                // alert('咬着自己了!')
                clearInterval(test_failue)
                clearInterval(auto_move)
            }
            // 胜利条件 没有写
            // else if(){

            // }

        }, 100)

        // 检测是否撞到了自己
        function test_body() {
            // let cubes = document.querySelectorAll('.snake_body .cube')
            // let head = document.querySelector('.snake_body .head')
            // let [hl, ht] = get_position(head)
            // for (let i = 0; i < cubes.length - 1; i++) {
            //     let [bl, bt] = get_position(cubes[i])
            //     if (hl == bl && ht == bt) {
            //         return true
            //     }
            // }
            // 没有问题
            // return false

            // 通过小蛇存放的位置来判断头部是否和某一个身体部位重合
            console.log(snake_arr[0].move_direction);
            for (let i = snake_arr.length - 1; i > 0; i--) {
                if (snake_arr[0].left == snake_arr[i].left && snake_arr[0].top == snake_arr[i].top) {
                    return true
                }
            }
            return false
        }
    </script>
</body>

</html>

猜你喜欢

转载自blog.csdn.net/Andye11/article/details/129037337