从零开始构建水果记忆翻牌游戏:HTML, CSS 与 JavaScript 教程

本教程将手把手教你使用纯粹的 HTML, CSS 和 JavaScript 技术栈,构建一个经典且有趣的记忆翻牌游戏。你将学习如何动态生成游戏卡片、处理用户交互、管理游戏状态,并通过优雅的动画和音效提升用户体验。无论你是前端初学者还是希望通过一个小项目来巩固基础,本文都将为你提供一份详细且可操作的实践指南。

目录


1. 项目概览与功能介绍

本项目是一个经典的记忆翻牌小游戏,玩家需要在限定次数内,通过翻转卡片,找出所有成对的水果图案。当一对卡片被成功匹配时,它们将保持翻开状态,否则在短暂停顿后自动翻回背面。游戏目标是使用最少的点击次数完成所有匹配。

1.1 核心功能

  • 动态卡片生成: 游戏开始时,自动创建并排列所有卡片。
  • 卡片随机排列: 每次游戏开始,卡片位置都会被打乱,确保游戏的可玩性。
  • 翻转交互效果: 玩家点击卡片时,卡片会以 3D 效果翻转,展示其正面图案。
  • 匹配逻辑判断: 翻开两张卡片后,判断它们是否为一对。
  • 游戏状态管理: 跟踪已匹配的卡片对数和玩家的点击次数。
  • 游戏结束提示: 当所有卡片都被匹配后,弹出提示告知玩家游戏结束。
  • 用户体验优化: 增加卡片翻转和匹配成功的音效,并提供游戏重置功能。

1.2 技术栈

  • HTML: 构建游戏面板、卡片容器以及信息显示区域。
  • CSS: 定义游戏界面的样式,特别是实现卡片的 3D 翻转动画效果。
  • JavaScript: 实现所有核心游戏逻辑,包括卡片生成、随机化、事件监听、状态管理和胜负判断。

2. 环境准备与项目结构

2.1 前置知识

在开始之前,请确保你已具备以下基础知识:

  • HTML 基础: 了解 HTML 标签、元素和属性。
  • CSS 基础: 了解 Flexbox、Grid 等布局方式,以及 transformtransition 等动画属性。
  • 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-3dperspective 属性来创建 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 完整代码

现在,你已经掌握了各个文件的核心内容,将它们组合起来,你就得到了一个完整的记忆翻牌游戏。

4.2 运行与测试

  1. 将上述三个文件和 assets 文件夹保存在同一个主文件夹中。
  2. 用你喜欢的浏览器(如 Chrome, Firefox)直接打开 index.html 文件。
  3. 点击卡片,测试游戏的翻转、匹配和胜利逻辑是否正常工作。

5. 展望与功能扩展

本教程提供的版本是一个功能完备但相对基础的游戏。如果你希望进一步提升它,可以考虑以下几个方向:

  • 难度选择: 增加一个下拉菜单,让玩家可以选择不同难度的游戏,例如 4x4、5x5 或 6x6 的卡片网格。这需要你调整 cardData 数组的大小和 CSS 的 grid-template-columns 属性。
  • 计时器和分数系统: 添加一个计时器,记录玩家完成游戏所用的时间,并根据时间和点击次数计算最终得分。
  • 本地存储: 使用 localStorage 来保存玩家的最佳成绩,从而增加游戏的挑战性。
  • 游戏模式: 增加多种游戏模式,例如“限时模式”或“错误次数限制模式”。
  • 高级 UI/UX:
    • 增加一个开始游戏的欢迎界面。
    • 使用 CSS 动画库(如 Animate.css)来添加更多动效,例如卡片匹配成功时的闪光效果。
    • 优化移动设备上的适配,确保游戏在手机上也能流畅运行。
  • 面向对象重构: 对于更复杂的项目,可以考虑使用面向对象编程(OOP)的思想来重构代码,将游戏逻辑封装到不同的类中,例如 Card 类和 Game 类,从而提高代码的可维护性和可扩展性。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

weixin_pk138132

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值