使用HTML、CSS和JavaScript构建贪吃蛇游戏
在这篇文章中,我将详细介绍如何利用HTML、CSS和JavaScript来开发一款经典的贪吃蛇游戏。我们不会依赖任何外部库,所有代码都将在浏览器环境中运行。这个项目不仅是一项有趣的技术实践,也是锻炼你编程思维和解决问题能力的绝佳机会。
贪吃蛇游戏的核心玩法很简单:控制一条蛇在游戏区域内移动,避开障碍物,并尽可能多地吞噬食物。每当蛇成功吃到食物时,它的身体就会增长一段长度。随着游戏进行,蛇的身体越来越长,操控难度也会随之增加。
玩家需要注意,蛇不能撞到游戏边界或者自身的身体。所以,随着蛇的长度增加,游戏挑战性也随之提高。
本教程的目标是指导你完成一个完整的贪吃蛇游戏构建过程,让你深入理解其中的原理。
你可以从我的 GitHub 仓库找到完整的源代码。游戏的演示版本托管在 GitHub Pages。
准备工作
这个项目将使用HTML、CSS和JavaScript进行构建。虽然我们会使用基本的HTML和CSS进行页面布局和样式设置,但JavaScript才是我们关注的重点。因此,你应当对JavaScript有一定的了解。如果你还不熟悉JavaScript,我建议你先学习一些相关的基础知识。当然,你还需要一个代码编辑器和一个浏览器来编写和测试代码,如果你正在阅读这篇文章,你很可能已经拥有了这两样工具。
项目设置
首先,我们需要创建项目文件。在一个空文件夹中,创建一个名为`index.html`的文件,并加入以下HTML标记:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <link rel="stylesheet" href="https://wilku.top/javascript-snake-tutorial-explained/./styles.css" /> <title>Snake</title> </head> <body> <div id="game-over-screen"> <h1>Game Over</h1> </div> <canvas id="canvas" width="420" height="420"> </canvas> <script src="./snake.js"></script> </body> </html>
这段代码创建了一个基础的“游戏结束”界面,我们将通过JavaScript来控制它的显示与隐藏。它还定义了一个画布(canvas)元素,我们将在其上绘制游戏地图、蛇和食物。此外,这段HTML代码还引入了样式表(styles.css)和JavaScript代码文件(snake.js)。
接着,创建一个名为`styles.css`的文件,并添加以下CSS样式:
* { margin: 0; padding: 0; box-sizing: border-box; font-family: 'Courier New', Courier, monospace; } body { height: 100vh; display: flex; flex-direction: column; justify-content: center; align-items: center; background-color: #00FFFF; } #game-over-screen { background-color: #FF00FF; width: 500px; height: 200px; border: 5px solid black; position: absolute; align-items: center; justify-content: center; display: none; }
这段CSS代码首先重置了所有元素的内外边距,并将盒模型设置为`border-box`,同时设置了默认字体。对于`body`元素,我们将其高度设置为视口的100%,并使用Flexbox将内容居中显示。我们还为背景设置了青色。最后,我们为“游戏结束”屏幕设置了样式,包括尺寸、边框、背景颜色以及绝对定位,确保其在页面中心显示。默认情况下,我们将`display`属性设置为`none`,使其初始状态是隐藏的。
最后,我们需要创建一个`snake.js`文件,我们将在接下来的章节中编写这个文件的内容。
定义全局变量
在`snake.js`文件中,首先声明一些全局变量。这些变量将在整个游戏中被使用,所以将它们放在文件的顶部:
// 获取HTML元素的引用 let gameOverScreen = document.getElementById("game-over-screen"); let canvas = document.getElementById("canvas"); // 获取画布的绘图上下文 let ctx = canvas.getContext("2d");
这里我们获取了对“游戏结束”屏幕和画布元素的引用,并且创建了一个绘图上下文,这个上下文将用于在画布上进行绘制操作。
接着,添加以下变量定义,用于设置游戏网格和单位长度:
// 定义游戏网格 let gridSize = 400; let unitLength = 10;
`gridSize`定义了游戏网格的大小(以像素为单位),`unitLength`定义了游戏中的基本单位长度,这个单位长度将用于定义蛇、食物和游戏边界的尺寸和移动步长。
最后,我们还需要一些变量来追踪游戏状态:
// 定义游戏状态变量 let snake = []; let foodPosition = { x: 0, y: 0 }; let direction = "right"; let collided = false;
`snake`数组用于存储蛇的身体位置,每个元素代表蛇的一个身体部分,包含x和y坐标。`foodPosition`变量保存食物的当前位置。`direction`变量记录蛇的移动方向,`collided`是一个布尔值,用于标记是否发生碰撞。
声明函数
为了更好地组织代码,我们将游戏逻辑分解为多个函数。以下是我们需要声明的函数及其功能:
function setUp() {} function doesSnakeOccupyPosition(x, y) {} function checkForCollision() {} function generateFood() {} function move() {} function turn(newDirection) {} function onKeyDown(e) {} function gameLoop() {}
`setUp`函数负责初始化游戏,`checkForCollision`检查蛇是否发生碰撞,`doesSnakeOccupyPosition`检查某个位置是否被蛇占据,`generateFood`生成食物,`move`控制蛇的移动,`turn`改变蛇的移动方向,`onKeyDown`监听键盘事件,`gameLoop`控制游戏的主要循环。
定义函数
现在我们开始定义这些函数。每个函数都将有详细的解释和注释,确保你理解其背后的逻辑。
`setUp` 函数
`setUp`函数的主要任务是设置游戏初始状态,包括绘制游戏边界、初始化蛇的位置以及生成初始食物位置:
// 在画布上绘制边界 // 画布的大小是网格大小加上两侧边框的厚度 canvasSideLength = gridSize + unitLength * 2; // 绘制一个覆盖整个画布的黑色矩形 ctx.fillRect(0, 0, canvasSideLength, canvasSideLength); // 清除黑色矩形的中心区域,形成游戏区域,留下黑色边框 ctx.clearRect(unitLength, unitLength, gridSize, gridSize); // 接下来,初始化蛇的头部和尾部位置 // 蛇的初始长度为6个单位长度或60px // 蛇头的位置在网格中心前方3个单位 const headPosition = Math.floor(gridSize / 2) + 30; // 蛇尾的位置在网格中心后方3个单位 const tailPosition = Math.floor(gridSize / 2) - 30; // 从尾部到头部,以单位长度为步长循环 for (let i = tailPosition; i <= headPosition; i += unitLength) { // 存储蛇身体的位置,并在画布上绘制 snake.push({ x: i, y: Math.floor(gridSize / 2) }); // 在该位置绘制一个单位长度的矩形 ctx.fillRect(x, y, unitLength, unitLength); } // 生成食物 generateFood();
`doesSnakeOccupyPosition` 函数
此函数接受x和y坐标作为参数,检查蛇的身体是否占据了该位置。它使用数组的find方法来检查蛇的身体中是否有位置与传入的坐标匹配:
function doesSnakeOccupyPosition(x, y) { return !!snake.find((position) => { return position.x == x && y == foodPosition.y; }); }
`checkForCollision` 函数
此函数负责检查蛇是否与游戏边界或者自身发生碰撞。如果发生碰撞,它会将`collided`变量设置为`true`:
function checkForCollision() { const headPosition = snake.slice(-1)[0]; // 检查与左右边界的碰撞 if (headPosition.x < 0 || headPosition.x >= gridSize - 1) { collided = true; } // 检查与上下边界的碰撞 if (headPosition.y < 0 || headPosition.y >= gridSize - 1) { collided = true; } // 检查与自身身体的碰撞 const body = snake.slice(0, -2); if ( body.find( (position) => position.x == headPosition.x && position.y == headPosition.y ) ) { collided = true; } }
`generateFood` 函数
此函数使用do-while循环来生成食物的位置,并确保食物的位置不与蛇的身体重叠。一旦找到合适的位置,食物会被绘制在画布上:
function generateFood() { let x = 0, y = 0; do { x = Math.floor((Math.random() * gridSize) / 10) * 10; y = Math.floor((Math.random() * gridSize) / 10) * 10; } while (doesSnakeOccupyPosition(x, y)); foodPosition = { x, y }; ctx.fillRect(x, y, unitLength, unitLength); }
`move` 函数
此函数负责控制蛇的移动。它首先复制蛇头的位置,然后根据当前方向更新蛇头的坐标,并将新的蛇头添加到蛇的数组中。如果蛇吃到了食物,则生成新的食物,否则,蛇的尾部会被移除,从而保持蛇的长度不变:
function move() { // 创建蛇头位置的副本 const headPosition = Object.assign({}, snake.slice(-1)[0]); switch (direction) { case "left": headPosition.x -= unitLength; break; case "right": headPosition.x += unitLength; break; case "up": headPosition.y -= unitLength; break; case "down": headPosition.y += unitLength; } // 将新的蛇头添加到数组中 snake.push(headPosition); ctx.fillRect(headPosition.x, headPosition.y, unitLength, unitLength); // 检查蛇是否吃到食物 const isEating = foodPosition.x == headPosition.x && foodPosition.y == headPosition.y; if (isEating) { // 生成新的食物位置 generateFood(); } else { // 如果蛇没有吃到食物,则移除蛇尾 tailPosition = snake.shift(); // 清除尾部在画布上的位置 ctx.clearRect(tailPosition.x, tailPosition.y, unitLength, unitLength); } }
`turn` 函数
此函数用于改变蛇的移动方向,但它会检查新的移动方向是否与当前方向垂直。例如,当蛇向上或向下移动时,它只能向左或向右转,反之亦然:
function turn(newDirection) { switch (newDirection) { case "left": case "right": // 只有当蛇的原始移动方向是向上或向下时,才允许向左或向右转 if (direction == "up" || direction == "down") { direction = newDirection; } break; case "up": case "down": // 只有当蛇的原始移动方向是向左或向右时,才允许向上或向下转 if (direction == "left" || direction == "right") { direction = newDirection; } break; } }
`onKeyDown` 函数
此函数是一个事件监听器,用于响应键盘按键事件。它会根据用户按下的箭头键调用相应的`turn`函数来改变蛇的移动方向:
function onKeyDown(e) { switch (e.key) { case "ArrowDown": turn("down"); break; case "ArrowUp": turn("up"); break; case "ArrowLeft": turn("left"); break; case "ArrowRight": turn("right"); break; } }
`gameLoop` 函数
`gameLoop`函数是游戏的主循环。它会调用`move`函数来移动蛇,调用`checkForCollision`函数来检查碰撞。如果发生碰撞,则停止游戏循环并显示“游戏结束”屏幕:
function gameLoop() { move(); checkForCollision(); if (collided) { clearInterval(timer); gameOverScreen.style.display = "flex"; } }
启动游戏
最后,添加以下代码来启动游戏:
setUp(); document.addEventListener("keydown", onKeyDown); let timer = setInterval(gameLoop, 200);
这段代码首先调用`setUp`函数来初始化游戏,然后添加`keydown`事件监听器来处理键盘输入,并使用`setInterval`函数来启动游戏循环。
总结
至此,你的JavaScript代码应该与我GitHub仓库中的代码类似。如果有任何问题,可以仔细检查我的仓库中的代码。接下来,你可能想了解如何在JavaScript中创建图像滑块。