文章目录
手把手教你用HTML5 Canvas开发记忆翻牌游戏(Memory Card)
记忆翻牌游戏是一款经典的益智小游戏,玩家通过翻牌寻找配对的图案,考验记忆力和观察力。本文将带你从零开始,使用HTML5 Canvas和原生JavaScript实现这款游戏,掌握二维网格布局、状态管理和交互逻辑等核心技术。

一、游戏核心原理与准备
在开始编码前,我们先明确记忆翻牌游戏的核心机制:
- 卡牌(Cards):背面朝上排列在网格中,每张卡牌有唯一配对
- 翻牌逻辑:每次最多翻开两张牌,若图案相同则保持翻开,否则翻回
- 胜利条件:所有卡牌都找到配对并保持翻开状态
- 状态管理:记录每张卡牌的位置、图案、是否翻开、是否匹配等状态
准备工作:创建一个HTML文件(命名为memory-game.html),我们将在这个文件中完成所有开发。
二、步骤1:搭建基础HTML结构
首先创建游戏的基本页面框架,包含Canvas元素和必要的样式:
<!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>
body {
margin: 0;
padding: 20px;
display: flex;
flex-direction: column;
align-items: center;
background-color: #f5f5f5;
font-family: Arial, sans-serif;
}
#gameCanvas {
border: 3px solid #333;
background-color: #e0e0e0;
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
}
.game-info {
margin: 15px 0;
font-size: 1.2em;
color: #333;
}
</style>
</head>
<body>
<h1>记忆翻牌游戏</h1>
<div class="game-info">点击卡牌翻牌,寻找相同的图案进行配对</div>
<!-- 游戏画布 -->
<canvas id="gameCanvas" width="450" height="450"></canvas>
<script>
// 游戏代码将写在这里
</script>
</body>
</html>
代码说明:
- 创建了一个450x450像素的Canvas画布,适合展示网格状的卡牌
- 添加了简单的样式,使画布有阴影效果,增强立体感
- 包含游戏说明文字,指导玩家操作
- 预留
<script>标签用于编写游戏逻辑
三、步骤2:初始化游戏参数与卡牌数据
接下来定义游戏的核心参数和卡牌数据,包括卡牌尺寸、网格布局、图案等。在<script>标签中添加:
// 获取Canvas元素和绘图上下文
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
// 游戏参数
const cardCount = 16; // 卡牌总数(必须为偶数)
const pairsCount = cardCount / 2; // 配对数量
const cols = 4; // 列数
const rows = cardCount / cols; // 行数
// 卡牌样式参数
const cardWidth = 80;
const cardHeight = 80;
const padding = 20; // 卡牌间距
const startX = (canvas.width - (cols * cardWidth + (cols - 1) * padding)) / 2; // 起始X坐标(居中)
const startY = (canvas.height - (rows * cardHeight + (rows - 1) * padding)) / 2; // 起始Y坐标(居中)
// 卡牌数据数组
let cards = [];
// 游戏状态
let flippedCards = []; // 当前翻开的卡牌(最多2张)
let matchedPairs = 0; // 已匹配的对数
let isProcessing = false; // 是否正在处理匹配(防止快速连续翻牌)
代码说明:
- 游戏参数:设置为16张卡牌(8对),4x4网格布局,适合记忆游戏的难度
- 卡牌尺寸:每张卡牌80x80像素,间距20像素,通过计算使整个卡牌网格居中显示
- 核心数组:
cards:存储所有卡牌的详细信息(位置、图案、状态等)flippedCards:记录当前翻开的卡牌,用于判断是否匹配isProcessing:防止玩家在两张牌未完成判断前快速翻开更多牌,保证游戏逻辑正确
四、步骤3:生成卡牌数据与图案
创建函数生成卡牌数据,包括随机图案、位置和初始状态:
// 生成卡牌图案(使用数字1-8表示8种图案)
function generateSymbols() {
const symbols = [];
// 每种图案添加两张(形成配对)
for (let i = 1; i <= pairsCount; i++) {
symbols.push(i);
symbols.push(i);
}
// 打乱图案顺序( Fisher-Yates 洗牌算法)
for (let i = symbols.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[symbols[i], symbols[j]] = [symbols[j], symbols[i]];
}
return symbols;
}
// 初始化卡牌数据
function initCards() {
const symbols = generateSymbols();
cards = [];
for (let row = 0; row < rows; row++) {
for (let col = 0; col < cols; col++) {
// 计算卡牌位置
const x = startX + col * (cardWidth + padding);
const y = startY + row * (cardHeight + padding);
const index = row * cols + col; // 计算在数组中的索引
cards.push({
x: x,
y: y,
width: cardWidth,
height: cardHeight,
symbol: symbols[index], // 图案(数字)
flipped: false, // 是否翻开
matched: false, // 是否匹配
row: row,
col: col
});
}
}
}
代码说明:
generateSymbols():生成16个数字(1-8各出现两次),并通过洗牌算法打乱顺序,确保图案随机分布initCards():- 计算每张卡牌的位置(基于行号和列号)
- 为每张卡牌分配图案、初始状态(未翻开、未匹配)
- 存储卡牌的完整信息到
cards数组中
- 位置计算:通过行列索引和间距,确保卡牌整齐排列在画布中央
五、步骤4:绘制游戏元素
编写函数绘制卡牌(正面和背面)、游戏状态和胜利提示:
// 绘制单张卡牌
function drawCard(card) {
// 绘制卡牌边框
ctx.strokeStyle = '#333';
ctx.lineWidth = 2;
ctx.strokeRect(card.x, card.y, card.width, card.height);
if (card.flipped || card.matched) {
// 绘制卡牌正面(显示图案)
ctx.fillStyle = '#fff';
ctx.fillRect(card.x + 2, card.y + 2, card.width - 4, card.height - 4);
// 绘制图案(使用不同的形状区分)
drawSymbol(card.x + card.width / 2, card.y + card.height / 2, card.symbol);
} else {
// 绘制卡牌背面
ctx.fillStyle = '#4285f4'; // 蓝色背面
ctx.fillRect(card.x + 2, card.y + 2, card.width - 4, card.height - 4);
// 背面纹理(网格图案)
ctx.fillStyle = 'rgba(255, 255, 255, 0.2)';
for (let i = 0; i < 3; i++) {
for (let j = 0; j < 3; j++) {
ctx.fillRect(
card.x + 15 + i * 20,
card.y + 15 + j * 20,
10, 10
);
}
}
}
}
// 绘制卡牌图案(根据数字绘制不同形状)
function drawSymbol(x, y, symbol) {
ctx.fillStyle = '#333';
ctx.lineWidth = 3;
ctx.save();
ctx.translate(x, y); // 移动到中心点
switch (symbol) {
case 1: // 圆形
ctx.beginPath();
ctx.arc(0, 0, 20, 0, Math.PI * 2);
ctx.fill();
break;
case 2: // 正方形
ctx.fillRect(-20, -20, 40, 40);
break;
case 3: // 三角形
ctx.beginPath();
ctx.moveTo(0, -25);
ctx.lineTo(25, 20);
ctx.lineTo(-25, 20);
ctx.closePath();
ctx.fill();
break;
case 4: // 十字
ctx.fillRect(-5, -20, 10, 40);
ctx.fillRect(-20, -5, 40, 10);
break;
case 5: // 五角星(简化版)
ctx.beginPath();
ctx.moveTo(0, -20);
ctx.lineTo(5, 10);
ctx.lineTo(25, 10);
ctx.lineTo(10, 20);
ctx.lineTo(15, 40);
ctx.lineTo(0, 25);
ctx.lineTo(-15, 40);
ctx.lineTo(-10, 20);
ctx.lineTo(-25, 10);
ctx.lineTo(-5, 10);
ctx.closePath();
ctx.fill();
break;
case 6: // 六边形
const hexSize = 20;
ctx.beginPath();
for (let i = 0; i < 6; i++) {
const angle = (i * 2 * Math.PI / 6) - Math.PI / 2;
const px = hexSize * Math.cos(angle);
const py = hexSize * Math.sin(angle);
if (i === 0) ctx.moveTo(px, py);
else ctx.lineTo(px, py);
}
ctx.closePath();
ctx.fill();
break;
case 7: // 心形(简化版)
ctx.scale(0.8, 0.8);
ctx.beginPath();
ctx.moveTo(0, -15);
ctx.bezierCurveTo(-25, -40, -55, -15, 0, 20);
ctx.bezierCurveTo(55, -15, 25, -40, 0, -15);
ctx.fill();
break;
case 8: // 钻石形
ctx.rotate(Math.PI / 4);
ctx.fillRect(-15, -15, 30, 30);
break;
}
ctx.restore();
}
// 绘制所有卡牌
function drawAllCards() {
cards.forEach(card => drawCard(card));
}
// 绘制胜利提示
function drawVictory() {
// 半透明背景
ctx.fillStyle = 'rgba(0, 0, 0, 0.7)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// 胜利文字
ctx.fillStyle = '#fff';
ctx.font = '30px Arial';
ctx.textAlign = 'center';
ctx.fillText('恭喜你赢了!', canvas.width / 2, canvas.height / 2 - 20);
// 重新开始提示
ctx.font = '20px Arial';
ctx.fillText('点击屏幕重新开始', canvas.width / 2, canvas.height / 2 + 20);
}
代码说明:
drawCard():根据卡牌状态(翻开/未翻开/已匹配)绘制不同样式,未翻开时显示蓝色背面带网格纹理drawSymbol():为不同数字绘制独特形状(圆形、正方形、三角形等),使玩家能直观区分图案- 形状绘制:使用Canvas的基本绘图API(
arc、rect、bezierCurveTo等)绘制多样化图案 - 胜利提示:游戏胜利时显示半透明背景和提示文字,引导玩家重新开始
六、步骤5:处理用户输入(翻牌逻辑)
实现鼠标点击翻牌的交互逻辑,包括判断点击位置、限制翻牌数量、处理匹配检测:
// 处理鼠标点击
canvas.addEventListener('click', handleClick);
function handleClick(e) {
// 如果正在处理匹配或游戏已结束,不响应点击
if (isProcessing || matchedPairs === pairsCount) {
// 如果所有牌都已匹配,点击重新开始
if (matchedPairs === pairsCount) {
resetGame();
}
return;
}
// 获取鼠标在Canvas中的坐标
const rect = canvas.getBoundingClientRect();
const mouseX = e.clientX - rect.left;
const mouseY = e.clientY - rect.top;
// 检查点击是否在卡牌上
cards.forEach(card => {
// 只处理未翻开且未匹配的卡牌
if (!card.flipped && !card.matched &&
mouseX >= card.x && mouseX <= card.x + card.width &&
mouseY >= card.y && mouseY <= card.y + card.height) {
// 最多同时翻开两张牌
if (flippedCards.length < 2) {
card.flipped = true;
flippedCards.push(card);
// 当翻开两张牌时,检查是否匹配
if (flippedCards.length === 2) {
isProcessing = true; // 标记为正在处理
setTimeout(checkMatch, 1000); // 延迟1秒检查,让玩家看清两张牌
}
}
}
});
// 重绘游戏
draw();
}
代码说明:
- 点击处理:监听Canvas的点击事件,计算鼠标在画布中的相对坐标
- 卡牌判断:检查点击位置是否在未翻开且未匹配的卡牌上
- 翻牌限制:每次最多翻开两张牌,防止玩家一次翻开所有牌
- 匹配延迟:翻开两张牌后,延迟1秒再检查是否匹配,给玩家足够时间记住图案位置
- 游戏重置:当所有牌都匹配后,点击屏幕会触发重置游戏
七、步骤6:实现匹配检测与游戏状态更新
编写代码判断两张翻开的卡牌是否匹配,并更新游戏状态:
// 检查两张翻开的牌是否匹配
function checkMatch() {
const [card1, card2] = flippedCards;
if (card1.symbol === card2.symbol) {
// 匹配成功
card1.matched = true;
card2.matched = true;
matchedPairs++;
} else {
// 匹配失败,翻回
card1.flipped = false;
card2.flipped = false;
}
// 重置状态
flippedCards = [];
isProcessing = false;
// 重绘游戏
draw();
}
// 绘制游戏
function draw() {
// 清空画布
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 绘制所有卡牌
drawAllCards();
// 如果所有配对都已找到,显示胜利提示
if (matchedPairs === pairsCount) {
drawVictory();
}
}
// 重置游戏
function resetGame() {
initCards();
flippedCards = [];
matchedPairs = 0;
isProcessing = false;
draw();
}
代码说明:
checkMatch():比较两张翻开卡牌的图案,相同则标记为匹配,不同则翻回- 状态更新:匹配成功后增加
matchedPairs计数,用于判断游戏是否胜利 draw():主绘制函数,清空画布并重新绘制所有卡牌,胜利时显示提示resetGame():重置所有游戏状态和卡牌数据,实现重新开始功能
八、步骤7:启动游戏
最后,编写初始化代码启动游戏:
// 初始化并启动游戏
initCards();
draw();
代码说明:
- 游戏启动流程:先初始化卡牌数据(
initCards()),再绘制初始画面(draw()) - 初始画面:所有卡牌背面朝上,等待玩家点击翻牌
九、完整代码与运行效果
将以上所有代码整合后,保存并在浏览器中打开,你将获得一个功能完整的记忆翻牌游戏:
- 4x4网格排列的16张卡牌,每张卡牌有唯一配对
- 点击卡牌翻牌,每次最多翻开两张
- 图案相同则保持翻开,不同则自动翻回
- 所有卡牌配对成功后显示胜利提示,点击可重新开始
- 多样化的图案设计(圆形、正方形、三角形等),易于区分
十、扩展功能建议(进阶练习)
如果想进一步提升游戏体验,可以尝试添加这些功能:
- 难度级别:增加不同难度(如3x4、5x5网格),调整卡牌数量
- 计时功能:记录完成游戏的时间,挑战最快速度
- 步数统计:记录翻牌次数,越少越好
- 翻牌动画:添加卡牌翻开的旋转动画,增强视觉效果
- 主题切换:提供多种卡牌样式和背景主题
- 高分榜:使用
localStorage记录最快完成时间或最少步数
通过本教程,你已经掌握了记忆翻牌游戏的核心开发技术,包括二维网格布局、状态管理、用户交互和匹配逻辑。这些知识可以应用到其他棋盘类或记忆类游戏开发中,祝你编程愉快!

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



