从零开始:用 HTML、CSS、JavaScript 制作一个贪吃蛇游戏
一、项目介绍
贪吃蛇是一款经典的小游戏。玩家通过方向键控制蛇的移动,吃掉食物后蛇身会变长,同时游戏得分增加。如果蛇撞到墙壁或者自己的身体,游戏就会结束。
本项目具有以下特点:
-
纯前端实现:只使用 HTML、CSS 和 JavaScript。
-
无需图片:使用 Canvas 绘制蛇、食物和游戏背景。
-
无需依赖:不需要安装第三方库,也不需要服务器。
-
支持键盘操作:使用方向键或 WASD 控制移动。
-
完整游戏功能:开始、暂停、重新开始、计分和最高分记录。
二、完整代码
创建一个名为 snake.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>
* {
box-sizing: border-box;
}
body {
margin: 0;
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
padding: 20px;
background: #101827;
color: #f1f5f9;
font-family: system-ui, sans-serif;
}
.game {
width: min(100%, 460px);
text-align: center;
}
h1 {
margin-bottom: 8px;
letter-spacing: 3px;
}
.subtitle {
color: #94a3b8;
font-size: 14px;
margin-bottom: 22px;
}
.scores {
display: flex;
gap: 12px;
margin-bottom: 14px;
}
.score-box {
flex: 1;
padding: 12px;
background: #1e293b;
border-radius: 12px;
}
.score-box span {
display: block;
color: #94a3b8;
font-size: 12px;
}
.score-box strong {
display: block;
margin-top: 5px;
font-size: 24px;
}
canvas {
display: block;
width: 100%;
height: auto;
border: 2px solid #334155;
border-radius: 12px;
background: #0b1220;
box-shadow: 0 12px 40px #0004;
}
#status {
min-height: 24px;
margin: 14px 0;
color: #a7f3d0;
font-size: 14px;
}
.buttons {
display: flex;
gap: 10px;
}
button {
flex: 1;
padding: 12px 8px;
border: 0;
border-radius: 9px;
background: #22c55e;
color: #052e16;
font-size: 15px;
font-weight: 700;
cursor: pointer;
}
button:hover {
filter: brightness(1.1);
}
button.secondary {
background: #334155;
color: #f8fafc;
}
.help {
margin-top: 18px;
color: #94a3b8;
font-size: 12px;
line-height: 1.9;
}
.mobile-controls {
display: grid;
grid-template-columns: repeat(3, 58px);
justify-content: center;
gap: 7px;
margin-top: 18px;
}
.mobile-controls button {
min-height: 48px;
padding: 8px;
font-size: 20px;
background: #334155;
color: white;
}
.mobile-controls .empty {
visibility: hidden;
}
@media (min-width: 600px) {
.mobile-controls {
display: none;
}
}
</style>
</head>
<body>
<main class="game">
<h1>贪吃蛇</h1>
<div class="subtitle">专注每一次移动,挑战你的最高分</div>
<div class="scores">
<div class="score-box">
<span>当前得分</span>
<strong id="score">0</strong>
</div>
<div class="score-box">
<span>历史最高分</span>
<strong id="best">0</strong>
</div>
</div>
<canvas id="board" width="400" height="400"></canvas>
<div id="status">点击“开始游戏”开启挑战</div>
<div class="buttons">
<button id="start">开始游戏</button>
<button id="pause" class="secondary">暂停</button>
<button id="restart" class="secondary">重新开始</button>
</div>
<div class="mobile-controls">
<button class="empty"> </button>
<button data-dir="up">▲</button>
<button class="empty"> </button>
<button data-dir="left">◀</button>
<button data-dir="down">▼</button>
<button data-dir="right">▶</button>
</div>
<div class="help">
方向键 / WASD 控制移动<br>
空格键暂停 · 吃掉绿色食物获得分数<br>
撞到墙壁或自身,游戏结束
</div>
</main>
<script>
const canvas = document.getElementById("board");
const ctx = canvas.getContext("2d");
const scoreEl = document.getElementById("score");
const bestEl = document.getElementById("best");
const statusEl = document.getElementById("status");
const startBtn = document.getElementById("start");
const pauseBtn = document.getElementById("pause");
const CELL = 20;
const SIZE = canvas.width / CELL;
let snake, food, direction, nextDirection;
let score = 0;
let best = loadBest();
let timer = null;
let running = false;
let gameOver = false;
let paused = false;
bestEl.textContent = best;
function loadBest() {
try {
return Number(localStorage.getItem("snakeBest")) || 0;
} catch {
return 0;
}
}
function saveBest() {
try {
localStorage.setItem("snakeBest", String(best));
} catch {}
}
function reset() {
clearInterval(timer);
timer = null;
snake = [
{ x: 10, y: 10 },
{ x: 9, y: 10 },
{ x: 8, y: 10 }
];
direction = { x: 1, y: 0 };
nextDirection = { ...direction };
score = 0;
running = false;
paused = false;
gameOver = false;
scoreEl.textContent = score;
pauseBtn.textContent = "暂停";
startBtn.textContent = "开始游戏";
statusEl.textContent = "准备好了吗?点击开始游戏";
placeFood();
draw();
}
function placeFood() {
const empty = [];
for (let y = 0; y < SIZE; y++) {
for (let x = 0; x < SIZE; x++) {
if (!snake.some(part => part.x === x && part.y === y)) {
empty.push({ x, y });
}
}
}
if (empty.length === 0) {
win();
return;
}
food = empty[Math.floor(Math.random() * empty.length)];
}
function draw() {
ctx.fillStyle = "#0b1220";
ctx.fillRect(0, 0, canvas.width, canvas.height);
// 绘制网格
ctx.strokeStyle = "#172338";
ctx.lineWidth = 1;
for (let i = 0; i <= SIZE; i++) {
ctx.beginPath();
ctx.moveTo(i * CELL, 0);
ctx.lineTo(i * CELL, canvas.height);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(0, i * CELL);
ctx.lineTo(canvas.width, i * CELL);
ctx.stroke();
}
// 绘制食物
if (food) {
ctx.fillStyle = "#fb7185";
ctx.beginPath();
ctx.arc(
food.x * CELL + CELL / 2,
food.y * CELL + CELL / 2,
CELL * 0.36,
0,
Math.PI * 2
);
ctx.fill();
}
// 绘制蛇身
snake.forEach((part, index) => {
const gap = 2;
ctx.fillStyle = index === 0 ? "#86efac" : "#22c55e";
ctx.beginPath();
ctx.roundRect(
part.x * CELL + gap,
part.y * CELL + gap,
CELL - gap * 2,
CELL - gap * 2,
5
);
ctx.fill();
// 蛇头眼睛
if (index === 0) {
ctx.fillStyle = "#14532d";
const ex = part.x * CELL;
const ey = part.y * CELL;
let eyes;
if (direction.x === 1) {
eyes = [[13, 6], [13, 14]];
} else if (direction.x === -1) {
eyes = [[5, 6], [5, 14]];
} else if (direction.y === -1) {
eyes = [[6, 5], [14, 5]];
} else {
eyes = [[6, 13], [14, 13]];
}
eyes.forEach(([x, y]) => {
ctx.beginPath();
ctx.arc(ex + x, ey + y, 2, 0, Math.PI * 2);
ctx.fill();
});
}
});
if (paused || gameOver) {
ctx.fillStyle = "#020617bb";
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = "#ffffff";
ctx.textAlign = "center";
ctx.font = "bold 28px system-ui";
ctx.fillText(
gameOver ? "游戏结束" : "已暂停",
canvas.width / 2,
canvas.height / 2
);
}
}
function tick() {
direction = { ...nextDirection };
const head = {
x: snake[0].x + direction.x,
y: snake[0].y + direction.y
};
const ate = head.x === food.x && head.y === food.y;
// 未吃到食物时,尾巴本次会移动,因此允许进入原尾部格子
const body = ate ? snake : snake.slice(0, -1);
const hitWall =
head.x < 0 || head.x >= SIZE ||
head.y < 0 || head.y >= SIZE;
const hitSelf = body.some(
part => part.x === head.x && part.y === head.y
);
if (hitWall || hitSelf) {
endGame();
return;
}
snake.unshift(head);
if (ate) {
score += 10;
scoreEl.textContent = score;
if (score > best) {
best = score;
bestEl.textContent = best;
saveBest();
}
placeFood();
if (gameOver) return;
} else {
snake.pop();
}
draw();
}
function startGame() {
if (running) return;
if (gameOver) reset();
running = true;
paused = false;
startBtn.textContent = "游戏进行中";
statusEl.textContent = "游戏进行中,加油!";
timer = setInterval(tick, 120);
draw();
}
function togglePause() {
if (!running && !paused) return;
if (paused) {
paused = false;
running = true;
timer = setInterval(tick, 120);
pauseBtn.textContent = "暂停";
statusEl.textContent = "游戏继续";
} else {
clearInterval(timer);
timer = null;
running = false;
paused = true;
pauseBtn.textContent = "继续";
statusEl.textContent = "游戏已暂停";
}
draw();
}
function endGame() {
clearInterval(timer);
timer = null;
running = false;
paused = false;
gameOver = true;
startBtn.textContent = "再玩一次";
statusEl.textContent = `游戏结束!最终得分:${score}`;
draw();
}
function win() {
clearInterval(timer);
timer = null;
running = false;
paused = false;
gameOver = true;
startBtn.textContent = "再玩一次";
statusEl.textContent = `恭喜通关!最终得分:${score}`;
draw();
}
function changeDirection(dir) {
const dirs = {
up: { x: 0, y: -1 },
down: { x: 0, y: 1 },
left: { x: -1, y: 0 },
right: { x: 1, y: 0 }
};
const target = dirs[dir];
if (!target) return;
// 禁止直接反向移动
if (
target.x + direction.x === 0 &&
target.y + direction.y === 0
) return;
nextDirection = target;
}
document.addEventListener("keydown", event => {
const keys = {
ArrowUp: "up",
w: "up",
W: "up",
ArrowDown: "down",
s: "down",
S: "down",
ArrowLeft: "left",
a: "left",
A: "left",
ArrowRight: "right",
d: "right",
D: "right"
};
if (keys[event.key]) {
event.preventDefault();
if (!running && !paused && !gameOver) {
startGame();
}
changeDirection(keys[event.key]);
}
if (event.code === "Space") {
event.preventDefault();
togglePause();
}
});
document.querySelectorAll("[data-dir]").forEach(button => {
button.addEventListener("click", () => {
changeDirection(button.dataset.dir);
if (!running && !paused && !gameOver) {
startGame();
}
});
});
startBtn.addEventListener("click", startGame);
pauseBtn.addEventListener("click", togglePause);
document.getElementById("restart").addEventListener("click", reset);
reset();
</script>
</body>
</html>
三、如何运行游戏
-
新建一个文本文件,命名为
snake.html。 -
将上面的完整代码复制到文件中并保存。
-
使用 Chrome、Edge 或 Firefox 打开这个 HTML 文件。
-
点击“开始游戏”,使用键盘方向键控制蛇移动。
如果你在 Windows 或 WSL 环境中开发,也可以在文件所在目录通过浏览器打开。这个游戏不需要运行 npm install,也不需要启动后端服务。
四、核心代码解析
1. 使用 Canvas 绘制游戏画面
const canvas = document.getElementById("board");
const ctx = canvas.getContext("2d");
Canvas 提供二维绘图能力,可以直接绘制网格、蛇身和食物,无需准备任何图片资源。
2. 使用数组表示蛇的身体
let snake = [
{ x: 10, y: 10 },
{ x: 9, y: 10 },
{ x: 8, y: 10 }
];
数组中的第一个元素代表蛇头,其余元素代表蛇身。每次移动时,在数组头部添加新的蛇头,并删除末尾的蛇身,即可实现移动效果。
3. 实现吃食物和增长
snake.unshift(head);
if (ate) {
score += 10;
} else {
snake.pop();
}
当蛇头碰到食物时,蛇身不删除尾部,因此长度增加;否则删除最后一个元素,保持长度不变。
4. 实现游戏循环
timer = setInterval(tick, 120);
每隔 120 毫秒执行一次游戏逻辑,包括移动蛇身、检查碰撞、判断是否吃到食物以及刷新画面。
五、总结
通过这个小项目,我们完成了一个不依赖图片和第三方库的浏览器贪吃蛇游戏,同时学习了 Canvas 绘图、JavaScript 定时器、键盘事件、数组操作和碰撞检测等基础知识。
后续还可以增加难度递增、音效、排行榜、主题切换、触屏滑动和最高分统计等功能,让游戏更加完整。
建议下一步: 将这个小游戏部署到 GitHub Pages 或自己的博客网站中,让访客无需下载文件,直接在线游玩。
- 点赞
- 收藏
- 关注作者
评论(0)