Flutter for OpenHarmony游戏集合App实战之推箱子墙壁地板
推箱子是一个经典的益智游戏,玩家推动箱子到指定位置。游戏场景由墙壁、地板、目标点组成。墙壁不能穿过,地板可以行走,目标点是箱子要放的位置。这篇来聊聊推箱子的场景渲染。推箱子的关卡设计很有讲究,用字符串表示关卡是一种经典的做法,简单直观。墙壁: 棕色,不可穿过地板: 浅灰色,可以行走目标点: 浅绿色,箱子要放这里玩家: 蓝色人形图标箱子: 橙色方块到位箱子: 绿色方块颜色区分清晰,玩家一眼就能看懂场
通过网盘分享的文件:game_flutter_openharmony.zip
链接: https://pan.baidu.com/s/1ryUS1A0zcvXGrDaStu530w 提取码: tqip
前言
推箱子是一个经典的益智游戏,玩家推动箱子到指定位置。
游戏场景由墙壁、地板、目标点组成。墙壁不能穿过,地板可以行走,目标点是箱子要放的位置。
这篇来聊聊推箱子的场景渲染。推箱子的关卡设计很有讲究,用字符串表示关卡是一种经典的做法,简单直观。
状态变量
int currentLevel = 0;
late List<String> level;
int playerX = 0, playerY = 0;
int moves = 0;
这些变量定义了游戏状态:
- currentLevel: 当前关卡索引
- level: 当前关卡数据
- playerX, playerY: 玩家位置
- moves: 移动步数
关卡数据
final List<List<String>> levels = [
['#####', '# #', '# B #', '# . #', '#@ #', '#####'],
['######', '# #', '# BB #', '# .. #', '#@ #', '######'],
['#######', '# #', '# BBB #', '# ... #', '# @ #', '#######'],
];
用字符串数组表示关卡,每个字符代表一种元素:
- #: 墙壁,不可穿过
- 空格: 地板,可以行走
- B: 箱子,可以推动
- .: 目标点,箱子要放这里
- @: 玩家初始位置
- +: 玩家在目标点上
- *****: 箱子在目标点上
这种表示方法很直观,一眼就能看出关卡布局。而且修改关卡很方便,直接编辑字符串就行。
关卡设计
三个关卡难度递增:
- 关卡1:1个箱子,1个目标点
- 关卡2:2个箱子,2个目标点
- 关卡3:3个箱子,3个目标点
箱子数量和目标点数量必须相等,否则无法通关。
加载关卡
void _loadLevel() {
level = levels[currentLevel].map((r) => r.padRight(10)).toList();
// 找到玩家位置
for (int y = 0; y < level.length; y++) {
int x = level[y].indexOf('@');
if (x == -1) x = level[y].indexOf('+');
if (x != -1) {
playerX = x;
playerY = y;
break;
}
}
moves = 0;
}
加载关卡时需要做几件事:补齐行长度、找到玩家位置、重置步数。
padRight补齐
level = levels[currentLevel].map((r) => r.padRight(10)).toList();
padRight(10)把每行补齐到10个字符,保证所有行长度一致。
不然有些行短,渲染时会出问题。比如第一关的行长度是5,补齐到10后右边会有5个空格。
找玩家位置
for (int y = 0; y < level.length; y++) {
int x = level[y].indexOf('@');
if (x == -1) x = level[y].indexOf('+');
遍历每一行,找@(玩家在地板上)或+(玩家在目标点上)。
indexOf返回字符的位置,找不到返回-1。
场景渲染
Column(
mainAxisSize: MainAxisSize.min,
children: level.map((row) => Row(
mainAxisSize: MainAxisSize.min,
children: row.split('').map((c) => Container(
width: 32, height: 32,
decoration: BoxDecoration(
color: c == '#' ? Colors.brown : (c == '.' || c == '+' || c == '*') ? Colors.green[200] : Colors.grey[300],
border: Border.all(color: Colors.grey[400]!, width: 0.5),
),
child: Center(child: _getIcon(c)),
)).toList(),
)).toList(),
),
这段代码把关卡数据渲染成可视化的场景。
双层嵌套
外层Column对应行,内层Row对应列。
level.map遍历每一行,row.split('').map遍历每个字符。
split('')把字符串拆成单个字符的列表,比如"###"变成[‘#’, ‘#’, ‘#’]。
mainAxisSize.min
mainAxisSize: MainAxisSize.min,
让Column和Row只占用需要的空间,不会撑满整个屏幕。
如果不加这个,Column会占满整个高度,Row会占满整个宽度,格子之间会有很大的间隙。
格子大小
width: 32, height: 32,
每个格子32x32像素,正方形。这个大小在手机上看起来刚好,不会太大也不会太小。
颜色映射
color: c == '#' ? Colors.brown : (c == '.' || c == '+' || c == '*') ? Colors.green[200] : Colors.grey[300],
这是一个嵌套的三元运算符,根据字符决定格子的背景色。
墙壁
c == '#' ? Colors.brown
墙壁用棕色,模拟砖墙。棕色给人坚固、不可穿越的感觉。
目标点
(c == '.' || c == '+' || c == '*') ? Colors.green[200]
目标点用浅绿色。包括三种情况:
- .: 空的目标点,等待箱子
- +: 玩家站在目标点上
- *****: 箱子在目标点上,任务完成
绿色表示"目标"、“成功”,玩家一看就知道箱子要推到这里。
普通地板
: Colors.grey[300]
其他情况(空格、@、B)用浅灰色。灰色是中性色,不会干扰其他元素。
三元运算符的嵌套
条件1 ? 值1 : (条件2 ? 值2 : 值3)
这种写法等价于if-else if-else,但更简洁。不过嵌套太多会影响可读性。
格子边框
border: Border.all(color: Colors.grey[400]!, width: 0.5),
每个格子有0.5像素的灰色边框,形成网格效果。
边框很细,不会太突兀,但能区分格子。没有边框的话,相邻的同色格子会连成一片。
背景色
Container(
padding: const EdgeInsets.all(8),
color: Colors.grey[800],
整个场景外面包一层深灰色背景,和格子形成对比。
padding让格子不贴着边缘,有呼吸感。
_getIcon方法
Widget? _getIcon(String c) {
if (c == '@' || c == '+') return const Icon(Icons.person, color: Colors.blue, size: 24);
if (c == 'B') return Container(width: 20, height: 20, decoration: BoxDecoration(color: Colors.orange, borderRadius: BorderRadius.circular(4)));
if (c == '*') return Container(width: 20, height: 20, decoration: BoxDecoration(color: Colors.green, borderRadius: BorderRadius.circular(4)));
return null;
}
根据字符返回对应的图标或Widget。这个方法决定了格子里显示什么内容。
玩家
if (c == '@' || c == '+') return const Icon(Icons.person, color: Colors.blue, size: 24);
用Material Icons的person图标,蓝色,24像素。
@是玩家在普通地板上,+是玩家在目标点上,都显示同样的图标。玩家用蓝色,和其他元素区分开。
普通箱子
if (c == 'B') return Container(width: 20, height: 20, decoration: BoxDecoration(color: Colors.orange, borderRadius: BorderRadius.circular(4)));
橙色方块,20x20像素,4像素圆角。
比格子小一点(32 vs 20),留出边距,看起来更像一个"物体"而不是填满整个格子。
橙色表示"待处理",玩家需要把它推到目标点。
到位的箱子
if (c == '*') return Container(width: 20, height: 20, decoration: BoxDecoration(color: Colors.green, borderRadius: BorderRadius.circular(4)));
绿色方块,表示箱子已经推到目标点了。
颜色从橙色变成绿色,给玩家明确的反馈:这个箱子已经到位了,不用再管它。
其他
return null;
墙壁、地板、目标点不需要额外图标,返回null。它们只靠背景色区分。
方向按钮
Padding(
padding: const EdgeInsets.all(16),
child: Column(children: [
Row(mainAxisAlignment: MainAxisAlignment.center, children: [_dirBtn(Icons.arrow_upward, 0, -1)]),
Row(mainAxisAlignment: MainAxisAlignment.center, children: [
_dirBtn(Icons.arrow_back, -1, 0), const SizedBox(width: 48), _dirBtn(Icons.arrow_forward, 1, 0),
]),
Row(mainAxisAlignment: MainAxisAlignment.center, children: [_dirBtn(Icons.arrow_downward, 0, 1)]),
]),
),
十字形排列的方向按钮,方便玩家操作。
布局结构
三行:
- 第一行:上按钮
- 第二行:左按钮、间隔、右按钮
- 第三行:下按钮
SizedBox(width: 48)在左右按钮之间留出空间,让布局更像十字形。
_dirBtn
Widget _dirBtn(IconData icon, int dx, int dy) => IconButton(icon: Icon(icon, size: 32), onPressed: () => _move(dx, dy));
封装方向按钮,传入图标和移动方向。
dx和dy是移动方向:
- 上:(0, -1)
- 下:(0, 1)
- 左:(-1, 0)
- 右:(1, 0)
手势操作
GestureDetector(
onVerticalDragEnd: (d) => d.primaryVelocity! < 0 ? _move(0, -1) : _move(0, 1),
onHorizontalDragEnd: (d) => d.primaryVelocity! < 0 ? _move(-1, 0) : _move(1, 0),
除了按钮,还支持滑动操作。
和2048一样的逻辑,根据速度方向判断滑动方向。primaryVelocity是滑动结束时的速度,负值表示向上或向左。
关卡标题
AppBar(title: Text('推箱子 - 关卡 ${currentLevel + 1}'),
显示当前关卡号,让玩家知道进度。currentLevel从0开始,显示时加1。
步数显示
Padding(padding: const EdgeInsets.all(8), child: Text('步数: $moves', style: const TextStyle(fontSize: 18))),
显示当前步数,玩家可以追求更少步数通关。
颜色设计总结
- 墙壁: 棕色,不可穿过
- 地板: 浅灰色,可以行走
- 目标点: 浅绿色,箱子要放这里
- 玩家: 蓝色人形图标
- 箱子: 橙色方块
- 到位箱子: 绿色方块
颜色区分清晰,玩家一眼就能看懂场景。好的颜色设计能大大提升游戏体验。
小结
这篇讲了推箱子的墙壁地板,核心知识点:
- 字符串关卡:用字符表示不同元素,直观易编辑
- padRight补齐:保证所有行长度一致
- 双层map:遍历行和列,生成二维网格
- 嵌套三元运算符:根据字符设置颜色
- mainAxisSize.min:让布局紧凑
- _getIcon方法:根据字符返回图标
- 颜色区分:墙壁棕色、地板灰色、目标点绿色
- 方向按钮:十字形排列,直观易用
场景渲染是推箱子的基础,画好了场景,下一步就是实现推箱子的逻辑了。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐
所有评论(0)