【基於Flutter&Flame 的飛機大戰開發筆記】重構敵機
theme: cyanosis highlight: xcode
前言
Component
實現碰撞檢測,添加了碰撞的反饋效果後,整個效果就暫時閉環了。本文會著重在敵機Component
的重構工作。藉此機會將其餘型別的敵機Component
新增進來。
筆者將這一系列文章收錄到以下專欄,歡迎有興趣的同學閱讀:
抽象
之前的類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
/// Map with the mapping each state to the flag removeOnFinish
final Map
/// Map with the available states for this animation group
Map``
這裡的
範型T可以設定成上面定義的
EnemyState。
animations為**不同狀態對應的
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
Future
Future
在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
,否則為down
。hit
播放完需要變更回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
@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
的重構就告一段落了,後續還會有一些小改動。先來看看目前的效果吧
總結
敵機Component
的重構就完成了,定時生成的規則可能有點粗糙,這個後續可能會考慮優化。關於敵機的屬性,目前是寫死的,後續可以考慮做成本地配置。
- 用華為CameraKit實現預覽和拍照
- 重溫今日頭條螢幕適配方案
- 【基於Flutter&Flame 的飛機大戰開發筆記】展示面板及重新開始選單
- 【基於Flutter&Flame 的飛機大戰開發筆記】利用bloc管理遊戲狀態
- 【基於Flutter&Flame 的飛機大戰開發筆記】子彈升級和補給
- 【基於Flutter&Flame 的飛機大戰開發筆記】重構敵機
- 【基於Flutter&Flame 的飛機大戰開發筆記】子彈發射及碰撞檢測
- 【基於Flutter&Flame 的飛機大戰開發筆記】敵機生成器
- 【基於Flutter&Flame 的飛機大戰開發筆記】搭建專案及建立一架戰機
- 一文搞定移動端接入ncnn模型(包括Android、iOS)
- 在Android上實現Metal的計算Demo
- 移動端執行JS指令碼除錯方案-單元測試
- 為什麼Glide4.x中的AppGlideModule不應該出現在Library中
- CameraX OpenGL預覽的全新版本
-
有關Swift Codable解析成Dictionary
的一些事 - 在iOS應用上進行記憶體監控
- 體驗一下用Metal畫圖
- 在iOS上進行WebP編碼是一種怎樣的體驗之為何cpu佔用如此之高?
- 在iOS上進行WebP編碼是一種怎樣的體驗?