【Flutter&Flame游戏 - 捌】装弹完毕 | 角色武器发射

3,270 阅读6分钟

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


前言

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

第一季完结,谢谢支持 ~


1. 本文目标

今天来看一下角色如何发射子弹,这里把 子弹 作为 发射物 的统称。少数人不要杠,明明是弓箭,非说是子弹。关于子弹,有些注意点,首先它是基于某个角色进行产出的;其次,它会被频繁创建和销毁。它被销毁的时机包括:命中物体时,移出屏幕,或者超出射程,又或者固定在诞生几秒后自动移除等。

这里使用射程来对子弹进行移除,对水平发射而言,射程就是子弹在水平方向上的偏移距离,如下图蓝框所示区域:


2. 主动触发帧动画

前面我们的弓手是不断循环的帧动画,现在来先看一下如何主动触发:比如下面案例中,按下键盘的 J 键就执行一次动画,代码详见 【08/01】


AdventureronLoad 方法中,指定 playingfalse 可以在开始不会执行帧动画。将 loop 置为 false ,帧就不会重复执行;通过 animationonComplete 回调方法,可以监听到帧动画结束的时机。这里当结束时,触发 _onLastFrame ,置为第一帧:

---->[08/01/Adventurer$onLoad]----
playing = false;
animation = SpriteAnimation.spriteList(
  sprites,
  stepTime: 0.15,
  loop: false,
);
animation!.onComplete = _onLastFrame;

---->[08/01/Adventurer$_onLastFrame]----
void _onLastFrame() {
  animation!.currentIndex = 0;
  animation!.update(0);
}

那如何让执行帧动画呢,很简单:将 playing 置为 true ,然后触发 animationreset 方法即可。如下通过 shoot 方法完成,只要在监听 J 按键,触发 shoot 即方法可。

void shoot() {
  playing = true;
  animation!.reset();
}

3. 子弹的发射

如下,定义 Bullet 构建来表述子弹角色,在构造时指定图片 sprite 和最大射程 maxRange 。子弹在诞生之后,就会一直处于运动状态,可以覆写 update 方法,根据时间和速度计算偏移量。如下 tag1 处所示:当偏移总量大于 maxRange 时,进行移除。

class Bullet extends SpriteComponent {
  double _speed = 200;
  final double maxRange;

  Bullet({required Sprite sprite, required this.maxRange})
      : super(sprite: sprite);

  double _length = 0;

  @override
  void update(double dt) {
    super.update(dt);
    Vector2 ds = Vector2(1, 0) * _speed * dt;
    _length += ds.length;
    position.add(ds);
    if (_length > maxRange) { // tag1
      _length = 0;
      removeFromParent();
    }
  }
}

接下来只要在 Adventurer 动画序列完成后,也就是 _onLastFrame 回调方法中添加子弹即可。这里有两个知识点,其一 priority 可以确定构件的优先级,默认情况下,后被添加的的显示在上层。这里要让子弹在角色下方,把角色优先级高于子弹即可。
第二点是:这里使用 gameRef 添加子弹,而添加入 Adventurer 自身中。因为如果添加到 Adventurer ,其作为子构件,会伴随 Adventurer 移动,这并不符合尝试。比如你扔个石头,离手后它不会随着你的移动而移动。代码详见:【08/02】

---->[08/02/Adventurer]----
late Sprite bulletSprite;

---->[onload]----
bulletSprite = await gameRef.loadSprite('adventurer/weapon_arrow.png');  

void _onLastFrame() async{
  animation!.currentIndex = 0;
  animation!.update(0);
  
  // 添加子弹
  Bullet bullet = Bullet(sprite: bulletSprite,maxRange: 200);
  bullet.size = Vector2(32, 32);
  bullet.anchor = Anchor.center;
  bullet.priority = 1;
  priority = 2;
  bullet.position = position-Vector2(0,-3);
  gameRef.add(bullet);
}

4. 命中处理 - 极简版

如下图所示,接下来把前几篇的知识串联一下:综合角色移动、子弹发射、怪兽受伤害,做个小场景。其中弓箭和怪物的碰撞检测,使用最精简的方式:矩形区域。代码详见:【08/03】

这种校验的思路是:在每帧触发 update 时,校验怪物的矩形区域是否包含某点。比如说,当弓箭的中心在怪物的矩形域中,就表示命中。代码处理如下:

@override
void update(double dt){
  super.update(dt);
  final Iterable<Bullet> bullets = children.whereType<Bullet>();
  for(Bullet bullet in bullets){
    if(bullet.shouldRemove){
      continue;
    }
    if(monster.containsPoint(bullet.absoluteCenter)){ // tag1
      bullet.removeFromParent();
      monster.loss(50);
      break;
    }
  }
}

其中上面tag1 处的 absoluteCenter 代表构件中心的绝对坐标,如下以该点为圆心画了一个小圆示意:


另外,大家可以基于此自己尝试实现怪兽不断发射子弹,攻击主角的功能。经历了这八篇的研究,完成了一个小的交互,也借此简单认识了一下 Flame 框架的使用。到现在算是个尝鲜,还有一些比较重要的基础概念还没涉及:比如 Component 的生命周期、各种 Effect 效果、相机操作、高级的碰撞检测等。在后续会逐步介绍,那本文就到这里,明天见 ~

\