【Flutter&Flame游戏 - 拾壹】探索构件 | Component 使用细节

7,984 阅读5分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第 12 天,点击查看活动详情


前言

这是一套 张风捷特烈 出品的 Flutter&Flame 系列教程,发布于掘金社区。如果你在其他平台看到本文,可以根据对于链接移步到掘金中查看。因为文章可能会更新、修正,一切以掘金文章版本为准。本系列文章一览:

第一季完结,谢谢支持 ~


1.关于 Compoent 树

如下图场景,每个显示的物体都是 Component ,它们形成一个树形结构。代码见 【11/01】

各种角色通过 add 方法添加到树中,此时的树型结构如下:

现在有个问题:因为血条和血量是被加入到 Adventurer 构件中的,所以 Adventurer 的变换行为也会引起血条的变换。如下角色沿 Y 轴镜像,可以看到血条及文字也发生了镜像,这并不是我们所期望的。

那如何解决呢?思路很简单,既然 Adventurer 有单独镜像的需求,那就不能是血条的父级。将两者从父子关系变为兄弟关系即可,这里将血条封装为 LifeComponent 构建,和 Adventurer 一起存在于 HeroComponent 中:


2.角色移动中的镜像反转

现在想实现如下效果:如果触点在角色左侧,角色会镜像反转到左侧,反之,镜像反转到右侧。这样的目的是为了角色可以选择攻击的方向,比如面向左侧攻击左侧怪物:代码见 【11/02】


因为这里只有左右反转,在 HeroComponent 中定义一个 isLeftbool 值用于记录状态。如果需要支持其他方向,比如上、下、左上、右下等,可以通过枚举来维护。

---->[HeroComponent]----
bool isLeft = true;
late Adventurer adventurer;
late LifeComponent lifeComponent;

在点击屏幕时,触发 toTarget 方法,在开始可以通过 _checkFlip 方法来对 isLeft 属性进行维护,已经在需要反转是通过 flip 反转角色:

---->[HeroComponent#toTarget]----
void toTarget(Vector2 target) {
  _checkFlip(target);
  // 略同...
}

void _checkFlip(Vector2 target){
  if (target.x < position.x) {
    if (isLeft) {
      flip();
      isLeft = false;
    }
  }
  if (target.x > position.x) {
    if (!isLeft) {
      flip();
      isLeft = true;
    }
  }
}

用于只想要让主角反转,所以在 flip 中,执行 adventurer.flip 即可。这样就不会影响血条的显示:

void flip({
  bool x = false,
  bool y = true,
}) {
  adventurer.flip(x: x, y: y);
}

---->[HeroComponent#flip]----
void flip({
  bool x = false,
  bool y = true,
}) {
  adventurer.flip(x: x, y: y);
}

另外关于反转,还需要注意子弹的发射方向。因为前面的子弹总是向右侧发射的,如果面朝左侧,应该向左运动,如下所示:

处理起来也比较简单,根据 isLeft 确实向左还是向右发射即可,如下tag1

---->[Bullet]----
@override
void update(double dt) {
  super.update(dt);
  Vector2 ds = Vector2(isLeft ? 1 : -1, 0) * speed * dt; // tag1
  _length += ds.length;
  position.add(ds);
  if (_length > maxRange) {
    _length = 0;
    removeFromParent();
  }
}

3. 关于属性的维护

前面为了方便演示,对于角色的属性,写的比较零散,比如速度、攻击力等。在这里既然可以封装了 HeroComponent 来维护主角类。就可以定义一个 HeroAttr 类来维护主角的属性,如下所示:

class HeroAttr {
  double life; // 生命值
  double speed; // 速度
  double attackSpeed; // 攻击速度
  double attackRange; // 射程
  double attack; // 攻击力
  double crit; // 暴击率
  double critDamage;  // 暴击伤害

  HeroAttr({
    required this.life,
    required this.speed,
    required this.attackSpeed,
    required this.attackRange,
    required this.attack,
    required this.crit,
    required this.critDamage,
  });
}

这样在构建 HeroComponent 时,传入 HeroAttr 对象来确定该对象的属性信息。

---->[TolyGame#onLoad]----
final HeroAttr heroAttr = HeroAttr(
  life: 3000,
  speed: 100,
  attackSpeed: 200,
  attackRange: 200,
  attack: 50,
  crit: 0.75,
  critDamage: 1.5,
);
player = HeroComponent(attr: heroAttr);
add(player);

这样在怪物损失生命值,可以根据 HeroAttr 的属性进行计算:

---->[Liveable]----
void loss(HeroAttr attr) {
  double point = attr.attack;
  double crit = attr.crit;
  double critDamage = attr.critDamage;
  bool isCrit = _random.nextDouble() < crit;
  if (isCrit) {
    point = point * critDamage;
  }
  _damageText.addDamage(-point.toInt(), isCrit: isCrit);
}

添加子弹时,可以根据 HeroAttr 的属性信息确定攻速和射程:


本篇,我们继续拓展了角色的功能,知道父级构件的变换会影响子级组件,所以在使用构件时需要注意构件间的关系。另外通过 HeroAttr 封装了角色信息,这样通过 HeroComponent 就可以添加多个主角节点,就可以双人模式打怪。

到这里,可以看到 TolyGame 中非常乱,下一章我来介绍一下,如何对多个角色和怪物进行管理,包括怪物的生成、发射子弹、命中主角等。那本文就到这里,明天见 ~

\