通过网盘分享的文件:game_flutter_openharmony.zip
链接: https://pan.baidu.com/s/1ryUS1A0zcvXGrDaStu530w 提取码: tqip

前言

华容道是一个经典的滑块拼图游戏,目标是把曹操移到底部出口。

游戏里有不同大小的方块,代表不同的人物:曹操是2x2的大方块,关羽张飞是1x2的竖条,黄忠是2x1的横条,小兵是1x1的小方块。

这篇来聊聊这些人物方块怎么实现。华容道的方块大小不一,渲染起来比普通的棋盘游戏复杂一些,需要用绝对定位来布局。
请添加图片描述

状态变量

List<Block> blocks = [];
int moves = 0;

blocks存储所有方块,moves记录移动步数。

Block数据结构

class Block {
  int id, x, y, w, h;
  Color color;
  String name;
  Block({required this.id, required this.x, required this.y, required this.w, required this.h, required this.color, required this.name});
}

每个方块有这些属性:

  • id: 唯一标识,用于区分不同方块
  • x, y: 左上角坐标(格子单位)
  • w, h: 宽度和高度(格子单位)
  • color: 颜色
  • name: 人物名字

坐标和尺寸都用格子单位,不是像素。棋盘是4x5的格子,所以x范围是0-3,y范围是0-4。

为什么用格子单位

用格子单位而不是像素的好处:

  1. 逻辑简单,移动就是坐标加减1
  2. 碰撞检测容易,直接比较坐标
  3. 渲染时再转换成像素,适配不同屏幕

required关键字

Block({required this.id, required this.x, ...})

Dart的命名参数默认是可选的,加required表示必须传入。这样创建Block时不会漏掉参数。

初始布局

void _initGame() {
  moves = 0;
  blocks = [
    Block(id: 0, x: 1, y: 0, w: 2, h: 2, color: Colors.red, name: '曹操'),
    Block(id: 1, x: 0, y: 0, w: 1, h: 2, color: Colors.orange, name: '关羽'),
    Block(id: 2, x: 3, y: 0, w: 1, h: 2, color: Colors.orange, name: '张飞'),
    Block(id: 3, x: 0, y: 2, w: 1, h: 2, color: Colors.green, name: '赵云'),
    Block(id: 4, x: 3, y: 2, w: 1, h: 2, color: Colors.green, name: '马超'),
    Block(id: 5, x: 1, y: 2, w: 2, h: 1, color: Colors.blue, name: '黄忠'),
    Block(id: 6, x: 1, y: 3, w: 1, h: 1, color: Colors.purple, name: '兵1'),
    Block(id: 7, x: 2, y: 3, w: 1, h: 1, color: Colors.purple, name: '兵2'),
    Block(id: 8, x: 0, y: 4, w: 1, h: 1, color: Colors.purple, name: '兵3'),
    Block(id: 9, x: 3, y: 4, w: 1, h: 1, color: Colors.purple, name: '兵4'),
  ];
}

这是经典的"横刀立马"布局,是华容道最著名的开局之一。

曹操

Block(id: 0, x: 1, y: 0, w: 2, h: 2, color: Colors.red, name: '曹操'),

2x2的大方块,红色,在顶部中间(x=1, y=0)。

曹操是最重要的方块,目标就是把他移到底部出口。红色最醒目,让玩家一眼就能找到目标。

五虎将

Block(id: 1, x: 0, y: 0, w: 1, h: 2, color: Colors.orange, name: '关羽'),
Block(id: 2, x: 3, y: 0, w: 1, h: 2, color: Colors.orange, name: '张飞'),
Block(id: 3, x: 0, y: 2, w: 1, h: 2, color: Colors.green, name: '赵云'),
Block(id: 4, x: 3, y: 2, w: 1, h: 2, color: Colors.green, name: '马超'),
Block(id: 5, x: 1, y: 2, w: 2, h: 1, color: Colors.blue, name: '黄忠'),

关羽、张飞、赵云、马超是1x2的竖条,黄忠是2x1的横条。

用不同颜色区分:

  • 关羽、张飞:橙色,在曹操两侧
  • 赵云、马超:绿色,在下方两侧
  • 黄忠:蓝色,横着挡在曹操下面

小兵

Block(id: 6, x: 1, y: 3, w: 1, h: 1, color: Colors.purple, name: '兵1'),
Block(id: 7, x: 2, y: 3, w: 1, h: 1, color: Colors.purple, name: '兵2'),
Block(id: 8, x: 0, y: 4, w: 1, h: 1, color: Colors.purple, name: '兵3'),
Block(id: 9, x: 3, y: 4, w: 1, h: 1, color: Colors.purple, name: '兵4'),

4个1x1的小方块,紫色。小兵最灵活,可以填补空隙。

空位

初始布局有两个空位:(1, 4)和(2, 4),正好在出口位置。这两个空位是解题的关键,需要利用它们来移动方块。

棋盘布局

AspectRatio(
  aspectRatio: 4 / 5,
  child: LayoutBuilder(builder: (_, constraints) {
    double cellW = constraints.maxWidth / 4;
    double cellH = constraints.maxHeight / 5;

棋盘的布局需要考虑宽高比和格子大小的计算。

宽高比

aspectRatio: 4 / 5,

棋盘是4列5行,宽高比4:5。AspectRatio会保持这个比例,不管屏幕多大。

LayoutBuilder

LayoutBuilder(builder: (_, constraints) {

LayoutBuilder可以获取父组件给的约束(constraints),包括最大宽度和高度。

格子大小

double cellW = constraints.maxWidth / 4;
double cellH = constraints.maxHeight / 5;

用LayoutBuilder获取实际尺寸,除以格子数得到每个格子的像素大小。

这样不管屏幕多大,格子都能正确缩放。

方块渲染

...blocks.map((b) => Positioned(
  left: b.x * cellW + 2, top: b.y * cellH + 2,
  child: GestureDetector(
    // ... 手势处理 ...
    child: Container(
      width: b.w * cellW - 4, height: b.h * cellH - 4,
      decoration: BoxDecoration(color: b.color, borderRadius: BorderRadius.circular(4),
        boxShadow: [BoxShadow(color: Colors.black26, blurRadius: 2, offset: const Offset(1, 1))]),
      child: Center(child: Text(b.name, style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold))),
    ),
  ),
)),

这段代码把每个Block渲染成一个可拖动的方块。

Stack + Positioned

用Stack布局,每个方块用Positioned定位。Stack允许子组件重叠,Positioned可以指定精确位置。

left: b.x * cellW + 2, top: b.y * cellH + 2,

位置是格子坐标乘以格子大小,加2像素留出间隙。

方块大小

width: b.w * cellW - 4, height: b.h * cellH - 4,

宽高是格子数乘以格子大小,减4像素(左右各2)留出间隙。

这样方块之间有缝隙,看起来更清晰,也更像真实的滑块拼图。

装饰

decoration: BoxDecoration(
  color: b.color, 
  borderRadius: BorderRadius.circular(4),
  boxShadow: [BoxShadow(color: Colors.black26, blurRadius: 2, offset: const Offset(1, 1))]),
  • color: 方块颜色,从Block对象获取
  • borderRadius: 4像素圆角,让方块看起来更柔和
  • boxShadow: 阴影,增加立体感

人物名字

child: Center(child: Text(b.name, style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold))),

白色加粗文字,居中显示人物名字。让玩家知道每个方块代表谁。

棋盘背景

Container(
  decoration: BoxDecoration(color: Colors.brown[300], border: Border.all(color: Colors.brown[800]!, width: 4)),

棕色背景,深棕色边框,模拟木质棋盘。

颜色选择

color: Colors.brown[300],

浅棕色背景,和方块的颜色形成对比。

border: Border.all(color: Colors.brown[800]!, width: 4),

深棕色边框,4像素宽,让棋盘有边界感。

Colors.brown[800]!后面的感叹号是空断言,因为Colors.brown[800]返回的是Color?类型。

出口

Positioned(left: cellW, bottom: 0, child: Container(width: cellW * 2, height: 4, color: Colors.brown[300])),

底部中间有个出口,用和背景同色的方块盖住边框,形成缺口。

出口宽度是2个格子(和曹操一样宽),曹操要从这里出去。

颜色设计

不同类型的方块用不同颜色:

  • 曹操(2x2): 红色,最醒目,是游戏目标
  • 关羽、张飞(1x2竖): 橙色,在曹操两侧
  • 赵云、马超(1x2竖): 绿色,在下方两侧
  • 黄忠(2x1横): 蓝色,横着的方块
  • 小兵(1x1): 紫色,最小的方块

颜色帮助玩家快速识别方块类型。同类型的方块用同样的颜色,让玩家知道它们的大小和形状是一样的。

💡 颜色选择有讲究。曹操用红色因为他是主角,要最显眼。其他颜色按方块大小分组,同类型同颜色。

展开运算符

...blocks.map((b) => Positioned(...)),

...是展开运算符,把map返回的Iterable展开成多个Widget,放到Stack的children里。

等价于:

children: [
  // 出口
  Positioned(...),
  // 方块们
  Positioned(...), // 曹操
  Positioned(...), // 关羽
  // ...
]

展开运算符让代码更简洁,不需要手动把每个方块加到列表里。

步数显示

Padding(padding: const EdgeInsets.all(16), child: Text('步数: $moves', style: const TextStyle(fontSize: 20))),

显示当前移动了多少步,让玩家追求更少步数通关。

"横刀立马"布局最少需要81步,能在100步内完成就很不错了。

目标提示

const Padding(padding: EdgeInsets.all(16), child: Text('目标: 将曹操移到底部出口', style: TextStyle(color: Colors.grey))),

底部灰色文字提示游戏目标。对于不熟悉华容道的玩家,这个提示很有帮助。

胜利条件

if (block.id == 0 && block.x == 1 && block.y == 3) _showWinDialog();

曹操(id=0)移到坐标(1, 3)时胜利。

为什么是(1, 3)而不是(1, 4)?因为曹操是2x2的,y=3时他的底部正好在y=5的位置,也就是出口。

重置按钮

IconButton(icon: const Icon(Icons.refresh), onPressed: () => setState(_initGame)),

点击刷新按钮重置游戏,方块回到初始位置,步数清零。

小结

这篇讲了华容道的人物方块,核心知识点:

  • Block类:封装位置、大小、颜色、名字,数据结构清晰
  • 格子坐标:用格子单位而不是像素,逻辑简单
  • LayoutBuilder:获取实际尺寸计算格子大小,适配不同屏幕
  • Stack + Positioned:绝对定位布局,适合不规则大小的方块
  • 间隙处理:位置+2,大小-4,留出缝隙
  • 颜色分类:不同类型方块用不同颜色,便于识别
  • 展开运算符:…把Iterable展开成多个Widget
  • 出口设计:用同色方块盖住边框形成缺口

方块是华容道的基础,画好了方块,下一步就是实现滑动移动了。


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

Logo

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

更多推荐