文章目录
手把手教你用HTML5 Canvas开发拼图游戏(Jigsaw Puzzle)
拼图游戏是经典的益智类游戏,通过将打乱的图片碎片重新拼接成完整图片来锻炼空间思维能力。本文将带你从零开始,使用HTML5 Canvas和原生JavaScript实现一款拼图游戏,掌握图形分割、拖拽交互、碰撞检测等核心技术。

一、游戏核心原理与准备
在开始编码前,我们先明确拼图游戏的核心机制:
- 拼图块(Pieces):将完整图片分割成若干规则碎片(本文用颜色块模拟图片)
- 网格布局:拼图块按固定网格排列,每个位置对应一个正确的拼图块
- 拖拽交互:玩家可拖拽拼图块移动,靠近正确位置时自动吸附
- 胜利条件:所有拼图块都放置在正确位置
准备工作:创建一个HTML文件(命名为jigsaw-puzzle.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: #f0f0f0;
font-family: Arial, sans-serif;
}
#gameCanvas {
border: 3px solid #333;
background-color: #fff;
box-shadow: 0 5px 15px 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="600" height="600"></canvas>
<script>
// 游戏代码将写在这里
</script>
</body>
</html>
代码说明:
- 创建了一个600x600像素的Canvas画布,适合展示中等复杂度的拼图
- 添加了简单的样式,使画布有阴影效果,增强立体感
- 包含游戏说明文字,指导玩家操作
- 预留
<script>标签用于编写游戏逻辑
三、步骤2:初始化拼图参数与数据
定义拼图的核心参数(如尺寸、难度),并创建拼图块数据结构。在<script>标签中添加:
// 获取Canvas元素和绘图上下文
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
// 拼图参数
const puzzleSize = 4; // 4x4拼图(可改为3或5调整难度)
const pieceWidth = canvas.width / puzzleSize; // 每个拼图块的宽度
const pieceHeight = canvas.height / puzzleSize; // 每个拼图块的高度
// 存储所有拼图块的数组
let pieces = [];
// 拖拽状态变量
let draggedPiece = null; // 当前被拖拽的拼图块
let offsetX = 0; // 鼠标与拼图块左上角的X偏移
let offsetY = 0; // 鼠标与拼图块左上角的Y偏移
// 游戏状态
let completed = false; // 是否完成拼图
代码说明:
- 拼图难度:通过
puzzleSize设置为4x4(16块),可改为3x3(简单)或5x5(困难) - 拼图块尺寸:根据画布大小和拼图数量自动计算,确保均匀分割
- 核心数组:
pieces将存储所有拼图块的位置、目标位置、颜色等信息 - 拖拽变量:记录当前被拖拽的拼图块及鼠标偏移,确保拖拽时鼠标位置与拼图块相对位置不变
四、步骤3:生成拼图块数据
创建函数生成拼图块,包括随机颜色(模拟图片内容)、初始位置和目标位置:
// 初始化拼图块
function initPieces() {
pieces = [];
for (let row = 0; row < puzzleSize; row++) {
for (let col = 0; col < puzzleSize; col++) {
// 生成随机颜色(模拟图片的不同区域)
const hue = (row * puzzleSize + col) * (360 / (puzzleSize * puzzleSize));
const color = `hsl(${hue}, 70%, 60%)`;
// 计算目标位置(正确位置)
const targetX = col * pieceWidth;
const targetY = row * pieceHeight;
// 随机初始位置(在画布范围内)
const startX = Math.random() * (canvas.width - pieceWidth);
const startY = Math.random() * (canvas.height - pieceHeight);
pieces.push({
x: startX,
y: startY,
width: pieceWidth,
height: pieceHeight,
targetX: targetX,
targetY: targetY,
color: color,
isDragging: false,
isCorrect: false // 是否已放置在正确位置
});
}
}
}
代码说明:
- 颜色生成:使用HSL颜色模式,根据拼图块的行列索引生成渐变颜色,模拟图片的不同区域,便于玩家识别
- 目标位置:每个拼图块的正确位置由其行列索引计算,形成完整网格
- 初始位置:随机分布在画布范围内,确保游戏开始时拼图是打乱的
- 状态标记:
isCorrect用于记录拼图块是否已放置在正确位置
五、步骤4:绘制游戏元素
编写函数绘制拼图块、网格线(辅助玩家对齐)和胜利提示:
// 绘制单个拼图块
function drawPiece(piece) {
// 绘制拼图块主体
ctx.fillStyle = piece.color;
ctx.fillRect(piece.x, piece.y, piece.width - 1, piece.height - 1);
// 绘制边框(增强立体感)
ctx.strokeStyle = '#333';
ctx.lineWidth = 1;
ctx.strokeRect(piece.x, piece.y, piece.width - 1, piece.height - 1);
// 为已正确放置的拼图块添加标记
if (piece.isCorrect) {
ctx.strokeStyle = 'green';
ctx.lineWidth = 3;
ctx.strokeRect(piece.x + 2, piece.y + 2, piece.width - 5, piece.height - 5);
}
// 为正在拖拽的拼图块添加高亮效果
if (piece.isDragging) {
ctx.strokeStyle = '#ff9800';
ctx.lineWidth = 2;
ctx.setLineDash([5, 3]); // 虚线
ctx.strokeRect(piece.x - 2, piece.y - 2, piece.width + 2, piece.height + 2);
ctx.setLineDash([]); // 重置为实线
}
}
// 绘制所有拼图块
function drawAllPieces() {
// 先绘制已正确放置的拼图块(放在底层)
pieces.filter(p => p.isCorrect).forEach(piece => drawPiece(piece));
// 再绘制未正确放置的拼图块(放在上层,便于拖拽)
pieces.filter(p => !p.isCorrect).forEach(piece => drawPiece(piece));
}
// 绘制目标网格(辅助玩家对齐)
function drawGrid() {
ctx.strokeStyle = '#ddd';
ctx.lineWidth = 1;
// 绘制水平线
for (let i = 1; i < puzzleSize; i++) {
ctx.beginPath();
ctx.moveTo(0, i * pieceHeight);
ctx.lineTo(canvas.width, i * pieceHeight);
ctx.stroke();
}
// 绘制垂直线
for (let i = 1; i < puzzleSize; i++) {
ctx.beginPath();
ctx.moveTo(i * pieceWidth, 0);
ctx.lineTo(i * pieceWidth, canvas.height);
ctx.stroke();
}
}
// 绘制胜利提示
function drawVictory() {
// 半透明背景
ctx.fillStyle = 'rgba(0, 0, 0, 0.7)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// 胜利文字
ctx.fillStyle = '#fff';
ctx.font = '36px Arial';
ctx.textAlign = 'center';
ctx.fillText('恭喜完成拼图!', canvas.width / 2, canvas.height / 2 - 30);
// 重新开始提示
ctx.font = '20px Arial';
ctx.fillText('点击屏幕重新开始', canvas.width / 2, canvas.height / 2 + 30);
}
// 绘制整个游戏画面
function draw() {
// 清空画布
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 绘制网格线
drawGrid();
// 绘制所有拼图块
drawAllPieces();
// 如果完成拼图,显示胜利提示
if (completed) {
drawVictory();
}
}
代码说明:
drawPiece():绘制单个拼图块,根据状态添加不同样式(正确位置的绿色边框、拖拽中的虚线高亮)- 绘制顺序:先绘制已正确放置的拼图块(底层),再绘制可拖拽的拼图块(上层),避免遮挡
drawGrid():绘制浅色网格线,帮助玩家识别正确的拼图位置- 胜利提示:游戏完成时显示半透明背景和提示文字,引导玩家重新开始
六、步骤5:实现拖拽交互功能
编写代码处理鼠标事件,实现拼图块的拖拽功能:
// 鼠标按下事件(开始拖拽)
canvas.addEventListener('mousedown', startDrag);
function startDrag(e) {
if (completed) {
// 如果已完成拼图,点击则重新开始
resetGame();
return;
}
// 获取鼠标在Canvas中的坐标
const rect = canvas.getBoundingClientRect();
const mouseX = e.clientX - rect.left;
const mouseY = e.clientY - rect.top;
// 检查鼠标是否点击了未正确放置的拼图块
for (let i = pieces.length - 1; i >= 0; i--) {
const piece = pieces[i];
if (!piece.isCorrect &&
mouseX >= piece.x && mouseX <= piece.x + piece.width &&
mouseY >= piece.y && mouseY <= piece.y + piece.height) {
// 记录被拖拽的拼图块
draggedPiece = piece;
draggedPiece.isDragging = true;
// 计算鼠标与拼图块左上角的偏移(确保拖拽时鼠标位置不变)
offsetX = mouseX - piece.x;
offsetY = mouseY - piece.y;
// 将当前拼图块移到数组末尾(确保绘制在最上层)
pieces.push(pieces.splice(i, 1)[0]);
break;
}
}
draw();
}
// 鼠标移动事件(拖拽中)
canvas.addEventListener('mousemove', drag);
function drag(e) {
if (!draggedPiece) return;
// 获取鼠标在Canvas中的坐标
const rect = canvas.getBoundingClientRect();
const mouseX = e.clientX - rect.left;
const mouseY = e.clientY - rect.top;
// 更新拼图块位置(考虑偏移量)
draggedPiece.x = mouseX - offsetX;
draggedPiece.y = mouseY - offsetY;
draw();
}
// 鼠标释放事件(结束拖拽)
canvas.addEventListener('mouseup', endDrag);
canvas.addEventListener('mouseleave', endDrag); // 鼠标离开画布时结束拖拽
function endDrag() {
if (!draggedPiece) return;
// 检查是否靠近目标位置(吸附效果)
const tolerance = 20; // 吸附容差(像素)
if (Math.abs(draggedPiece.x - draggedPiece.targetX) < tolerance &&
Math.abs(draggedPiece.y - draggedPiece.targetY) < tolerance) {
// 吸附到正确位置
draggedPiece.x = draggedPiece.targetX;
draggedPiece.y = draggedPiece.targetY;
draggedPiece.isCorrect = true;
// 检查是否所有拼图块都已正确放置
checkCompletion();
}
// 重置拖拽状态
draggedPiece.isDragging = false;
draggedPiece = null;
draw();
}
代码说明:
- 拖拽流程:
startDrag:鼠标按下时,判断是否点击了可拖拽的拼图块,记录拖拽状态和偏移量drag:鼠标移动时,根据鼠标位置和偏移量更新拼图块位置endDrag:鼠标释放时,判断是否靠近目标位置,实现吸附效果
- 吸附功能:当拼图块靠近正确位置(容差20像素)时,自动移动到精确位置,提升游戏体验
- 层级管理:被拖拽的拼图块移到数组末尾,确保绘制在最上层,避免被其他块遮挡
- 边界处理:鼠标离开画布时结束拖拽,避免异常状态
七、步骤6:实现胜利条件检测与游戏重置
编写代码判断是否所有拼图块都已正确放置,并实现游戏重置功能:
// 检查是否完成拼图
function checkCompletion() {
// 检查所有拼图块是否都已正确放置
completed = pieces.every(piece => piece.isCorrect);
}
// 重置游戏
function resetGame() {
initPieces(); // 重新生成拼图块
completed = false; // 重置完成状态
draw(); // 重绘游戏
}
代码说明:
checkCompletion():使用every方法检查所有拼图块的isCorrect状态,全部为true则判定为完成resetGame():重新初始化拼图块(打乱位置)并重置状态,实现游戏重新开始功能
八、步骤7:启动游戏
最后,编写初始化代码启动游戏:
// 初始化并启动游戏
initPieces();
draw();
代码说明:
- 游戏启动流程:先初始化拼图块数据(
initPieces()),再绘制初始画面(draw()) - 初始画面:拼图块随机分布在画布上,等待玩家拖拽拼接
九、完整代码与运行效果
将以上所有代码整合后,保存并在浏览器中打开,你将获得一个功能完整的拼图游戏:
- 4x4网格的16个拼图块,每个块有独特颜色
- 鼠标可拖拽拼图块移动,靠近正确位置时自动吸附
- 已正确放置的拼图块显示绿色边框
- 所有块归位后显示胜利提示,点击可重新开始
- 网格线辅助玩家对齐拼图块
十、扩展功能建议(进阶练习)
如果想进一步提升游戏体验,可以尝试添加这些功能:
- 自定义图片:允许玩家上传自己的图片作为拼图素材(使用
drawImage切割图片) - 难度选择:提供多种网格尺寸(3x3、4x4、5x5)供玩家选择
- 计时功能:记录完成拼图的时间,挑战最快速度
- 预览功能:显示完整图片的缩略图作为参考
- 旋转功能:允许拼图块旋转(增加难度)
- 提示功能:高亮显示某个拼图块的正确位置
通过本教程,你已经掌握了拼图游戏的核心开发技术,包括图形分割、拖拽交互、碰撞检测和吸附逻辑。这些知识可以应用到其他拖拽类或拼图类游戏开发中,祝你编程愉快!

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



