本教程将手把手教你使用纯粹的 HTML, CSS 和 JavaScript 技术栈,构建一个经典且有趣的记忆翻牌游戏。你将学习如何动态生成游戏卡片、处理用户交互、管理游戏状态,并通过优雅的动画和音效提升用户体验。无论你是前端初学者还是希望通过一个小项目来巩固基础,本文都将为你提供一份详细且可操作的实践指南。
目录
1. 项目概览与功能介绍
本项目是一个经典的记忆翻牌小游戏,玩家需要在限定次数内,通过翻转卡片,找出所有成对的水果图案。当一对卡片被成功匹配时,它们将保持翻开状态,否则在短暂停顿后自动翻回背面。游戏目标是使用最少的点击次数完成所有匹配。
1.1 核心功能
- 动态卡片生成: 游戏开始时,自动创建并排列所有卡片。
- 卡片随机排列: 每次游戏开始,卡片位置都会被打乱,确保游戏的可玩性。
- 翻转交互效果: 玩家点击卡片时,卡片会以 3D 效果翻转,展示其正面图案。
- 匹配逻辑判断: 翻开两张卡片后,判断它们是否为一对。
- 游戏状态管理: 跟踪已匹配的卡片对数和玩家的点击次数。
- 游戏结束提示: 当所有卡片都被匹配后,弹出提示告知玩家游戏结束。
- 用户体验优化: 增加卡片翻转和匹配成功的音效,并提供游戏重置功能。
1.2 技术栈
- HTML: 构建游戏面板、卡片容器以及信息显示区域。
- CSS: 定义游戏界面的样式,特别是实现卡片的 3D 翻转动画效果。
- JavaScript: 实现所有核心游戏逻辑,包括卡片生成、随机化、事件监听、状态管理和胜负判断。
2. 环境准备与项目结构
2.1 前置知识
在开始之前,请确保你已具备以下基础知识:
- HTML 基础: 了解 HTML 标签、元素和属性。
- CSS 基础: 了解 Flexbox、Grid 等布局方式,以及
transform
和transition
等动画属性。 - JavaScript 基础: 掌握变量、数组、函数、事件处理和 DOM 操作。
- 一个代码编辑器(如 VS Code)和任意一个现代浏览器。
2.2 项目文件结构
为了保持代码的清晰和可维护性,我们将项目组织成以下文件结构:
fruit-memory-game/
├── index.html
├── style.css
├── script.js
└── assets/
├── images/
│ ├── apple.png
│ ├── banana.png
│ ├── ...
└── sounds/
├── flip.mp3
└── match.mp3
重要提示: 你需要准备一些水果图案的图片(例如
apple.png
,banana.png
等)和一些简单的音效文件(例如翻牌音效flip.mp3
和匹配成功音效match.mp3
),并将它们放入相应的文件夹中。这对于完整体验教程至关重要。
3. 核心逻辑与实现步骤
本节将详细分解项目的实现步骤,并解释每段代码的作用。
3.1 HTML 结构搭建:游戏面板与信息展示
index.html
文件是整个项目的骨架。它包含了游戏面板容器和用于展示点击次数等信息的 UI 元素。
What: HTML 文件定义了页面的内容和结构。
How: 我们使用<div class="game-container">
来作为游戏卡片的容器,并用<h2 id="moves-count">
来显示点击次数。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>水果记忆翻牌游戏</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="game-wrapper">
<h1 class="game-title">水果记忆翻牌</h1>
<div class="game-info">
<h2>点击次数: <span id="moves-count">0</span></h2>
<button id="reset-button">重新开始</button>
</div>
<div class="game-container">
</div>
</div>
<audio id="flip-sound" src="assets/sounds/flip.mp3" preload="auto"></audio>
<audio id="match-sound" src="assets/sounds/match.mp3" preload="auto"></audio>
<script src="script.js"></script>
</body>
</html>
game-wrapper
类: 用于包裹整个游戏界面,方便进行居中和整体布局。game-container
类: 这是最重要的容器,所有游戏卡片都将通过 JavaScript 插入到这个元素中。moves-count
ID: 用于在 JavaScript 中获取并更新玩家的点击次数。reset-button
ID: 用于在游戏结束后或任何时候提供重新开始的功能。<audio>
标签: 预加载音效文件,这样在需要播放时可以立即响应,提升用户体验。
3.2 CSS 样式美化:创建翻转卡片效果
style.css
文件将为游戏添加视觉上的吸引力,特别是实现卡片的 3D 翻转效果。
3.2.1 基础布局与卡片样式
首先,我们为整个游戏界面和卡片定义基础样式。
/* fruit-memory-game/style.css */
body {
font-family: 'Arial', sans-serif;
background-color: #f0f0f0;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
margin: 0;
}
.game-wrapper {
text-align: center;
}
.game-info {
display: flex;
justify-content: center;
align-items: center;
gap: 20px;
margin-bottom: 20px;
}
.game-container {
display: grid;
grid-template-columns: repeat(4, 1fr); /* 4列布局,每列等宽 */
gap: 15px;
width: 600px; /* 固定游戏面板宽度 */
perspective: 1000px; /* 3D 效果的关键属性 */
}
.card {
width: 120px;
height: 120px;
position: relative;
transform-style: preserve-3d; /* 启用子元素的 3D 变换 */
transition: transform 0.5s; /* 翻转动画持续时间 */
cursor: pointer;
}
.card.flipped {
transform: rotateY(180deg); /* 翻转 180 度 */
}
.card-face {
position: absolute;
width: 100%;
height: 100%;
backface-visibility: hidden; /* 翻转背面时隐藏正面 */
border-radius: 8px;
display: flex;
justify-content: center;
align-items: center;
font-size: 50px;
}
.card-front {
background-color: #f7f7f7;
transform: rotateY(180deg);
}
.card-back {
background-color: #4CAF50;
border: 2px solid #388E3C;
display: flex;
justify-content: center;
align-items: center;
}
3.2.2 3D 翻转效果的实现
这是 CSS 的精髓所在。我们通过设置 transform-style: preserve-3d
和 perspective
属性来创建 3D 空间。
perspective: 1000px;
在父容器.game-container
上设置,为所有子元素(卡片)创建一个虚拟的 3D 视点。transform-style: preserve-3d;
在卡片.card
上设置,确保其子元素(卡片的正面和背面)在同一个 3D 空间中进行变换。backface-visibility: hidden;
在.card-face
上设置,当元素背面朝向观察者时,它将是不可见的,从而实现卡片翻转时只看到一面。.card.flipped
类: 这是一个关键的类,我们将通过 JavaScript 在用户点击时动态地给卡片添加这个类。当这个类被添加时,transform: rotateY(180deg)
会使卡片绕 Y 轴翻转 180 度,从而显示其背面。
3.3 JavaScript 核心逻辑:游戏状态管理与交互
script.js
文件是本项目的核心,它负责所有游戏逻辑的实现。
3.3.1 变量与常量初始化
首先,定义一些全局变量和常量来管理游戏状态和元素。
// fruit-memory-game/script.js
// 获取 DOM 元素
const gameContainer = document.querySelector('.game-container');
const movesCountElement = document.getElementById('moves-count');
const resetButton = document.getElementById('reset-button');
const flipSound = document.getElementById('flip-sound');
const matchSound = document.getElementById('match-sound');
// 游戏状态变量
let moves = 0; // 玩家点击次数
let flippedCards = []; // 存储当前翻开的两张卡片
let matchedPairs = 0; // 已匹配成功的卡片对数
let canFlip = true; // 控制是否可以继续翻牌,防止玩家快速点击
// 水果图片数组(需要与 assets/images 目录下的图片名匹配)
const fruits = ['apple', 'banana', 'grape', 'lemon', 'orange', 'strawberry', 'watermelon', 'cherry'];
// 游戏卡片数据:每种水果需要两张
const cardData = [...fruits, ...fruits];
3.3.2 游戏状态管理的核心数据结构
cardData
数组是游戏的核心数据源,它包含了所有卡片的数据,通过复制数组的方式来确保每种水果都有两张卡片。
3.3.3 游戏初始化函数 initializeGame()
这个函数将在游戏开始和重置时被调用。它负责打乱卡片顺序并动态生成 HTML 元素。
// fruit-memory-game/script.js
// ... (接上面变量定义)
/**
* 随机打乱数组
* @param {Array} array
*/
function shuffle(array) {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[array[i], array[j]] = [array[j], array[i]];
}
}
/**
* 初始化游戏:打乱卡片并生成 HTML
*/
function initializeGame() {
shuffle(cardData); // 打乱卡片顺序
gameContainer.innerHTML = ''; // 清空游戏面板
cardData.forEach(fruit => {
// 创建卡片元素
const card = document.createElement('div');
card.classList.add('card');
card.dataset.fruit = fruit; // 将水果名称作为数据属性存储,方便后续匹配
// 创建卡片正面(水果图片)
const cardFront = document.createElement('div');
cardFront.classList.add('card-face', 'card-front');
const img = document.createElement('img');
img.src = `assets/images/${fruit}.png`;
img.alt = fruit;
cardFront.appendChild(img);
// 创建卡片背面
const cardBack = document.createElement('div');
cardBack.classList.add('card-face', 'card-back');
card.appendChild(cardFront);
card.appendChild(cardBack);
// 添加点击事件监听器
card.addEventListener('click', handleCardClick);
gameContainer.appendChild(card);
});
// 重置游戏状态
moves = 0;
movesCountElement.textContent = moves;
flippedCards = [];
matchedPairs = 0;
canFlip = true;
}
// 首次加载页面时初始化游戏
initializeGame();
// 给重置按钮添加事件监听
resetButton.addEventListener('click', initializeGame);
3.3.4 卡片点击事件处理函数 handleCardClick()
这是游戏交互的核心函数,它负责处理玩家点击卡片时的所有逻辑。
// fruit-memory-game/script.js
// ... (接上面初始化函数)
/**
* 处理卡片点击事件
* @param {Event} event
*/
function handleCardClick(event) {
// 确保可以继续翻牌
if (!canFlip) return;
const clickedCard = event.currentTarget;
// 如果卡片已经被翻开或已经匹配,则忽略点击
if (clickedCard.classList.contains('flipped') || clickedCard.classList.contains('matched')) {
return;
}
// 播放翻牌音效
flipSound.currentTime = 0; // 重置音效,确保可以连续播放
flipSound.play();
// 翻转卡片
clickedCard.classList.add('flipped');
flippedCards.push(clickedCard);
// 更新点击次数
moves++;
movesCountElement.textContent = moves;
// 当翻开两张卡片时,进行匹配检查
if (flippedCards.length === 2) {
canFlip = false; // 暂时禁用翻牌,防止第三次点击
setTimeout(checkMatch, 1000); // 1秒后执行匹配检查
}
}
3.3.5 匹配检查与游戏结束逻辑
checkMatch()
函数在翻开两张卡片后被调用,它判断这两张卡片是否匹配。
// fruit-memory-game/script.js
// ... (接上面 handleCardClick)
/**
* 检查两张翻开的卡片是否匹配
*/
function checkMatch() {
const [card1, card2] = flippedCards;
const fruit1 = card1.dataset.fruit;
const fruit2 = card2.dataset.fruit;
if (fruit1 === fruit2) {
// 匹配成功
matchSound.currentTime = 0;
matchSound.play();
card1.classList.add('matched');
card2.classList.add('matched');
matchedPairs++;
// 检查游戏是否结束
if (matchedPairs === cardData.length / 2) {
setTimeout(() => {
alert(`恭喜你!你用 ${moves} 次点击找到了所有卡片!`);
}, 500);
}
} else {
// 匹配失败,将卡片翻回背面
card1.classList.remove('flipped');
card2.classList.remove('flipped');
}
// 重置状态
flippedCards = [];
canFlip = true; // 重新启用翻牌
}
setTimeout()
: 这是一个关键技巧。我们使用它来延迟checkMatch()
的执行,让玩家有时间看清第二张翻开的卡片。dataset
属性: 我们利用element.dataset.fruit
来访问 HTML 元素上自定义的data-fruit
属性,这是在 DOM 中存储和获取自定义数据的最佳实践。canFlip
变量: 这是一个重要的状态管理变量。当flippedCards.length
达到 2 时,我们立即将canFlip
设为false
,从而防止玩家在等待checkMatch()
期间再次点击卡片,造成游戏逻辑混乱。
3.3.6 优化:添加音效和游戏重置功能
我们通过 HTMLMediaElement.currentTime = 0;
和 play()
方法来控制音效的播放。resetButton.addEventListener('click', initializeGame);
则实现了游戏重置功能,将所有状态重置为初始值,并重新生成卡片。
4. 完整代码与运行指南
4.1 完整代码
现在,你已经掌握了各个文件的核心内容,将它们组合起来,你就得到了一个完整的记忆翻牌游戏。
index.html
文件:请参考 3.1 章节style.css
文件:请参考 3.2 章节script.js
文件:请参考 3.3 章节
4.2 运行与测试
- 将上述三个文件和
assets
文件夹保存在同一个主文件夹中。 - 用你喜欢的浏览器(如 Chrome, Firefox)直接打开
index.html
文件。 - 点击卡片,测试游戏的翻转、匹配和胜利逻辑是否正常工作。
5. 展望与功能扩展
本教程提供的版本是一个功能完备但相对基础的游戏。如果你希望进一步提升它,可以考虑以下几个方向:
- 难度选择: 增加一个下拉菜单,让玩家可以选择不同难度的游戏,例如 4x4、5x5 或 6x6 的卡片网格。这需要你调整
cardData
数组的大小和 CSS 的grid-template-columns
属性。 - 计时器和分数系统: 添加一个计时器,记录玩家完成游戏所用的时间,并根据时间和点击次数计算最终得分。
- 本地存储: 使用
localStorage
来保存玩家的最佳成绩,从而增加游戏的挑战性。 - 游戏模式: 增加多种游戏模式,例如“限时模式”或“错误次数限制模式”。
- 高级 UI/UX:
- 增加一个开始游戏的欢迎界面。
- 使用 CSS 动画库(如 Animate.css)来添加更多动效,例如卡片匹配成功时的闪光效果。
- 优化移动设备上的适配,确保游戏在手机上也能流畅运行。
- 面向对象重构: 对于更复杂的项目,可以考虑使用面向对象编程(OOP)的思想来重构代码,将游戏逻辑封装到不同的类中,例如
Card
类和Game
类,从而提高代码的可维护性和可扩展性。