【基於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的重構就完成了,定時生成的規則可能有點粗糙,這個後續可能會考慮優化。關於敵機的屬性,目前是寫死的,後續可以考慮做成本地配置。