通过网盘分享的文件: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]

三种情况都是目标点:

  • .: 空的目标点,等待箱子
  • +: 玩家站在目标点上
  • *****: 箱子在目标点上

背景色一致,让玩家知道这些位置是目标点。即使有玩家或箱子在上面,背景色也能提示这是目标位置。

推箱子的限制

推箱子有几个重要限制:

  1. 只能推不能拉:玩家只能往前推箱子,不能把箱子拉过来
  2. 一次推一个:不能同时推两个箱子
  3. 不能推到墙:箱子不能进入墙壁
  4. 不能推到箱子:箱子不能推到另一个箱子上

这些限制在代码里都有体现:

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

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐