Flutter 框架跨平台鸿蒙开发 - 从零开发经典2048游戏
滑动合并算法:移除零 → 合并 → 移除零 → 补零方向统一化:通过翻转/转置复用向左滑动逻辑状态管理:分数、最高分、游戏结束、胜利判定交互设计:触屏滑动 + 键盘控制视觉设计:经典配色方案2048的魅力在于规则简单却策略丰富。希望这篇文章能帮你理解游戏背后的算法思想,也欢迎挑战更高分数!🎮 完整源码已上传,欢迎Star支持!作者:Flutter游戏开发者发布日期:2026年1月16日版权声明:
🎮 Flutter + HarmonyOS 实战:从零开发经典2048游戏
运行效果图预览

📋 文章导读
| 章节 | 内容概要 | 预计阅读 |
|---|---|---|
| 一 | 2048游戏规则与设计思路 | 3分钟 |
| 二 | 核心数据结构设计 | 5分钟 |
| 三 | 滑动合并算法详解 | 12分钟 |
| 四 | 游戏状态管理 | 6分钟 |
| 五 | UI界面与交互实现 | 8分钟 |
| 六 | 完整源码与运行 | 3分钟 |
💡 写在前面:2048是一款风靡全球的数字益智游戏,玩法简单却让人欲罢不能。本文将带你用Flutter从零实现这款经典游戏,重点讲解滑动合并算法的设计思路。无论你是想学习游戏开发,还是想深入理解算法,这篇文章都值得一读。
一、游戏规则与设计
1.1 2048游戏规则
2048的规则可以用三句话概括:
-
滑动合并
- 向任意方向滑动,相同数字的方块会合并成它们的和
1.2 功能设计
1.3 方块颜色设计
2048的经典配色是游戏体验的重要组成部分:
| 数值 | 背景色 | 文字色 | 说明 |
|---|---|---|---|
| 0 | #CDC1B4 |
- | 空格 |
| 2 | #EEE4DA |
#776E65 |
浅米色 |
| 4 | #EDE0C8 |
#776E65 |
米色 |
| 8 | #F2B179 |
白色 | 橙色 |
| 16 | #F59563 |
白色 | 深橙 |
| 32 | #F67C5F |
白色 | 红橙 |
| 64 | #F65E3B |
白色 | 红色 |
| 128 | #EDCF72 |
白色 | 金黄 |
| 256 | #EDCC61 |
白色 | 深金 |
| 512 | #EDC850 |
白色 | 橙金 |
| 1024 | #EDC53F |
白色 | 亮金 |
| 2048 | #EDC22E |
白色 | 金色 |
| >2048 | #3C3A32 |
白色 | 深灰 |
二、核心数据结构
2.1 棋盘表示
使用4×4的二维数组存储棋盘状态:
// 棋盘大小
static const int gridSize = 4;
// 棋盘数据:0表示空格,其他为数字
late List<List<int>> grid;
// 初始化棋盘
void _initGame() {
grid = List.generate(
gridSize,
(_) => List.generate(gridSize, (_) => 0),
);
_addRandomTile(); // 添加第一个方块
_addRandomTile(); // 添加第二个方块
}
2.2 棋盘状态示例
初始状态: 滑动后:
┌────┬────┬────┬────┐ ┌────┬────┬────┬────┐
│ │ 2 │ │ │ │ 2 │ │ │ │
├────┼────┼────┼────┤ ├────┼────┼────┼────┤
│ │ │ │ 2 │ ← │ 2 │ │ │ │
├────┼────┼────┼────┤ ├────┼────┼────┼────┤
│ │ │ │ │ │ │ │ │ │
├────┼────┼────┼────┤ ├────┼────┼────┼────┤
│ │ │ │ │ │ 4 │ │ │ │ ← 新生成
└────┴────┴────┴────┘ └────┴────┴────┴────┘
2.3 游戏状态变量
// 当前分数
int score = 0;
// 历史最高分
int bestScore = 0;
// 游戏是否结束
bool gameOver = false;
// 是否已达到2048
bool hasWon = false;
// 达到2048后是否继续
bool continueAfterWin = false;
三、滑动合并算法
3.1 算法核心思想
滑动合并是2048的核心算法。我们只需要实现向左滑动的逻辑,其他三个方向可以通过旋转/翻转来复用。
3.2 向左滑动算法
/// 向左滑动一行
/// 输入: [2, 0, 2, 4]
/// 输出: [4, 4, 0, 0]
List<int> _slideLeft(List<int> row) {
// 第一步:移除所有零,让数字靠左
// [2, 0, 2, 4] → [2, 2, 4]
List<int> newRow = row.where((x) => x != 0).toList();
// 第二步:合并相邻相同的数字
for (int i = 0; i < newRow.length - 1; i++) {
if (newRow[i] == newRow[i + 1]) {
newRow[i] *= 2; // 合并:2+2=4
score += newRow[i]; // 加分
newRow[i + 1] = 0; // 被合并的位置置零
// 检查是否达到2048
if (newRow[i] == 2048 && !hasWon) {
hasWon = true;
}
}
}
// [2, 2, 4] → [4, 0, 4]
// 第三步:再次移除零
// [4, 0, 4] → [4, 4]
newRow = newRow.where((x) => x != 0).toList();
// 第四步:补齐零到原长度
// [4, 4] → [4, 4, 0, 0]
while (newRow.length < gridSize) {
newRow.add(0);
}
return newRow;
}
3.3 合并过程图解
以 [2, 2, 4, 4] 向左滑动为例:
原始: [2, 2, 4, 4]
↓
移除零: [2, 2, 4, 4] (无变化)
↓
合并: [4, 0, 8, 0] (2+2=4, 4+4=8)
↓
移除零: [4, 8]
↓
补零: [4, 8, 0, 0]
3.4 四方向移动实现
bool _move(String direction) {
// 保存旧状态,用于判断是否有变化
List<List<int>> oldGrid = grid.map((row) => List<int>.from(row)).toList();
switch (direction) {
case 'left':
// 直接对每行应用向左滑动
for (int i = 0; i < gridSize; i++) {
grid[i] = _slideLeft(grid[i]);
}
break;
case 'right':
// 先翻转,向左滑动,再翻转回来
for (int i = 0; i < gridSize; i++) {
grid[i] = _slideLeft(grid[i].reversed.toList()).reversed.toList();
}
break;
case 'up':
// 对每列应用向左滑动(列转行)
for (int j = 0; j < gridSize; j++) {
List<int> column = [for (int i = 0; i < gridSize; i++) grid[i][j]];
column = _slideLeft(column);
for (int i = 0; i < gridSize; i++) {
grid[i][j] = column[i];
}
}
break;
case 'down':
// 列翻转后向左滑动,再翻转回来
for (int j = 0; j < gridSize; j++) {
List<int> column = [for (int i = 0; i < gridSize; i++) grid[i][j]];
column = _slideLeft(column.reversed.toList()).reversed.toList();
for (int i = 0; i < gridSize; i++) {
grid[i][j] = column[i];
}
}
break;
}
// 检查是否有变化
return _hasChanged(oldGrid, grid);
}
3.5 方向转换关系
| 方向 | 转换方式 | 说明 |
|---|---|---|
| ← 左 | 直接处理 | 基础算法 |
| → 右 | 翻转 → 左滑 → 翻转 | 利用对称性 |
| ↑ 上 | 列转行 → 左滑 → 行转列 | 转置处理 |
| ↓ 下 | 列转行 → 翻转 → 左滑 → 翻转 → 行转列 | 组合转换 |
3.6 算法复杂度
| 操作 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 单行滑动 | O(n)O(n)O(n) | O(n)O(n)O(n) |
| 整体移动 | O(n2)O(n^2)O(n2) | O(n2)O(n^2)O(n2) |
| 检查变化 | O(n2)O(n^2)O(n2) | O(1)O(1)O(1) |
其中 n=4n = 4n=4(棋盘边长),实际运算量很小。
四、游戏状态管理
4.1 随机生成方块
/// 在空白位置随机生成一个方块
/// 90%概率生成2,10%概率生成4
void _addRandomTile() {
// 收集所有空白位置
List<List<int>> emptyTiles = [];
for (int i = 0; i < gridSize; i++) {
for (int j = 0; j < gridSize; j++) {
if (grid[i][j] == 0) {
emptyTiles.add([i, j]);
}
}
}
// 随机选择一个空白位置
if (emptyTiles.isNotEmpty) {
final pos = emptyTiles[random.nextInt(emptyTiles.length)];
// 90%概率为2,10%概率为4
grid[pos[0]][pos[1]] = random.nextDouble() < 0.9 ? 2 : 4;
}
}
4.2 游戏结束判定
/// 检查是否还能移动
bool _canMove() {
// 检查是否有空格
for (int i = 0; i < gridSize; i++) {
for (int j = 0; j < gridSize; j++) {
if (grid[i][j] == 0) return true;
}
}
// 检查是否有相邻相同的数字
for (int i = 0; i < gridSize; i++) {
for (int j = 0; j < gridSize; j++) {
// 检查右边
if (j < gridSize - 1 && grid[i][j] == grid[i][j + 1]) return true;
// 检查下边
if (i < gridSize - 1 && grid[i][j] == grid[i + 1][j]) return true;
}
}
return false;
}
4.3 滑动处理流程
void _handleSwipe(String direction) {
if (gameOver) return;
setState(() {
// 1. 执行移动
bool moved = _move(direction);
if (moved) {
// 2. 生成新方块
_addRandomTile();
// 3. 更新最高分
if (score > bestScore) {
bestScore = score;
}
// 4. 检查游戏是否结束
if (!_canMove()) {
gameOver = true;
}
}
});
// 5. 检查是否首次达到2048
if (hasWon && !continueAfterWin) {
_showWinDialog();
}
}
五、UI界面与交互
5.1 整体布局
5.2 手势识别
GestureDetector(
// 垂直滑动
onVerticalDragEnd: (details) {
if (details.velocity.pixelsPerSecond.dy < -100) {
_handleSwipe('up'); // 向上滑动
} else if (details.velocity.pixelsPerSecond.dy > 100) {
_handleSwipe('down'); // 向下滑动
}
},
// 水平滑动
onHorizontalDragEnd: (details) {
if (details.velocity.pixelsPerSecond.dx < -100) {
_handleSwipe('left'); // 向左滑动
} else if (details.velocity.pixelsPerSecond.dx > 100) {
_handleSwipe('right'); // 向右滑动
}
},
child: // 棋盘Widget
)
5.3 键盘支持
KeyboardListener(
focusNode: FocusNode()..requestFocus(),
onKeyEvent: (event) {
if (event is KeyDownEvent) {
switch (event.logicalKey) {
case LogicalKeyboardKey.arrowUp:
case LogicalKeyboardKey.keyW:
_handleSwipe('up');
break;
case LogicalKeyboardKey.arrowDown:
case LogicalKeyboardKey.keyS:
_handleSwipe('down');
break;
case LogicalKeyboardKey.arrowLeft:
case LogicalKeyboardKey.keyA:
_handleSwipe('left');
break;
case LogicalKeyboardKey.arrowRight:
case LogicalKeyboardKey.keyD:
_handleSwipe('right');
break;
}
}
},
child: // 游戏界面
)
5.4 方块渲染
Widget _buildTile(int value) {
return AnimatedContainer(
duration: const Duration(milliseconds: 100),
decoration: BoxDecoration(
color: _getTileColor(value),
borderRadius: BorderRadius.circular(4),
),
child: Center(
child: value != 0
? Text(
'$value',
style: TextStyle(
color: _getTextColor(value),
fontSize: _getFontSize(value),
fontWeight: FontWeight.bold,
),
)
: null,
),
);
}
/// 根据数值大小调整字体
double _getFontSize(int value) {
if (value < 100) return 40;
if (value < 1000) return 32;
if (value < 10000) return 26;
return 22;
}
六、完整源码与运行
6.1 项目结构
flutter_2048/
├── lib/
│ └── main.dart # 2048游戏代码(约350行)
├── ohos/ # 鸿蒙平台配置
├── pubspec.yaml # 依赖配置
└── README.md # 项目说明
6.2 运行命令
# 获取依赖
flutter pub get
# 运行游戏
flutter run
# 运行到鸿蒙设备
flutter run -d ohos
6.3 功能清单
| 功能 | 状态 | 说明 |
|---|---|---|
| 4×4游戏棋盘 | ✅ | 经典尺寸 |
| 四方向滑动 | ✅ | 触屏+键盘 |
| 数字合并 | ✅ | 核心算法 |
| 随机生成 | ✅ | 90%为2,10%为4 |
| 分数统计 | ✅ | 实时更新 |
| 最高分记录 | ✅ | 本次游戏内 |
| 胜利判定 | ✅ | 达到2048 |
| 失败判定 | ✅ | 无法移动 |
| 继续挑战 | ✅ | 2048后可继续 |
| 重新开始 | ✅ | 一键重置 |
七、扩展方向
7.1 功能扩展
7.2 动画增强
// 使用 AnimatedPositioned 实现滑动动画
// 使用 ScaleTransition 实现合并动画
// 使用 FadeTransition 实现新方块出现动画
7.3 本地存储
// 使用 shared_preferences 保存最高分
Future<void> _saveBestScore() async {
final prefs = await SharedPreferences.getInstance();
await prefs.setInt('bestScore', bestScore);
}
Future<void> _loadBestScore() async {
final prefs = await SharedPreferences.getInstance();
bestScore = prefs.getInt('bestScore') ?? 0;
}
八、常见问题
Q1: 为什么只实现向左滑动,其他方向怎么办?这是一个经典的算法技巧:通过变换将问题统一化。
- 向右 = 翻转 → 向左 → 翻转
- 向上 = 转置 → 向左 → 转置
- 向下 = 转置 → 翻转 → 向左 → 翻转 → 转置
这样只需要维护一份核心逻辑,减少代码重复和bug。
Q2: 为什么新方块90%是2,10%是4?这是原版2048的设定,目的是:
- 让游戏初期更容易上手(2更容易合并)
- 增加一定的随机性和挑战(偶尔出现4)
- 平衡游戏难度
需要同时满足两个条件:
- 没有空格(棋盘已满)
- 没有相邻的相同数字(无法合并)
只检查一个条件是不够的。比如棋盘满了但还有相邻相同数字,仍然可以继续。
Q4: 达到2048后为什么可以继续?原版2048允许玩家在达到2048后继续挑战更高分数(4096、8192甚至更高)。这增加了游戏的可玩性,让高手有更多追求的目标。
九、总结
本文从零实现了经典的2048游戏,核心技术点包括:
- 滑动合并算法:移除零 → 合并 → 移除零 → 补零
- 方向统一化:通过翻转/转置复用向左滑动逻辑
- 状态管理:分数、最高分、游戏结束、胜利判定
- 交互设计:触屏滑动 + 键盘控制
- 视觉设计:经典配色方案
2048的魅力在于规则简单却策略丰富。希望这篇文章能帮你理解游戏背后的算法思想,也欢迎挑战更高分数!
🎮 完整源码已上传,欢迎Star支持!
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐

所有评论(0)