用HTML5 Canvas和原生JS手搓一个Emoji消消乐(附完整源码和算法解析)
用HTML5 Canvas和原生JS手搓一个Emoji消消乐附完整源码和算法解析在移动游戏风靡的今天消除类游戏因其简单易上手、又兼具策略性的特点始终占据着一席之地。作为前端开发者自己动手实现一个消除游戏不仅能巩固Canvas绘图和JavaScript基础更能深入理解游戏开发中的核心算法逻辑。本文将带你从零开始用纯前端技术打造一个Emoji主题的消消乐游戏并重点解析其中的匹配检测、重力模拟等关键算法。1. 游戏基础架构设计消消乐游戏的核心架构可以分解为三个主要部分游戏状态管理、用户交互处理和游戏逻辑更新。我们先从最基础的Canvas绘制开始。1.1 Canvas初始化与网格绘制首先需要设置游戏画布和基本参数const canvas document.getElementById(gameCanvas); const ctx canvas.getContext(2d); const GRID_SIZE 8; // 8x8网格 const CELL_SIZE canvas.width / GRID_SIZE; const EMOJIS [, , , , , ]; // 6种不同Emoji初始化游戏网格时我们需要确保不会一开始就出现可消除的组合function initGrid() { let grid Array(GRID_SIZE).fill().map(() Array(GRID_SIZE).fill(null)); // 确保初始网格没有可消除的组合 for (let y 0; y GRID_SIZE; y) { for (let x 0; x GRID_SIZE; x) { let validEmojis [...EMOJIS]; // 检查左侧两个格子 if (x 2 grid[x-1][y] grid[x-2][y]) { validEmojis validEmojis.filter(e e ! grid[x-1][y]); } // 检查上方两个格子 if (y 2 grid[x][y-1] grid[x][y-2]) { validEmojis validEmojis.filter(e e ! grid[x][y-1]); } grid[x][y] validEmojis[Math.floor(Math.random() * validEmojis.length)]; } } return grid; }提示这种初始化方式虽然复杂一些但能确保游戏开始时玩家就有可操作的空间而不是需要先随机交换才能开始游戏。1.2 游戏状态管理我们需要跟踪几个关键状态let gameState { grid: initGrid(), selectedCell: null, // 当前选中的格子 score: 0, isAnimating: false, // 是否正在执行动画 animationQueue: [] // 动画队列 };2. 核心游戏逻辑实现2.1 交换与匹配检测玩家交换两个相邻格子后需要立即检查是否形成了三个或以上相同的连续Emojifunction checkMatches(grid) { const matches new Set(); // 使用Set避免重复记录 // 水平检测 for (let y 0; y GRID_SIZE; y) { let streak 1; for (let x 1; x GRID_SIZE; x) { if (grid[x][y] grid[x][y] grid[x-1][y]) { streak; } else { if (streak 3) { for (let i x - streak; i x; i) { matches.add(${i},${y}); } } streak 1; } } // 行末检查 if (streak 3) { for (let i GRID_SIZE - streak; i GRID_SIZE; i) { matches.add(${i},${y}); } } } // 垂直检测类似逻辑 // ... return Array.from(matches).map(str { const [x, y] str.split(,).map(Number); return {x, y}; }); }2.2 消除与重力模拟消除匹配的格子后上方的格子需要下落填补空缺顶部生成新的格子function applyGravity(grid) { let moved false; for (let x 0; x GRID_SIZE; x) { // 从下往上检查 let emptyY GRID_SIZE - 1; for (let y GRID_SIZE - 1; y 0; y--) { if (grid[x][y]) { if (y ! emptyY) { grid[x][emptyY] grid[x][y]; grid[x][y] null; moved true; } emptyY--; } } // 填充顶部空缺 for (let y 0; y emptyY; y) { grid[x][y] EMOJIS[Math.floor(Math.random() * EMOJIS.length)]; moved true; } } return moved; }3. 性能优化与高级功能3.1 匹配检测算法优化原始的水平垂直双重扫描算法虽然直观但在大型网格上可能效率不高。我们可以优化为单次扫描function optimizedCheckMatches(grid) { const matches new Set(); const directions [ [1, 0], // 水平 [0, 1], // 垂直 [1, 1], // 对角线 [1, -1] // 反对角线 ]; for (let y 0; y GRID_SIZE; y) { for (let x 0; x GRID_SIZE; x) { if (!grid[x][y]) continue; for (const [dx, dy] of directions) { let streak 1; let nx x dx, ny y dy; const matchedCells [${x},${y}]; while (nx 0 nx GRID_SIZE ny 0 ny GRID_SIZE grid[nx][ny] grid[x][y]) { matchedCells.push(${nx},${ny}); streak; nx dx; ny dy; } if (streak 3) { matchedCells.forEach(cell matches.add(cell)); } } } } return Array.from(matches).map(str { const [x, y] str.split(,).map(Number); return {x, y}; }); }3.2 动画效果实现为了让游戏体验更流畅我们可以添加交换、消除和下落动画function animateSwap(cell1, cell2, callback) { const duration 200; // 毫秒 const startTime performance.now(); const originalPositions { cell1: {x: cell1.x * CELL_SIZE, y: cell1.y * CELL_SIZE}, cell2: {x: cell2.x * CELL_SIZE, y: cell2.y * CELL_SIZE} }; function step(currentTime) { const elapsed currentTime - startTime; const progress Math.min(elapsed / duration, 1); // 计算中间位置 const cell1X originalPositions.cell1.x (originalPositions.cell2.x - originalPositions.cell1.x) * progress; const cell1Y originalPositions.cell1.y (originalPositions.cell2.y - originalPositions.cell1.y) * progress; const cell2X originalPositions.cell2.x (originalPositions.cell1.x - originalPositions.cell2.x) * progress; const cell2Y originalPositions.cell2.y (originalPositions.cell1.y - originalPositions.cell2.y) * progress; // 重绘 drawGrid(); drawCell(cell1, cell1X, cell1Y); drawCell(cell2, cell2X, cell2Y); if (progress 1) { requestAnimationFrame(step); } else { callback(); } } requestAnimationFrame(step); }4. 完整游戏循环与用户体验优化4.1 游戏主循环将各个模块组合起来形成完整的游戏循环function gameLoop() { if (gameState.isAnimating) { requestAnimationFrame(gameLoop); return; } // 处理动画队列 if (gameState.animationQueue.length 0) { const animation gameState.animationQueue.shift(); gameState.isAnimating true; animation(() { gameState.isAnimating false; }); requestAnimationFrame(gameLoop); return; } // 检查是否有匹配 const matches checkMatches(gameState.grid); if (matches.length 0) { // 处理消除和下落 removeMatches(matches); updateScore(matches.length); gameState.animationQueue.push( cb animateRemove(matches, () { applyGravity(gameState.grid); cb(); }) ); } drawGrid(); requestAnimationFrame(gameLoop); }4.2 特殊效果与得分加成为了增加游戏深度可以实现连击系统和特殊效果function updateScore(matchCount, isCombo false) { let baseScore matchCount * 10; // 连击加成 if (isCombo) { baseScore * gameState.comboMultiplier; gameState.comboMultiplier 0.5; } else { gameState.comboMultiplier 1; } gameState.score Math.floor(baseScore); updateScoreDisplay(); } function createSpecialEffects(matches) { // 4连及以上创建炸弹效果 if (matches.length 4) { const center matches[Math.floor(matches.length / 2)]; return {type: bomb, x: center.x, y: center.y, radius: 2}; } return null; }实现一个完整的消消乐游戏需要考虑的细节远不止这些比如关卡设计、难度曲线、音效管理等。本文提供的代码已经实现了核心玩法你可以在此基础上继续扩展。在实际开发中建议使用TypeScript来获得更好的类型安全或者考虑使用游戏引擎如Phaser来简化开发流程。
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2438323.html
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!