Flutter for OpenHarmony游戏集合App实战之华容道人物方块
本文介绍了华容道游戏的人物方块实现方法。游戏采用4x5的棋盘布局,包含不同大小的方块代表曹操(2x2)、五虎将(1x2或2x1)和小兵(1x1)。文章详细讲解了Block类的数据结构设计,包括坐标、尺寸、颜色等属性,并解释了使用格子单位而非像素的优势。同时展示了经典的"横刀立马"初始布局方案,以及通过Stack+Positioned实现方块渲染的技术细节,包括尺寸计算、样式装饰
通过网盘分享的文件: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
- 碰撞检测容易,直接比较坐标
- 渲染时再转换成像素,适配不同屏幕
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
更多推荐
所有评论(0)