Flutter for OpenHarmony游戏集合App实战之推箱子箱子目标点
本文介绍了推箱子游戏的核心玩法实现逻辑。主要包含移动逻辑处理,其中玩家移动(_move方法)会进行边界检查、墙壁检查,并处理推箱子操作。推箱子时需要检查目标位置是否可推,然后更新箱子状态(B表示普通箱子,*表示到位箱子)。玩家移动后会清除原位置并设置新位置状态。游戏通过_checkWin方法检查是否所有箱子都到位(B消失),胜利时弹出对话框显示步数并提供重玩或下一关选项。整个实现通过状态管理和字符
通过网盘分享的文件:game_flutter_openharmony.zip
链接: https://pan.baidu.com/s/1ryUS1A0zcvXGrDaStu530w 提取码: tqip
前言
上一篇讲了推箱子的场景渲染,这篇来聊聊核心玩法:推箱子到目标点。
推箱子的规则是:玩家只能推不能拉,箱子只能推到空地或目标点,不能推到墙壁或另一个箱子上。
这些规则看起来简单,但实现起来需要考虑很多情况:边界检查、墙壁检查、箱子检查、状态转换等。
移动逻辑
void _move(int dx, int dy) {
int nx = playerX + dx, ny = playerY + dy;
if (ny < 0 || ny >= level.length || nx < 0 || nx >= level[ny].length) return;
String target = level[ny][nx];
if (target == '#') return;
这是玩家移动的核心方法,处理所有的移动逻辑。
计算新位置
int nx = playerX + dx, ny = playerY + dy;
玩家当前位置加上移动方向,得到目标位置。
dx和dy是移动方向,取值-1、0或1。比如向右移动是dx=1, dy=0。
边界检查
if (ny < 0 || ny >= level.length || nx < 0 || nx >= level[ny].length) return;
目标位置不能超出关卡范围。四个边界都要检查:
- ny < 0:上边界
- ny >= level.length:下边界
- nx < 0:左边界
- nx >= level[ny].length:右边界
墙壁检查
String target = level[ny][nx];
if (target == '#') return;
目标位置是墙壁,不能移动。直接return,不执行后面的逻辑。
推箱子
setState(() {
if (target == 'B' || target == '*') {
int bx = nx + dx, by = ny + dy;
if (by < 0 || by >= level.length || bx < 0 || bx >= level[by].length) return;
String boxTarget = level[by][bx];
if (boxTarget == '#' || boxTarget == 'B' || boxTarget == '*') return;
_setCell(by, bx, boxTarget == '.' ? '*' : 'B');
}
如果目标位置有箱子,需要处理推箱子的逻辑。
检测箱子
if (target == 'B' || target == '*') {
目标位置有箱子(B是普通箱子,*是在目标点上的箱子)。
两种情况都算有箱子,都需要推。
箱子的目标位置
int bx = nx + dx, by = ny + dy;
箱子被推的方向和玩家移动方向一样,所以箱子的新位置是玩家目标位置再往前一格。
这就是"推"的含义:玩家走到箱子的位置,箱子被推到前面一格。
箱子边界检查
if (by < 0 || by >= level.length || bx < 0 || bx >= level[by].length) return;
箱子不能被推出边界。和玩家的边界检查一样。
箱子目标检查
String boxTarget = level[by][bx];
if (boxTarget == '#' || boxTarget == 'B' || boxTarget == '*') return;
箱子不能被推到:
- #: 墙壁
- B: 另一个箱子
- *****: 另一个在目标点上的箱子
这就是"只能推一个箱子"的规则。如果前面还有箱子,就推不动。
移动箱子
_setCell(by, bx, boxTarget == '.' ? '*' : 'B');
把箱子放到新位置:
- 如果新位置是目标点(.),箱子变成*(到位)
- 否则箱子还是B(未到位)
这里还没有清除箱子的原位置,因为玩家会走到那里,后面会处理。
移动玩家
_setCell(playerY, playerX, level[playerY][playerX] == '+' ? '.' : ' ');
_setCell(ny, nx, target == '.' || target == '*' ? '+' : '@');
playerX = nx; playerY = ny;
moves++;
推完箱子(如果有的话),接下来移动玩家。
清除原位置
_setCell(playerY, playerX, level[playerY][playerX] == '+' ? '.' : ' ');
玩家离开原位置:
- 如果原位置是+(玩家在目标点上),变成.(空目标点)
- 否则变成空格(普通地板)
这里的判断很重要:玩家离开后,原位置要恢复成正确的状态。
设置新位置
_setCell(ny, nx, target == '.' || target == '*' ? '+' : '@');
玩家到达新位置:
- 如果新位置是目标点(.)或有箱子的目标点(*),玩家变成+
- 否则玩家是@
注意:如果新位置原来有箱子(B或*),箱子已经被推走了,玩家走到的是箱子原来的位置。如果那个位置是目标点(*的情况),玩家要变成+。
更新坐标
playerX = nx; playerY = ny;
moves++;
更新玩家坐标,步数加1。
_setCell方法
void _setCell(int y, int x, String c) {
level[y] = level[y].substring(0, x) + c + level[y].substring(x + 1);
}
Dart的字符串是不可变的,不能直接修改某个字符。
这个方法用substring拼接的方式"修改"字符:
substring(0, x): 取x之前的部分c: 新字符substring(x + 1): 取x之后的部分
拼起来就是把第x个字符替换成c。
为什么不用List
也可以把字符串转成List,修改后再转回来:
var chars = level[y].split('');
chars[x] = c;
level[y] = chars.join('');
但substring的方式更简洁,性能也差不多。
胜利检查
if (_checkWin()) _showWinDialog();
每次移动后检查是否胜利。
bool _checkWin() {
for (var row in level) {
if (row.contains('B')) return false;
}
return true;
}
遍历所有行,如果还有B(不在目标点上的箱子),就没赢。
所有箱子都变成*(在目标点上),就赢了。
为什么只检查B
因为*表示箱子在目标点上,已经到位了。只要没有B,说明所有箱子都到位了。
这个检查方法很简洁,不需要计数,只需要看有没有B。
胜利对话框
void _showWinDialog() {
showDialog(context: context, builder: (_) => AlertDialog(
title: const Text('恭喜!'), content: Text('完成关卡 ${currentLevel + 1}! 步数: $moves'),
actions: [
TextButton(onPressed: () { Navigator.pop(context); setState(_loadLevel); }, child: const Text('重玩')),
if (currentLevel < levels.length - 1) TextButton(onPressed: () {
Navigator.pop(context);
setState(() { currentLevel++; _loadLevel(); });
}, child: const Text('下一关')),
],
));
}
胜利后显示对话框,包含关卡号和步数。
重玩按钮
TextButton(onPressed: () { Navigator.pop(context); setState(_loadLevel); }, child: const Text('重玩')),
重新加载当前关卡。Navigator.pop关闭对话框,setState触发重新渲染。
下一关按钮
if (currentLevel < levels.length - 1) TextButton(onPressed: () {
Navigator.pop(context);
setState(() { currentLevel++; _loadLevel(); });
}, child: const Text('下一关')),
如果不是最后一关,显示下一关按钮。
currentLevel++切换到下一关,然后加载。
注意这里用了if表达式,只有条件满足才会创建这个按钮。这是Dart的集合if语法。
箱子状态变化
箱子有两种状态:
B - 普通箱子
橙色方块,还没推到目标点。
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)));
颜色变化给玩家明确的反馈:这个箱子已经到位了。
状态转换
箱子的状态转换:
- B → *:箱子被推到目标点上
-
- → B:箱子被推离目标点(虽然不常见,但可能发生)
目标点的显示
目标点用浅绿色背景:
(c == '.' || c == '+' || c == '*') ? Colors.green[200]
三种情况都是目标点:
- .: 空的目标点,等待箱子
- +: 玩家站在目标点上
- *****: 箱子在目标点上
背景色一致,让玩家知道这些位置是目标点。即使有玩家或箱子在上面,背景色也能提示这是目标位置。
推箱子的限制
推箱子有几个重要限制:
- 只能推不能拉:玩家只能往前推箱子,不能把箱子拉过来
- 一次推一个:不能同时推两个箱子
- 不能推到墙:箱子不能进入墙壁
- 不能推到箱子:箱子不能推到另一个箱子上
这些限制在代码里都有体现:
if (boxTarget == '#' || boxTarget == 'B' || boxTarget == '*') return;
这行代码检查了墙壁和箱子两种情况。
死局
推箱子容易出现死局:箱子被推到角落,再也推不出来了。
当前实现没有检测死局,玩家只能重新开始。
简单的死局检测
检测死局比较复杂,需要判断箱子是否被"卡住"了。简单的判断是:箱子在角落(两面是墙)且不在目标点上。
bool _isDeadlock(int x, int y) {
if (level[y][x] != 'B') return false; // 只检查未到位的箱子
bool wallUp = y == 0 || level[y-1][x] == '#';
bool wallDown = y == level.length-1 || level[y+1][x] == '#';
bool wallLeft = x == 0 || level[y][x-1] == '#';
bool wallRight = x == level[y].length-1 || level[y][x+1] == '#';
return (wallUp || wallDown) && (wallLeft || wallRight);
}
但这只是最简单的情况,还有更复杂的死局(比如两个箱子互相卡住)。
撤销功能
当前实现没有撤销功能,玩家走错了只能重新开始。
可以加一个历史记录:
List<String> history = [];
void _move(int dx, int dy) {
history.add(level.join('
')); // 保存当前状态
// ... 移动逻辑 ...
}
void _undo() {
if (history.isEmpty) return;
level = history.removeLast().split('
');
// 重新找玩家位置
setState(() {});
}
小结
这篇讲了推箱子的箱子目标点,核心知识点:
- 推箱子逻辑:玩家移动方向就是箱子被推的方向
- 状态转换:B→*(箱子到目标点),*→B(箱子离开目标点)
- 玩家状态:@(普通地板)和+(目标点上)
- _setCell方法:用substring拼接修改字符串
- 胜利条件:没有B(所有箱子都在目标点上)
- 颜色反馈:橙色未到位,绿色已到位
- 死局问题:箱子被推到角落无法移动
推箱子的核心就是状态转换,理解了B、*、@、+、.这几个字符的含义和转换规则,游戏逻辑就清楚了。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐
所有评论(0)