Flutter 框架跨平台鸿蒙开发 - 从零开发经典推箱子游戏

🎮 Flutter + HarmonyOS 实战:从零开发经典推箱子游戏


运行效果图

📋 文章导读

章节 内容概要 预计阅读
推箱子游戏介绍与设计思路 3分钟
游戏核心数据结构设计 5分钟
游戏逻辑实现详解 10分钟
UI界面与交互设计 8分钟
关卡设计与扩展 5分钟
完整源码与运行 3分钟

💡 写在前面:推箱子(Sokoban)是一款诞生于1982年的经典益智游戏,玩家需要在有限空间内将所有箱子推到指定位置。别看规则简单,想要用最少步数通关可不容易。本文将带你用Flutter从零实现这款经典游戏,顺便聊聊游戏开发中的一些设计思想。


一、游戏设计思路

1.1 推箱子的游戏规则

推箱子的规则可以用一句话概括:把所有箱子推到目标点上。但这里面有几个关键约束:
:
玩家只能从箱子的一侧推动它,不能站在箱子前面把它拉过来
一次推一个 不能同时推动两个或多个箱子 不能推到墙角 箱子一旦被推到墙角,就再也推不出来了(死局) 步数越少越好 虽然没有步数限制,但用最少步数通关才是高手

1.2 游戏元素定义

在开始编码之前,我们先明确游戏中有哪些元素:
游戏元素
静态元素
墙壁 Wall
地板 Floor
目标点 Target
动态元素
玩家 Player
箱子 Box
组合状态
箱子在目标点上
玩家在目标点上

1.3 状态机设计

游戏的核心是状态管理。每个格子都有自己的状态,我们用数字来表示:

数值 含义 说明
0 地板 可以自由通行
1 墙壁 不可通行
2 目标点 箱子的目的地
3 箱子 需要被推动的对象
4 玩家 初始位置标记
5 箱子+目标点 箱子已到达目标

二、核心数据结构

2.1 地图表示

推箱子的地图本质上是一个二维数组。我们用 List<List<int>> 来存储:

dart 复制代码
// 关卡数据示例:7x7 的入门关卡
final List<List<int>> level1 = [
  [1, 1, 1, 1, 1, 1, 1],
  [1, 0, 0, 0, 0, 0, 1],
  [1, 0, 3, 2, 0, 0, 1],  // 3=箱子, 2=目标点
  [1, 0, 0, 0, 3, 2, 1],
  [1, 0, 4, 0, 0, 0, 1],  // 4=玩家初始位置
  [1, 0, 0, 0, 0, 0, 1],
  [1, 1, 1, 1, 1, 1, 1],
];

用表格来可视化这个关卡:

0 1 2 3 4 5 6
0 🧱 🧱 🧱 🧱 🧱 🧱 🧱
1 🧱 🧱
2 🧱 📦 🎯 🧱
3 🧱 📦 🎯 🧱
4 🧱 🧑 🧱
5 🧱 🧱
6 🧱 🧱 🧱 🧱 🧱 🧱 🧱

2.2 坐标系统

Flutter的坐标系统是左上角为原点,向右为X正方向,向下为Y正方向:

复制代码
    X →
  ┌─────────────┐
Y │ (0,0) (1,0) │
↓ │ (0,1) (1,1) │
  └─────────────┘

移动方向与坐标变化的对应关系:

方向 dx dy 说明
↑ 上 0 -1 Y坐标减小
↓ 下 0 +1 Y坐标增大
← 左 -1 0 X坐标减小
→ 右 +1 0 X坐标增大

三、游戏逻辑实现

3.1 移动逻辑流程图

玩家每次移动都需要经过一系列判断,这是整个游戏最核心的逻辑:


墙壁
地板/目标点
箱子
墙壁/箱子
地板
目标点


玩家按下方向键
目标位置是否越界?
不移动
目标位置是什么?
玩家移动到目标位置
箱子前方是什么?
箱子移动
箱子移动到目标点
步数+1
所有目标点都有箱子?
游戏胜利!
继续游戏

3.2 移动函数实现

dart 复制代码
/// 移动玩家
/// [dx] X方向偏移量 (-1, 0, 1)
/// [dy] Y方向偏移量 (-1, 0, 1)
void _move(int dx, int dy) {
  int newX = playerX + dx;
  int newY = playerY + dy;
  
  // 第一步:边界检查
  if (newY < 0 || newY >= gameMap.length || 
      newX < 0 || newX >= gameMap[0].length) {
    return;
  }
  
  int targetTile = gameMap[newY][newX];
  
  // 第二步:墙壁检查
  if (targetTile == 1) return;
  
  // 第三步:箱子处理
  if (targetTile == 3 || targetTile == 5) {
    int boxNewX = newX + dx;
    int boxNewY = newY + dy;
    
    // 箱子目标位置检查
    if (boxNewY < 0 || boxNewY >= gameMap.length || 
        boxNewX < 0 || boxNewX >= gameMap[0].length) {
      return;
    }
    
    int boxTargetTile = gameMap[boxNewY][boxNewX];
    
    // 箱子不能推到墙或另一个箱子上
    if (boxTargetTile == 1 || boxTargetTile == 3 || boxTargetTile == 5) {
      return;
    }
    
    setState(() {
      // 更新箱子位置
      gameMap[boxNewY][boxNewX] = (boxTargetTile == 2) ? 5 : 3;
      // 恢复原箱子位置
      gameMap[newY][newX] = (targetTile == 5) ? 2 : 0;
      // 更新玩家位置
      playerX = newX;
      playerY = newY;
      moves++;
    });
  } else {
    // 普通移动(地板或目标点)
    setState(() {
      playerX = newX;
      playerY = newY;
      moves++;
    });
  }
  
  // 第四步:胜利检查
  _checkWin();
}

3.3 状态转换表

箱子移动时的状态转换逻辑:

箱子原位置 箱子目标位置 原位置变为 目标位置变为
箱子(3) 地板(0) 地板(0) 箱子(3)
箱子(3) 目标点(2) 地板(0) 箱子在目标点(5)
箱子在目标点(5) 地板(0) 目标点(2) 箱子(3)
箱子在目标点(5) 目标点(2) 目标点(2) 箱子在目标点(5)

3.4 胜利条件判断

胜利的条件很简单:地图上不存在空的目标点(值为2的格子)

dart 复制代码
/// 检查是否胜利
void _checkWin() {
  for (var row in gameMap) {
    if (row.contains(2)) return; // 还有未完成的目标点
  }
  
  // 所有目标点都被覆盖,游戏胜利!
  _showWinDialog();
}

四、UI界面设计

4.1 整体布局结构

游戏区域
Body
Scaffold
AppBar - 标题栏
Body - 主体
步数显示区
游戏区域
控制按钮区
GridView
格子1
格子2
...

4.2 配色方案

游戏采用大地色系,营造温馨的视觉效果:

元素 颜色 色值 说明
墙壁 🟫 brown.shade800 深棕色,厚重感
地板 🟨 brown.shade200 浅棕色,柔和
目标点 🟩 green.shade300 浅绿色,醒目
箱子 🟧 orange.shade600 橙色,突出
箱子完成 🟢 green.shade600 深绿色,成就感
玩家 🔵 blue.shade700 蓝色,易识别

4.3 格子渲染逻辑

dart 复制代码
/// 获取格子颜色
Color _getTileColor(int tile, bool isPlayer) {
  if (isPlayer) return Colors.blue.shade700;
  
  switch (tile) {
    case 1: return Colors.brown.shade800;  // 墙
    case 2: return Colors.green.shade300;  // 目标点
    case 3: return Colors.orange.shade600; // 箱子
    case 5: return Colors.green.shade600;  // 箱子在目标点
    default: return Colors.brown.shade200; // 地板
  }
}

/// 获取格子图标
Widget? _getTileIcon(int tile, bool isPlayer) {
  if (isPlayer) {
    return const Icon(Icons.person, color: Colors.white, size: 28);
  }
  switch (tile) {
    case 2: return Icon(Icons.close, color: Colors.green.shade700, size: 20);
    case 3: return const Icon(Icons.inventory_2, color: Colors.white, size: 24);
    case 5: return const Icon(Icons.check_box, color: Colors.white, size: 24);
    default: return null;
  }
}

4.4 键盘与触控支持

游戏同时支持键盘操作和屏幕触控:

操作方式
键盘方向键
WASD键 W S A D
屏幕按钮 🔼 🔽 ◀️ ▶️
dart 复制代码
KeyboardListener(
  focusNode: FocusNode()..requestFocus(),
  onKeyEvent: (event) {
    if (event is KeyDownEvent) {
      switch (event.logicalKey) {
        case LogicalKeyboardKey.arrowUp:
        case LogicalKeyboardKey.keyW:
          _move(0, -1);
          break;
        case LogicalKeyboardKey.arrowDown:
        case LogicalKeyboardKey.keyS:
          _move(0, 1);
          break;
        // ... 其他方向
      }
    }
  },
  child: // 游戏界面
)

五、关卡设计

5.1 关卡难度曲线

好的关卡设计应该遵循循序渐进的原则:
第1关

入门
第2关

进阶
第3关

挑战
2个箱子

直线推动
4个箱子

需要规划
4个箱子

空间受限

5.2 关卡数据结构

dart 复制代码
final List<List<List<int>>> levels = [
  // 第1关 - 入门:2个箱子,空间充足
  [
    [1, 1, 1, 1, 1, 1, 1],
    [1, 0, 0, 0, 0, 0, 1],
    [1, 0, 3, 2, 0, 0, 1],
    [1, 0, 0, 0, 3, 2, 1],
    [1, 0, 4, 0, 0, 0, 1],
    [1, 0, 0, 0, 0, 0, 1],
    [1, 1, 1, 1, 1, 1, 1],
  ],
  
  // 第2关 - 进阶:4个箱子,对称布局
  [
    [1, 1, 1, 1, 1, 1, 1, 1],
    [1, 0, 0, 0, 0, 0, 0, 1],
    [1, 0, 2, 3, 0, 3, 2, 1],
    [1, 0, 0, 0, 4, 0, 0, 1],
    [1, 0, 2, 3, 0, 3, 2, 1],
    [1, 0, 0, 0, 0, 0, 0, 1],
    [1, 1, 1, 1, 1, 1, 1, 1],
  ],
  
  // 第3关 - 挑战:中间有障碍
  [
    [1, 1, 1, 1, 1, 1, 1, 1],
    [1, 0, 0, 1, 0, 0, 0, 1],
    [1, 0, 3, 0, 3, 0, 0, 1],
    [1, 2, 0, 0, 0, 2, 0, 1],
    [1, 0, 3, 4, 3, 0, 0, 1],
    [1, 2, 0, 0, 0, 2, 0, 1],
    [1, 0, 0, 1, 0, 0, 0, 1],
    [1, 1, 1, 1, 1, 1, 1, 1],
  ],
];

5.3 关卡设计原则

设计一个好玩的推箱子关卡,需要注意以下几点:

  1. 避免死局:确保玩家不会一开始就把箱子推到无法移动的位置
  2. 有解且唯一:最好有一个最优解,让玩家有追求的目标
  3. 空间适中:太大显得空旷,太小则操作困难
  4. 渐进难度:从简单到复杂,给玩家成长的空间

5.4 最优步数参考

关卡 箱子数 参考最优步数 难度评级
第1关 2 12步
第2关 4 24步 ⭐⭐
第3关 4 32步 ⭐⭐⭐

六、完整源码

6.1 项目结构

复制代码
flutter_sokoban/
├── lib/
│   └── main.dart       # 游戏主代码(约200行)
├── ohos/               # 鸿蒙平台配置
├── pubspec.yaml        # 依赖配置
└── README.md           # 项目说明

6.2 运行命令

bash 复制代码
# 获取依赖
flutter pub get

# 运行到模拟器/设备
flutter run

# 运行到鸿蒙设备
flutter run -d ohos

# 构建发布包
flutter build hap --release

6.3 核心代码统计

模块 代码行数 功能说明
数据定义 ~30行 关卡数据、状态变量
游戏逻辑 ~80行 移动、碰撞、胜利判断
UI渲染 ~90行 界面布局、样式
合计 ~200行 完整可运行的推箱子游戏

七、扩展方向

如果你想继续完善这个游戏,这里有一些思路:

7.1 功能扩展

推箱子游戏
撤销功能
关卡编辑器
云端存档
排行榜
音效系统
记录历史状态
可视化编辑
跨设备同步

7.2 撤销功能实现思路

dart 复制代码
// 使用栈来保存历史状态
List<GameState> history = [];

class GameState {
  final List<List<int>> map;
  final int playerX;
  final int playerY;
  
  GameState(this.map, this.playerX, this.playerY);
}

void _move(int dx, int dy) {
  // 移动前保存状态
  history.add(GameState(
    gameMap.map((row) => List<int>.from(row)).toList(),
    playerX,
    playerY,
  ));
  
  // ... 执行移动逻辑
}

void _undo() {
  if (history.isEmpty) return;
  
  final state = history.removeLast();
  setState(() {
    gameMap = state.map;
    playerX = state.playerX;
    playerY = state.playerY;
    moves--;
  });
}

八、常见问题

Q1: 为什么玩家位置要单独存储,而不是放在地图数组里?

将玩家位置单独存储有两个好处:

  1. 简化状态管理:玩家移动时不需要修改地图数组
  2. 处理"玩家站在目标点上"的情况更简单

如果把玩家放在地图里,当玩家站在目标点上时,需要额外的状态来记住这个位置原本是目标点。
Q2: 如何判断一个关卡是否有解?

这是一个经典的算法问题。可以使用 BFS(广度优先搜索)或 A* 算法来求解:

dart 复制代码
// 伪代码
bool canSolve(GameState initial) {
  Queue<GameState> queue = Queue();
  Set<String> visited = {};
  
  queue.add(initial);
  
  while (queue.isNotEmpty) {
    var state = queue.removeFirst();
    
    if (isWin(state)) return true;
    
    for (var direction in [up, down, left, right]) {
      var newState = move(state, direction);
      if (!visited.contains(newState.hash)) {
        visited.add(newState.hash);
        queue.add(newState);
      }
    }
  }
  
  return false;
}

Q3: 如何避免箱子被推到死角?

可以在移动后检测"死锁"情况:

  1. 角落死锁:箱子被推到两面都是墙的角落
  2. 边缘死锁:箱子沿着墙边,且该边没有目标点

检测到死锁后可以给玩家提示,或者自动撤销这一步。


九、总结

本文从零开始,用约200行Dart代码实现了一个完整的推箱子游戏。整个开发过程涵盖了:

  1. 游戏设计:分析规则,定义元素和状态
  2. 数据结构:用二维数组表示地图,用数字编码状态
  3. 核心逻辑:移动判断、碰撞检测、胜利条件
  4. UI实现:GridView渲染、键盘监听、触控支持
  5. 关卡设计:难度曲线、设计原则

推箱子虽然是个小游戏,但麻雀虽小五脏俱全。通过这个项目,你可以学到状态管理、碰撞检测、关卡设计等游戏开发的基础知识。

希望这篇文章对你有所帮助,如果觉得不错,别忘了点赞收藏!


🎮 完整源码已上传,欢迎Star支持!


作者 :Flutter游戏开发者
发布日期 :2026年1月15日
版权声明:本文为原创文章,转载请注明出处


欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

相关推荐
哈哈你是真的厉害2 小时前
React Native 鸿蒙跨平台开发:TemperatureConverter 温度换算器
react native·react.js·harmonyos
LawrenceLan2 小时前
17.Flutter 零基础入门(十七):StatelessWidget 与 State 的第一次分离
开发语言·前端·flutter·dart
大雷神2 小时前
Harmony应用中 HAR 包开发与发布到openharmony中的完整指南
harmonyos
柒儿吖2 小时前
Flutter跨平台三方库image_picker在鸿蒙中的使用指南
flutter·华为·harmonyos
世人万千丶2 小时前
鸿蒙跨端框架Flutter学习day 2、常用UI组件-折行布局 Wrap & Chip
学习·flutter·ui·华为·harmonyos·鸿蒙
柒儿吖2 小时前
Flutter跨平台三方库file_selector在鸿蒙中的使用指南
flutter·华为·harmonyos
柒儿吖2 小时前
Flutter跨平台三方库url_launcher在鸿蒙中的使用指南
flutter·华为·harmonyos
云边散步2 小时前
godot2D游戏教程系列一(2)
游戏
小雨青年2 小时前
鸿蒙 HarmonyOS 6 | 逻辑核心 (04):原生网络库 RCP 高性能实战
网络·华为·harmonyos