前言
上篇文章介绍了碰撞检测,本文将以此为基础,添加碰撞后的效果以及之前的逻辑优化。
笔者将这一系列文章收录到以下专栏,欢迎有兴趣的同学阅读:
碰撞检测逻辑变更
之前笔者监听的是碰撞中的回调,考虑到此回调会在碰撞过程中回调多次。所以打算改用碰撞开始的回调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
。也可以注册animation
的onComplete
回调。
// class Enemy1
Enemy1({required Vector2 initPosition, required Vector2 size})
: super(position: initPosition, size: size, removeOnFinish: true);
or
animation!.onComplete = () {
removeFromParent();
};
战机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
的干扰,这里先将自动发射的逻辑停掉。来看看效果
修正敌机Component的碰撞箱
由于敌机Component
的实际绘制的效果会比它的size
小一点,这就导致RectangleHitbox
的面积会大一点,碰撞触发的概率会变大。所以这里大概将RectangleHitbox
的size
缩小到敌机Component
的 80% ,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);