Hexo 博客实战:光影拼图游戏开发指南

前言

五子棋之后,本文介绍光影拼图游戏的开发。相比其他游戏,拼图的核心难点在于图片处理与碎片管理:如何将一张图片切割成碎片,实现拖拽吸附,并保证在移动端和桌面端都有良好的交互体验。

拼图游戏是一个看似简单但技术实现复杂的益智游戏,实际开发中需要解决:

  1. 图片处理:如何动态加载、切割图片,并处理加载失败的情况
  2. 碎片管理:如何生成、定位、旋转和吸附碎片
  3. 响应式设计:如何在不同屏幕尺寸下保持游戏体验
  4. 移动端适配:如何统一处理触摸和鼠标事件
  5. 性能优化:如何管理图片资源和动画帧

本文重点讲解三个核心问题:

  1. 图片切割与碎片生成:Canvas 图片处理技术
  2. 拖拽吸附算法:碎片位置匹配与自动吸附
  3. 跨平台事件处理:统一鼠标与触摸交互

项目结构

1
2
3
4
5
6
7
8
9
10
11
source/ai-games/puzzle/
├── index.md # 游戏页面入口(HTML + CSS)
└── modules/
├── puzzle-config.js # 配置模块(难度、颜色、动画参数)
├── puzzle-core.js # 核心逻辑(碎片生成、吸附算法)
├── puzzle-render.js # 渲染模块(Canvas 绘制、动画)
├── puzzle-ai.js # AI 提示系统
└── puzzle-game.js # 游戏入口(整合模块、事件处理)

source/ai-games/gamejs/
└── game-manager.js # 游戏生命周期管理器(复用)

为什么拼图需要不同的架构?

拼图游戏与五子棋在技术实现上有本质区别:

游戏 核心算法 技术难点 资源管理
五子棋 Minimax 搜索 AI 博弈树 轻量
拼图 图片处理+吸附 图片切割+响应式 图片资源+动画帧

拼图的特殊性:

  • 图片资源:需要动态加载外部图片,处理跨域和加载失败
  • Canvas 渲染:需要实时绘制碎片、网格、动画效果
  • 响应式设计:碎片位置需要随屏幕尺寸动态调整
  • 事件处理:需要同时支持鼠标和触摸交互

模块化架构设计

游戏采用分层模块化设计,将图片处理、渲染、逻辑分离:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
┌─────────────────────────────────────────────────────────┐
│ HTML 页面 │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────┐ │
│ │ game-canvas │ │ 游戏信息区 │ │ 操作按钮区域 │ │
│ └─────────────┘ └─────────────┘ └─────────────────┘ │
└─────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────┐
│ PuzzleGame (游戏入口) │
│ 职责:整合所有模块、控制游戏流程、管理状态 │
│ ├─ PuzzleCore → 碎片生成、吸附算法、状态管理 │
│ ├─ PuzzleRender → Canvas 渲染、动画效果 │
│ └─ PuzzleAI → 提示系统、AI 辅助 │
└─────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────┐
│ GameManager (生命周期管理) │
│ 职责:统一管理游戏注册、初始化、清理 │
│ ├─ register() → 注册游戏到管理器 │
│ ├─ initGame() → 初始化指定游戏 │
│ └─ cleanupGame() → 清理指定游戏资源 │
└─────────────────────────────────────────────────────────┘

各模块职责详解

PuzzleConfig(配置中心)

  • 集中管理所有常量配置
  • 难度参数(网格大小、提示数量、时间限制)
  • 颜色方案、动画参数
  • 图片 API 配置

PuzzleCore(核心逻辑)

  • 图片加载与切割
  • 碎片生成与位置计算
  • 吸附算法实现
  • 游戏状态管理

PuzzleRender(渲染引擎)

  • Canvas 绘制碎片、网格、背景
  • 动画效果(旋转、高亮、粒子)
  • 响应式缩放适配
  • 性能优化(离屏渲染)

PuzzleAI(智能提示)

  • 距离评估算法
  • 提示冷却机制
  • 视觉反馈效果

PuzzleGame(流程控制)

  • 事件监听与处理
  • 游戏状态流转
  • 模块间通信协调

PJAX 兼容性处理

拼图游戏复用五子棋的 GameManager 方案,但需要处理额外的资源清理:

拼图特有的清理需求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
function cleanupPuzzleGame() {
// 1. 停止动画渲染(必须!否则内存泄漏)
if (window.PuzzleRender) {
window.PuzzleRender.stopAnimation();
}

// 2. 清除选中状态
if (window.PuzzleCore) {
window.PuzzleCore.clearSelection();
}

// 3. 重置游戏状态
if (gameState) {
gameState.isPlaying = false;
gameState.isComplete = false;
gameState.isFailed = false;
gameState.currentPiece = null;
gameState.selectedPiece = null;
gameState.moveHistory = [];
if (gameState.timerInterval) {
clearInterval(gameState.timerInterval);
gameState.timerInterval = null;
}
}

// 4. 清除 window 定时器
if (window._puzzleTimer) {
clearInterval(window._puzzleTimer);
window._puzzleTimer = null;
}

// 5. 移除事件监听
const canvas = document.getElementById('game-canvas');
if (canvas) {
canvas.removeEventListener('mousedown', window._puzzleMouseDown);
canvas.removeEventListener('mousemove', window._puzzleMouseMove);
canvas.removeEventListener('mouseup', window._puzzleMouseUp);
canvas.removeEventListener('mouseleave', window._puzzleMouseUp);
canvas.removeEventListener('dblclick', window._puzzleClick);
canvas.removeEventListener('touchstart', window._puzzleTouchStart);
canvas.removeEventListener('touchmove', window._puzzleTouchMove);
canvas.removeEventListener('touchend', window._puzzleTouchEnd);
}

// 6. 清除 window 变量引用
window._puzzleMouseDown = null;
window._puzzleMouseMove = null;
window._puzzleMouseUp = null;
window._puzzleClick = null;
window._puzzleTouchStart = null;
window._puzzleTouchMove = null;
window._puzzleTouchEnd = null;

// 7. 重新获取canvas并清除内容
const currentCanvas = document.getElementById('game-canvas');
if (currentCanvas) {
const ctx = currentCanvas.getContext('2d');
if (ctx) {
ctx.clearRect(0, 0, currentCanvas.width, currentCanvas.height);
}
}

console.log('光影拼图游戏已清理');
}

关键清理点

  1. 动画帧:必须调用 cancelAnimationFrame,否则持续运行消耗 CPU
  2. 图片资源:虽然浏览器会自动回收,但显式设置为 null 有助于 GC
  3. 事件监听:必须移除,否则 PJAX 切换后旧监听器仍在
  4. 定时器:游戏计时器必须清除

图片资源的 PJAX 处理

拼图游戏使用外部图片 API,需要特殊处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
async function startGame() {
showLoading();

// 清除之前的选中状态
if (window.PuzzleCore) {
window.PuzzleCore.clearSelection();
}
gameState.currentPiece = null;
gameState.selectedPiece = null;

gameState.isPlaying = true;
gameState.isComplete = false;
gameState.isFailed = false;
gameState.level = 1;
gameState.imageSeed = Date.now();
gameState.moveHistory = [];

const size = getCanvasSize();

// 先尝试异步加载,15秒超时后使用同步模式
const loadPromise = window.PuzzleCore.init(1, size.width, size.height, gameState.imageSeed);

let initData = null;
try {
initData = await Promise.race([
loadPromise,
new Promise((resolve) => setTimeout(() => resolve(null), 15000))
]);
} catch (e) {
console.warn('图片加载失败');
}

// 如果超时或失败,使用同步模式(纯色碎片)
if (!initData || !initData.pieces) {
console.log('使用纯色模式');
initData = window.PuzzleCore.initSync(1, size.width, size.height);
}

updateUI(initData);
hideLoading();
startTimer();
startRender();
}

超时处理策略

  1. 15秒超时:防止图片加载卡死整个游戏
  2. 降级方案:加载失败时使用纯色碎片
  3. 种子机制:使用时间戳作为随机种子,确保每次游戏图片不同

核心算法实现

图片处理与碎片生成

拼图游戏的核心是将一张完整图片切割成多个碎片:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
generatePuzzle(config, canvasWidth, canvasHeight) {
this.pieces = [];
this.targetGrid = [];

const gridSize = config.grid;
const cellSize = Math.min(canvasWidth, canvasHeight) / (gridSize + 2);

this.cellSize = cellSize;

const gridWidth = gridSize * cellSize;
const gridHeight = gridSize * cellSize;
const gridOffsetX = (canvasWidth - gridWidth) / 2;
const gridOffsetY = (canvasHeight - gridHeight) / 2;

this.gridOffsetX = gridOffsetX;
this.gridOffsetY = gridOffsetY;

// 生成目标网格位置
for (let row = 0; row < gridSize; row++) {
for (let col = 0; col < gridSize; col++) {
const targetX = gridOffsetX + col * cellSize;
const targetY = gridOffsetY + row * cellSize;

this.targetGrid.push({
row: row,
col: col,
x: targetX,
y: targetY,
occupied: false
});
}
}

// 生成碎片
const pieceCount = gridSize * gridSize;
const isGeometric = Math.random() > 0.5;
const colors = window.PuzzleConfig.getColorPalette(isGeometric);

for (let i = 0; i < pieceCount; i++) {
const targetPos = this.targetGrid[i];
const row = Math.floor(i / gridSize);
const col = i % gridSize;

// 计算碎片在原始图片中的位置
const srcX = col * (this.puzzleImage.width / gridSize);
const srcY = row * (this.puzzleImage.height / gridSize);
const srcWidth = this.puzzleImage.width / gridSize;
const srcHeight = this.puzzleImage.height / gridSize;

// 随机初始位置(避开网格区域)
let initX, initY;
const side = Math.random() > 0.5 ? 'left' : 'right';

if (side === 'left') {
initX = Math.random() * (gridOffsetX - cellSize - 20) + 10;
} else {
initX = gridOffsetX + gridWidth + Math.random() * (canvasWidth - gridOffsetX - gridWidth - cellSize - 10) + 10;
}

initY = Math.random() * (canvasHeight - cellSize - 20) + 10;

// 随机旋转角度(如果启用旋转)
const rotation = this.enableRotation ? Math.floor(Math.random() * 4) * 90 : 0;
const targetRotation = 0;

const piece = {
id: i,
width: cellSize,
height: cellSize,
currentX: initX,
currentY: initY,
displayX: initX,
displayY: initY,
targetX: targetPos.x,
targetY: targetPos.y,
row: row,
col: col,
rotation: rotation,
targetRotation: targetRotation,
isLocked: false,
isSelected: false,
color: colors[i % colors.length],
srcX: srcX,
srcY: srcY,
srcWidth: srcWidth,
srcHeight: srcHeight,
shape: isGeometric ? (Math.random() > 0.5 ? 'circle' : 'square') : 'square'
};

this.pieces.push(piece);
}

// 打乱碎片顺序(Fisher-Yates 洗牌算法)
for (let i = this.pieces.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[this.pieces[i], this.pieces[j]] = [this.pieces[j], this.pieces[i]];
}
}

关键算法

  1. 网格计算:根据难度计算网格大小和位置
  2. 图片切割:使用 Canvas 的 drawImage 参数切割图片
  3. 随机分布:碎片初始位置避开目标网格区域
  4. 洗牌算法:确保碎片顺序随机

碎片吸附算法

吸附算法是拼图游戏的核心交互逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
checkSnap(pieceId) {
const piece = this.pieces.find(p => p.id === pieceId);
if (!piece || piece.isLocked) return { snapped: false };

// 计算与所有目标位置的距离
let minDistance = Infinity;
let targetIndex = -1;

for (let i = 0; i < this.targetGrid.length; i++) {
const target = this.targetGrid[i];
if (target.occupied) continue;

const dx = piece.currentX - target.x;
const dy = piece.currentY - target.y;
const distance = Math.sqrt(dx * dx + dy * dy);

if (distance < minDistance) {
minDistance = distance;
targetIndex = i;
}
}

// 检查是否在吸附范围内
const snapDistance = window.PuzzleConfig.ANIMATION.snapDistance;
const rotationMatch = !this.enableRotation ||
Math.abs(piece.rotation - piece.targetRotation) < 45;

if (minDistance < snapDistance && rotationMatch && targetIndex >= 0) {
const target = this.targetGrid[targetIndex];

// 验证行列匹配(可选,增强游戏性)
const rowMatch = piece.row === target.row;
const colMatch = piece.col === target.col;

if (rowMatch && colMatch) {
// 精确匹配,锁定碎片
piece.currentX = target.x;
piece.currentY = target.y;
piece.rotation = piece.targetRotation;
piece.isLocked = true;
target.occupied = true;
this.completedPieces++;

// 检查是否完成
const isComplete = this.completedPieces === this.pieces.length;

return {
snapped: true,
isComplete: isComplete,
targetIndex: targetIndex
};
} else if (minDistance < snapDistance * 0.5) {
// 距离很近但不匹配行列,仍然吸附但给提示
piece.currentX = target.x;
piece.currentY = target.y;

return {
snapped: true,
isComplete: false,
targetIndex: targetIndex,
warning: '位置正确但行列不匹配'
};
}
}

return { snapped: false };
}

吸附逻辑

  1. 距离计算:使用欧几里得距离公式
  2. 旋转匹配:检查碎片旋转角度是否接近目标
  3. 行列验证:增强游戏难度,要求行列匹配
  4. 分级吸附:完全匹配锁定,部分匹配只吸附不锁定

响应式设计实现

拼图游戏需要在不同设备上保持体验:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
resize() {
if (!this.canvas) return;

const container = this.canvas.parentElement;
const oldWidth = this.canvas.width || 500;
const oldHeight = this.canvas.height || 400;
const newWidth = Math.min(600, container.clientWidth - 40);
const newHeight = Math.min(500, window.innerHeight * 0.6);

if (oldWidth > 0 && oldHeight > 0 && window.PuzzleCore && window.PuzzleCore.pieces) {
const scaleX = newWidth / oldWidth;
const scaleY = newHeight / oldHeight;

window.PuzzleCore.pieces.forEach(p => {
if (!p.isLocked) {
p.currentX *= scaleX;
p.currentY *= scaleY;
p.currentX = Math.max(0, Math.min(newWidth - p.width, p.currentX));
p.currentY = Math.max(0, Math.min(newHeight - p.height, p.currentY));
}
p.targetX *= scaleX;
p.targetY *= scaleY;
});

if (window.PuzzleCore.gridOffsetX) {
window.PuzzleCore.gridOffsetX *= scaleX;
window.PuzzleCore.gridOffsetY *= scaleY;
}

window.PuzzleCore.canvasWidth = newWidth;
window.PuzzleCore.canvasHeight = newHeight;
}

this.canvas.width = newWidth;
this.canvas.height = newHeight;
}

响应式策略

  1. 比例缩放:所有位置按比例调整
  2. 边界检查:确保碎片不超出画布
  3. 网格重算:目标位置同步缩放
  4. 性能考虑:只在必要时重算

AI 提示系统

拼图游戏的 AI 不是博弈对手,而是辅助提示系统:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
getBestHint(core) {
if (!this.canUseHint(core)) return null;

const unlockedPieces = core.pieces.filter(p => !p.isLocked);
if (unlockedPieces.length === 0) return null;

let bestPiece = null;
let minScore = Infinity;

unlockedPieces.forEach(piece => {
const distance = Math.sqrt(
Math.pow(piece.currentX - piece.targetX, 2) +
Math.pow(piece.currentY - piece.targetY, 2)
);

let rotationPenalty = 0;
if (core.enableRotation && piece.rotation !== piece.targetRotation) {
rotationPenalty = 50;
}

const score = distance + rotationPenalty;

if (score < minScore) {
minScore = score;
bestPiece = piece;
}
});

return bestPiece;
}

提示算法

  1. 距离评估:计算碎片当前位置与目标位置的距离
  2. 旋转惩罚:如果旋转不正确,增加难度分数
  3. 冷却机制:防止玩家滥用提示
  4. 视觉反馈:高亮显示目标位置

移动端优化

拼图游戏需要优秀的移动端体验:

统一事件处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
function getCanvasCoords(e) {
const rect = canvas.getBoundingClientRect();
if (e.touches && e.touches.length > 0) {
return {
x: e.touches[0].clientX - rect.left,
y: e.touches[0].clientY - rect.top
};
}
return {
x: e.clientX - rect.left,
y: e.clientY - rect.top
};
}

function handleCanvasMouseDown(e) {
if (!gameState.isPlaying || gameState.isComplete) return;

const coords = getCanvasCoords(e);
const piece = window.PuzzleCore.getPieceAt(coords.x, coords.y);

if (piece && !piece.isLocked) {
// 只选中,不开始拖拽
gameState.selectedPiece = piece;
gameState.dragStartX = coords.x;
gameState.dragStartY = coords.y;
window.PuzzleCore.selectPiece(piece.id);

// 移动端振动反馈
if (navigator.vibrate && e.type === 'touchstart') {
navigator.vibrate(10);
}
} else if (piece && piece.isLocked) {
window.PuzzleCore.clearSelection();
gameState.selectedPiece = null;
}
}

拖拽阈值处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
function handleCanvasMouseMove(e) {
const pieceToMove = gameState.currentPiece || gameState.selectedPiece;
if (!pieceToMove || gameState.isComplete) return;

const coords = getCanvasCoords(e);

// 如果还没有开始拖拽,检查是否移动超过阈值
if (!gameState.currentPiece && gameState.selectedPiece) {
const dx = coords.x - gameState.dragStartX;
const dy = coords.y - gameState.dragStartY;
const distance = Math.sqrt(dx * dx + dy * dy);

// 只有移动超过15px才开始拖拽(给双击留出空间)
if (distance > 15) {
// 开始拖拽
gameState.currentPiece = gameState.selectedPiece;
gameState.dragOffset = {
x: coords.x - gameState.selectedPiece.currentX,
y: coords.y - gameState.selectedPiece.currentY
};
canvas.style.cursor = 'grabbing';
}
}

// 如果已经开始拖拽
if (gameState.currentPiece) {
if (e.cancelable && e.type === 'touchmove') {
e.preventDefault(); // 防止页面滚动
}

window.PuzzleCore.movePiece(
gameState.currentPiece.id,
coords.x - gameState.dragOffset.x,
coords.y - gameState.dragOffset.y
);
}
}

移动端优化点

  1. 统一坐标:鼠标和触摸事件使用相同坐标计算
  2. 拖拽阈值:防止误触,需要移动一定距离才开始拖拽
  3. 振动反馈:增强触觉体验
  4. 滚动阻止:拖拽时阻止页面滚动

性能优化

图片加载优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
init(level, canvasWidth = 500, canvasHeight = 400, imageSeed = null) {
this.currentLevel = level;
this.canvasWidth = canvasWidth;
this.canvasHeight = canvasHeight;

const config = window.PuzzleConfig.getDifficulty(level);
this.hintsRemaining = config.hints;
this.enableRotation = config.rotation;
this.gridSize = config.grid;

const seed = imageSeed || Date.now();
const imageUrl = `${window.PuzzleConfig.IMAGE_BASE_URL}/${canvasWidth}/${canvasHeight}?random=${seed}`;
this.puzzleImage = new Image();
this.puzzleImage.crossOrigin = 'anonymous';

return new Promise((resolve) => {
this.puzzleImage.onload = () => {
this.imageLoaded = true;
this.generatePuzzle(config, canvasWidth, canvasHeight);
this.completedPieces = 0;

resolve({
pieces: this.pieces,
gridSize: this.gridSize,
hints: this.hintsRemaining,
level: level,
cellSize: this.cellSize
});
};

this.puzzleImage.onerror = () => {
this.imageLoaded = false;
console.warn('图片加载失败,使用纯色模式');
this.generatePuzzle(config, canvasWidth, canvasHeight);
this.completedPieces = 0;

resolve({
pieces: this.pieces,
gridSize: this.gridSize,
hints: this.hintsRemaining,
level: level,
cellSize: this.cellSize
});
};

this.puzzleImage.src = imageUrl;
});
}

动画帧管理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
startAnimation(updateFunc) {
this.stopAnimation();

let lastTime = 0;
const animate = (currentTime) => {
const delta = currentTime - lastTime || 0;
lastTime = currentTime;

if (updateFunc) {
updateFunc(delta);
}

this.animationFrame = requestAnimationFrame(animate);
};

this.animationFrame = requestAnimationFrame(animate);
}

stopAnimation() {
if (this.animationFrame) {
cancelAnimationFrame(this.animationFrame);
this.animationFrame = null;
}
}

性能优化策略

  1. 图片懒加载:使用 Promise 异步加载
  2. 降级方案:图片失败时使用纯色模式
  3. 动画帧管理:正确启动和停止动画循环
  4. 内存回收:清理不再需要的资源引用

难度配置系统

拼图游戏采用渐进式难度设计:

1
2
3
4
5
6
7
8
9
10
11
12
DIFFICULTY: {
1: { grid: 2, size: 4, hints: 6, rotation: true, time: 60 },
2: { grid: 2, size: 4, hints: 6, rotation: true, time: 60 },
3: { grid: 3, size: 9, hints: 5, rotation: true, time: 90 },
4: { grid: 3, size: 9, hints: 5, rotation: true, time: 90 },
5: { grid: 3, size: 9, hints: 4, rotation: true, time: 90 },
6: { grid: 4, size: 16, hints: 5, rotation: true, time: 120 },
7: { grid: 4, size: 16, hints: 4, rotation: true, time: 120 },
8: { grid: 4, size: 16, hints: 3, rotation: true, time: 120 },
9: { grid: 5, size: 25, hints: 4, rotation: true, time: 150 },
10: { grid: 5, size: 25, hints: 4, rotation: true, time: 150 }
}

难度参数

  • grid:网格大小(2×2 到 5×5)
  • size:碎片总数
  • hints:提示次数
  • rotation:是否启用旋转
  • time:时间限制(秒)

游戏流程图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
┌─────────────────────────────────────────────────────────────────┐
│ 用户访问页面 │
└─────────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────┐
│ GameManager 检测游戏 │
│ document.contains(canvas) → 检测到 puzzle 页面 │
└─────────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────┐
│ 加载所有模块脚本 │
│ puzzle-config.js → puzzle-core.js → puzzle-render.js → ... │
│ register('puzzle', { init, cleanup }) │
└─────────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────┐
│ initPuzzleGame() │
│ new PuzzleCore() → 加载图片 → 生成碎片 │
│ → new PuzzleRender() → 启动动画 │
│ → 绑定事件监听 │
└─────────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────┐
│ 游戏主循环 │
│ 1. 用户拖拽碎片 → checkSnap() → 吸附判断 │
│ 2. 碎片锁定 → 更新完成计数 → 检查通关 │
│ 3. 时间倒计时 → 超时判断 → 游戏结束 │
│ 4. 请求提示 → AI 计算 → 高亮显示 │
└─────────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────┐
│ 通关判断 │
│ 所有碎片锁定 → 显示通关界面 → 下一关按钮 │
│ 时间耗尽 → 显示失败界面 → 重新开始按钮 │
└─────────────────────────────────────────────────────────────────┘

总结

光影拼图游戏的开发涉及多个技术领域:

  1. Canvas 图形处理:图片切割、碎片绘制、动画渲染
  2. 交互算法:拖拽吸附、距离计算、旋转匹配
  3. 响应式设计:跨设备适配、动态缩放、触摸优化
  4. 资源管理:图片加载、内存回收、性能优化
  5. 游戏设计:难度曲线、提示系统、反馈机制

相比五子棋的 AI 博弈,拼图游戏更注重图形处理和用户体验。通过模块化设计和良好的架构,我们实现了:

  • 可维护的代码结构
  • 优秀的跨平台体验
  • 稳定的性能表现
  • 良好的可扩展性

拼图游戏展示了如何在静态博客中集成复杂的图形应用,为后续更多类型的游戏开发提供了技术基础。


相关链接: