
Flutter 勇闯2D像素游戏之路(一):一个 Hero 的诞生
Flutter 勇闯2D像素游戏之路(二):绘制加载游戏地图
Flutter 勇闯2D像素游戏之路(三):人物与地图元素的交互
前言
在上篇文章中,我们初识了 2D 地图编辑器 Tiled ,完成了从 地图绘制、资源加载 到 基础墙体碰撞 的搭建,为游戏世界奠定了最基本的空间规则。
但仅有 能走、能挡 的地图,还远不足以构成一个真正有生命力的游戏场景。
真正让地图 活 起来的,是 人物与地图元素之间的交互。
在游戏中,地图并不仅仅是静态的背景,它往往承载着多种功能性的元素,例如:
- 可被触发的 宝箱、机关
- 控制通行逻辑的 钥匙与门
- 带来风险与挑战的 陷阱、伤害型障碍物

这些元素都需要与人物产生行为上的联动:
接触、判断、反馈、结果,共同构成完整的交互闭环。
因此,本篇文章将着重于构建 人物与地图元素的交互 ,逐步 介绍、实现 几类常见的地图交互对象,构建一个更加丰富、可扩展的地图交互体系。
前情提示
1. 地图

在进行本章的内容实践之前,请先将之前地图中 门、宝箱、钥匙、地刺元素 先行清除。
本章将把他们从单纯装饰件,进化到真正的可交互件。
接下来所需要用到的 门、宝箱、钥匙、地刺 等图片素材,已在 github仓库 中添加。
2. 墙体

首先,先感谢兄弟的反馈 😊
接下来我们就用 Object Layer 和 Tile Layer 实现上一期的墙体碰撞区,看看区别。

Object Layer

Tile Layer
从图中可以看到:
- Tile Layer 的碰撞区完美和地图中 wall 图层匹配
- Object Layer 的碰撞区完美和 Collisions 对象层匹配
在本文素材的前提之下:
- 如果按 Tile Layer,那么碰撞区范围就偏大了,需要你重新绘制墙体,但是你会发现素材中墙体与下面的地板是在一块的,不能分离,这就不太完美。
- 而 Object Layer 可以让我们在不破坏改变墙体的前提下,更精细调整墙体的碰撞区,让它更合理。
因此,各有利弊 ,看大家的 取舍。
本文中水面的碰撞区就是 Tile Layer 方法,大家可以看一看。
MyHero
一. 本章目标

二. 人物碰撞矩形优化
1. 优化前

dart
// hero_component.dart
@override
Future<void> onLoad() async {
...
size = Vector2(100, 100);
position = Vector2(1000, 1000);
_hitbox = RectangleHitbox();
add(_hitbox);
}
默认情况下,人物的碰撞区域 RectangleHitbox 会 继承父组件的 size ,也就是说碰撞矩形就是 100×100,所以碰撞框很大,通常会比人物实际可视部分大。
这样就导致 上图中问题 ,在一个狭窄的走道内,由于你的超大碰撞区域,而被卡住。
2. 优化后

dart
// hero_component.dart
@override
Future<void> onLoad() async {
...
size = Vector2(100, 100);
position = Vector2(1000, 1000);
_hitbox = RectangleHitbox.relative(
Vector2(0.5, 0.2), // 碰撞区域占组件宽高比例
parentSize: size,
position: Vector2(size.x * 0.25, size.y * 0.7), // 碰撞区域的偏移
);
add(_hitbox);
}
优化后,我们使用 RectangleHitbox.relative 手动缩小碰撞矩形,并调整偏移,使碰撞区域只覆盖角色脚底,即可获取 正常的游戏体验:
- 碰撞矩形与角色可视部分更贴合,避免卡墙。
- 在狭窄走道中角色可以顺畅移动。
- 保留足够的碰撞检测区域,保证游戏逻辑和物理交互仍然有效。
✨总结 :
通过简单缩小并下移碰撞矩形,实现了视觉与碰撞逻辑的分离,就可以极大提升可控性和玩家体验。
三. 统一碰撞管理
1. 必要性

在游戏开发中,墙体、门、障碍区域等 碰撞对象 往往分布在地图的不同层级或组件中。
这类对象的共同特点是:不一定需要渲染,但必须参与碰撞判断。
如果每个组件各自实现碰撞逻辑,容易出现以下问题:
- 逻辑分散 :碰撞判断分布在多个组件中,角色移动与阻挡逻辑难以统一管理。
- 重复实现:不同碰撞对象往往需要相同的尺寸、位置和碰撞检测代码,容易产生冗余。
- 扩展不便:新增一种可碰撞对象时,需要重复编写或修改现有碰撞相关逻辑。
因此,有必要建立一个统一的 碰撞管理,将 可参与碰撞 的能力集中管理。
2. 实现
(1)新建碰撞基类
抽象出一个 BlockerComponent 作为 统一的碰撞基类,后续所有需要参与碰撞的组件直接继承该类即可,避免在各个组件中重复编写碰撞逻辑。
dart
// blocker_component.dart
abstract class BlockerComponent extends SpriteComponent
with CollisionCallbacks {
late final RectangleHitbox hitbox;
BlockerComponent({
super.position,
required super.size,
bool addHitbox = true,
}) {
if (addHitbox) {
hitbox = RectangleHitbox();
add(hitbox);
}
}
@override
Future<void> onLoad() async {
// 创建 1×1 的透明 sprite,占位以满足 SpriteComponent 要求
final recorder = PictureRecorder();
final canvas = Canvas(recorder);
final paint = Paint()..color = const Color(0x00000000);
canvas.drawRect(const Rect.fromLTWH(0, 0, 1, 1), paint);
final picture = recorder.endRecording();
final image = await picture.toImage(1, 1);
sprite = Sprite(image);
}
bool collidesWith(Rect heroRect) {
return hitbox.toAbsoluteRect().overlaps(heroRect);
}
}
-
作为可碰撞组件的基础类
- 继承
SpriteComponent,统一管理位置与尺寸 - 混入
CollisionCallbacks,接入 Flame 碰撞体系 - 使用
RectangleHitbox作为实际的碰撞区域
- 继承
-
解决 SpriteComponent 必须绑定 sprite 的限制
- 在
onLoad中创建一个1×1的透明 sprite - 组件本身不可见,但在引擎层面是合法的渲染组件
- 在
-
提供手动碰撞检测能力
- 通过
collidesWith(Rect heroRect)判断是否发生重叠 - 适用于角色移动过程中的阻挡判断与位移修正
- 通过
(2)修改代码
首先,我们就可以将原来的 wall_component.dart 继承了 BlockerComponent。
dart
// wall_component.dart
class WallComponent extends BlockerComponent {
WallComponent({Vector2? position, required Vector2 size})
: super(position: position, size: size);
}
其次,在 my_game.dart 中,我们将原来只用于收集墙体的列表修改:
dart
// my_game.dart
final List<WallComponent> walls ---> final List<BlockerComponent> blockers
// 统一添加管理
blockers.add();
最后,在 hero_component.dart 中修改:
- 碰撞性能优化集合
dart
final Set<BlockerComponent> _nearbyBlockers = {};
late RectangleHitbox _hitbox;
- 碰撞检测方法
dart
bool _wouldCollideWithBlockers() {
final heroRect = _hitbox.toAbsoluteRect();
// 优先使用游戏维护的 blockers 集合
for (final blocker in game.blockers) {
if (blocker.collidesWith(heroRect)) return true;
}
...
// 兼容附近 blockers
for (final nearby in _nearbyBlockers) {
if (nearby.collidesWith(heroRect)) return true;
}
return false;
}
- 碰撞回调
dart
@override
void onCollisionStart(
Set<Vector2> intersectionPoints,
PositionComponent other,
) {
super.onCollisionStart(intersectionPoints, other);
if (other is BlockerComponent) {
_nearbyBlockers.add(other);
}
}
@override
void onCollisionEnd(PositionComponent other) {
super.onCollisionEnd(other);
if (other is BlockerComponent) {
_nearbyBlockers.remove(other);
}
}
四. Tile Layer 构建水面碰撞区
构建水面碰撞区,简单来说就是遍历 Tiled 地图中的 water 图层,将所有 非空瓦片 转换为对应的 WaterComponent,并按瓦片位置和大小添加到游戏世界中,用于表示水面。
方式一:逐瓦片创建组件(仅展示)

dart
final waterLayer = tiled.tileMap.getLayer<TileLayer>('water');
if (waterLayer != null && waterLayer.data != null) {
final width = waterLayer.width;
final height = waterLayer.height;
final data = waterLayer.data!;
for (var y = 0; y < height; y++) {
for (var x = 0; x < width; x++) {
final index = y * width + x;
final gid = data[index];
if (gid != 0) {
final water = WaterComponent(
position: Vector2(
x * tileSize * mapScale,
y * tileSize * mapScale,
),
size: Vector2(
tileSize * mapScale,
tileSize * mapScale,
),
);
// 添加至统一碰撞列表
blockers.add(water);
await world.add(water);
}
}
}
}
这里将地图中每一个 8x8 的水瓦片,都单独变成一个 WaterComponent 对象。
可以看见地图中水面数量巨大,产生了 大量组件 ,极大降低渲染和碰撞效率。
方式二:批量合并创建组件(推荐)

dart
// my_game.dart
final waterLayer = tiled.tileMap.getLayer<TileLayer>('water');
if (waterLayer != null && waterLayer.data != null) {
await addMergedTileLayerV2(
tileData: waterLayer.data!,
width: waterLayer.width,
height: waterLayer.height,
tileSize: tileSize,
scale: mapScale,
createComponent: (position, size) async {
final water = WaterComponent(position: position, size: size);
// 添加至统一碰撞列表
blockers.add(water);
return water;
},
parent: world,
);
}
这种方式通过 addMergedTileLayerV2 将相邻的水瓦片批量合并为较少的组件:
- 大幅减少
Component数量,提高性能 - 保持碰撞逻辑和渲染一致
- 可以统一加入
blockers列表,方便英雄碰撞预测
可以看见地图中,数量巨大的组件,经过
addMergedTileLayerV2方法批量合并后,仅为3个了。
批量合并方法
dart
// common.dart
/// 扫描 TileLayer 数据,将连续非零瓦片合并成矩形组件(水平+垂直合并)
/// [tileData] - TileLayer.data,按行展开
/// [width] - TileLayer 宽度(列数)
/// [height] - TileLayer 高度(行数)
/// [tileSize] - 每个瓦片大小
/// [scale] - 地图缩放
/// [createComponent] - 创建每个碰撞块的方法
/// [parent] - 父组件,用于添加生成的组件
Future<void> addMergedTileLayerV2({
required List<int> tileData,
required int width,
required int height,
required double tileSize,
double scale = 1.0,
required Future<PositionComponent> Function(Vector2 position, Vector2 size)
createComponent,
required Component parent,
}) async {
// Step1: 先按行生成水平合并块
List<List<int>> rects = []; // 每行的开始列和宽度
List<List<int>> horizontalRects = List.generate(height, (_) => []);
for (var y = 0; y < height; y++) {
int startX = -1;
for (var x = 0; x <= width; x++) {
final isFilled = x < width && tileData[y * width + x] != 0;
if (isFilled && startX == -1) {
startX = x;
}
if ((!isFilled || x == width) && startX != -1) {
horizontalRects[y].addAll([startX, x - startX]);
startX = -1;
}
}
}
// Step2: 垂直合并相同列和宽度的块
List<List<int>> processed = List.generate(height, (_) => List.filled(width, 0));
for (var y = 0; y < height; y++) {
for (var i = 0; i < horizontalRects[y].length; i += 2) {
final xStart = horizontalRects[y][i];
final w = horizontalRects[y][i + 1];
if (processed[y][xStart] == 1) continue;
// 尝试向下合并
int rectHeight = 1;
for (var yy = y + 1; yy < height; yy++) {
bool canMerge = false;
for (var j = 0; j < horizontalRects[yy].length; j += 2) {
if (horizontalRects[yy][j] == xStart &&
horizontalRects[yy][j + 1] == w) {
canMerge = true;
break;
}
}
if (canMerge) {
rectHeight++;
for (var col = xStart; col < xStart + w; col++) {
processed[yy][col] = 1;
}
} else {
break;
}
}
// 创建组件
final position =
Vector2(xStart * tileSize * scale, y * tileSize * scale);
final size = Vector2(w * tileSize * scale, rectHeight * tileSize * scale);
final block = await createComponent(position, size);
await parent.add(block);
for (var col = xStart; col < xStart + w; col++) {
processed[y][col] = 1;
}
}
}
}
addMergedTileLayerV2用于 扫描 TileLayer 的瓦片数据 ,将相邻的非空瓦片先进行水平方向合并 ,再在此基础上进行垂直方向合并 ,最终把多个连续瓦片合并为更大的矩形组件。
五. 宝箱 - 无验证交互式解锁

1. 简述
宝箱 是一种无需条件验证的可交互地图对象,通过角色接触即可触发交互。
-
继承关系:无需继承碰撞基类
-
功能:提供可拾取奖励
-
交互逻辑:
- 玩家靠近宝箱即可触发打开动作
- 打开后玩家可选择是否拾取奖励
2. Tiled 前置工作

- 新建
Object Layer图层,命名为treasure。 - 点击使用上方矩形框选工具。
- 在地图中所需位置,正确绘制宝箱大小的矩形。
- 添加属性:
- type:trasure
- status:表示 宝箱状态 ,一共有三种:closed(关闭) 、full(打开未拿取) 、enmpty(打开已拿取)
3. 构建宝箱类
dart
// treasure_component.dart
class TreasureComponent extends SpriteComponent
with HasGameReference<MyGame>, CollisionCallbacks {
String status;
late RectangleHitbox _hitbox;
TreasureComponent({
required this.status,
required Vector2 position,
required Vector2 size,
}) : super(position: position, size: size);
@override
Future<void> onLoad() async {
await super.onLoad();
sprite = await Sprite.load(
status == 'closed'
? 'closed_treasure.png'
: status == 'full'
? 'full_treasure.png'
: 'empty_treasure.png',
);
_hitbox = RectangleHitbox();
add(_hitbox);
}
@override
void onCollisionStart(
Set<Vector2> intersectionPoints,
PositionComponent other,
) {
super.onCollisionStart(intersectionPoints, other);
if (other is HeroComponent) {
attemptOpen(other);
}
}
Future<void> _open() async {
if (status == 'closed') {
status = 'full';
sprite = await Sprite.load('full_treasure.png');
}
}
Future<void> _collect(HeroComponent hero) async {
if (status == 'full') {
status = 'empty';
sprite = await Sprite.load('empty_treasure.png');
UiNotify.showToast(game, '获得宝物');
}
}
void attemptOpen(HeroComponent hero) {
if (status == 'closed') {
_open();
} else if (status == 'full') {
final exists = game.camera.viewport.children
.query<DialogComponent>()
.isNotEmpty;
if (!exists) {
final dialog = DialogComponent.confirm(
message: '是否拿取宝物',
onConfirm: () => _collect(hero),
onCancel: () {},
);
game.camera.viewport.add(dialog);
}
}
}
}
TreasureComponent 作为宝箱实体类,负责 显示与交互 的逻辑:
-
继承
SpriteComponent+ 碰撞回调- 具备位置、尺寸和碰撞检测能力
-
状态管理
status = closed:关闭状态,触碰后自动打开status = full:已打开状态,弹出对话框确认是否拾取status = empty:已拿取状态,仅显示空宝箱
-
视图更新
- 根据宝箱状态动态加载不同贴图
-
交互逻辑
- 人物触碰到宝箱区域后,根据当前状态,决定下一步行为
4. 加载宝箱
dart
// my_game.dart
final treasureLayer = tiled.tileMap.getLayer<ObjectGroup>('treasure');
if (treasureLayer != null) {
for (final obj in treasureLayer.objects) {
if (obj.properties['type']?.value == 'treasure') {
final status = obj.properties['status']!.value as String;
final x = mapScale * obj.x;
final y = mapScale * obj.y;
final w = mapScale * obj.width;
final h = mapScale * obj.height;
final treasureComponent = TreasureComponent(
status: status,
position: Vector2(x, y),
size: Vector2(w, h),
);
treasureComponent.debugMode = true;
await world.add(treasureComponent);
}
}
}
六. 钥匙与门 - 验证交互式解锁

1. 简述
钥匙与门 是一组具备条件验证的交互对象,通过 拾取钥匙 → 解锁对应门 实现地图通行控制。
-
继承关系:门继承碰撞基类,当门未打开时具备阻挡能力,与墙体或水面碰撞逻辑一致
-
钥匙 :可拾取对象,角色接触后获得对应
keyId -
门:阻挡地图通行,需角色持有匹配钥匙才能打开
-
交互逻辑:
- 角色与门交互时,判断是否持有匹配钥匙
- 条件满足时门打开,否则保持阻挡状态
2. Tiled 前置工作

- 新建两个
Object Layer图层,分别命名为key、door。 - 点击使用上方矩形框选工具。
- 在地图中所需位置,正确绘制钥匙和门大小的矩形。
- 添加属性:
钥匙:- type:key
- keyId:对应指定的钥匙与门
门:- type:door
- keyId:对应指定的钥匙与门
- status:表示 门状态 ,一共有两种:closed(关闭) 、open(打开)
3. 构建钥匙类
dart
// key_component.dart
class KeyComponent extends SpriteComponent
with HasGameReference<MyGame>, CollisionCallbacks {
final String keyId;
KeyComponent({
required this.keyId,
required Vector2 position,
required Vector2 size,
}) : super(position: position, size: size);
@override
Future<void> onLoad() async {
await super.onLoad();
sprite = await Sprite.load('key.png'); // 直接加载 assets/key.png
add(RectangleHitbox());
}
@override
void onCollisionStart(Set<Vector2> intersectionPoints, PositionComponent other) {
super.onCollisionStart(intersectionPoints, other);
if (other is HeroComponent) {
other.addKey(keyId);
removeFromParent(); // 拾取后移除当前 tile
}
}
}
KeyComponent 作为可拾取的钥匙实体,用于角色解锁对应门:
-
继承
SpriteComponent+ 碰撞回调- 具备位置、尺寸和碰撞检测能力
-
钥匙标识
keyId- 用于与门的
keyId对应,实现解锁验证
- 用于与门的
-
碰撞拾取逻辑
- 当角色碰到钥匙:
将keyId存入角色钥匙集合
从地图中移除钥匙实体,避免重复拾取
- 当角色碰到钥匙:
4. 构建门类
dart
// door_component.dart
class DoorComponent extends BlockerComponent with HasGameReference<MyGame> {
final String keyId;
bool isOpen;
DoorComponent({
required this.keyId,
required this.isOpen,
super.position,
required super.size,
}) : super(
addHitbox: !isOpen);
@override
Future<void> onLoad() async {
await super.onLoad();
sprite = await Sprite.load(isOpen ? 'open_door.png' : 'closed_door.png');
}
void _unlock() async {
if (!isOpen) {
isOpen = true;
sprite = await Sprite.load('open_door.png');
hitbox.removeFromParent();
}
}
void attemptOpen(HeroComponent hero) {
if (!isOpen && hero.hasKey(keyId)) {
unlock();
} else if (!isOpen) {
UiNotify.showToast(game, '需要钥匙 $keyId 才能打开');
}
}
}
DoorComponent 作为门的实体类,主要职责是 阻挡角色通行并支持钥匙验证解锁:
-
继承
BlockerComponent- 默认有碰撞体阻挡角色
- 可根据
isOpen状态决定是否添加碰撞体
-
状态管理
isOpen = false:门关闭,阻挡角色isOpen = true:门打开,移除碰撞体,允许角色通过
-
钥匙验证逻辑
-
attemptOpen(hero):当角色接触门时触发- 如果角色持有匹配
keyId,调用_unlock()打开门 - 否则弹出提示,告知需要钥匙
- 如果角色持有匹配
-
-
视图更新
- 根据门状态动态加载不同贴图(
open_door.png/closed_door.png) - 打开时同步移除碰撞体,保证角色能通过
- 根据门状态动态加载不同贴图(
5. 加载钥匙与门
dart
// my_game.dart
// ---- 处理 Key Layer 中的钥匙 ----
final keyLayer = tiled.tileMap.getLayer<ObjectGroup>('key');
if (keyLayer != null) {
for (final obj in keyLayer.objects) {
if (obj.properties['type']?.value == 'key') {
final keyId = obj.properties['keyId']!.value as String;
final x = mapScale * obj.x;
final y = mapScale * obj.y;
final w = mapScale * obj.width;
final h = mapScale * obj.height;
final keyComponent = KeyComponent(
keyId: keyId,
// Tiled 对象坐标为左上或底对齐;这里使用底对齐放置更符合 tile 外观
position: Vector2(x, y),
size: Vector2(w, h),
);
keyComponent.debugMode = true;
await world.add(keyComponent);
}
}
}
// ---- 处理 Door Layer 中的门 ----
final doorLayer = tiled.tileMap.getLayer<ObjectGroup>('door');
if (doorLayer != null) {
for (final obj in doorLayer.objects) {
if (obj.properties['type']?.value == 'door') {
final keyId = obj.properties['keyId']!.value as String;
final x = mapScale * obj.x;
final y = mapScale * obj.y;
final w = mapScale * obj.width;
final h = mapScale * obj.height;
final doorComponent = DoorComponent(
keyId: keyId,
isOpen: obj.properties['status']?.value == 'open' ? true : false,
// Tiled 对象坐标为左上或底对齐;这里使用底对齐放置更符合 tile 外观
position: Vector2(x, y),
size: Vector2(w, h),
);
doorComponent.debugMode = true;
await world.add(doorComponent);
}
}
}
6. 完善逻辑
在 HeroComponent 中,为角色添加 钥匙管理与门验证 功能:
(1). 钥匙管理
dart
final Set<String> keys = {};
- 存储角色已拾取的钥匙
keyId集合
dart
void addKey(String keyId) {
keys.add(keyId);
UiNotify.showToast(game, '获得钥匙: $keyId');
}
- 角色拾取钥匙时调用
- 弹出提示通知玩家获得钥匙
dart
bool hasKey(String keyId) => keys.contains(keyId);
- 判断角色是否持有指定钥匙
- 用于门解锁验证
(2). 门交互逻辑
dart
for (final door in game.world.children.query<DoorComponent>()) {
if (!door.isOpen && door.collidesWith(heroRect)) {
door.attemptOpen(this);
if (!door.isOpen) return true;
}
}
在角色移动碰撞检测 _wouldCollideWithBlockers() 中:
- 遍历所有门组件
- 检测角色是否与关闭的门发生碰撞
- 调用
door.attemptOpen(this)尝试解锁 - 如果门仍未打开,则返回阻挡信息,阻止角色移动
七. 地刺

1. 简述
地刺 是一种周期性造成伤害的地图障碍,通过状态切换形成完整的伤害与死亡判定逻辑。
-
继承关系:无需继承碰撞基类
-
功能:对接触的角色造成伤害
-
状态:突出与收起两种状态,周期性切换
-
交互逻辑:
- 角色接触突出状态的地刺会受到伤害
- 触发死亡判定后可触发游戏重启或复活逻辑
- 周期性切换状态增强地图挑战性
2. Tiled 前置工作

- 新建
Object Layer图层,命名为thorn。 - 点击使用上方矩形框选工具。
- 在地图中所需位置,正确绘制地刺大小的矩形。
- 添加属性:
- type:thorn
- status:表示 地刺状态 ,一共有两种:on(突出) 、off(收入)
3. 构建地刺类
dart
// thorn_component.dart
class ThornComponent extends SpriteComponent
with HasGameReference<MyGame>, CollisionCallbacks {
String status;
late RectangleHitbox _hitbox;
double _elapsed = 0;
double period = 2.0;
bool hurted = false;
ThornComponent({super.position, required super.size, required this.status});
@override
Future<void> onLoad() async {
await super.onLoad();
sprite = await Sprite.load(
status == 'on' ? 'thorn_on.png' : 'thorn_off.png',
);
_hitbox = RectangleHitbox();
add(_hitbox);
}
@override
void onCollisionStart(
Set<Vector2> intersectionPoints,
PositionComponent other,
) {
super.onCollisionStart(intersectionPoints, other);
if (other is HeroComponent) {
attemptDamage(other);
}
}
void attemptDamage(HeroComponent hero) {
if (status == 'on' && !hurted) {
hero.loseHp(1);
hurted = true;
}
}
void _toggle() async {
if (status == 'on') {
status = 'off';
} else {
status = 'on';
hurted = false;
}
sprite = await Sprite.load(
status == 'on' ? 'thorn_on.png' : 'thorn_off.png',
);
}
@override
void update(double dt) {
super.update(dt);
_elapsed += dt;
if (_elapsed >= period) {
_elapsed = 0;
_toggle();
}
}
}
ThornComponent 负责地刺的显示、周期切换和伤害逻辑:
-
继承
SpriteComponent+ 碰撞回调- 具备位置、尺寸和碰撞检测能力
-
状态管理
on:突出,触碰造成伤害off:收起,不造成伤害
-
伤害逻辑
attemptDamage(hero):保证每周期只造成一次伤害
-
周期切换
_toggle()在固定period时间间隔内切换状态,并更新贴图
4. 加载地刺
dart
// my_game.dart
// ---- 处理 thorn 中的荆棘 ----
final thornLayer = tiled.tileMap.getLayer<ObjectGroup>('thorn');
if (thornLayer != null) {
for (final obj in thornLayer.objects) {
if (obj.properties['type']?.value == 'thorn') {
final status = obj.properties['status']!.value as String;
final x = mapScale * obj.x;
final y = mapScale * obj.y;
final w = mapScale * obj.width;
final h = mapScale * obj.height;
final thornComponent = ThornComponent(
status: status,
position: Vector2(x, y),
size: Vector2(w, h),
);
thornComponent.debugMode = true;
await world.add(thornComponent);
}
}
}
5. 完善逻辑
在 HeroComponent 中, 实现了角色 生命值管理 + 死亡判定 的功能:
-
增加
hp属性,记录角色生命值dart// 生命值 int hp = 5; -
loseHp(amount):被地刺等障碍伤害时调用,扣除生命值并显示提示dartvoid loseHp(int amount) { hp = hp - amount; if (hp <= 0) { UiNotify.showToast(game, '死亡'); } else { UiNotify.showToast(game, 'HP -$amount 剩余 $hp'); } } -
当
hp <= 0:触发gameOver()- 播放死亡动画
- 2 秒后显示重新开始弹窗
dartFuture<void> gameOver() async { if (_isGameOver) return; _isGameOver = true; // 播放死亡动画 if (animations.containsKey(HeroState.dead)) { _setState(HeroState.dead); } // 等待 2 秒,让死亡动画播放 await Future.delayed(const Duration(seconds: 2)); // 显示重新开始弹窗 final exists = game.camera.viewport.children .query<RestartOverlay>() .isNotEmpty; if (!exists) { game.camera.viewport.add(RestartOverlay()); } }
八. 总结与展望
总结
本章主要介绍了 Flutter&Flame 开发 2D像素游戏 关于 人物与地图元素交互 的基础实践。
通过上述步骤,我们完成了宝箱、钥匙、门 和 地刺 这几类常见元素的交互 。
截至目前为止,游戏主要包括了以下内容:
- 角色与动画 :使用精灵图 (
SpriteSheet) 创建角色,支持 idle/run 等动画状态切换。 - 玩家交互:通过摇杆控制角色移动,并根据方向翻转动画。
- 地图加载 :通过
Tiled绘制并在 Flame 中加载的 2d像素地图。 - 地图交互 :通过组件化模式,新建了多个可供交互的组件如(门、钥匙、宝箱、地刺),为游戏增加了互动性。
- 统一碰撞区检测 :将角色与 需要产生碰撞 的物体统一管理,并实现碰撞时的 平滑侧移。
展望
-
思考 🤔 一个有趣的游戏机制ing ...
-
完成攻击与技能系统,包括动画切换、攻击范围和远程弹道。
-
实现怪物生成、自动攻击与玩家碰撞逻辑。
-
支持局域网多玩家联机功能。

之前尝试的Demo预览