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