基于 HTML5 的贪吃蛇小游戏实现

一、引言

在 Web 开发的世界中,利用 HTML、CSS 和 JavaScript 构建有趣的互动游戏是一项充满乐趣和挑战的任务。本文将详细介绍一个基于 HTML5 的贪吃蛇小游戏的实现过程,通过对代码的解析,带你了解如何打造一个经典的贪吃蛇游戏,并在网页上流畅运行。

二、游戏界面与基本样式

游戏的界面主要由一个表示游戏地图的div(类名为snake-map)、用于显示操作按钮的div(类名为operate)以及用于展示游戏说明的div(类名为desc)组成。

在 CSS 部分,snake-map被设置为相对定位,并通过flex布局使其内部元素水平垂直居中。游戏中的每个方块(蛇身、苹果和空白区域)都是一个绝对定位的div(类名为snake-item),通过设置不同的背景颜色来区分它们,如蛇身是绿色(类名为snake),苹果是深红色(类名为apple)。

操作按钮(类名为btn)同样使用flex布局,方便用户通过点击来控制蛇的移动方向。为了提升用户体验,当按钮被按下时,会通过active伪类改变背景颜色和文字颜色。

三、游戏逻辑代码解析

  1. 引入外部工具库:通过import语句从https://unpkg.com/@3r/tool引入了一些工具函数,如v2(可能用于表示二维向量)、Maths(数学相关操作)、Randoms(生成随机数)和cloneDeep(深度克隆对象)。
  2. 游戏参数初始化:定义了游戏地图的宽度width和高度height,并创建了一个二维数组map来表示游戏地图,初始值都为0(表示空白格)。同时,定义了一个枚举对象mapEnum,用于标识地图上不同元素的类型,如blank(空白格)、snake(蛇)和apple(苹果)。
  3. 蛇和移动方向:用一个数组snake来存储蛇的身体部分的位置,初始时蛇有两个身体部分。movingDirection变量用于记录蛇的移动方向,初始方向为向右。
  4. DOM 元素获取与变量声明:获取游戏地图的 DOM 元素snakeConDom、操作按钮的 DOM 元素集合operateDom,并创建一个对象snakeMapDom用于存储地图上每个方块对应的 DOM 元素。此外,还声明了一些变量用于记录地图的最大边界maxYmaxX,以及控制游戏循环的intervalTimeintervalId
  5. 初始化函数init:该函数负责创建游戏地图的 DOM 结构,根据map数组的大小动态生成每个方块的 DOM 元素,并设置它们的位置和初始样式。同时,将蛇的初始位置在地图上进行标记,设置操作按钮的点击事件监听器,获取地图的最大边界,并生成游戏中的第一个苹果。
  6. 渲染地图函数renderMap:遍历map数组,根据每个位置的元素类型,为对应的 DOM 元素添加或移除相应的类名,从而更新游戏地图的显示状态。
  7. 生成苹果函数generateApple:通过生成随机坐标,检查该位置是否为空白格,如果是则将其设置为苹果(在map数组中标记为mapEnum.apple),否则重新生成随机坐标,直到找到合适的位置。
  8. 移动函数moving:这是游戏的核心逻辑函数。首先判断当前的移动方向是否为0(即停止移动),如果是则直接返回。然后计算蛇头的新位置newHead,根据新位置的元素类型进行不同的处理:
    • 如果新位置是空白格,将蛇尾从数组中移除,并在新位置添加蛇头,同时更新地图上的元素标记。
    • 如果新位置是苹果,生成一个新的苹果,并在新位置添加蛇头,增加蛇的长度。
    • 如果新位置是蛇本身或超出地图边界,则游戏结束,清除游戏循环定时器,并在游戏地图上显示游戏结束的提示信息。
  9. 按键处理函数onKeyDown:根据用户按下的按键(向上、向下、向左、向右箭头键),计算对应的移动方向。在设置新的移动方向之前,会检查是否与当前禁止的移动方向(即蛇当前移动的反方向)相同,如果相同则不进行操作,避免蛇反向移动导致游戏逻辑错误。
  10. 事件监听器与游戏启动:通过document.addEventListener("keydown", (ev) => onKeyDown(ev.key))监听键盘按键事件,调用onKeyDown函数处理用户的按键操作。最后,调用init函数初始化游戏,并调用renderMap函数渲染初始地图,通过setInterval(moving, intervalTime)启动游戏循环,每隔intervalTime时间调用moving函数更新游戏状态。

 完整代码展示

<!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">
    <link rel="stylesheet" href="./assets/global.css">

    <style>
        .snake-map {
            position: relative;
            display: flex;
            justify-content: center;
            align-items: center;
        }

        .snake-map .snake-item {
            background-color: saddlebrown;
            position: absolute;
            width: 20px;
            height: 20px;
        }

        .snake-map .snake-item.snake {
            background-color: green;
        }

        .snake-map .snake-item.apple {
            background-color: crimson;
        }

        .operate {
            display: flex;
            flex-wrap: wrap;
            margin: 0 20px;
            width: 120px;
            padding: 20px 0;
        }

        .operate .btn {
            width: 40px;
            height: 40px;
            display: flex;
            align-items: center;
            justify-content: center;
            user-select: none;
        }

        .operate .btn:active {
            background-color: saddlebrown;
            color: #fff;
            transition: all .4s ease-in-out;
        }

        .operate .flex {
            width: 100%;
        }

        .desc {
            margin: 20px;
            font-size: 12px;
            color: rgba(0, 0, 0, 0.7);
        }
    </style>
</head>

<body>
    <div class="snake-map"> </div>

    <div class="desc">玩法:通过上下左右键或屏幕上的上下左右将使其绿色线条(🐍)吃到红色方块(🍎)记录得分。</div>
    <div class="operate">
        <div class="btn flex">上</div>
        <div class="btn">左</div>
        <div class="btn">下</div>
        <div class="btn">右</div>
    </div>
    <script type="module">
        import { v2, Maths, Randoms, cloneDeep } from "https://unpkg.com/@3r/tool";

        let width = 20, height = 20;

        let map = [
            [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
            [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
            [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
            [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
            [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
            [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
            [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
            [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
            [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
            [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
            [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
            [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
            [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
            [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
            [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        ]

        let mapEnum = {
            blank: 0, // 空白格
            snake: 1, // 蛇 🐍
            apple: 2, // 苹果 🍎
        }
        // 蛇数据
        let snake = [v2(5, 5), v2(5, 6)]
        // 移动方向
        let movingDirection = v2(1, 0)
        // 容器
        let snakeConDom = document.querySelector('.snake-map')
        // 操作元素
        let operateDom = document.querySelectorAll('.operate > .btn')
        // 地图
        let snakeMapDom = {}

        // 地图最大边界
        let maxY = 0;
        let maxX = 0;

        // 延迟
        let intervalTime = 500;
        let intervalId = 0;

        // 初始化
        function init() {
            let ylength = map.length, xlength = 0;
            for (let y = 0; y < map.length; y++) {
                const xmap = map[y];
                xlength = Math.max(xlength, xmap.length)
                for (let x = 0; x < xmap.length; x++) {
                    let snakeItem = document.createElement('div')
                    snakeItem.classList.add('snake-item')
                    snakeItem.setAttribute(`style`, `left:${x * width}px;top:${y * height}px`)
                    snakeConDom.appendChild(snakeItem)
                    snakeMapDom[`${x},${y}`] = snakeItem
                }
            }

            for (let i = 0; i < snake.length; i++) {
                const snakeBody = snake[i];
                map[snakeBody.y][snakeBody.x] = mapEnum.snake;
            }
            snakeConDom.setAttribute(`style`, `width:${xlength * width}px;height:${ylength * height}px`)

            for (let i = 0; i < operateDom.length; i++) {
                const btn = operateDom.item(i);
                btn.addEventListener('click', function () {
                    onKeyDown(['ArrowUp', 'ArrowLeft', 'ArrowDown', 'ArrowRight'][i])
                })
            }

            maxY = ylength;
            maxX = xlength;

            generateApple();
        }
        // 渲染地图
        function renderMap() {
            for (let y = 0; y < map.length; y++) {
                const xmap = map[y];
                for (let x = 0; x < xmap.length; x++) {
                    let snakeItem = snakeMapDom[`${x},${y}`]
                    if (xmap[x] == mapEnum.snake) snakeItem.classList.add('snake')
                    else snakeItem.classList.remove('snake')
                    if (xmap[x] == mapEnum.apple) snakeItem.classList.add('apple')
                    else snakeItem.classList.remove('apple')
                }
            }
        }
        // 生成苹果
        function generateApple() {
            let apple = v2(Randoms.getRandomInt(0, maxX), Randoms.getRandomInt(0, maxY))
            if (map[apple.y][apple.x] != mapEnum.blank) return generateApple();
            map[apple.y][apple.x] = mapEnum.apple;
            return;
        }
        // 移动
        function moving() {
            // 运动为0时则停止运动
            if (Maths.equal(movingDirection, v2(0, 0))) return;
            // 头
            let head = cloneDeep(snake[0])
            // 新头
            let newHead = head.plus(movingDirection)
            // 当新头部是空白时
            if (map?.[newHead.y]?.[newHead.x] == mapEnum.blank) {
                // 尾巴
                let tail = snake.pop()
                map[tail.y][tail.x] = mapEnum.blank;
                map[newHead.y][newHead.x] = mapEnum.snake;
                snake.unshift(newHead)
            }
            // 再生成一个新🍎
            else if (map?.[newHead.y]?.[newHead.x] == mapEnum.apple) {
                generateApple()
                map[newHead.y][newHead.x] = mapEnum.snake;
                snake.unshift(newHead)
            }
            else {
                clearInterval(intervalId)
                snakeConDom.innerHTML = `游戏结束,得分为${snake.length - 2}`
            }
            renderMap()
        }

        function onKeyDown(key) {
            let direction = v2(0, 0)
            if (key == 'ArrowUp') direction = v2(0, -1)
            if (key == 'ArrowDown') direction = v2(0, 1)
            if (key == 'ArrowLeft') direction = v2(-1, 0)
            if (key == 'ArrowRight') direction = v2(1, 0)

            // 如果没有运动方向
            if (Maths.equal(movingDirection, direction)) return;
            let cannotMovingDirection = snake[1].subtract(snake[0]);
            // 如果移动方向是禁止的
            if (Maths.equal(cannotMovingDirection, direction)) return;
            movingDirection = direction;
        }

        document.addEventListener("keydown", (ev) => onKeyDown(ev.key))

        init();
        renderMap()

        intervalId = setInterval(moving, intervalTime)

    </script>
</body>

</html>

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值