【基于Flutter&Flame 的飞机大战开发笔记】飞机碰撞效果及优化

193 阅读3分钟

前言

上篇文章介绍了碰撞检测,本文将以此为基础,添加碰撞后的效果以及之前的逻辑优化

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

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

碰撞检测逻辑变更

之前笔者监听的是碰撞中的回调,考虑到此回调会在碰撞过程中回调多次。所以打算改用碰撞开始的回调onCollisionStart。ps:也可以重写onCollisionStartCallback

又由于碰撞回调如果在两个Component上注册的话,在飞机大战这个场景下可能会出现一次事件两个Component处理不一致的问题。所以暂时改为只在敌机Component上面作监听,然后在分发给碰撞目标。ps:这方面如果有更好建议的同学欢迎评论留言。

// class Enemy1
@override
void onCollisionStart(
    Set<Vector2> intersectionPoints, PositionComponent other) {
  super.onCollisionStart(intersectionPoints, other);
  if (destroyed) return;
  if (other is Player) {
    other.loss();
    playing = true;
  } else if (other is Bullet1) {
    other.loss();
    playing = true;
  }
}

上述代码多了一些新东西,下面会讲到。大概是碰撞开始时,如果碰撞目标是战机Component会调用其loss()作出相应的反馈,譬如扣除其生命值,撞击效果,甚至是game over;子弹Component同理

敌机Component的击毁效果

当碰撞发生后,目前这个是小号的敌机Component,所以碰撞后就会击毁了。在onLoad时添加多几帧作为击毁的动画效果,同时先将playing = false,这里不需要自动播放

// class Enemy1
@override
Future<void> onLoad() async {
  List<Sprite> sprites = [];
  sprites.add(await Sprite.load('enemy/enemy1.png'));
  for (int i = 1; i <= 4; i++) {
    sprites.add(await Sprite.load('enemy/enemy1_down$i.png'));
  }

  playing = false;
  。。。
}

在碰撞发生后,如果需要移除敌机Component,就将playing = true,击毁效果就会播放。为防止播放过程中产生的碰撞,这里自定义了一个destroyed标记位。

// class Enemy1
bool get destroyed => isRemoving || playing;

这就有了上面的代码,这里再贴一下

// class Enemy1
@override
void onCollisionStart(
    Set<Vector2> intersectionPoints, PositionComponent other) {
  super.onCollisionStart(intersectionPoints, other);
  if (destroyed) return;
  if (other is Player) {
    other.loss();
    playing = true;
  } else if (other is Bullet1) {
    other.loss();
    playing = true;
  }
}

至于怎么让敌机Component在播放完成后从Component树中移除,这里可以在父类构造方法中传入removeOnFinish: true。也可以注册animationonComplete回调。

// class Enemy1
Enemy1({required Vector2 initPosition, required Vector2 size})
    : super(position: initPosition, size: size, removeOnFinish: true);
    
or

animation!.onComplete = () {
    removeFromParent();
};

Record_2022-07-08-15-21-14_13914082904e1b7ce2b619733dc8fcfe_.gif

战机Component的碰撞效果

目前战机Component只会在与敌机Component碰撞时才会出现反馈,这里先考虑这一种情况。笔者期望的效果是:

  • 碰撞后机身会有短暂的白色渐变
  • 碰撞后机身会有短暂的随机振动
  • 上述两个效果持续0.5s

基于这3点,来看看上述讲到的loss()方法

// class Player
void loss() {
  removeAll(children.whereType<ColorEffect>());
  removeAll(children.whereType<MoveByEffect>());

  ColorEffect collisionColorEffect = ColorEffect(
      Colors.white,
      const Offset(0.0, 0.5),
      EffectController(duration: 0.25, reverseDuration: 0.25));
  MoveByEffect collisionNoiseEffect = MoveByEffect(
      Vector2(4, 0), NoiseEffectController(duration: 0.5, frequency: 20));

  add(collisionNoiseEffect);
  add(collisionColorEffect);
}
  • 白色渐变效果这里使用ColorEffect实现,设置从0%到50%的渐变然后恢复,0%-50%-0%的时候各为0.25s。这个过程默认是线性的。
  • 随机振动这里使用MoveByEffect配合噪音效果控制NoiseEffectController实现。
    • MoveByEffect意为移动了多少位移,这里是设置振动偏移量的意思。
    // move_by_effect.dart
    MoveByEffect(
      Vector2 offset,
      EffectController controller, {
      PositionProvider? target,
      void Function()? onComplete,
    })  : _offset = offset.clone(),
          super(controller, target, onComplete: onComplete);
    
    • NoiseEffectController是一个效果控制器,这里会随机产生数值来作为偏移的增量。这里设置时长为0.5s,频率为20
    // noise_effect_controller.dart
    NoiseEffectController({
      required double duration,
      required this.frequency,
      this.taperingCurve = Curves.easeInOutCubic,
      Random? random,
    })  : assert(duration > 0, 'duration must be positive'),
          assert(frequency > 0, 'frequency parameter must be positive'),
          noise = SimplexNoise(random),
          super(duration);
    。。。      
    @override
    double get progress {
      final x = timer / duration;
      final amplitude = taperingCurve.transform(1 - x);
      return noise.noise2D(x * frequency, 0) * amplitude;
    }      
    
  • 为了防止在其过程中又和其他发生碰撞,所以每次都会先将前一次的Effect移除掉。

为了避免子弹Component的干扰,这里先将自动发射的逻辑停掉。来看看效果 Record_2022-07-08-16-14-16_13914082904e1b7ce2b619733dc8fcfe_.gif

修正敌机Component的碰撞箱

由于敌机Component的实际绘制的效果会比它的size小一点,这就导致RectangleHitbox的面积会大一点碰撞触发的概率会变大。所以这里大概将RectangleHitboxsize缩小到敌机Component80%position也需要移动 10% 来保证RectangleHitbox居中。ps:这里锚点默认为左上角

// class Enemy1
add(RectangleHitbox(
    size: Vector2(size.x * 0.8, size.y * 0.8),
    position: Vector2(size.x * 0.1, size.y * 0.1))
  ..debugMode = true);