手把手教你用HTML5 Canvas开发记忆翻牌游戏(Memory Card)

文章目录

手把手教你用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(arcrectbezierCurveTo等)绘制多样化图案
  • 胜利提示:游戏胜利时显示半透明背景和提示文字,引导玩家重新开始

六、步骤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张卡牌,每张卡牌有唯一配对
  • 点击卡牌翻牌,每次最多翻开两张
  • 图案相同则保持翻开,不同则自动翻回
  • 所有卡牌配对成功后显示胜利提示,点击可重新开始
  • 多样化的图案设计(圆形、正方形、三角形等),易于区分

十、扩展功能建议(进阶练习)

如果想进一步提升游戏体验,可以尝试添加这些功能:

  1. 难度级别:增加不同难度(如3x4、5x5网格),调整卡牌数量
  2. 计时功能:记录完成游戏的时间,挑战最快速度
  3. 步数统计:记录翻牌次数,越少越好
  4. 翻牌动画:添加卡牌翻开的旋转动画,增强视觉效果
  5. 主题切换:提供多种卡牌样式和背景主题
  6. 高分榜:使用localStorage记录最快完成时间或最少步数

通过本教程,你已经掌握了记忆翻牌游戏的核心开发技术,包括二维网格布局、状态管理、用户交互和匹配逻辑。这些知识可以应用到其他棋盘类或记忆类游戏开发中,祝你编程愉快!

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值