HTML | 结合Canvas开发具有智能寻路功能的贪吃蛇小游戏实战详解
·
前言
大家好,今天分享一个具备智能自动寻路功能的贪吃蛇游戏开发全过程。这个项目不同于普通的贪吃蛇游戏,它能够让蛇自己思考、规划路径,安全地吃到食物并避开障碍物。整个项目的核心亮点在于自动模式的寻路算法设计,我会详细讲解BFS寻路、安全度评估、多策略评分等核心技术的实现原理。

一、项目概述
1.1 技术栈
- HTML5 Canvas - 游戏画布渲染
- 原生JavaScript - 游戏逻辑与算法实现
- BFS广度优先搜索 - 核心寻路算法
- 启发式评分系统 - 智能决策机制
1.2 功能特性
| 功能 | 描述 |
|---|---|
| 手动控制 | 键盘方向键/WASD控制蛇的移动方向 |
| 自动寻路 | AI自动规划最优路径,安全觅食 |
| 可调速度 | 滑块控制游戏运行速度 |
| 可调画布 | 支持不同尺寸的游戏区域 |
| 自定义颜色 | 可自定义蛇身颜色 |
| 最高分记录 | localStorage持久化存储 |
二、游戏核心架构
2.1 数据结构设计
// 游戏状态变量
let snake = []; // 蛇身数组,每节是一个坐标对象
let food = {}; // 食物位置
let direction = { x: 1, y: 0 }; // 当前移动方向
let nextDirection = { x: 1, y: 0 };// 下一帧的移动方向
let isAutoMode = false; // 是否为自动模式
const gridSize = 20; // 网格单元大小(像素)
let tileCount; // 网格数量(画布大小/网格大小)
设计要点:
- 使用数组存储蛇身,索引0为蛇头,便于快速访问
direction和nextDirection分离,避免一帧内方向突变导致的作弊tileCount动态计算,适配不同尺寸画布
2.2 游戏循环机制
function startGame() {
if (gameLoop) {
clearInterval(gameLoop);
}
init();
// 根据模式选择不同的更新函数
gameLoop = setInterval(isAutoMode ? autoPlay : update, gameSpeed);
}
核心逻辑:
- 使用
setInterval实现固定时间间隔的游戏循环 - 自动模式和手动模式共用渲染函数
draw(),仅更新逻辑不同 - 模式切换时重建定时器,确保流畅过渡
三、手动模式实现
3.1 键盘事件处理
document.addEventListener('keydown', (e) => {
if (isAutoMode) return; // 自动模式下禁用手动控制
switch (e.key) {
case 'ArrowUp':
case 'w':
case 'W':
if (direction.y !== 1) nextDirection = { x: 0, y: -1 };
break;
// ... 其他方向类似处理
}
});
关键点:
direction.y !== 1防止180度反向移动(蛇不能直接掉头)- 支持方向键和WASD两种控制方式
- 自动模式下自动忽略键盘输入
3.2 碰撞检测与移动
function update() {
direction = { ...nextDirection };
const head = {
x: snake[0].x + direction.x,
y: snake[0].y + direction.y
};
// 边界碰撞检测
if (head.x < 0 || head.x >= tileCount || head.y < 0 || head.y >= tileCount) {
gameOver();
return;
}
// 自身碰撞检测
if (snake.some(segment => segment.x === head.x && segment.y === head.y)) {
gameOver();
return;
}
snake.unshift(head); // 新蛇头加入
// 吃食物判定
if (head.x === food.x && head.y === food.y) {
score += 10;
spawnFood(); // 生成新食物
} else {
snake.pop(); // 未吃到食物则移除蛇尾
}
draw();
}
四、【核心亮点】自动寻路系统详解
这是本文的重点部分,让我们一起探索如何让蛇拥有"智能"。
4.1 整体架构
自动寻路系统采用分层决策架构:
┌─────────────────────────────────────────────┐
│ 自动寻路决策流程 │
├─────────────────────────────────────────────┤
│ 1. 获取所有有效移动方向 │
│ ↓ │
│ 2. 对每个方向进行多维度评分 │
│ - 路径评分(到食物的距离) │
│ - 安全评分(移动后的可活动空间) │
│ - 跟随蛇尾加分 │
│ ↓ │
│ 3. 选择得分最高的方向 │
└─────────────────────────────────────────────┘
4.2 BFS寻路算法
**广度优先搜索(BFS)**是寻路的核心,它能找到两点之间的最短路径。
function findPath(start, end, snakeBody, willEatFood) {
const queue = [[start]]; // 队列存储路径
const visited = new Set(); // 已访问节点集合
visited.add(`${start.x},${start.y}`);
// 将蛇身标记为障碍物(吃食物时蛇尾会移动,故不减1)
for (let i = 0; i < snakeBody.length - (willEatFood ? 0 : 1); i++) {
visited.add(`${snakeBody[i].x},${snakeBody[i].y}`);
}
while (queue.length > 0) {
const path = queue.shift();
const current = path[path.length - 1];
// 到达终点,返回完整路径
if (current.x === end.x && current.y === end.y) {
return path;
}
// 探索四个方向的邻居
const neighbors = [
{ x: current.x + 1, y: current.y }, // 右
{ x: current.x - 1, y: current.y }, // 左
{ x: current.x, y: current.y + 1 }, // 下
{ x: current.x, y: current.y - 1 } // 上
];
for (const neighbor of neighbors) {
const key = `${neighbor.x},${neighbor.y}`;
// 边界检查 + 障碍物检查 + 是否已访问
if (!visited.has(key) &&
neighbor.x >= 0 && neighbor.x < tileCount &&
neighbor.y >= 0 && neighbor.y < tileCount) {
visited.add(key);
queue.push([...path, neighbor]); // 扩展路径
}
}
}
return null; // 无法到达
}
算法复杂度:O(V + E),V为节点数,E为边数
关键参数说明:
| 参数 | 类型 | 说明 |
|---|---|---|
| start | Object | 起始坐标 |
| end | Object | 目标坐标(食物位置) |
| snakeBody | Array | 蛇身数组 |
| willEatFood | Boolean | 移动后是否吃掉食物(影响障碍物计算) |
4.3 安全度评估
仅仅找到食物是不够的,蛇还需要知道自己移动后是否安全。安全度定义为:从新蛇头位置能到达的最大空格子数。
function calculateSafety(newHead, snakeBody, willEatFood) {
const reachable = new Set(); // 可达格子集合
const queue = [newHead];
// 构建障碍物集合(蛇身)
const obstacles = new Set();
for (let i = 0; i < snakeBody.length - (willEatFood ? 0 : 1); i++) {
obstacles.add(`${snakeBody[i].x},${snakeBody[i].y}`);
}
// BFS洪水填充,计算可达空间
while (queue.length > 0) {
const current = queue.shift();
const key = `${current.x},${current.y}`;
if (reachable.has(key)) continue;
reachable.add(key);
const neighbors = [
{ x: current.x + 1, y: current.y },
{ x: current.x - 1, y: current.y },
{ x: current.x, y: current.y + 1 },
{ x: current.x, y: current.y - 1 }
];
for (const neighbor of neighbors) {
const nKey = `${neighbor.x},${neighbor.y}`;
if (!reachable.has(nKey) &&
!obstacles.has(nKey) &&
neighbor.x >= 0 && neighbor.x < tileCount &&
neighbor.y >= 0 && neighbor.y < tileCount) {
queue.push(neighbor);
}
}
}
return reachable.size; // 返回可达格子数量
}
设计思路:
- 使用BFS洪水填充算法遍历所有可达格子
- 吃食物时蛇尾会移动,所以蛇尾不算障碍物
- 可达空间越大,蛇越安全
4.4 智能方向选择 - 多策略评分系统
这是整个自动系统的核心决策逻辑。我们给每种可能的移动方向打分,选择得分最高的。
function getAutoDirection() {
const head = snake[0];
const tail = snake[snake.length - 1];
// 1. 先过滤掉无效移动
const validMoves = allDirections.filter(dir => {
const nextHead = { x: head.x + dir.x, y: head.y + dir.y };
// 边界检查
if (nextHead.x < 0 || nextHead.x >= tileCount ||
nextHead.y < 0 || nextHead.y >= tileCount) {
return false;
}
// 撞自己检测(动态计算是否吃食物)
const moveWillEatFood = (nextHead.x === food.x && nextHead.y === food.y);
const checkLength = moveWillEatFood ? snake.length : snake.length - 1;
for (let i = 0; i < checkLength; i++) {
if (snake[i].x === nextHead.x && snake[i].y === nextHead.y) {
return false;
}
}
// 不能180度掉头
if (dir.x === -direction.x && dir.y === -direction.y) {
return false;
}
return true;
});
if (validMoves.length === 0) {
return direction; // 无路可走,保持原方向
}
// 2. 对每个有效移动进行评分
let bestMove = validMoves[0];
let bestScore = -Infinity;
for (const move of validMoves) {
const newHead = { x: head.x + move.x, y: head.y + move.y };
let score = 0;
const moveWillEatFood = (newHead.x === food.x && newHead.y === food.y);
// 【评分项1】路径评分:能到达食物的路径越短越好
const pathToFood = findPath(newHead, food, snake, moveWillEatFood);
if (pathToFood) {
score += (tileCount * tileCount - pathToFood.length) * 10;
} else {
score -= 500; // 无法到达食物,严重惩罚
}
// 【评分项2】安全评分:移动后的安全空间越大越好
const safety = calculateSafety(newHead, snake, moveWillEatFood);
score += safety * 5;
// 【评分项3】跟随蛇尾策略:朝蛇尾方向移动可以保持空间连通
const tailDirectionBonus = (Math.abs(newHead.x - tail.x) + Math.abs(newHead.y - tail.y));
score += tailDirectionBonus;
// 【评分项4】贪心评分:朝食物方向移动
const distToFood = Math.abs(newHead.x - food.x) + Math.abs(newHead.y - food.y);
score += (tileCount * 2 - distToFood);
// 【评分项5】安全底线:如果安全空间太小,直接否决
if (safety < tileCount * 3) {
score -= 1000;
}
if (score > bestScore) {
bestScore = score;
bestMove = move;
}
}
return bestMove;
}
评分策略详解:
| 评分项 | 权重 | 说明 |
|---|---|---|
| 路径评分 | ×10 | 路径越短得分越高,确保高效觅食 |
| 安全评分 | ×5 | 可达空间越大越安全 |
| 跟随蛇尾 | +N | 保持与蛇尾连通,避免被困 |
| 贪心加分 | +N | 曼哈顿距离越近越好 |
| 安全底线 | -1000 | 安全空间低于阈值直接淘汰 |
4.5 自动游戏主循环
function autoPlay() {
if (!isAutoMode) return;
// 检查食物是否可达,不可达则重新生成
if (!isFoodReachable()) {
spawnFood();
draw();
}
// 获取最优方向并执行
nextDirection = getAutoDirection();
update();
}
五、食物生成优化
普通贪食蛇可能生成无法到达的食物,导致游戏陷入死局。这就需要优化食物生成逻辑:
function spawnFood() {
let attempts = 0;
const maxAttempts = 100;
let bestPosition = null;
let bestSafety = 0;
while (attempts < maxAttempts) {
const candidate = {
x: Math.floor(Math.random() * tileCount),
y: Math.floor(Math.random() * tileCount)
};
// 跳过蛇身占据的位置
if (snake.some(segment => segment.x === candidate.x && segment.y === candidate.y)) {
attempts++;
continue;
}
// 跳过不可达的位置
if (!findPath(snake[0], candidate, snake, false)) {
attempts++;
continue;
}
// 计算该位置的安全度
const safety = calculateSafety(snake[0], snake, false);
// 优先选择安全度高的位置
if (safety > bestSafety) {
bestSafety = safety;
bestPosition = candidate;
}
// 安全度足够高时直接使用
if (safety >= tileCount * tileCount * 0.3) {
food = candidate;
return;
}
attempts++;
}
// 使用最佳备选位置
food = bestPosition || generateRandomFreePosition();
}
六、完整代码
以下是小游戏的完整实现,直接保存为HTML文件运行:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>贪吃蛇游戏 - 自动模式</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
min-height: 100vh;
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
padding: 20px;
}
h1 {
color: #e94560;
text-align: center;
margin-bottom: 20px;
font-size: 2.5rem;
text-shadow: 0 0 20px rgba(233, 69, 96, 0.5);
}
.game-container {
background: rgba(255, 255, 255, 0.1);
border-radius: 20px;
padding: 30px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
backdrop-filter: blur(10px);
}
.stats {
display: flex;
justify-content: space-between;
margin-bottom: 15px;
color: #fff;
font-size: 1.2rem;
}
.stat {
background: rgba(233, 69, 96, 0.2);
padding: 10px 20px;
border-radius: 10px;
border: 1px solid rgba(233, 69, 96, 0.3);
}
.stat span {
color: #e94560;
font-weight: bold;
}
#gameCanvas {
border: 3px solid #e94560;
border-radius: 10px;
background: #0a0a0a;
display: block;
margin: 0 auto;
}
.controls {
margin-top: 20px;
display: flex;
gap: 15px;
justify-content: center;
flex-wrap: wrap;
}
button {
padding: 12px 30px;
font-size: 1rem;
border: none;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s ease;
font-weight: bold;
}
#autoBtn {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
#autoBtn.active {
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
box-shadow: 0 0 20px rgba(245, 87, 108, 0.5);
}
#restartBtn {
background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
color: white;
}
.settings-panel {
background: rgba(255, 255, 255, 0.1);
border-radius: 15px;
padding: 20px;
margin-top: 20px;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
}
.setting-item {
display: flex;
flex-direction: column;
gap: 8px;
background: rgba(0, 0, 0, 0.2);
padding: 15px;
border-radius: 10px;
}
.setting-item label {
color: #fff;
font-size: 0.95rem;
font-weight: 600;
}
.mode-indicator {
text-align: center;
margin-top: 15px;
padding: 10px;
border-radius: 8px;
font-weight: bold;
transition: all 0.3s ease;
}
.mode-indicator.manual {
background: rgba(102, 126, 234, 0.2);
color: #667eea;
border: 1px solid #667eea;
}
.mode-indicator.auto {
background: rgba(245, 87, 108, 0.2);
color: #f5576c;
border: 1px solid #f5576c;
}
.instructions {
margin-top: 20px;
color: rgba(255, 255, 255, 0.7);
text-align: center;
font-size: 0.9rem;
}
</style>
</head>
<body>
<h1>贪吃蛇游戏</h1>
<div class="game-container">
<div class="stats">
<div class="stat">得分:<span id="score">0</span></div>
<div class="stat">长度:<span id="length">3</span></div>
<div class="stat">最高分:<span id="highScore">0</span></div>
</div>
<canvas id="gameCanvas" width="400" height="400"></canvas>
<div class="controls">
<button id="autoBtn">自动游戏</button>
<button id="restartBtn">重新开始</button>
</div>
<div id="modeIndicator" class="mode-indicator manual">
当前模式:手动控制
</div>
<div class="settings-panel">
<div class="setting-item">
<label>游戏速度</label>
<input type="range" id="speedSlider" min="50" max="300" value="100" step="10">
<span id="speedValue">100ms</span>
</div>
<div class="setting-item">
<label>区域大小</label>
<input type="range" id="sizeSlider" min="300" max="600" value="400" step="100">
<span id="sizeValue">400x400</span>
</div>
<div class="setting-item">
<label>蛇身颜色</label>
<input type="color" id="colorPicker" value="#4ade80">
<span id="colorValue">#4ade80</span>
</div>
</div>
<div class="instructions">
<p>手动模式:使用 ↑↓←→ 或 WASD 控制方向</p>
<p>自动模式:蛇会自动寻找食物并避开墙壁和自身</p>
</div>
</div>
<script>
// ========== 完整游戏代码(省略前述已展示的核心算法)==========
// 初始化游戏
function init() {
canvas.width = canvasSize;
canvas.height = canvasSize;
tileCount = canvas.width / gridSize;
snake = [
{ x: 5, y: 10 },
{ x: 4, y: 10 },
{ x: 3, y: 10 }
];
direction = { x: 1, y: 0 };
nextDirection = { x: 1, y: 0 };
score = 0;
scoreEl.textContent = score;
lengthEl.textContent = snake.length;
spawnFood();
draw();
}
// 绘制游戏
function draw() {
ctx.fillStyle = '#0a0a0a';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// 绘制网格
ctx.strokeStyle = 'rgba(255, 255, 255, 0.05)';
ctx.lineWidth = 0.5;
for (let i = 0; i < tileCount; i++) {
ctx.beginPath();
ctx.moveTo(i * gridSize, 0);
ctx.lineTo(i * gridSize, canvas.height);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(0, i * gridSize);
ctx.lineTo(canvas.width, i * gridSize);
ctx.stroke();
}
// 绘制食物
ctx.fillStyle = '#e94560';
ctx.shadowColor = '#e94560';
ctx.shadowBlur = 15;
ctx.beginPath();
ctx.arc(food.x * gridSize + gridSize / 2, food.y * gridSize + gridSize / 2, gridSize / 2 - 2, 0, Math.PI * 2);
ctx.fill();
ctx.shadowBlur = 0;
// 绘制蛇
snake.forEach((segment, index) => {
const gradient = ctx.createRadialGradient(
segment.x * gridSize + gridSize / 2,
segment.y * gridSize + gridSize / 2,
0,
segment.x * gridSize + gridSize / 2,
segment.y * gridSize + gridSize / 2,
gridSize / 2
);
if (index === 0) {
gradient.addColorStop(0, snakeColor);
gradient.addColorStop(1, adjustColor(snakeColor, -20));
} else {
const alpha = 1 - (index / snake.length) * 0.5;
gradient.addColorStop(0, hexToRgba(snakeColor, alpha));
gradient.addColorStop(1, hexToRgba(adjustColor(snakeColor, -20), alpha));
}
ctx.fillStyle = gradient;
ctx.beginPath();
ctx.roundRect(segment.x * gridSize + 1, segment.y * gridSize + 1, gridSize - 2, gridSize - 2, 4);
ctx.fill();
});
}
// 更多实现细节见文章前述代码...
</script>
</body>
</html>
七、运行效果
7.1 手动模式
- 使用键盘方向键或WASD控制蛇的移动
- 适合练习操作技巧
7.2 自动模式
- 点击"自动游戏"按钮开启AI模式
- 蛇会自动规划最优路径觅食
- 遇到危险会主动规避
- 可以实时调整速度观察AI决策过程
八、技术总结
8.1 核心算法回顾
| 算法 | 用途 | 时间复杂度 |
|---|---|---|
| BFS寻路 | 找到从蛇头到食物的最短路径 | O(V+E) |
| 洪水填充 | 计算某位置的安全空间大小 | O(V+E) |
| 多策略评分 | 综合评估选择最优移动方向 | O(4×V) |
8.2 可扩展方向
- A*算法优化 - 使用启发式搜索加速长距离寻路
- Hamiltonian路径 - 蛇走完整个棋盘的死路解决方案
- 机器学习 - 训练神经网络学习更好的决策策略
- 多蛇模式 - 添加AI对手实现对战
结语
通过本文,我们完整实现了一个具备智能自动寻路功能的贪吃蛇游戏。核心在于BFS寻路算法配合多维度安全评分系统,能够在保证安全的前提下高效觅食。
完整代码已在上文提供,可以直接复制运行测试。建议在自动模式下降低游戏速度(调整滑块到较慢值),观察自动决策过程和路径选择。
如果有任何问题,欢迎在评论区留言讨论!
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)