【基于Flutter&Flame 的飞机大战开发笔记】子弹升级和补给

2,518 阅读4分钟

前言

敌机Component重构之后,飞机大战的基础能力就基本成型了。本文会对子弹Component作一次升级,添加游戏中的子弹道具,以及它与战机Component碰撞处理

笔者将这一系列文章收录到以下专栏,欢迎有兴趣的同学阅读:

基于Flutter&Flame 的飞机大战开发笔记

子弹特性

前面只是简单的定义了类Bullet1作为子弹Component,根据已有的属性可以定义一个属于子弹的父类Bullet,这里继续使用抽象类。

abstract class Bullet extends SpriteAnimationComponent with CollisionCallbacks {
  Bullet({required this.speed, required this.attack})
      : super(size: Vector2(5, 11));

  double speed;
  int attack;

  Future<SpriteAnimation> bulletAnimation();

  @override
  Future<void> onLoad() async {
    animation = await bulletAnimation();

    add(MoveEffect.to(
        Vector2(position.x, -size.y), EffectController(speed: speed),
        onComplete: () {
      removeFromParent();
    }));
    
    add(RectangleHitbox());
  }

  void loss() {
    removeFromParent();
  }
}

定义比较简单

  • 子弹拥有属于自己的速度speed,伤害attack
  • 固定size = 5 * 11
  • 由于不同子弹的贴图不同,定义一个抽象方法bulletAnimation()由子类加载。
  • 使用MoveEffect代替之前的s = v * t控制移动,这个前文有讲到。这里是由初始位置(一般是战机Component的头部)移动到屏幕最上方
  • loss()方法是与敌机Component发生碰撞时的调用。

以上所列出的就是目前子弹的共有特性了。

升级子弹

游戏中战机Component会通过获得道具的方式,短暂获取强化版子弹。这里用类Bullet2表示

class Bullet2 extends Bullet {
  Bullet2({required super.speed, required super.attack});

  @override
  Future<SpriteAnimation> bulletAnimation() async {
    List<Sprite> sprites = [];
    sprites.add(await Sprite.load('bullet/bullet2.png'));
    final SpriteAnimation spriteAnimation =
        SpriteAnimation.spriteList(sprites, stepTime: 0.15);
    return spriteAnimation;
  }
}

这里会加载另外一种贴图作为子弹的样式。

先来看看最终效果吧

VID_20220711191853_.gif

子弹补给

如上面的效果,游戏中会随机生成子弹补给。这里定义一个抽象类Supply补给Component的效果是匀速向下移动并带有小幅度的晃动

  • 匀速向下移动继续采用MoveEffect的方式。
  • 小幅度的晃动需要使用RotateEffect做一个小幅度的旋转。由于需要一个吊起来来回晃动的效果,所以我们还需要将锚点设置为topCenter,令旋转支点为上方居中的位置。

故最后的逻辑为

abstract class Supply extends SpriteComponent with HasGameRef {
  Supply({position, size})
      : super(position: position, size: size, anchor: Anchor.topCenter);

  @override
  Future<void> onLoad() async {
    add(MoveEffect.to(
        Vector2(position.x, gameRef.size.y), EffectController(speed: 40.0),
        onComplete: () {
      removeFromParent();
    }));

    add(RotateEffect.by(15 / 180 * pi,
        EffectController(duration: 2, reverseDuration: 2, infinite: true)));

    add(RectangleHitbox()..debugMode = true);
  }
}
  • 移动距离是从屏幕最上方向屏幕最下方移动
  • 旋转角度是弧度,这里是(15 / 180)pi,需要来回晃动,所以这里设置了正反方向的持续时间且效果是无限循环的
  • 最后需要添加RectangleHitbox,作为与战机Component的碰撞检测。ps:碰撞检测的逻辑在子类实现,这里目前是类BulletSupply

子类的逻辑几乎只有碰撞检测,这个在下文会讲到,先来看看这个实现的效果 Record_2022-07-11-17-25-45_13914082904e1b7ce2b619733dc8fcfe_.gif

升级与恢复

子弹补给Component中,碰撞检测与战机Component发生碰撞后

  • 会触发其upgradeBullet()方法,此时会变更bulletType,即子弹类型。
  • 添加一个0.15sColorEffect,作为子弹升级的反馈
  • 开启一个Timer,定时5s,作为子弹恢复回正常的计时。ps:_bulletUpgradeTimer设置了autoStart = false不允许自动开启。当调用start()时,进度会重置,可理解为连续获得补给计时会重置。
// class BulletSupply
@override
void onCollisionStart(
    Set<Vector2> intersectionPoints, PositionComponent other) {
  super.onCollisionStart(intersectionPoints, other);
  if (other is Player) {
    other.upgradeBullet();
    removeFromParent();
  }
}

// class Player
int bulletType = 1;

_bulletUpgradeTimer = Timer(5, onTick: _downgradeBullet, autoStart: false);

void upgradeBullet() {
  bulletType = 2;
  add(ColorEffect(Colors.blue.shade900, const Offset(0.3, 0.0),
      EffectController(duration: 0.15)));

  _bulletUpgradeTimer.start();
}

void _downgradeBullet() {
  bulletType = 1;
}

// timer.dart
/// Start the timer from 0.
void start() {
  reset();
  resume();
}

由于战机Component类Player本身拥有一个定时器用于发射子弹,故在子弹类型bulletType修改后,下一次发射子弹的逻辑就需要变更

// class Player
void _addBullet() {
  if (bulletType == 2) {
    final Bullet2 bullet2 = Bullet2(speed: 400, attack: 2);
    bullet2.priority = 1;
    bullet2.position = Vector2(position.x + size.x / 2 - 10, position.y + 10);

    final Bullet2 bullet2a = Bullet2(speed: 400, attack: 2);
    bullet2a.priority = 1;
    bullet2a.position =
        Vector2(position.x + size.x / 2 + 10, position.y + 10);

    gameRef.add(bullet2);
    gameRef.add(bullet2a);
  } else {
    final Bullet1 bullet1 = Bullet1(speed: 200, attack: 1);
    bullet1.priority = 1;
    bullet1.position = Vector2(position.x + size.x / 2, position.y);
    gameRef.add(bullet1);
  }
}

补给生成

还记得那个敌机生成器EnemyCreator吗?这里暂时通过它的定时来生成补给

// class EnemyCreator
void _createEnemy() {
  final width = gameRef.size.x;
  double x = _random.nextDouble() * width;
  final double random = _random.nextDouble();
  if (random < 0.05) {
    final size = Vector2(60, 75);
    if (width - x < size.x / 2) {
      x = width - size.x / 2;
    } else if (x < size.x / 2) {
      x = size.x / 2;
    }
    final enemySupply =
        BulletSupply(position: Vector2(x, -size.y), size: size);

    add(enemySupply);
    return;
  }
  。。。

需要注意的是:由于补给Component锚点被修改为topCenter,所以position的位置也被移动到了topCenter。这里的边界计算需要按此情况作适配。

最后

本文记录子弹类型的扩展、子弹补给的效果和生成实现以及战机Component的子弹升级的逻辑。至此,飞机大战的功能基本上都实现了,剩下附加道具、计分系统以及生命值系统等功能。后续会考虑参考官方的bloc例子对整个项目的结构进行修改。