【基于Flutter&Flame 的飞机大战开发笔记】重构敌机

语言: CN / TW / HK

theme: cyanosis highlight: xcode


前言

Component实现碰撞检测,添加了碰撞的反馈效果后,整个效果就暂时闭环了。本文会着重敌机Component的重构工作。借此机会将其余类型的敌机Component添加进来

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

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

抽象

之前的类Enemy1是一种类型的敌机Component,它具备了敌机在飞机大战中的基本功能: - 在无碰撞情况下,从屏幕最上方匀速移动到屏幕最下方,最终从Component树中移除。 - 具备碰撞检测的能力,与战机Component/子弹Component发生碰撞时,会有生命值减少情况。 - 生命值为0时,产生销毁/击毁效果。

其实这里还缺了一个与战机Component/子弹Component发生碰撞时,减少生命值的效果。结合上述几点,我们需要添加多种敌机Component到屏幕上。所以需要将敌机Component的特性进一步抽象出来。

结合上述的特性,定义了抽象类Enemy

SpriteAnimationComponent

先来说说不同状态的动画帧播放方案。前文我们利用SpriteAnimationComponent的播放能力,在敌机Component被击毁时设置playing = true。但此时敌机Component至少有3种状态,分别是正常、被攻击、销毁,可以定义一个枚举 dart enum EnemyState { idle, hit, down, }

这里可以用SpriteAnimationGroupComponent代替SpriteAnimationComponent,通过设置参数current来切换当前的状态,从而切换对应的动画效果。大概瞄一下源码吧 ```dart // sprite_animation_group_component.dart class SpriteAnimationGroupComponent 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 removeOnFinish;

/// Map with the available states for this animation group Map? animations; `` 这里的范型T可以设置成上面定义的EnemyStateanimations为**不同状态对应的SpriteAnimation**。还有一个removeOnFinish`,可以理解是哪个状态播放完成后,Component自动移除

```dart // 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状态播放完成后自动移除dart // 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都有,所以这里定义为可空。 ```dart // class Enemy Future idleSpriteAnimation();

Future hitSpriteAnimation();

Future downSpriteAnimation(); ```

onLoad中加载。注意这里hit状态播放完成后,需要将状态重置到idle状态dart // 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,否则为downhit播放完需要变更回idle,与上述逻辑对应上。 dart // 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。这是为了保证每次变更都是从第一帧开始,不会造成画面异常。 dart // class Enemy set _enemyState(EnemyState state) { if (state == EnemyState.hit) { animations?[state]?.reset(); } current = state; }

Component的移动

之前是通过s = v * t,在update方法回调中更新position的方式实现移动的。这里改成使用MoveEffect实现dart // class Enemy add(MoveEffect.to( Vector2(position.x, gameRef.size.y), EffectController(speed: speed), onComplete: () { removeFromParent(); }));

传入speed,会使用SpeedEffectController,默认是线性移动的。 dart // 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状态。 ```dart // class Enemy2 @override Future hitSpriteAnimation() async { List 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,即小中大类型。属性值就见文知意吧。 dart // 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中,我们通过区间控制每个类型的生成概率dart 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的重构就告一段落了,后续还会有一些小改动。先来看看目前的效果吧

Record_2022-07-11-16-39-13_13914082904e1b7ce2b619733dc8fcfe_.gif

总结

敌机Component的重构就完成了,定时生成的规则可能有点粗糙,这个后续可能会考虑优化。关于敌机的属性,目前是写死的,后续可以考虑做成本地配置。