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

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐