手把手教你用HTML5 Canvas编写消消乐游戏(附源码)

文章目录

手把手教你用HTML5 Canvas编写消消乐游戏

消消乐是一款经典的休闲游戏,核心玩法是交换相邻元素,形成三个或以上相同元素的连线并消除。本文将通过HTML5 Canvas和JavaScript从零实现一个简单的消消乐游戏,逐行解析代码逻辑,让你轻松掌握游戏开发要点。
在这里插入图片描述
在这里插入图片描述

一、基础HTML结构搭建

首先创建游戏的基础页面结构,包含游戏画布和分数显示区域。

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>Canvas消消乐</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <div class="game-container">
        <!-- 分数显示 -->
        <div class="score-panel">分数: <span id="score">0</span></div>
        <!-- 游戏画布 -->
        <canvas id="gameCanvas" width="400" height="400"></canvas>
    </div>
    <script src="game.js"></script>
</body>
</html>
  • <!DOCTYPE html>:声明HTML5文档类型
  • game-container:游戏容器,用于布局分数面板和画布
  • score-panel:显示当前游戏分数
  • canvas:核心绘图区域,设置宽高为400x400像素
  • 引入外部CSS和JS文件,分离样式和逻辑

二、样式设计(CSS)

为游戏添加基础样式,美化界面并调整布局。

body {
    display: flex;
    flex-direction: column;
    align-items: center;
    background-color: #f0f0f0;
    font-family: Arial, sans-serif;
}

.game-container {
    margin-top: 20px;
    padding: 15px;
    background-color: #fff;
    border-radius: 10px;
    box-shadow: 0 0 10px rgba(0,0,0,0.2);
}

.score-panel {
    margin-bottom: 10px;
    font-size: 20px;
    font-weight: bold;
    color: #333;
}

#gameCanvas {
    border: 2px solid #333;
    background-color: #eee;
}
  • body使用flex布局居中显示游戏容器
  • 为容器添加阴影和圆角,增强视觉效果
  • 画布添加边框和浅灰色背景,区分游戏区域

三、核心游戏逻辑(JavaScript)

这部分是游戏的核心,包含初始化、绘图、交互和游戏规则实现。

1. 初始化游戏参数

首先获取画布上下文并定义游戏基础参数。

// 获取画布和上下文
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
const scoreElement = document.getElementById('score');

// 游戏参数
const gridSize = 8; // 8x8网格
const tileSize = 50; // 每个方块大小(像素)
let score = 0;
let board = []; // 存储游戏网格数据
let selectedTile = null; // 选中的方块位置
  • getContext('2d'):获取Canvas 2D绘图上下文,用于绘制图形
  • gridSize:定义游戏网格为8x8(共64个方块)
  • tileSize:每个方块的尺寸(50px),400px画布刚好容纳8个方块(8*50=400)
  • board:二维数组,存储每个位置的方块颜色索引
  • selectedTile:记录鼠标选中的方块位置(用于交换逻辑)

2. 生成随机游戏网格

初始化网格数据,确保初始状态没有可消除的方块(避免游戏一开始就自动消除)。

// 方块颜色数组
const colors = [
    '#ff6b6b', '#4ecdc4', '#45b7d1', 
    '#ffcc5c', '#a77dc2', '#ff8b94'
];

// 初始化游戏板
function initBoard() {
    do {
        board = [];
        // 生成随机网格
        for (let y = 0; y < gridSize; y++) {
            const row = [];
            for (let x = 0; x < gridSize; x++) {
                // 随机选择颜色(排除与上方和左方相同的颜色,减少初始可消除组合)
                let colorIndex;
                do {
                    colorIndex = Math.floor(Math.random() * colors.length);
                } while (
                    (y > 0 && colorIndex === board[y-1][x]) || // 与上方相同
                    (x > 0 && colorIndex === row[x-1]) // 与左方相同
                );
                row.push(colorIndex);
            }
            board.push(row);
        }
    } while (hasMatches()); // 如果初始有可消除组合,重新生成
}
  • colors:定义6种方块颜色(用十六进制色值表示)
  • initBoard:通过do-while循环生成网格,确保初始状态无消除组合
  • 内部循环生成每行数据时,通过do-while避免与上方和左方方块颜色相同,减少初始可消除概率
  • hasMatches():检查是否有可消除的方块组合(后面实现)

3. 绘制游戏界面

将网格数据绘制到Canvas上,包括方块和选中状态。

// 绘制游戏板
function drawBoard() {
    // 清空画布
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    
    // 绘制所有方块
    for (let y = 0; y < gridSize; y++) {
        for (let x = 0; x < gridSize; x++) {
            const colorIndex = board[y][x];
            // 绘制方块
            ctx.fillStyle = colors[colorIndex];
            ctx.fillRect(x * tileSize, y * tileSize, tileSize - 2, tileSize - 2);
            
            // 如果是选中的方块,绘制边框
            if (selectedTile && selectedTile.x === x && selectedTile.y === y) {
                ctx.strokeStyle = '#ffffff';
                ctx.lineWidth = 3;
                ctx.strokeRect(x * tileSize, y * tileSize, tileSize - 2, tileSize - 2);
            }
        }
    }
}
  • drawBoard:核心绘图函数,每次游戏状态变化后调用
  • clearRect:清空画布,避免重影
  • 双层循环绘制每个方块:使用fillRect绘制带间隙的方块(tileSize - 2实现间隙)
  • 选中的方块通过白色边框标记,增强交互反馈

4. 处理鼠标交互

实现鼠标点击选中和交换方块的逻辑。

// 转换鼠标位置到网格坐标
function getTilePosition(mouseX, mouseY) {
    return {
        x: Math.floor(mouseX / tileSize),
        y: Math.floor(mouseY / tileSize)
    };
}

// 检查两个方块是否相邻
function isAdjacent(tile1, tile2) {
    const dx = Math.abs(tile1.x - tile2.x);
    const dy = Math.abs(tile1.y - tile2.y);
    // 相邻条件:x或y相差1,且不同时相差
    return (dx === 1 && dy === 0) || (dx === 0 && dy === 1);
}

// 交换两个方块
function swapTiles(tile1, tile2) {
    [board[tile1.y][tile1.x], board[tile2.y][tile2.x]] = 
    [board[tile2.y][tile2.x], board[tile1.y][tile1.x]];
}

// 鼠标点击事件
canvas.addEventListener('click', (e) => {
    const rect = canvas.getBoundingClientRect();
    const mouseX = e.clientX - rect.left;
    const mouseY = e.clientY - rect.top;
    const tile = getTilePosition(mouseX, mouseY);

    // 检查点击位置是否在网格内
    if (tile.x < 0 || tile.x >= gridSize || tile.y < 0 || tile.y >= gridSize) {
        return;
    }

    // 第一次点击:选中方块
    if (!selectedTile) {
        selectedTile = tile;
    } else {
        // 第二次点击:如果相邻则交换
        if (isAdjacent(selectedTile, tile)) {
            swapTiles(selectedTile, tile);
            
            // 交换后检查是否有匹配,如果没有则交换回来
            if (!hasMatches()) {
                swapTiles(selectedTile, tile); // 无效交换,恢复原状
            } else {
                // 有效交换,处理消除逻辑
                processMatches();
            }
        }
        // 无论是否交换,都取消选中状态
        selectedTile = null;
    }
    // 重新绘制界面
    drawBoard();
});
  • getTilePosition:将鼠标点击的像素坐标转换为网格坐标(如(120, 80)转换为x=2, y=1)
  • isAdjacent:判断两个方块是否上下或左右相邻(消消乐规则:只能交换相邻方块)
  • swapTiles:使用ES6解构赋值交换两个方块的颜色索引
  • 点击事件处理:
    • 第一次点击记录选中位置
    • 第二次点击若相邻则交换,检查是否形成可消除组合
    • 无效交换(无消除)则恢复原状,有效交换则进入消除流程

5. 消除逻辑实现

检测并消除符合条件的方块,计算分数并更新网格。

// 检查是否有可消除的方块
function hasMatches() {
    return getMatches().length > 0;
}

// 获取所有可消除的方块位置
function getMatches() {
    const matches = [];
    
    // 检查水平方向匹配(至少3个连续相同)
    for (let y = 0; y < gridSize; y++) {
        let x = 0;
        while (x < gridSize - 2) { // 预留至少2个位置检查
            const color = board[y][x];
            if (color === board[y][x+1] && color === board[y][x+2]) {
                // 找到匹配,记录所有连续相同的位置
                let endX = x + 2;
                while (endX + 1 < gridSize && board[y][endX + 1] === color) {
                    endX++;
                }
                // 添加匹配位置到数组
                for (let i = x; i <= endX; i++) {
                    matches.push({x: i, y: y});
                }
                x = endX + 1; // 跳过已匹配的位置
            } else {
                x++;
            }
        }
    }
    
    // 检查垂直方向匹配
    for (let x = 0; x < gridSize; x++) {
        let y = 0;
        while (y < gridSize - 2) {
            const color = board[y][x];
            if (color === board[y+1][x] && color === board[y+2][x]) {
                let endY = y + 2;
                while (endY + 1 < gridSize && board[endY + 1][x] === color) {
                    endY++;
                }
                for (let i = y; i <= endY; i++) {
                    matches.push({x: x, y: i});
                }
                y = endY + 1;
            } else {
                y++;
            }
        }
    }
    
    return matches;
}

// 处理消除并更新分数
function processMatches() {
    const matches = getMatches();
    if (matches.length === 0) return;
    
    // 计算分数(每个消除的方块得10分)
    score += matches.length * 10;
    scoreElement.textContent = score;
    
    // 清除匹配的方块(设置为null标记)
    matches.forEach(tile => {
        board[tile.y][tile.x] = null;
    });
    
    // 让方块下落填补空白
    dropTiles();
    
    // 填充新方块
    fillEmptyTiles();
    
    // 检查是否有新的匹配(连锁反应)
    if (hasMatches()) {
        // 延迟处理连锁消除,增强视觉效果
        setTimeout(processMatches, 500);
    }
}
  • getMatches:核心检测函数,分别检查水平和垂直方向的连续相同方块:
    • 水平检查:遍历每行,寻找至少3个连续相同颜色的方块
    • 垂直检查:遍历每列,逻辑与水平检查类似
    • 返回所有匹配方块的位置数组
  • processMatches:处理消除流程:
    • 计算分数(每个方块10分)并更新显示
    • 将匹配的方块标记为null(空白)
    • 调用dropTiles让上方方块下落填补空白
    • 调用fillEmptyTiles在顶部生成新方块
    • 处理连锁消除(通过setTimeout添加延迟,提升体验)

6. 方块下落与填充

实现消除后方块下落和顶部补充新方块的逻辑。

// 让方块下落填补空白
function dropTiles() {
    for (let x = 0; x < gridSize; x++) {
        let emptyCount = 0;
        // 从下往上处理列,让方块下落
        for (let y = gridSize - 1; y >= 0; y--) {
            if (board[y][x] === null) {
                emptyCount++;
            } else if (emptyCount > 0) {
                // 将当前方块下移到空白位置
                board[y + emptyCount][x] = board[y][x];
                board[y][x] = null;
            }
        }
    }
}

// 在顶部填充新方块
function fillEmptyTiles() {
    for (let x = 0; x < gridSize; x++) {
        for (let y = 0; y < gridSize; y++) {
            if (board[y][x] === null) {
                // 生成随机颜色(避免与下方方块颜色相同)
                let colorIndex;
                do {
                    colorIndex = Math.floor(Math.random() * colors.length);
                } while (y > 0 && colorIndex === board[y-1][x]);
                board[y][x] = colorIndex;
            }
        }
    }
}
  • dropTiles:处理下落逻辑(类似俄罗斯方块下落):
    • 按列处理,从底部向上遍历
    • 统计空白位置数量(emptyCount),将非空白方块下移填补
  • fillEmptyTiles:在顶部空白位置生成新方块:
    • 为每个空白位置生成随机颜色
    • 避免与正下方方块颜色相同(减少无效组合)

7. 启动游戏

最后初始化并启动游戏。

// 启动游戏
function startGame() {
    score = 0;
    scoreElement.textContent = score;
    initBoard();
    drawBoard();
}

// 开始游戏
startGame();
  • startGame:初始化分数、网格并绘制界面
  • 页面加载后自动启动游戏

四、完整源码(复制粘贴即可运行)

<!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>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }
        body {
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
            min-height: 100vh;
            background: linear-gradient(135deg, #1a2a6c, #b21f1f, #fdbb2d);
            font-family: 'Arial Rounded MT Bold', 'Arial', sans-serif;
            color: white;
            overflow-x: hidden;
            padding: 20px;
        }
        .game-container {
            position: relative;
            margin: 20px 0;
        }
        canvas {
            background: rgba(255, 255, 255, 0.1);
            border-radius: 15px;
            box-shadow: 0 10px 30px rgba(0, 0, 0, 0.4);
            backdrop-filter: blur(5px);
        }
        .game-header {
            text-align: center;
            margin-bottom: 15px;
        }
        h1 {
            font-size: 2.8rem;
            margin-bottom: 10px;
            text-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
            background: linear-gradient(to right, #ff8a00, #e52e71);
            -webkit-background-clip: text;
            -webkit-text-fill-color: transparent;
        }
        .stats {
            display: flex;
            justify-content: space-between;
            width: 600px;
            margin-bottom: 10px;
            font-size: 1.2rem;
            background: rgba(255, 255, 255, 0.1);
            padding: 15px;
            border-radius: 10px;
            backdrop-filter: blur(5px);
        }
        .instructions {
            margin-top: 15px;
            text-align: center;
            max-width: 600px;
            line-height: 1.6;
            background: rgba(255, 255, 255, 0.1);
            padding: 15px;
            border-radius: 10px;
            backdrop-filter: blur(5px);
        }
        .controls {
            display: flex;
            gap: 10px;
            margin-top: 15px;
            flex-wrap: wrap;
            justify-content: center;
        }
        button {
            background: linear-gradient(135deg, #6a11cb 0%, #2575fc 100%);
            border: none;
            color: white;
            padding: 12px 25px;
            border-radius: 50px;
            font-size: 1rem;
            cursor: pointer;
            transition: all 0.3s ease;
            box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
        }
        button:hover {
            transform: translateY(-3px);
            box-shadow: 0 8px 20px rgba(0, 0, 0, 0.3);
        }
        button:active {
            transform: translateY(1px);
        }
        .combo-display {
            position: absolute;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            font-size: 2.5rem;
            font-weight: bold;
            color: #ffeb3b;
            text-shadow: 0 0 10px rgba(255, 235, 59, 0.7);
            opacity: 0;
            transition: opacity 0.3s;
            pointer-events: none;
        }
        .level-complete {
            position: absolute;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            background: rgba(0, 0, 0, 0.85);
            padding: 30px;
            border-radius: 15px;
            text-align: center;
            display: none;
            backdrop-filter: blur(5px);
            box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
            z-index: 10;
        }
        .level-complete h2 {
            font-size: 2.5rem;
            margin-bottom: 20px;
            color: #2ed573;
        }
        .special-effects {
            position: absolute;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            pointer-events: none;
            overflow: hidden;
            border-radius: 15px;
        }
        .effect {
            position: absolute;
            border-radius: 50%;
            background: radial-gradient(circle, rgba(255,255,255,0.8) 0%, rgba(255,255,255,0) 70%);
            animation: float 1.5s ease-out forwards;
        }
        @keyframes float {
            0% {
                transform: translateY(0) scale(1);
                opacity: 1;
            }
            100% {
                transform: translateY(-100px) scale(0);
                opacity: 0;
            }
        }
        .power-up {
            display: flex;
            align-items: center;
            gap: 10px;
            margin-top: 15px;
            background: rgba(255, 255, 255, 0.1);
            padding: 10px 20px;
            border-radius: 10px;
            backdrop-filter: blur(5px);
        }
        .power-up-icon {
            width: 30px;
            height: 30px;
            border-radius: 50%;
            background: linear-gradient(135deg, #ff9a9e 0%, #fad0c4 100%);
            display: flex;
            align-items: center;
            justify-content: center;
            font-weight: bold;
            box-shadow: 0 3px 10px rgba(0, 0, 0, 0.2);
        }
    </style>
</head>
<body>
    <div class="game-header">
        <h1>消消乐游戏</h1>
        <div class="stats">
            <div class="score">得分: <span id="score">0</span></div>
            <div class="level">关卡: <span id="currentLevel">1</span></div>
            <div class="moves">剩余步数: <span id="movesLeft">20</span></div>
            <div class="target">目标: <span id="targetScore">1000</span></div>
        </div>
    </div>
    
    <div class="game-container">
        <canvas id="gameCanvas" width="500" height="500"></canvas>
        <div class="special-effects" id="effects"></div>
        <div class="combo-display" id="comboDisplay">连击!</div>
        <div class="level-complete" id="levelComplete">
            <h2>恭喜过关!</h2>
            <div class="score">得分: <span id="levelScore">0</span></div>
            <button id="nextLevelBtn">下一关</button>
        </div>
    </div>
    
    <div class="power-up">
        <div class="power-up-icon">4</div>
        <div>4连消: 消除整行</div>
        <div class="power-up-icon">5</div>
        <div>5连消: 消除整行和整列</div>
        <div class="power-up-icon">T</div>
        <div>T型消: 消除十字区域</div>
    </div>
    
    <div class="instructions">
        <p>点击并交换相邻方块,使三个或更多相同颜色的方块连成一线即可消除</p>
        <p>4个相同方块消除整行,5个相同方块消除整行和整列,T型消除十字区域</p>
        <p>目标是在步数用尽前达到目标分数!</p>
    </div>
    
    <div class="controls">
        <button id="newGameBtn">新游戏</button>
        <button id="hintBtn">提示</button>
        <button id="shuffleBtn">重排</button>
    </div>

    <script>
        // 获取Canvas和上下文
        const canvas = document.getElementById('gameCanvas');
        const ctx = canvas.getContext('2d');
        const effectsContainer = document.getElementById('effects');
        const comboDisplay = document.getElementById('comboDisplay');

        // 游戏常量
        const GRID_SIZE = 8;
        const TILE_SIZE = 60;
        const TILE_MARGIN = 2;
        const ANIMATION_SPEED = 0.2;
        const COLORS = [
            '#FF5252', // 红色
            '#FF4081', // 粉色
            '#E040FB', // 紫色
            '#7C4DFF', // 深紫
            '#536DFE', // 蓝色
            '#448AFF', // 亮蓝
            '#40C4FF', // 天蓝
            '#18FFFF', // 青色
            '#64FFDA', // 青绿
            '#69F0AE'  // 绿色
        ];

        // 游戏状态
        let gameState = {
            grid: [],
            selectedTile: null,
            score: 0,
            level: 1,
            movesLeft: 20,
            targetScore: 1000,
            isSwapping: false,
            isAnimating: false,
            combo: 0,
            lastMatchTime: 0
        };

        // 初始化游戏
        function initGame() {
            gameState.grid = [];
            gameState.selectedTile = null;
            gameState.score = 0;
            gameState.level = 1;
            gameState.movesLeft = 20;
            gameState.targetScore = 1000;
            gameState.isSwapping = false;
            gameState.isAnimating = false;
            gameState.combo = 0;
            gameState.lastMatchTime = 0;

            // 创建初始网格
            for (let row = 0; row < GRID_SIZE; row++) {
                gameState.grid[row] = [];
                for (let col = 0; col < GRID_SIZE; col++) {
                    gameState.grid[row][col] = {
                        type: Math.floor(Math.random() * 6), // 使用前6种颜色
                        row: row,
                        col: col,
                        x: col * (TILE_SIZE + TILE_MARGIN) + TILE_MARGIN,
                        y: row * (TILE_SIZE + TILE_MARGIN) + TILE_MARGIN,
                        scale: 1,
                        alpha: 1,
                        removing: false
                    };
                }
            }

            // 确保初始网格没有匹配
            while (findMatches().length > 0) {
                shuffleGrid();
            }

            updateUI();
            drawGame();
            document.getElementById('levelComplete').style.display = 'none';
        }

        // 绘制游戏
        function drawGame() {
            ctx.clearRect(0, 0, canvas.width, canvas.height);
            
            // 绘制网格背景
            ctx.fillStyle = 'rgba(255, 255, 255, 0.05)';
            ctx.fillRect(0, 0, canvas.width, canvas.height);
            
            // 绘制网格线
            ctx.strokeStyle = 'rgba(255, 255, 255, 0.1)';
            ctx.lineWidth = 1;
            
            for (let i = 0; i <= GRID_SIZE; i++) {
                const pos = i * (TILE_SIZE + TILE_MARGIN);
                ctx.beginPath();
                ctx.moveTo(0, pos);
                ctx.lineTo(canvas.width, pos);
                ctx.stroke();
                
                ctx.beginPath();
                ctx.moveTo(pos, 0);
                ctx.lineTo(pos, canvas.height);
                ctx.stroke();
            }

            // 绘制方块
            for (let row = 0; row < GRID_SIZE; row++) {
                for (let col = 0; col < GRID_SIZE; col++) {
                    const tile = gameState.grid[row][col];
                    if (!tile) continue;
                    
                    ctx.save();
                    
                    // 应用动画效果
                    ctx.translate(tile.x + TILE_SIZE/2, tile.y + TILE_SIZE/2);
                    ctx.scale(tile.scale, tile.scale);
                    ctx.globalAlpha = tile.alpha;
                    
                    // 绘制方块主体
                    const gradient = ctx.createRadialGradient(-10, -10, 5, 0, 0, TILE_SIZE/2);
                    gradient.addColorStop(0, lightenColor(COLORS[tile.type], 30));
                    gradient.addColorStop(1, COLORS[tile.type]);
                    
                    ctx.fillStyle = gradient;
                    ctx.beginPath();
                    ctx.roundRect(-TILE_SIZE/2, -TILE_SIZE/2, TILE_SIZE, TILE_SIZE, 10);
                    ctx.fill();
                    
                    // 绘制内部高光
                    ctx.fillStyle = 'rgba(255, 255, 255, 0.2)';
                    ctx.beginPath();
                    ctx.roundRect(-TILE_SIZE/2 + 5, -TILE_SIZE/2 + 5, TILE_SIZE - 10, TILE_SIZE - 10, 5);
                    ctx.fill();
                    
                    // 绘制边框
                    ctx.strokeStyle = 'rgba(255, 255, 255, 0.3)';
                    ctx.lineWidth = 2;
                    ctx.beginPath();
                    ctx.roundRect(-TILE_SIZE/2, -TILE_SIZE/2, TILE_SIZE, TILE_SIZE, 10);
                    ctx.stroke();
                    
                    // 绘制选中效果
                    if (gameState.selectedTile && 
                        gameState.selectedTile.row === row && 
                        gameState.selectedTile.col === col) {
                        ctx.strokeStyle = '#FFEB3B';
                        ctx.lineWidth = 4;
                        ctx.beginPath();
                        ctx.roundRect(-TILE_SIZE/2, -TILE_SIZE/2, TILE_SIZE, TILE_SIZE, 10);
                        ctx.stroke();
                        
                        // 绘制脉动效果
                        const pulseSize = Math.sin(Date.now() / 200) * 5 + 10;
                        ctx.strokeStyle = 'rgba(255, 235, 59, 0.5)';
                        ctx.lineWidth = 2;
                        ctx.beginPath();
                        ctx.roundRect(-TILE_SIZE/2 - pulseSize/2, -TILE_SIZE/2 - pulseSize/2, 
                                     TILE_SIZE + pulseSize, TILE_SIZE + pulseSize, 10 + pulseSize/2);
                        ctx.stroke();
                    }
                    
                    ctx.restore();
                }
            }
        }

        // 工具函数:颜色变亮
        function lightenColor(color, percent) {
            const num = parseInt(color.slice(1), 16);
            const amt = Math.round(2.55 * percent);
            const R = (num >> 16) + amt;
            const G = (num >> 8 & 0x00FF) + amt;
            const B = (num & 0x0000FF) + amt;
            return "#" + (
                0x1000000 + 
                (R < 255 ? R < 1 ? 0 : R : 255) * 0x10000 + 
                (G < 255 ? G < 1 ? 0 : G : 255) * 0x100 + 
                (B < 255 ? B < 1 ? 0 : B : 255)
            ).toString(16).slice(1);
        }

        // 查找匹配
        function findMatches() {
            const matches = [];
            
            // 检查水平匹配
            for (let row = 0; row < GRID_SIZE; row++) {
                for (let col = 0; col < GRID_SIZE - 2; col++) {
                    const tile1 = gameState.grid[row][col];
                    const tile2 = gameState.grid[row][col + 1];
                    const tile3 = gameState.grid[row][col + 2];
                    
                    if (tile1 && tile2 && tile3 && 
                        tile1.type === tile2.type && tile2.type === tile3.type) {
                        
                        // 查找连续匹配
                        let matchLength = 3;
                        while (col + matchLength < GRID_SIZE && 
                               gameState.grid[row][col + matchLength] && 
                               gameState.grid[row][col + matchLength].type === tile1.type) {
                            matchLength++;
                        }
                        
                        // 添加匹配
                        const match = [];
                        for (let i = 0; i < matchLength; i++) {
                            match.push({row: row, col: col + i});
                        }
                        matches.push(match);
                        
                        col += matchLength - 1;
                    }
                }
            }
            
            // 检查垂直匹配
            for (let col = 0; col < GRID_SIZE; col++) {
                for (let row = 0; row < GRID_SIZE - 2; row++) {
                    const tile1 = gameState.grid[row][col];
                    const tile2 = gameState.grid[row + 1][col];
                    const tile3 = gameState.grid[row + 2][col];
                    
                    if (tile1 && tile2 && tile3 && 
                        tile1.type === tile2.type && tile2.type === tile3.type) {
                        
                        // 查找连续匹配
                        let matchLength = 3;
                        while (row + matchLength < GRID_SIZE && 
                               gameState.grid[row + matchLength][col] && 
                               gameState.grid[row + matchLength][col].type === tile1.type) {
                            matchLength++;
                        }
                        
                        // 添加匹配
                        const match = [];
                        for (let i = 0; i < matchLength; i++) {
                            match.push({row: row + i, col: col});
                        }
                        matches.push(match);
                        
                        row += matchLength - 1;
                    }
                }
            }
            
            return matches;
        }

        // 移除匹配的方块
        function removeMatches(matches) {
            if (matches.length === 0) return 0;
            
            let removedCount = 0;
            const removedTiles = [];
            
            // 标记要移除的方块
            matches.forEach(match => {
                match.forEach(pos => {
                    const tile = gameState.grid[pos.row][pos.col];
                    if (tile && !tile.removing) {
                        tile.removing = true;
                        removedTiles.push(tile);
                        removedCount++;
                        
                        // 创建消除特效
                        createRemoveEffect(tile.x + TILE_SIZE/2, tile.y + TILE_SIZE/2, COLORS[tile.type]);
                    }
                });
            });
            
            // 计算得分
            const baseScore = 10;
            const matchScore = matches.reduce((total, match) => total + baseScore * match.length * match.length, 0);
            const comboMultiplier = 1 + (gameState.combo * 0.2);
            const totalScore = Math.floor(matchScore * comboMultiplier);
            
            gameState.score += totalScore;
            gameState.combo++;
            gameState.lastMatchTime = Date.now();
            
            // 显示连击效果
            if (gameState.combo > 1) {
                showCombo();
            }
            
            // 开始移除动画
            animateRemoval(removedTiles, () => {
                // 实际移除方块
                matches.forEach(match => {
                    match.forEach(pos => {
                        gameState.grid[pos.row][pos.col] = null;
                    });
                });
                
                // 方块下落
                dropTiles();
            });
            
            return removedCount;
        }

        // 创建消除特效
        function createRemoveEffect(x, y, color) {
            for (let i = 0; i < 8; i++) {
                const effect = document.createElement('div');
                effect.className = 'effect';
                effect.style.left = x + 'px';
                effect.style.top = y + 'px';
                effect.style.width = '10px';
                effect.style.height = '10px';
                effect.style.background = `radial-gradient(circle, ${color} 0%, rgba(255,255,255,0) 70%)`;
                effect.style.animationDelay = (i * 0.1) + 's';
                
                effectsContainer.appendChild(effect);
                
                // 动画结束后移除元素
                setTimeout(() => {
                    if (effectsContainer.contains(effect)) {
                        effectsContainer.removeChild(effect);
                    }
                }, 1500);
            }
        }

        // 显示连击效果
        function showCombo() {
            comboDisplay.textContent = `${gameState.combo} 连击!`;
            comboDisplay.style.opacity = '1';
            
            setTimeout(() => {
                comboDisplay.style.opacity = '0';
            }, 1000);
        }

        // 方块下落
        function dropTiles() {
            let moved = false;
            
            // 从底部向上处理每一列
            for (let col = 0; col < GRID_SIZE; col++) {
                let emptySpaces = 0;
                
                for (let row = GRID_SIZE - 1; row >= 0; row--) {
                    if (gameState.grid[row][col] === null) {
                        emptySpaces++;
                    } else if (emptySpaces > 0) {
                        // 移动方块
                        const tile = gameState.grid[row][col];
                        gameState.grid[row][col] = null;
                        gameState.grid[row + emptySpaces][col] = tile;
                        tile.row = row + emptySpaces;
                        moved = true;
                    }
                }
                
                // 在顶部生成新方块
                for (let row = 0; row < emptySpaces; row++) {
                    gameState.grid[row][col] = {
                        type: Math.floor(Math.random() * 6),
                        row: row,
                        col: col,
                        x: col * (TILE_SIZE + TILE_MARGIN) + TILE_MARGIN,
                        y: -TILE_SIZE, // 从上方进入
                        scale: 1,
                        alpha: 1,
                        removing: false
                    };
                }
            }
            
            if (moved) {
                animateDrop(() => {
                    // 检查新的匹配
                    const newMatches = findMatches();
                    if (newMatches.length > 0) {
                        removeMatches(newMatches);
                    } else {
                        gameState.isAnimating = false;
                        checkLevelComplete();
                    }
                });
            } else {
                gameState.isAnimating = false;
                checkLevelComplete();
            }
        }

        // 动画移除
        function animateRemoval(tiles, callback) {
            let completed = 0;
            
            tiles.forEach(tile => {
                const startTime = Date.now();
                const duration = 300;
                
                function animate() {
                    const elapsed = Date.now() - startTime;
                    const progress = Math.min(elapsed / duration, 1);
                    
                    tile.scale = 1 - progress;
                    tile.alpha = 1 - progress;
                    
                    drawGame();
                    
                    if (progress < 1) {
                        requestAnimationFrame(animate);
                    } else {
                        completed++;
                        if (completed === tiles.length) {
                            callback();
                        }
                    }
                }
                
                animate();
            });
        }

        // 动画下落
        function animateDrop(callback) {
            const startTime = Date.now();
            const duration = 500;
            
            function animate() {
                const elapsed = Date.now() - startTime;
                const progress = Math.min(elapsed / duration, 1);
                
                // 更新所有方块位置
                for (let row = 0; row < GRID_SIZE; row++) {
                    for (let col = 0; col < GRID_SIZE; col++) {
                        const tile = gameState.grid[row][col];
                        if (!tile) continue;
                        
                        const targetY = row * (TILE_SIZE + TILE_MARGIN) + TILE_MARGIN;
                        if (tile.y !== targetY) {
                            tile.y = tile.y + (targetY - tile.y) * ANIMATION_SPEED;
                            
                            // 如果接近目标位置,直接设置
                            if (Math.abs(tile.y - targetY) < 0.5) {
                                tile.y = targetY;
                            }
                        }
                    }
                }
                
                drawGame();
                
                if (progress < 1) {
                    requestAnimationFrame(animate);
                } else {
                    callback();
                }
            }
            
            animate();
        }

        // 交换方块
        function swapTiles(tile1, tile2) {
            if (gameState.isAnimating || gameState.isSwapping) return false;
            
            // 检查是否相邻
            const rowDiff = Math.abs(tile1.row - tile2.row);
            const colDiff = Math.abs(tile1.col - tile2.col);
            
            if ((rowDiff === 1 && colDiff === 0) || (rowDiff === 0 && colDiff === 1)) {
                gameState.isSwapping = true;
                
                // 交换网格中的位置
                [gameState.grid[tile1.row][tile1.col], gameState.grid[tile2.row][tile2.col]] = 
                [gameState.grid[tile2.row][tile2.col], gameState.grid[tile1.row][tile1.col]];
                
                // 交换行列信息
                [tile1.row, tile2.row] = [tile2.row, tile1.row];
                [tile1.col, tile2.col] = [tile2.col, tile1.col];
                
                // 动画交换
                animateSwap(tile1, tile2, () => {
                    gameState.isSwapping = false;
                    
                    // 检查匹配
                    const matches = findMatches();
                    if (matches.length > 0) {
                        gameState.movesLeft--;
                        removeMatches(matches);
                    } else {
                        // 没有匹配,交换回来
                        [gameState.grid[tile1.row][tile1.col], gameState.grid[tile2.row][tile2.col]] = 
                        [gameState.grid[tile2.row][tile2.col], gameState.grid[tile1.row][tile1.col]];
                        
                        [tile1.row, tile2.row] = [tile2.row, tile1.row];
                        [tile1.col, tile2.col] = [tile2.col, tile1.col];
                        
                        animateSwap(tile1, tile2, () => {
                            gameState.isAnimating = false;
                        });
                    }
                    
                    updateUI();
                });
                
                return true;
            }
            
            return false;
        }

        // 动画交换
        function animateSwap(tile1, tile2, callback) {
            gameState.isAnimating = true;
            
            const startTime = Date.now();
            const duration = 300;
            
            const startX1 = tile1.x;
            const startY1 = tile1.y;
            const startX2 = tile2.x;
            const startY2 = tile2.y;
            
            const targetX1 = tile1.col * (TILE_SIZE + TILE_MARGIN) + TILE_MARGIN;
            const targetY1 = tile1.row * (TILE_SIZE + TILE_MARGIN) + TILE_MARGIN;
            const targetX2 = tile2.col * (TILE_SIZE + TILE_MARGIN) + TILE_MARGIN;
            const targetY2 = tile2.row * (TILE_SIZE + TILE_MARGIN) + TILE_MARGIN;
            
            function animate() {
                const elapsed = Date.now() - startTime;
                const progress = Math.min(elapsed / duration, 1);
                
                // 使用缓动函数
                const easeProgress = easeOutCubic(progress);
                
                tile1.x = startX1 + (targetX1 - startX1) * easeProgress;
                tile1.y = startY1 + (targetY1 - startY1) * easeProgress;
                tile2.x = startX2 + (targetX2 - startX2) * easeProgress;
                tile2.y = startY2 + (targetY2 - startY2) * easeProgress;
                
                drawGame();
                
                if (progress < 1) {
                    requestAnimationFrame(animate);
                } else {
                    tile1.x = targetX1;
                    tile1.y = targetY1;
                    tile2.x = targetX2;
                    tile2.y = targetY2;
                    
                    gameState.isAnimating = false;
                    callback();
                }
            }
            
            animate();
        }

        // 缓动函数
        function easeOutCubic(t) {
            return 1 - Math.pow(1 - t, 3);
        }

        // 检查关卡完成
        function checkLevelComplete() {
            if (gameState.score >= gameState.targetScore) {
                document.getElementById('levelScore').textContent = gameState.score;
                document.getElementById('levelComplete').style.display = 'block';
            } else if (gameState.movesLeft <= 0) {
                // 游戏结束
                setTimeout(() => {
                    alert('游戏结束!你的得分: ' + gameState.score);
                    initGame();
                }, 500);
            }
        }

        // 更新UI
        function updateUI() {
            document.getElementById('score').textContent = gameState.score;
            document.getElementById('currentLevel').textContent = gameState.level;
            document.getElementById('movesLeft').textContent = gameState.movesLeft;
            document.getElementById('targetScore').textContent = gameState.targetScore;
        }

        // 重排网格
        function shuffleGrid() {
            // 创建一个包含所有方块的数组
            const allTiles = [];
            for (let row = 0; row < GRID_SIZE; row++) {
                for (let col = 0; col < GRID_SIZE; col++) {
                    if (gameState.grid[row][col]) {
                        allTiles.push(gameState.grid[row][col]);
                    }
                }
            }
            
            // 随机打乱
            for (let i = allTiles.length - 1; i > 0; i--) {
                const j = Math.floor(Math.random() * (i + 1));
                [allTiles[i], allTiles[j]] = [allTiles[j], allTiles[i]];
            }
            
            // 重新分配位置
            let index = 0;
            for (let row = 0; row < GRID_SIZE; row++) {
                for (let col = 0; col < GRID_SIZE; col++) {
                    if (gameState.grid[row][col]) {
                        const tile = allTiles[index++];
                        tile.row = row;
                        tile.col = col;
                        tile.x = col * (TILE_SIZE + TILE_MARGIN) + TILE_MARGIN;
                        tile.y = row * (TILE_SIZE + TILE_MARGIN) + TILE_MARGIN;
                        gameState.grid[row][col] = tile;
                    }
                }
            }
            
            drawGame();
        }

        // 提示功能
        function showHint() {
            // 查找可能的匹配
            for (let row = 0; row < GRID_SIZE; row++) {
                for (let col = 0; col < GRID_SIZE; col++) {
                    const tile = gameState.grid[row][col];
                    if (!tile) continue;
                    
                    // 检查与右边方块的交换
                    if (col < GRID_SIZE - 1) {
                        const rightTile = gameState.grid[row][col + 1];
                        if (rightTile) {
                            // 模拟交换
                            [gameState.grid[row][col], gameState.grid[row][col + 1]] = 
                            [gameState.grid[row][col + 1], gameState.grid[row][col]];
                            
                            const matches = findMatches();
                            
                            // 恢复交换
                            [gameState.grid[row][col], gameState.grid[row][col + 1]] = 
                            [gameState.grid[row][col + 1], gameState.grid[row][col]];
                            
                            if (matches.length > 0) {
                                // 高亮显示提示
                                tile.hint = true;
                                rightTile.hint = true;
                                
                                setTimeout(() => {
                                    tile.hint = false;
                                    rightTile.hint = false;
                                    drawGame();
                                }, 2000);
                                
                                drawGame();
                                return;
                            }
                        }
                    }
                    
                    // 检查与下方方块的交换
                    if (row < GRID_SIZE - 1) {
                        const bottomTile = gameState.grid[row + 1][col];
                        if (bottomTile) {
                            // 模拟交换
                            [gameState.grid[row][col], gameState.grid[row + 1][col]] = 
                            [gameState.grid[row + 1][col], gameState.grid[row][col]];
                            
                            const matches = findMatches();
                            
                            // 恢复交换
                            [gameState.grid[row][col], gameState.grid[row + 1][col]] = 
                            [gameState.grid[row + 1][col], gameState.grid[row][col]];
                            
                            if (matches.length > 0) {
                                // 高亮显示提示
                                tile.hint = true;
                                bottomTile.hint = true;
                                
                                setTimeout(() => {
                                    tile.hint = false;
                                    bottomTile.hint = false;
                                    drawGame();
                                }, 2000);
                                
                                drawGame();
                                return;
                            }
                        }
                    }
                }
            }
            
            // 如果没有找到匹配,重排网格
            alert('没有可用的移动,正在重排...');
            shuffleGrid();
        }

        // 下一关
        function nextLevel() {
            gameState.level++;
            gameState.movesLeft = 20 + gameState.level * 2;
            gameState.targetScore = 1000 + gameState.level * 500;
            gameState.score = 0;
            gameState.combo = 0;
            
            initGame();
        }

        // 鼠标事件处理
        canvas.addEventListener('click', (e) => {
            if (gameState.isAnimating || gameState.isSwapping) return;
            
            const rect = canvas.getBoundingClientRect();
            const x = e.clientX - rect.left;
            const y = e.clientY - rect.top;
            
            // 计算点击的网格位置
            const col = Math.floor(x / (TILE_SIZE + TILE_MARGIN));
            const row = Math.floor(y / (TILE_SIZE + TILE_MARGIN));
            
            if (row >= 0 && row < GRID_SIZE && col >= 0 && col < GRID_SIZE) {
                const tile = gameState.grid[row][col];
                
                if (tile) {
                    if (gameState.selectedTile === null) {
                        // 第一次选择
                        gameState.selectedTile = tile;
                    } else {
                        // 第二次选择,尝试交换
                        if (gameState.selectedTile !== tile) {
                            if (swapTiles(gameState.selectedTile, tile)) {
                                gameState.selectedTile = null;
                            } else {
                                gameState.selectedTile = tile;
                            }
                        } else {
                            // 点击同一方块,取消选择
                            gameState.selectedTile = null;
                        }
                    }
                    
                    drawGame();
                }
            }
        });

        // 按钮事件
        document.getElementById('newGameBtn').addEventListener('click', initGame);
        document.getElementById('hintBtn').addEventListener('click', showHint);
        document.getElementById('shuffleBtn').addEventListener('click', shuffleGrid);
        document.getElementById('nextLevelBtn').addEventListener('click', nextLevel);

        // 初始化游戏
        initGame();
    </script>
</body>
</html>

五、游戏功能扩展建议

  1. 添加消除动画:使用Canvas过渡效果让消除的方块缩放或淡出
  2. 增加关卡系统:随分数提高增加网格大小或颜色种类
  3. 计时模式:限制时间内获取最高分
  4. 音效反馈:添加点击、消除、计分的音效

通过以上步骤,我们实现了一个基础的消消乐游戏,包含核心的选中、交换、消除、下落逻辑。Canvas提供了灵活的绘图能力,配合JavaScript事件处理,能轻松实现各类2D游戏。你可以基于这个框架继续扩展功能,打造更丰富的游戏体验。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值