前言
Component
实现碰撞检测,添加了碰撞的反馈效果后,整个效果就暂时闭环了。本文会着重在敌机Component
的重构工作。借此机会将其余类型的敌机Component
添加进来。
笔者将这一系列文章收录到以下专栏,欢迎有兴趣的同学阅读:
抽象
之前的类Enemy1
是一种类型的敌机Component
,它具备了敌机在飞机大战中的基本功能:
- 在无碰撞情况下,从屏幕最上方匀速移动到屏幕最下方,最终从
Component树
中移除。 - 具备碰撞检测的能力,与
战机Component/子弹Component
发生碰撞时,会有生命值减少情况。 - 生命值为0时,产生销毁/击毁效果。
其实这里还缺了一个与战机Component/子弹Component
发生碰撞时,减少生命值的效果。结合上述几点,我们需要添加多种敌机Component
到屏幕上。所以需要将敌机Component
的特性进一步抽象出来。
结合上述的特性,定义了抽象类Enemy
。
SpriteAnimationComponent
先来说说不同状态的动画帧播放方案。前文我们利用SpriteAnimationComponent
的播放能力,在敌机Component
被击毁时设置playing = true
。但此时敌机Component
至少有3种状态,分别是正常、被攻击、销毁,可以定义一个枚举
enum EnemyState {
idle,
hit,
down,
}
这里可以用SpriteAnimationGroupComponent
代替SpriteAnimationComponent
,通过设置参数current
来切换当前的状态,从而切换对应的动画效果。大概瞄一下源码吧
// sprite_animation_group_component.dart
class SpriteAnimationGroupComponent<T> extends PositionComponent
with HasPaint
implements SizeProvider {
/// Key with the current playing animation
T? current;
/// Map with the mapping each state to the flag removeOnFinish
final Map<T, bool> removeOnFinish;
/// Map with the available states for this animation group
Map<T, SpriteAnimation>? animations;
这里的范型T
可以设置成上面定义的EnemyState
。animations
为不同状态对应的SpriteAnimation
。还有一个removeOnFinish
,可以理解是哪个状态播放完成后,Component自动移除。
// sprite_animation_group_component.dart
SpriteAnimation? get animation => animations?[current];
@mustCallSuper
@override
void render(Canvas canvas) {
animation?.getSprite().render(
canvas,
size: size,
overridePaint: paint,
);
}
@mustCallSuper
@override
void update(double dt) {
animation?.update(dt);
if ((removeOnFinish[current] ?? false) && (animation?.done() ?? false)) {
removeFromParent();
}
}
render
方法会获取对应状态的SpriteAnimation
来渲染。update
会检测动画是否完成,该状态是否需要自动移除。ps:render
方法为每帧绘制的回调。
状态应用
构造方法中,初始的状态为idle
,设置down
状态播放完成后自动移除。
// class Enemy
Enemy(
{required Vector2 initPosition,
required Vector2 size,
required this.life,
required this.speed})
: super(
position: initPosition,
size: size,
current: EnemyState.idle,
removeOnFinish: {EnemyState.down: true}) {
animations = <EnemyState, SpriteAnimation>{};
}
定义三个抽象方法,用于加载不同状态下的SpriteAnimation
,由于hit
状态并非所有敌机Component
都有,所以这里定义为可空。
// class Enemy
Future<SpriteAnimation> idleSpriteAnimation();
Future<SpriteAnimation?> hitSpriteAnimation();
Future<SpriteAnimation> downSpriteAnimation();
在onLoad
中加载。注意这里hit
状态播放完成后,需要将状态重置到idle
状态。
// abstract class Enemy
@override
Future<void> onLoad() async {
animations?[EnemyState.idle] = await idleSpriteAnimation();
final hit = await hitSpriteAnimation();
hit?.onComplete = () {
_enemyState = EnemyState.idle;
};
if (hit != null) animations?[EnemyState.hit] = hit;
animations?[EnemyState.down] = await downSpriteAnimation();
。。。
在碰撞检测中
- 如果已经是
down
状态了,就无需触发等待动画播放完自动移除。 - 如果碰撞目标为
Player/Bullect1
,则需要处理,生命值未到达0前状态更改为hit
,否则为down
。hit
播放完需要变更回idle
,与上述逻辑对应上。
// class Enemy
@override
void onCollisionStart(
Set<Vector2> intersectionPoints, PositionComponent other) {
super.onCollisionStart(intersectionPoints, other);
if (current == EnemyState.down) return;
if (other is Player || other is Bullet1) {
if (current == EnemyState.idle) {
if (life > 1) {
_enemyState = EnemyState.hit;
life--;
} else {
_enemyState = EnemyState.down;
life = 0;
}
。。。
状态变成前,需要重置将要变更状态的SpriteAnimation
。这是为了保证每次变更都是从第一帧开始,不会造成画面异常。
// class Enemy
set _enemyState(EnemyState state) {
if (state == EnemyState.hit) {
animations?[state]?.reset();
}
current = state;
}
Component的移动
之前是通过s = v * t
,在update
方法回调中更新position
的方式实现移动的。这里改成使用MoveEffect
实现。
// class Enemy
add(MoveEffect.to(
Vector2(position.x, gameRef.size.y), EffectController(speed: speed),
onComplete: () {
removeFromParent();
}));
传入speed
,会使用SpeedEffectController
,默认是线性移动的。
// effect_contorller.dart
final isLinear = curve == Curves.linear;
if (isLinear) {
items.add(
duration != null
? LinearEffectController(duration)
: SpeedEffectController(LinearEffectController(0), speed: speed!),
);
}
这部分可参考官方示例: flame/aseprite_example.dart at main · flame-engine/flame (github.com)
新一代敌机Component
简单说一下重构后的敌机Component
,这里以第二个类型类Enemy2
为例,因为它的生命值高可以触发hit
状态。
// class Enemy2
@override
Future<SpriteAnimation?> hitSpriteAnimation() async {
List<Sprite> sprites = [];
sprites.add(await Sprite.load('enemy/enemy2_hit.png'));
sprites.add(await Sprite.load('enemy/enemy2.png'));
final spriteAnimation =
SpriteAnimation.spriteList(sprites, stepTime: 0.15, loop: false);
return spriteAnimation;
}
@override
RectangleHitbox rectangleHitbox() {
return RectangleHitbox(
size: Vector2(size.x, size.y * 0.9), position: Vector2(0, 0));
}
- 以重写
hit
状态的SpriteAnimation
加载为例,这里有一帧的被击中效果。 - 还需要输出一个
RectangleHitbox
,由于不同素材的尺寸有误差,所以这里单独作碰撞箱的修正。
大部分逻辑都在父类Enemy
实现,这里基本只需要实现抽象方法即可。
敌机生成器适配
还记得之前有一个敌机生成器EnemyCreator
,用于定时创建敌机Component
吗?由于添加了不同类型的敌机,所以它的定时触发方法_createEnemy
要作相应修改。在此之前,我们需要定义每款敌机的属性,1、2、3分别代表了类Enemy1、Enemy2、Enemy3
,即小中大类型。属性值就见文知意吧。
// class EnemyCreator
final enemyAttrMapping = {
1: EnemyAttr(size: Vector2(45, 45), life: 1, speed: 50.0),
2: EnemyAttr(size: Vector2(50, 60), life: 2, speed: 30.0),
3: EnemyAttr(size: Vector2(100, 150), life: 4, speed: 20.0)
};
_createEnemy
中,我们通过区间控制每个类型的生成概率。
void _createEnemy() {
final width = gameRef.size.x;
double x = _random.nextDouble() * width;
final double random = _random.nextDouble();
final EnemyAttr attr;
final Enemy enemy;
if (random < 0.5) {
// load Enemy1
} else if (random >= 0.5 && random < 0.8) {
// load Enemy2
} else {
// load Enemy3
}
add(enemy);
}
至此,敌机Component
的重构就告一段落了,后续还会有一些小改动。先来看看目前的效果吧
总结
敌机Component
的重构就完成了,定时生成的规则可能有点粗糙,这个后续可能会考虑优化。关于敌机的属性,目前是写死的,后续可以考虑做成本地配置。