Hexo 博客实战:贪吃蛇小游戏开发指南

前言

在静态博客中嵌入 JavaScript 小游戏是一种提升用户体验的好方法。之前用 Lua 为 Unity 实现过多种小游戏,现在尝试将经典的贪吃蛇游戏移植到 Hexo 博客中。

本文将详细介绍如何在 Hexo 博客中实现一个可玩的贪吃蛇小游戏,包括游戏页面搭建、核心逻辑实现以及移动端适配。

项目结构

游戏放置在 source/ai-games/snake/ 目录下,文件结构如下:

1
2
3
source/ai-games/snake/
├── index.md # 游戏页面(HTML + 样式)
└── snake-game.js # 游戏核心逻辑

这种独立目录的方式便于管理,每个游戏都有自己完整的资源。

游戏页面实现

游戏页面使用 Hexo 的 标签来嵌入原始 HTML 代码,这样可以避免 Hexo 对 HTML 标签进行转义。

Canvas 画布

1
<canvas id="game-canvas" width="400" height="400"></canvas>

游戏采用 20×20 网格,每个格子 20px,整体画布大小为 400×400 像素。

样式设计

1
2
3
4
5
6
7
canvas {
border: 2px solid #4CAF50;
border-radius: 8px;
background: #111;
box-shadow: 0 0 20px rgba(76, 175, 80, 0.3);
touch-action: none;
}

暗色系背景配合绿色边框,营造游戏氛围。touch-action: none 用于禁止移动端默认触摸行为,避免游戏时页面滚动。

核心逻辑实现

游戏逻辑全部封装在 snake-game.js 文件中,共约 309 行代码。

全局初始化函数

1
2
3
4
5
6
window.initSnakeGame = function() {
// 获取 DOM 元素
const canvas = document.getElementById('game-canvas');
const ctx = canvas.getContext('2d');
// ... 初始化逻辑
};

将游戏初始化函数挂载到 window 对象上,便于 PJAX 页面切换后重新调用。

游戏配置

1
2
3
4
5
6
7
8
9
const gridSize = 20;      // 20x20 网格
const cellSize = 20; // 每个格子 20px
let snake = [
{x: 10, y: 10},
{x: 9, y: 10},
{x: 8, y: 10}
];
let direction = 'RIGHT';
let nextDirection = 'RIGHT';

蛇身使用数组存储,每个元素表示蛇的一节坐标。directionnextDirection 分离的设计可以防止连续快速按键导致蛇身掉头自杀。

蛇的移动

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function move() {
// 应用下次方向(防止掉头)
const opposite = { 'UP': 'DOWN', 'DOWN': 'UP', 'LEFT': 'RIGHT', 'RIGHT': 'LEFT' };
if (nextDirection && opposite[nextDirection] !== direction) {
direction = nextDirection;
}

const head = snake[0];
let newHead = { ...head };
switch (direction) {
case 'RIGHT': newHead.x++; break;
case 'LEFT': newHead.x--; break;
case 'UP': newHead.y--; break;
case 'DOWN': newHead.y++; break;
}

// 判断是否吃到食物
const isEating = food && newHead.x === food.x && newHead.y === food.y;
let newSnake = [newHead, ...snake];
if (!isEating) newSnake.pop(); // 没吃到则移除尾部

snake = newSnake;
}

核心思路:

  • 每次移动在头部添加新方块
  • 吃到食物时不移除尾部,蛇身变长
  • 未吃到食物时移除尾部,保持蛇身长度不变

碰撞检测

1
2
3
4
5
6
7
8
9
10
11
12
// 边界碰撞
if (newHead.x < 0 || newHead.x >= gridSize ||
newHead.y < 0 || newHead.y >= gridSize) {
gameOver = true;
return;
}

// 自身碰撞
if (newSnake.slice(1).some(seg => seg.x === newHead.x && seg.y === newHead.y)) {
gameOver = true;
return;
}

食物生成

1
2
3
4
5
6
7
8
9
10
11
function randomFood() {
for (let i = 0; i < 1000; i++) {
const x = Math.floor(Math.random() * gridSize);
const y = Math.floor(Math.random() * gridSize);
// 避开蛇身
if (!snake.some(seg => seg.x === x && seg.y === y)) {
return {x, y};
}
}
return null;
}

使用循环尝试生成随机位置,直到找到不与蛇身重叠的位置。

绘制系统

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);

// 绘制网格线
for (let i = 0; i <= gridSize; i++) {
// ... 绘制横竖线
}

// 绘制蛇身
snake.forEach((seg, idx) => {
ctx.fillStyle = idx === 0 ? '#8bc34a' : '#4CAF50';
ctx.fillRect(seg.x * cellSize + 1, seg.y * cellSize + 1,
cellSize - 2, cellSize - 2);
// 蛇眼睛
if (idx === 0) {
// ... 绘制眼睛
}
});

// 绘制食物
if (food) {
ctx.fillStyle = '#f44336';
ctx.beginPath();
ctx.arc(food.x * cellSize + cellSize / 2,
food.y * cellSize + cellSize / 2, 8, 0, 2 * Math.PI);
ctx.fill();
}
}

绘制顺序:清空画布 → 网格线 → 蛇身 → 食物 → 游戏结束遮罩。

控制方式

键盘控制

1
2
3
4
5
6
7
8
9
function handleKeydown(e) {
if (e.key.startsWith('Arrow')) e.preventDefault();
switch (e.key) {
case 'ArrowUp': nextDirection = 'UP'; break;
case 'ArrowDown': nextDirection = 'DOWN'; break;
case 'ArrowLeft': nextDirection = 'LEFT'; break;
case 'ArrowRight': nextDirection = 'RIGHT'; break;
}
}

触摸滑动控制

1
2
3
4
5
6
7
8
9
10
canvas.addEventListener('touchend', function(e) {
const deltaX = touchEndX - touchStartX;
const deltaY = touchEndY - touchStartY;

if (Math.abs(deltaX) > Math.abs(deltaY)) {
nextDirection = deltaX > 0 ? 'RIGHT' : 'LEFT';
} else {
nextDirection = deltaY > 0 ? 'DOWN' : 'UP';
}
});

通过比较水平/垂直滑动距离来判断滑动方向,实现移动端手势控制。

PJAX 兼容处理

Hexo 的主题通常使用 PJAX 实现无刷新页面切换,这会导致 JavaScript 状态丢失。需要监听页面切换事件重新初始化游戏:

1
2
3
4
5
document.addEventListener('pjax:success', function() {
if (document.getElementById('game-canvas')) {
setTimeout(initGameWhenReady, 100);
}
});

同时需要在游戏初始化时清除旧定时器,防止多个游戏循环同时运行:

1
2
3
4
if (window._snakeTimer) {
clearInterval(window._snakeTimer);
window._snakeTimer = null;
}

总结

通过以上方案,我们在 Hexo 博客中成功实现了一个完整的贪吃蛇小游戏。核心要点总结:

功能 实现方式
游戏初始化 全局函数 window.initSnakeGame
蛇的移动 数组队列操作
碰撞检测 边界 + 自身遍历检测
控制方式 键盘方向键 + 触摸滑动
页面兼容 PJAX 事件监听 + 定时器清理

游戏已集成到博客的小游戏栏目中,可以直接访问体验。后续计划添加更多经典小游戏,如俄罗斯方块、扫雷等。


相关链接: