动手用 Web 实现一个 2048 游戏
文章目录
- 为什么选择 2048?
- 关键技术点与算法详解
- HTML 结构:搭建游戏界面
- CSS 样式:美化游戏界面
- JavaScript 核心逻辑:驱动游戏运行
- 1)数据结构:二维数组表示游戏网格
- 2)核心算法:添加随机方块
- 3)核心算法:方块移动与合并
- 4)事件监听与游戏流程
- 最后
近期文章:
- 【前端练手必备】从零到一,教你用JS写出风靡全球的“贪吃蛇”!
- Google Search Console 做SEO分析之“已发现未编入” 与 “已抓取未编入” 有什么区别?
- 如何通过 noindex 阻止网页被搜索引擎编入索引?
- 建站SEO优化之站点地图sitemap
- 个人建站做SEO网站外链这一点需要注意,做错了可能受到Google惩罚
- 一文搞懂SEO优化之站点robots.txt
- Node.js中那些常用的进程通信方式
- 实现篇:二叉树遍历收藏版
- 实现篇:LRU算法的几种实现
- 从底层视角看requestAnimationFrame的性能增强
- Nginx Upstream了解一下
- 一文搞懂 Markdown 文档规则
2048 游戏,这款曾经风靡全球的数字益智游戏,以其简洁的规则和深度的策略性吸引了无数玩家。它不仅是一款娱乐产品,更是许多初学者学习编程和前端开发的绝佳练手项目。今天,就来一起探索如何通过 Web 技术,一步步实现一个属于我们自己的 2048 游戏。
为什么选择 2048?
体验地址:2048游戏。2048 游戏看似简单,却蕴含了前端开发中的许多核心概念和技术:
- DOM 操作: 游戏界面中方块的生成、移动、合并,都需要频繁地操作 HTML 元素。
- 事件监听: 玩家通过键盘方向键控制方块移动,需要监听键盘事件。
- 数据结构与算法: 游戏的核心逻辑,如方块的移动、合并、新方块的生成,都需要高效的数据结构(如二维数组)和相应的算法支持。
- 响应式设计: 考虑到不同设备的屏幕尺寸,良好的响应式设计能提升用户体验。
- 游戏逻辑与状态管理: 维护游戏得分、游戏结束判断等,需要清晰的逻辑和状态管理。
对于前端开发者来说,完成一个 2048 游戏项目,不仅能巩固基础知识,还能在实践中提升解决问题的能力,体验将创意变为现实的乐趣。
关键技术点与算法详解
HTML 结构:搭建游戏界面
游戏界面主要由一个网格容器和多个方块单元组成。
<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>2048 游戏</title><link rel="stylesheet" href="style.css">
</head>
<body><div class="game-container"><h1>2048 游戏</h1><div class="score-container">分数: <span id="score">0</span></div><div id="game-grid"></div><div id="game-over-message" class="hidden">游戏结束!<button id="restart-button">重新开始</button></div></div><script src="script.js"></script>
</body>
</html>
game-grid
:游戏网格的容器,我们将在这里动态创建方块。score
:显示当前得分。game-over-message
:游戏结束时显示的提示信息。
CSS 样式:美化游戏界面
CSS 主要负责方块的布局、颜色、动画效果等。
/* style.css */
body {font-family: Arial, sans-serif;display: flex;justify-content: center;align-items: center;min-height: 100vh;background-color: #faf8ef;margin: 0;
}.game-container {background-color: #bbada0;padding: 20px;border-radius: 6px;box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);text-align: center;
}#game-grid {display: grid;grid-template-columns: repeat(4, 100px); /* 4x4 网格 */grid-template-rows: repeat(4, 100px);gap: 10px;margin-top: 20px;
}.tile {width: 100px;height: 100px;background-color: #cdc1b4;border-radius: 3px;display: flex;justify-content: center;align-items: center;font-size: 35px;font-weight: bold;color: #776e65;transition: transform 0.1s ease-in-out, background-color 0.1s ease-in-out;
}/* 不同数字方块的背景色和文字颜色 */
.tile-2 { background-color: #eee4da; color: #776e65; }
.tile-4 { background-color: #ede0c8; color: #776e65; }
.tile-8 { background-color: #f2b179; color: #f9f6f2; }
.tile-16 { background-color: #f59563; color: #f9f6f2; }
.tile-32 { background-color: #f67c5f; color: #f9f6f2; }
.tile-64 { background-color: #f65e3b; color: #f9f6f2; }
.tile-128 { background-color: #edcf72; color: #f9f6f2; }
.tile-256 { background-color: #edcc61; color: #f9f6f2; }
.tile-512 { background-color: #edc850; color: #f9f6f2; }
.tile-1024 { background-color: #edc53f; color: #f9f6f2; }
.tile-2048 { background-color: #edc22e; color: #f9f6f2; }.hidden {display: none;
}
grid
布局:轻松实现 4x4 的网格布局。.tile
:定义方块的基础样式。.tile-X
:根据方块的数值设置不同的背景色和文字颜色,增加视觉效果。transition
:为方块的移动和颜色变化添加平滑过渡动画。
JavaScript 核心逻辑:驱动游戏运行
JavaScript 是游戏的核心。
1)数据结构:二维数组表示游戏网格
我们使用一个 4x4 的二维数组来存储游戏网格中每个位置的数字。
// script.js
const GRID_SIZE = 4;
let gameGrid = []; // 存储游戏数据的二维数组
let score = 0;
let gameOver = false;const gameGridElement = document.getElementById('game-grid');
const scoreElement = document.getElementById('score');
const gameOverMessageElement = document.getElementById('game-over-message');
const restartButton = document.getElementById('restart-button');// 初始化游戏
function initializeGame() {gameGrid = Array(GRID_SIZE).fill(0).map(() => Array(GRID_SIZE).fill(0));score = 0;gameOver = false;scoreElement.textContent = score;gameOverMessageElement.classList.add('hidden');renderGrid();addRandomTile();addRandomTile();
}// 渲染游戏网格到 DOM
function renderGrid() {gameGridElement.innerHTML = ''; // 清空现有方块for (let r = 0; r < GRID_SIZE; r++) {for (let c = 0; c < GRID_SIZE; c++) {const tileValue = gameGrid[r][c];const tileElement = document.createElement('div');tileElement.classList.add('tile');if (tileValue > 0) {tileElement.textContent = tileValue;tileElement.classList.add(`tile-${tileValue}`);}gameGridElement.appendChild(tileElement);}}
}
gameGrid
:一个二维数组,gameGrid[r][c]
表示 (r, c) 位置的方块数值,0 表示空。initializeGame()
:初始化游戏状态,清空网格,重置分数,并生成两个初始方块。renderGrid()
:根据gameGrid
的数据,动态创建或更新 DOM 中的方块。
2)核心算法:添加随机方块
游戏开始和每次有效移动后,需要随机生成一个 2 或 4 的方块。
// 添加随机方块
function addRandomTile() {const emptyCells = [];for (let r = 0; r < GRID_SIZE; r++) {for (let c = 0; c < GRID_SIZE; c++) {if (gameGrid[r][c] === 0) {emptyCells.push({ r, c });}}}if (emptyCells.length > 0) {const randomIndex = Math.floor(Math.random() * emptyCells.length);const { r, c } = emptyCells[randomIndex];// 90% 的概率生成 2,10% 生成 4gameGrid[r][c] = Math.random() < 0.9 ? 2 : 4;}
}
emptyCells
:找到所有空闲的格子。- 随机选择一个空闲格子,并为其赋值 2 或 4。
3)核心算法:方块移动与合并
这是 2048 游戏最核心的逻辑。我们将实现向上、下、左、右四个方向的移动。以向左移动为例:
- 遍历每一行: 对每一行独立进行操作。
- 过滤非零元素: 将当前行中所有非零的方块提取出来。
- 合并相邻相同元素: 从左到右遍历提取出的方块,如果相邻的两个方块数值相同,则合并它们(前一个方块数值翻倍,后一个方块数值变为 0)。合并后,分数增加。
- 填充新行: 将合并后的非零方块从左到右填充到新的一行中,其余位置用 0 填充。
// 移动方块的核心函数
function slideTiles(row) {// 过滤掉所有 0let filteredRow = row.filter(num => num !== 0);// 合并相邻相同的数字for (let i = 0; i < filteredRow.length - 1; i++) {if (filteredRow[i] === filteredRow[i + 1]) {filteredRow[i] *= 2;score += filteredRow[i];filteredRow[i + 1] = 0; // 被合并的方块清零}}// 再次过滤掉所有 0filteredRow = filteredRow.filter(num => num !== 0);// 填充 0 到末尾while (filteredRow.length < GRID_SIZE) {filteredRow.push(0);}return filteredRow;
}// 处理向上移动
function moveUp() {let moved = false;for (let c = 0; c < GRID_SIZE; c++) {// 提取列数据let column = [];for (let r = 0; r < GRID_SIZE; r++) {column.push(gameGrid[r][c]);}let oldColumn = [...column]; // 复制一份旧的列数据let newColumn = slideTiles(column); // 对列数据进行滑动合并// 更新列数据到 gameGridfor (let r = 0; r < GRID_SIZE; r++) {gameGrid[r][c] = newColumn[r];}// 检查是否有移动发生if (JSON.stringify(oldColumn) !== JSON.stringify(newColumn)) {moved = true;}}return moved;
}// 处理向下移动 (类似 moveUp,但数组需要反转)
function moveDown() {let moved = false;for (let c = 0; c < GRID_SIZE; c++) {let column = [];for (let r = GRID_SIZE - 1; r >= 0; r--) { // 从下往上提取column.push(gameGrid[r][c]);}let oldColumn = [...column];let newColumn = slideTiles(column);for (let r = 0; r < GRID_SIZE; r++) {gameGrid[GRID_SIZE - 1 - r][c] = newColumn[r]; // 从下往上填充}if (JSON.stringify(oldColumn) !== JSON.stringify(newColumn)) {moved = true;}}return moved;
}// 处理向左移动
function moveLeft() {let moved = false;for (let r = 0; r < GRID_SIZE; r++) {let oldRow = [...gameGrid[r]]; // 复制一份旧的行数据gameGrid[r] = slideTiles(gameGrid[r]); // 对行数据进行滑动合并if (JSON.stringify(oldRow) !== JSON.stringify(gameGrid[r])) {moved = true;}}return moved;
}// 处理向右移动 (类似 moveLeft,但数组需要反转)
function moveRight() {let moved = false;for (let r = 0; r < GRID_SIZE; r++) {let row = [...gameGrid[r]].reverse(); // 反转行数据let oldRow = [...row];let newRow = slideTiles(row);gameGrid[r] = newRow.reverse(); // 再次反转填充if (JSON.stringify(oldRow) !== JSON.stringify(newRow)) {moved = true;}}return moved;
}
slideTiles(row)
:这是核心的滑动合并逻辑,它接受一个一维数组(行或列),并返回处理后的新数组。moveUp()
,moveDown()
,moveLeft()
,moveRight()
:分别调用slideTiles
对相应的行或列进行处理。需要注意的是,对于向上和向左移动,直接处理即可;对于向下和向右移动,需要先将行/列反转,处理后再反转回来。moved
变量:用于判断本次移动是否实际改变了游戏盘面,以便决定是否生成新的方块。
4)事件监听与游戏流程
我们需要监听键盘的方向键事件,并根据按下的方向调用相应的移动函数。
// 键盘事件监听
document.addEventListener('keyup', handleKeyPress);function handleKeyPress(event) {if (gameOver) return;let moved = false;switch (event.key) {case 'ArrowUp':moved = moveUp();break;case 'ArrowDown':moved = moveDown();break;case 'ArrowLeft':moved = moveLeft();break;case 'ArrowRight':moved = moveRight();break;default:return; // 忽略其他按键}if (moved) {addRandomTile();renderGrid();updateScore();checkGameOver();}
}// 更新分数显示
function updateScore() {scoreElement.textContent = score;
}// 检查游戏是否结束
function checkGameOver() {// 检查是否有空位for (let r = 0; r < GRID_SIZE; r++) {for (let c = 0; c < GRID_SIZE; c++) {if (gameGrid[r][c] === 0) {return; // 还有空位,游戏未结束}}}// 检查是否还有可合并的方块for (let r = 0; r < GRID_SIZE; r++) {for (let c = 0; c < GRID_SIZE; c++) {const current = gameGrid[r][c];// 检查右侧if (c < GRID_SIZE - 1 && current === gameGrid[r][c + 1]) {return;}// 检查下方if (r < GRID_SIZE - 1 && current === gameGrid[r + 1][c]) {return;}}}gameOver = true;gameOverMessageElement.classList.remove('hidden');
}// 重新开始按钮
restartButton.addEventListener('click', initializeGame);// 游戏初始化
initializeGame();
handleKeyPress()
:根据按下的方向键调用不同的移动函数。- 在每次有效移动后:
addRandomTile()
:生成新的随机方块。renderGrid()
:更新 UI。updateScore()
:更新分数。checkGameOver()
:判断游戏是否结束(没有空位且没有可合并的方块)。
restartButton
:点击重新开始按钮可以重置游戏。
最后
通过 HTML 搭建结构,CSS 美化外观,JavaScript 实现核心逻辑,我们就成功地实现了一个基础的 2048 游戏。这个项目不仅是一个很好的前端入门实践,更可以在此基础上进行扩展:
- 动画效果优化: 可以使用 CSS
transform
和transition
结合 JavaScript,实现更流畅的方块移动和合并动画。 - 触摸事件支持: 适配移动端,实现滑动操作。
- 保存/加载游戏: 使用
localStorage
将游戏进度保存到本地。 - AI 玩家: 尝试实现一个简单的 AI 算法来玩 2048。
- 计分板: 记录最高分数。
现在,你就可以动手尝试构建你自己的 2048 游戏了!亲自动手实现一遍还是大有益处。
- 体验地址
- 体验地址2