Flutter 勇闯2D像素游戏之路(三):人物与地图元素的交互

Flutter 勇闯2D像素游戏之路(一):一个 Hero 的诞生
Flutter 勇闯2D像素游戏之路(二):绘制加载游戏地图
Flutter 勇闯2D像素游戏之路(三):人物与地图元素的交互

前言

在上篇文章中,我们初识了 2D 地图编辑器 Tiled ,完成了从 地图绘制、资源加载基础墙体碰撞 的搭建,为游戏世界奠定了最基本的空间规则。

但仅有 能走、能挡 的地图,还远不足以构成一个真正有生命力的游戏场景。

真正让地图 起来的,是 人物与地图元素之间的交互

在游戏中,地图并不仅仅是静态的背景,它往往承载着多种功能性的元素,例如:

  • 可被触发的 宝箱、机关
  • 控制通行逻辑的 钥匙与门
  • 带来风险与挑战的 陷阱、伤害型障碍物

这些元素都需要与人物产生行为上的联动:
接触、判断、反馈、结果,共同构成完整的交互闭环。

因此,本篇文章将着重于构建 人物与地图元素的交互 ,逐步 介绍、实现 几类常见的地图交互对象,构建一个更加丰富、可扩展的地图交互体系。

前情提示

1. 地图

在进行本章的内容实践之前,请先将之前地图中 门、宝箱、钥匙、地刺元素 先行清除。

本章将把他们从单纯装饰件,进化到真正的可交互件。

接下来所需要用到的 门、宝箱、钥匙、地刺 等图片素材,已在 github仓库 中添加。

2. 墙体

首先,先感谢兄弟的反馈 😊

接下来我们就用 Object LayerTile 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. 必要性

在游戏开发中,墙体、门、障碍区域等 碰撞对象 往往分布在地图的不同层级或组件中。

这类对象的共同特点是:不一定需要渲染,但必须参与碰撞判断

如果每个组件各自实现碰撞逻辑,容易出现以下问题:

  1. 逻辑分散 :碰撞判断分布在多个组件中,角色移动与阻挡逻辑难以统一管理。
  2. 重复实现:不同碰撞对象往往需要相同的尺寸、位置和碰撞检测代码,容易产生冗余。
  3. 扩展不便:新增一种可碰撞对象时,需要重复编写或修改现有碰撞相关逻辑。

因此,有必要建立一个统一的 碰撞管理,将 可参与碰撞 的能力集中管理。

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 前置工作

  1. 新建 Object Layer 图层,命名为 treasure
  2. 点击使用上方矩形框选工具。
  3. 在地图中所需位置,正确绘制宝箱大小的矩形。
  4. 添加属性:
  • 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 前置工作

  1. 新建两个 Object Layer 图层,分别命名为 keydoor
  2. 点击使用上方矩形框选工具。
  3. 在地图中所需位置,正确绘制钥匙和门大小的矩形。
  4. 添加属性:
  • 钥匙
    • 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 前置工作

  1. 新建 Object Layer 图层,命名为 thorn
  2. 点击使用上方矩形框选工具。
  3. 在地图中所需位置,正确绘制地刺大小的矩形。
  4. 添加属性:
  • 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):被地刺等障碍伤害时调用,扣除生命值并显示提示

    dart 复制代码
     void loseHp(int amount) {
         hp = hp - amount;
         if (hp <= 0) {
           UiNotify.showToast(game, '死亡');
         } else {
           UiNotify.showToast(game, 'HP -$amount  剩余 $hp');
         }
     }
  • hp <= 0:触发 gameOver()

    • 播放死亡动画
    • 2 秒后显示重新开始弹窗
    dart 复制代码
      Future<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 ...

  • 完成攻击与技能系统,包括动画切换、攻击范围和远程弹道。

  • 实现怪物生成、自动攻击与玩家碰撞逻辑。

  • 支持局域网多玩家联机功能。

🚪 github 源码
💻 个人门户网站


之前尝试的Demo预览

相关推荐
wonder135795 小时前
UGUI重建流程和优化
unity·游戏开发·ugui
结局无敌5 小时前
Flutter:解构技术基因的创新密码与未来启示
flutter
QuantumLeap丶6 小时前
《Flutter全栈开发实战指南:从零到高级》- 25 -性能优化
android·flutter·ios
游戏技术分享6 小时前
【鸿蒙游戏技术分享 第71期】资质证明文件是否通过
游戏·华为·harmonyos
reddingtons7 小时前
PS 参考图像:线稿上色太慢?AI 3秒“喂”出精细厚涂
前端·人工智能·游戏·ui·aigc·游戏策划·游戏美术
遝靑7 小时前
深入 Flutter 自定义 RenderObject:打造高性能异形滚动列表
flutter
kirk_wang7 小时前
Flutter video_thumbnail 库在鸿蒙(OHOS)平台的适配实践
flutter·移动开发·跨平台·arkts·鸿蒙
走在路上的菜鸟7 小时前
Android学Dart学习笔记第十三节 注解
android·笔记·学习·flutter
小a杰.8 小时前
Flutter跨平台开发权威宝典:架构解析与实战进阶
flutter·架构