Flutter实战:打造经典连连看游戏
前言
连连看是一款经典的益智消除游戏,考验玩家的观察力和反应速度。本文将带你从零开始,使用Flutter开发一个功能完整的连连看游戏,包含路径查找、计时挑战、提示和重排等功能。
应用特色
- 🎮 经典玩法:完整还原经典连连看规则
- 🔗 路径算法:直线、一转角、两转角连接
- ⏱️ 计时挑战:限时完成,增加紧张感
- 💡 提示功能:3次提示机会
- 🔄 重排功能:2次重排机会
- 🎯 三种难度:简单、中等、困难
- 📊 得分系统:基础分+时间奖励+路径奖励
- 🏆 最高分:本地保存最高分记录
- ⏸️ 暂停功能:随时暂停/继续游戏
- 🎨 18种图案:丰富的图标和颜色
效果展示



连连看
游戏规则
选择两个相同图案
路径不超过2个转角
路径上无障碍物
消除所有方块
连接类型
直线连接 0转角
一转角连接 1转角
两转角连接 2转角
难度设置
简单 6×8 180秒
中等 8×10 240秒
困难 10×12 300秒
得分规则
基础分 100
时间奖励 剩余秒数
路径奖励 转角越少越高
数据模型设计
1. 方块模型
dart
class Tile {
int row;
int col;
int type; // 图案类型
bool isMatched = false;
Tile({
required this.row,
required this.col,
required this.type,
});
}
2. 路径点
dart
class PathPoint {
int row;
int col;
PathPoint(this.row, this.col);
}
3. 难度等级
dart
enum Difficulty {
easy(6, 8, 180, '简单'),
medium(8, 10, 240, '中等'),
hard(10, 12, 300, '困难');
final int rows;
final int cols;
final int timeLimit;
final String label;
const Difficulty(this.rows, this.cols, this.timeLimit, this.label);
}
4. 游戏状态
dart
enum GameState {
ready, // 准备中
playing, // 游戏中
paused, // 暂停
won, // 胜利
lost, // 失败
}
核心算法实现
1. 路径查找算法
连连看的核心是路径查找,需要判断两个方块是否可以连接:
dart
List<PathPoint>? _findPath(Tile start, Tile end) {
// 直线连接(0个转角)
if (_canConnectStraight(start, end)) {
return [
PathPoint(start.row, start.col),
PathPoint(end.row, end.col),
];
}
// 一个转角
final path1 = _canConnectOneCorner(start, end);
if (path1 != null) return path1;
// 两个转角
final path2 = _canConnectTwoCorners(start, end);
if (path2 != null) return path2;
return null;
}
2. 直线连接检测
dart
bool _canConnectStraight(Tile start, Tile end) {
if (start.row == end.row) {
// 同一行
final minCol = min(start.col, end.col);
final maxCol = max(start.col, end.col);
for (int col = minCol + 1; col < maxCol; col++) {
if (_board[start.row][col] != null &&
!_board[start.row][col]!.isMatched) {
return false;
}
}
return true;
} else if (start.col == end.col) {
// 同一列
final minRow = min(start.row, end.row);
final maxRow = max(start.row, end.row);
for (int row = minRow + 1; row < maxRow; row++) {
if (_board[row][start.col] != null &&
!_board[row][start.col]!.isMatched) {
return false;
}
}
return true;
}
return false;
}
算法说明:
- 检查两个方块是否在同一行或同一列
- 遍历中间的所有格子,确保没有障碍物
3. 一转角连接检测
dart
List<PathPoint>? _canConnectOneCorner(Tile start, Tile end) {
// 尝试转角点1: (start.row, end.col)
final corner1Row = start.row;
final corner1Col = end.col;
if (_isEmptyOrMatched(corner1Row, corner1Col)) {
final corner1 = Tile(row: corner1Row, col: corner1Col, type: 0);
if (_canConnectStraight(start, corner1) &&
_canConnectStraight(corner1, end)) {
return [
PathPoint(start.row, start.col),
PathPoint(corner1Row, corner1Col),
PathPoint(end.row, end.col),
];
}
}
// 尝试转角点2: (end.row, start.col)
final corner2Row = end.row;
final corner2Col = start.col;
if (_isEmptyOrMatched(corner2Row, corner2Col)) {
final corner2 = Tile(row: corner2Row, col: corner2Col, type: 0);
if (_canConnectStraight(start, corner2) &&
_canConnectStraight(corner2, end)) {
return [
PathPoint(start.row, start.col),
PathPoint(corner2Row, corner2Col),
PathPoint(end.row, end.col),
];
}
}
return null;
}
算法图解:
一转角连接示例:
转角点1: 转角点2:
A ----→ C A
↓ ↓
B C ----→ B
A: 起点 (start.row, start.col)
B: 终点 (end.row, end.col)
C: 转角点
4. 两转角连接检测
dart
List<PathPoint>? _canConnectTwoCorners(Tile start, Tile end) {
// 尝试水平延伸
for (int col = 0; col < _difficulty.cols; col++) {
if (col == start.col || col == end.col) continue;
if (_isEmptyOrMatched(start.row, col) &&
_isEmptyOrMatched(end.row, col)) {
final corner1 = Tile(row: start.row, col: col, type: 0);
final corner2 = Tile(row: end.row, col: col, type: 0);
if (_canConnectStraight(start, corner1) &&
_canConnectStraight(corner1, corner2) &&
_canConnectStraight(corner2, end)) {
return [
PathPoint(start.row, start.col),
PathPoint(start.row, col),
PathPoint(end.row, col),
PathPoint(end.row, end.col),
];
}
}
}
// 尝试垂直延伸
for (int row = 0; row < _difficulty.rows; row++) {
if (row == start.row || row == end.row) continue;
if (_isEmptyOrMatched(row, start.col) &&
_isEmptyOrMatched(row, end.col)) {
final corner1 = Tile(row: row, col: start.col, type: 0);
final corner2 = Tile(row: row, col: end.col, type: 0);
if (_canConnectStraight(start, corner1) &&
_canConnectStraight(corner1, corner2) &&
_canConnectStraight(corner2, end)) {
return [
PathPoint(start.row, start.col),
PathPoint(row, start.col),
PathPoint(row, end.col),
PathPoint(end.row, end.col),
];
}
}
}
return null;
}
算法图解:
两转角连接示例:
水平延伸: 垂直延伸:
A ----→ C1 A
↓ ↓
C2 ----→ B C1 ----→ C2
↓
B
5. 转角数计算
dart
int _getTurns(List<PathPoint> path) {
if (path.length <= 2) return 0;
int turns = 0;
for (int i = 1; i < path.length - 1; i++) {
final prev = path[i - 1];
final curr = path[i];
final next = path[i + 1];
final dir1 = _getDirection(prev, curr);
final dir2 = _getDirection(curr, next);
if (dir1 != dir2) {
turns++;
}
}
return turns;
}
String _getDirection(PathPoint from, PathPoint to) {
if (from.row == to.row) return 'horizontal';
if (from.col == to.col) return 'vertical';
return 'none';
}
得分系统
得分公式
dart
final baseScore = 100; // 基础分
final timeBonus = _timeRemaining; // 时间奖励
final pathBonus = (4 - _getTurns(path)) * 50; // 路径奖励
_score += baseScore + timeBonus + pathBonus;
得分详解
| 项目 | 计算方式 | 说明 |
|---|---|---|
| 基础分 | 100 | 每次消除固定得分 |
| 时间奖励 | 剩余秒数 | 剩余时间越多奖励越高 |
| 路径奖励 | (4 - 转角数) × 50 | 转角越少奖励越高 |
路径奖励示例:
- 直线连接(0转角):200分
- 一转角连接(1转角):150分
- 两转角连接(2转角):100分
游戏功能实现
1. 提示功能
dart
void _useHint() {
if (_hintsRemaining <= 0) return;
// 查找可以连接的一对方块
for (int i = 0; i < _difficulty.rows; i++) {
for (int j = 0; j < _difficulty.cols; j++) {
final tile1 = _board[i][j];
if (tile1 == null || tile1.isMatched) continue;
for (int m = 0; m < _difficulty.rows; m++) {
for (int n = 0; n < _difficulty.cols; n++) {
if (i == m && j == n) continue;
final tile2 = _board[m][n];
if (tile2 == null || tile2.isMatched) continue;
if (tile1.type == tile2.type) {
final path = _findPath(tile1, tile2);
if (path != null) {
setState(() {
_selectedTile = tile1;
_currentPath = path;
_hintsRemaining--;
});
// 3秒后清除提示
Future.delayed(const Duration(seconds: 3), () {
if (_selectedTile == tile1) {
setState(() {
_selectedTile = null;
_currentPath = [];
});
}
});
return;
}
}
}
}
}
}
}
2. 重排功能
dart
void _shuffle() {
if (_shufflesRemaining <= 0) return;
setState(() {
// 收集所有未匹配的方块类型
List<int> types = [];
for (var row in _board) {
for (var tile in row) {
if (tile != null && !tile.isMatched) {
types.add(tile.type);
}
}
}
// 打乱
types.shuffle();
// 重新分配
int index = 0;
for (var row in _board) {
for (var tile in row) {
if (tile != null && !tile.isMatched) {
tile.type = types[index++];
}
}
}
_shufflesRemaining--;
_selectedTile = null;
_currentPath = [];
});
}
3. 棋盘初始化
dart
void _initBoard() {
final rows = _difficulty.rows;
final cols = _difficulty.cols;
final totalTiles = rows * cols;
final pairCount = totalTiles ~/ 2;
// 生成配对的图案
List<int> types = [];
for (int i = 0; i < pairCount; i++) {
final type = i % _icons.length;
types.add(type);
types.add(type); // 每种图案两个
}
// 打乱
types.shuffle();
// 创建棋盘
_board = List.generate(
rows,
(i) => List.generate(
cols,
(j) => Tile(
row: i,
col: j,
type: types[i * cols + j],
),
),
);
}
UI组件设计
1. 方块渲染
dart
Widget _buildTile(int row, int col) {
final tile = _board[row][col];
if (tile == null) return const SizedBox.shrink();
final isSelected = _selectedTile == tile;
final isInPath = _currentPath.any((p) => p.row == row && p.col == col);
return GestureDetector(
onTap: () => _onTileTap(tile),
child: AnimatedOpacity(
opacity: tile.isMatched ? 0.0 : 1.0,
duration: const Duration(milliseconds: 300),
child: Container(
width: 60,
height: 60,
margin: const EdgeInsets.all(2),
decoration: BoxDecoration(
color: isSelected || isInPath
? Colors.yellow.withOpacity(0.5)
: _colors[tile.type % _colors.length].withOpacity(0.2),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: isSelected
? Colors.yellow
: _colors[tile.type % _colors.length],
width: isSelected ? 3 : 2,
),
boxShadow: isSelected
? [
BoxShadow(
color: Colors.yellow.withOpacity(0.5),
blurRadius: 10,
spreadRadius: 2,
),
]
: null,
),
child: Icon(
_icons[tile.type % _icons.length],
color: _colors[tile.type % _colors.length],
size: 32,
),
),
),
);
}
2. 路径绘制
使用CustomPainter绘制连接路径:
dart
class PathPainter extends CustomPainter {
final List<PathPoint> path;
PathPainter(this.path);
@override
void paint(Canvas canvas, Size size) {
if (path.length < 2) return;
final paint = Paint()
..color = Colors.yellow
..strokeWidth = 4
..style = PaintingStyle.stroke;
for (int i = 0; i < path.length - 1; i++) {
final start = path[i];
final end = path[i + 1];
final startOffset = Offset(
start.col * 64 + 32,
start.row * 64 + 32,
);
final endOffset = Offset(
end.col * 64 + 32,
end.row * 64 + 32,
);
canvas.drawLine(startOffset, endOffset, paint);
}
}
@override
bool shouldRepaint(PathPainter oldDelegate) {
return oldDelegate.path != path;
}
}
3. 状态栏
dart
Widget _buildStatusBar() {
return Container(
padding: const EdgeInsets.all(16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_buildStatusItem(
Icons.timer,
'$_timeRemaining秒',
_timeRemaining <= 30 ? Colors.red : Colors.blue,
),
_buildStatusItem(
Icons.star,
'$_score',
Colors.amber,
),
_buildStatusItem(
Icons.emoji_events,
'$_bestScore',
Colors.green,
),
],
),
);
}
技术要点详解
1. AnimatedOpacity动画
消除方块时的淡出效果:
dart
AnimatedOpacity(
opacity: tile.isMatched ? 0.0 : 1.0,
duration: const Duration(milliseconds: 300),
child: // 方块内容
)
2. CustomPainter绘制
自定义绘制连接路径:
dart
CustomPaint(
size: Size(
_difficulty.cols * 64.0,
_difficulty.rows * 64.0,
),
painter: PathPainter(_currentPath),
)
3. 双向滚动
支持横向和纵向滚动:
dart
SingleChildScrollView(
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: _buildBoard(),
),
)
4. Future.delayed延迟执行
延迟清除路径和选择:
dart
Future.delayed(const Duration(milliseconds: 300), () {
setState(() {
_selectedTile = null;
_currentPath = [];
});
});
游戏技巧
基本技巧
- 优先消除边缘:边缘方块更容易连接
- 记住位置:记住相同图案的位置
- 规划路径:选择转角少的路径得分更高
- 合理使用道具:在关键时刻使用提示和重排
高级技巧
- 预判死局:提前判断是否需要重排
- 时间管理:前期慢慢找,后期快速消除
- 路径优化:优先选择直线和一转角路径
- 道具策略:保留一次重排应对死局
功能扩展建议
1. 关卡模式
dart
class Level {
int number;
Difficulty difficulty;
int timeLimit;
int targetScore;
Level({
required this.number,
required this.difficulty,
required this.timeLimit,
required this.targetScore,
});
}
List<Level> levels = [
Level(number: 1, difficulty: Difficulty.easy, timeLimit: 180, targetScore: 5000),
Level(number: 2, difficulty: Difficulty.easy, timeLimit: 150, targetScore: 6000),
Level(number: 3, difficulty: Difficulty.medium, timeLimit: 240, targetScore: 8000),
];
2. 连击系统
dart
int _combo = 0;
DateTime? _lastMatchTime;
void _onMatch() {
final now = DateTime.now();
if (_lastMatchTime != null &&
now.difference(_lastMatchTime!).inSeconds < 3) {
_combo++;
_score += _combo * 50; // 连击奖励
} else {
_combo = 1;
}
_lastMatchTime = now;
}
3. 特殊道具
dart
enum SpecialPowerUp {
freeze, // 冻结时间
bomb, // 炸弹消除
rainbow, // 万能匹配
}
void _useBomb(int row, int col) {
// 消除周围3×3区域
for (int i = -1; i <= 1; i++) {
for (int j = -1; j <= 1; j++) {
final r = row + i;
final c = col + j;
if (r >= 0 && r < _difficulty.rows &&
c >= 0 && c < _difficulty.cols) {
_board[r][c]?.isMatched = true;
}
}
}
}
4. 成就系统
dart
class Achievement {
String id;
String name;
String description;
bool unlocked;
Achievement({
required this.id,
required this.name,
required this.description,
this.unlocked = false,
});
}
List<Achievement> achievements = [
Achievement(
id: 'speed_master',
name: '速度大师',
description: '在60秒内完成一局',
),
Achievement(
id: 'perfect_path',
name: '完美路径',
description: '连续10次使用直线连接',
),
];
5. 多人对战
dart
class MultiplayerGame {
Player player1;
Player player2;
List<List<Tile?>> sharedBoard;
void onPlayerMatch(Player player, Tile tile1, Tile tile2) {
// 玩家消除方块
player.score += 100;
// 检查胜负
if (_isAllMatched()) {
_declareWinner();
}
}
}
6. 音效
dart
import 'package:audioplayers/audioplayers.dart';
class AudioManager {
final AudioPlayer _player = AudioPlayer();
Future<void> playMatch() async {
await _player.play(AssetSource('audio/match.mp3'));
}
Future<void> playWin() async {
await _player.play(AssetSource('audio/win.mp3'));
}
Future<void> playTick() async {
await _player.play(AssetSource('audio/tick.mp3'));
}
}
算法优化
1. 路径缓存
dart
Map<String, List<PathPoint>?> _pathCache = {};
List<PathPoint>? _findPathCached(Tile start, Tile end) {
final key = '${start.row},${start.col}-${end.row},${end.col}';
if (_pathCache.containsKey(key)) {
return _pathCache[key];
}
final path = _findPath(start, end);
_pathCache[key] = path;
return path;
}
2. 提前检测死局
dart
bool _hasValidMoves() {
for (int i = 0; i < _difficulty.rows; i++) {
for (int j = 0; j < _difficulty.cols; j++) {
final tile1 = _board[i][j];
if (tile1 == null || tile1.isMatched) continue;
for (int m = i; m < _difficulty.rows; m++) {
for (int n = 0; n < _difficulty.cols; n++) {
if (i == m && j >= n) continue;
final tile2 = _board[m][n];
if (tile2 == null || tile2.isMatched) continue;
if (tile1.type == tile2.type && _findPath(tile1, tile2) != null) {
return true;
}
}
}
}
}
return false;
}
常见问题解答
Q1: 如何确保棋盘一定有解?
A: 可以在初始化时检测是否有可连接的方块对,如果没有则重新生成。
Q2: 路径查找算法的时间复杂度是多少?
A: 最坏情况下是O(n²),其中n是棋盘大小。可以通过缓存优化。
Q3: 如何实现更流畅的动画?
A: 使用AnimatedContainer、Hero动画或自定义Tween动画。
项目结构
lib/
├── main.dart # 主程序入口
├── models/
│ ├── tile.dart # 方块模型
│ ├── path_point.dart # 路径点
│ └── difficulty.dart # 难度配置
├── screens/
│ ├── game_page.dart # 游戏页面
│ └── menu_page.dart # 菜单页面
├── widgets/
│ ├── game_board.dart # 游戏棋盘
│ ├── tile_widget.dart # 方块组件
│ └── path_painter.dart # 路径绘制器
└── utils/
├── path_finder.dart # 路径查找算法
└── score_calculator.dart # 得分计算
总结
本文实现了一个功能完整的连连看游戏,涵盖了以下核心技术:
- 路径查找算法:直线、一转角、两转角连接检测
- 得分系统:基础分+时间奖励+路径奖励
- 提示功能:自动查找可连接的方块对
- 重排功能:打乱未匹配的方块
- CustomPainter:自定义绘制连接路径
- 动画效果:淡出动画和选中高亮
- 数据持久化:保存最高分记录
通过本项目,你不仅学会了如何实现连连看游戏,还掌握了Flutter中路径查找、自定义绘制、动画效果的核心技术。这些知识可以应用到更多益智游戏的开发。
挑战你的观察力,享受连连看的乐趣!
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net