如何用纯前端技术打造一个可定制的贪吃蛇游戏(附完整源码)
很多前端开发者都曾有过一个想法:亲手实现一个经典游戏,比如贪吃蛇。这不仅仅是为了怀旧,更是一个绝佳的练手项目,能让你把HTML、CSS和JavaScript的知识点串联起来,从静态页面跨越到动态交互的领域。但如果你已经掌握了基础实现,可能会觉得“画个方块,动起来,吃个点”的版本有些乏味。这篇文章,我想和你聊聊如何把这个经典项目玩出新花样——打造一个高度可定制、易于扩展的贪吃蛇游戏引擎。
我们的目标不是复制一个能跑起来的游戏,而是构建一个代码清晰、模块分明、配置灵活的框架。你可以轻松地调整游戏难度、更换皮肤主题、增加特殊道具,甚至改变核心规则。这非常适合那些希望在前端游戏开发上更进一步,或者想为自己的个人作品集增加一个亮眼项目的朋友。我们将从零开始,但重点会放在架构设计和扩展性上,最终你会得到一套完整的、可随意“魔改”的源码。
1. 项目架构与核心模块设计
在动手写第一行代码之前,花点时间思考架构是值得的。一个结构混乱的游戏代码,添加新功能时会变成一场灾难。我们将游戏逻辑分解为几个独立的模块,每个模块职责单一,通过清晰的接口进行通信。
1.1 模块化设计思路
传统的教学代码往往把所有变量和函数堆在一起,虽然简单直接,但维护和扩展困难。我们采用一种更工程化的思路:
- GameEngine (游戏引擎):负责协调整个游戏流程,控制主循环的启动、暂停与结束。它是游戏的大脑。
- Snake (蛇):作为一个独立的类(Class),管理蛇身每一节的坐标、移动方向、生长逻辑以及自身的绘制。
- FoodManager (食物管理器):不止是生成一个随机点。未来我们可以让它管理多种食物、生成规则(如不生成在蛇身上)、甚至食物特效。
- Renderer (渲染器):将游戏状态(蛇、食物、分数)绘制到Canvas上的模块。分离渲染逻辑后,更换主题(比如从像素风变为矢量图)会变得非常容易。
- InputHandler (输入处理器):集中管理所有用户输入(键盘、触摸、甚至未来可能的游戏手柄),并将其转化为游戏引擎能理解的控制命令。
- Config (配置中心):一个集中存放所有可调参数的对象。游戏速度、网格大小、颜色、初始长度等都从这里读取。
这种设计的好处是显而易见的。比如,你想把控制方式从键盘改成触屏,只需要修改或替换 InputHandler 模块,其他部分几乎不用动。
1.2 配置中心:游戏定制的起点
让我们先从最有趣的部分——配置开始。我们将所有可定制的参数集中在一个对象里,这是实现“可定制”的关键第一步。
// config.js
const GameConfig = {
// 画布与网格
canvasWidth: 600,
canvasHeight: 600,
gridSize: 20, // 每个网格的像素大小
// 游戏难度与规则
initialSpeed: 150, // 初始游戏循环间隔(毫秒),值越大越慢
speedIncrement: 0.95, // 每吃一个食物,速度增加的系数(小于1则变快)
initialSnakeLength: 3,
wallCollisionFatal: true, // 撞墙是否导致游戏结束?设为false可实现穿墙模式
// 视觉主题
themes: {
classic: {
backgroundColor: '#0f1b29',
snakeColor: '#4ade80',
foodColor: '#f87171',
gridLineColor: 'rgba(255, 255, 255, 0.05)'
},
neon: {
backgroundColor: '#000',
snakeColor: '#0ff',
foodColor: '#f0f',
gridLineColor: '#222'
},
retro: {
backgroundColor: '#000',
snakeColor: '#0f0',
foodColor: '#f00',
gridLineColor: '#333'
}
},
currentTheme: 'classic',
// 控制
controls: {
KEY_UP: ['ArrowUp', 'KeyW'],
KEY_DOWN: ['ArrowDown', 'KeyS'],
KEY_LEFT: ['ArrowLeft', 'KeyA'],
KEY_RIGHT: ['ArrowRight', 'KeyD'],
KEY_PAUSE: ['Space', 'KeyP']
}
};
// 导出配置,方便其他模块引用
export default GameConfig;
提示:将主题独立为配置项,意味着我们可以在游戏运行时动态切换主题,只需要调用渲染器的更新方法并传入新的主题名即可。
通过这样一个配置对象,游戏的“性格”就完全掌握在你手中了。想做一个慢速休闲版?调高 initialSpeed。想做霓虹炫彩风?切换到 neon 主题。想支持 WASD 和方向键双控制?已经在 controls 里定义好了。
2. 实现游戏核心类:蛇与食物
有了蓝图,我们开始浇筑核心部件。我们将使用ES6的Class语法来创建Snake和FoodManager,这让代码更易读、易维护。
2.1 Snake类:不仅仅是数组
蛇不再是一个简单的坐标数组,而是一个具有状态和行为的对象。
// snake.js
import GameConfig from './config.js';
class Snake {
constructor() {
this.reset();
}
reset() {
// 蛇身用一个数组表示,每个元素是{x, y}坐标
this.body = [];
this.direction = { x: 1, y: 0 }; // 初始向右移动
this.nextDirection = { x: 1, y: 0 }; // 用于缓冲输入,防止一帧内连续转向导致直接反向
this.length = GameConfig.initialSnakeLength;
// 初始化蛇身,放在画布中间偏左
const startX = Math.floor(GameConfig.canvasWidth / GameConfig.gridSize / 4);
const startY = Math.floor(GameConfig.canvasHeight / GameConfig.gridSize / 2);
for (let i = 0; i < this.length; i++) {
this.body.push({ x: startX - i, y: startY });
}
this.hasEaten = false; // 标记本帧是否吃到食物,用于决定是否增长
}
// 改变方向,使用缓冲防止180度直接转向
changeDirection(newDir) {
// 防止直接反向移动(例如正在向右时不能立即向左)
if (newDir.x !== -this.direction.x || newDir.y !== -this.direction.y) {
this.nextDirection = newDir;
}
}
// 更新蛇的位置
update() {
// 应用缓冲的方向
this.direction = this.nextDirection;
// 计算新的头部位置
const head = this.body[0];
const newHead = {
x: head.x + this.direction.x,
y: head.y + this.direction.y
};
// 处理穿墙逻辑(如果配置允许)
if (!GameConfig.wallCollisionFatal) {
const gridWidth = GameConfig.canvasWidth / GameConfig.gridSize;
const gridHeight = GameConfig.canvasHeight / GameConfig.gridSize;
newHead.x = (newHead.x + gridWidth) % gridWidth;
newHead.y = (newHead.y + gridHeight) % gridHeight;
}
// 将新头部加入数组
this.body.unshift(newHead);
// 如果没有吃到食物,则移除尾部,保持长度不变
if (!this.hasEaten) {
this.body.pop();
} else {
// 如果吃到了,长度增加,并重置标记
this.length++;
this.hasEaten = false;
}
}
// 检查是否吃到食物
checkFoodCollision(foodPosition) {
const head = this.body[0];
return head.x === foodPosition.x && head.y === foodPosition.y;
}
// 检查是否撞到自己
checkSelfCollision() {
const head = this.body[0];
// 从第二段开始检查(第一段是头部自身)
for (let i = 1; i < this.body.length; i++) {
if (head.x === this.body[i].x && head.y === this.body[i].y) {
return true;
}
}
return false;
}
// 检查是否撞墙(仅在墙致命时调用)
checkWallCollision() {
const head = this.body[0];
const gridWidth = GameConfig.canvasWidth / GameConfig.gridSize;
const gridHeight = GameConfig.canvasHeight / GameConfig.gridSize;
return head.x < 0 || head.x >= gridWidth || head.y < 0 || head.y >= gridHeight;
}
// 标记为“已吃”
grow

&spm=1001.2101.3001.5002&articleId=154669709&d=1&t=3&u=88f98f33b4d64aab81c1afb8cff5ea65)
1万+

被折叠的 条评论
为什么被折叠?



