【基於Flutter&Flame 的飛機大戰開發筆記】利用bloc管理遊戲狀態

語言: CN / TW / HK

theme: cyanosis highlight: xcode


前言

繼續來開發飛機大戰,遊戲內的基本構成都已經實現。剩下的就是面板功能了,譬如生命值、分數,還有之前一直沒有實現的導彈道具。本文將記錄如何利用bloc來做狀態管理

筆者將這一系列文章收錄到以下專欄,歡迎有興趣的同學閱讀:

基於Flutter&Flame 的飛機大戰開發筆記

遊戲中的bloc運用

由於本文重點不在理解開發模式,這裡貼一篇文章來介紹一下bloc,可以幫助您對此開發模式理解得更通透一些。

Flutter 狀態管理BLoC

在專案中需要新增依賴equatableflame_blocflutter_bloc yaml dependencies: flutter: sdk: flutter flame: ^1.2.0 flame_audio: ^1.0.2 equatable: ^2.0.3 flame_bloc: ^1.6.0 flutter_bloc: ^8.0.1

筆者基於bloc的思想,設計了對於遊戲狀態的幾個類: - GameStatusBlocbloc層,負責處理UI層傳遞過來的事件event,並更新狀態。ps:由於飛機大戰暫時沒有複雜邏輯,這裡的處理基本都是收到一個事件然後更新一個狀態。 - GameStatusState:狀態,這裡表示遊戲的全域性狀態,目前囊括了生命值、分數、遊戲狀態(playing、gameover...)等。 - GameStatusEvent:事件,這裡表示遊戲的全域性事件,譬如遊戲開始、遊戲結束、生命值增加或減少等。

遊戲開始事件為例,看看大概的資料流是怎麼走的: image.png

事件 GameStatusEvent

定義一個事件為遊戲開始,繼承自GameStatusEvent ```dart abstract class GameStatusEvent extends Equatable { const GameStatusEvent(); }

class GameStart extends GameStatusEvent { const GameStart();

@override List get props => []; } ```

狀態 GameStatusState

這裡對遊戲執行狀態有一個列舉GameStatus的定義 dart enum GameStatus { initial, // 初始化 playing, // 遊戲中 gameOver // 遊戲結束 } GameStatusState的定義包括生命值、分數、導彈道具數、遊戲執行狀態 dart class GameStatusState extends Equatable { final int score; final int lives; final GameStatus status; final int bombSupplyNumber; 。。。

bloc層 GameStatusBloc

GameStatusBloc定義了接收到事件GameStart後,如何更新狀態GameStatusState ```dart class GameStatusBloc extends Bloc { GameStatusBloc() : super(const GameStatusState.empty()) { 。。。

on<GameStart>((event, emit) {
  emit(state.copyWith(status: GameStatus.playing));
});

。。。

} } `` 這裡是將遊戲執行狀態GameStatus更新為playing`。

GameStatusBloc的物件會被儲存在Game中,當遊戲開始時,就會呼叫Game#gameStart()將事件傳送出去。ps:這裡類名被修改成SpaceGame,與之前的文章有些不同。 ```dart class SpaceGame extends FlameGame with HasDraggables, HasCollisionDetection { final GameStatusBloc gameStatusBloc;

SpaceGame({required this.gameStatusBloc});

。。。

void gameStart() { gameStatusBloc.add(const GameStart()); }

。。。 } ```

這樣再結合上述的流程圖,一個基於bloc管理的全域性狀態雛型就出來了。可以注意到上述的GameStatusBloc是通過構造方法傳遞下來的,接下來看看它真正建立的地方在哪。

結合flutter_bloc

GameStatusBloc是通過BlocProvider從Flutter的父Widget傳遞下去的,這裡使用MultiBlocProvider支援多個provider。筆者對之前的程式碼進行了擴充套件,GameView裡面包含了Flame中的GameWidget。這樣做主要是想利用Flutter的控制元件來編寫面板展示的邏輯,這個本文不涉及所以可暫不理會。 ```dart void main() { runApp(const MaterialApp( home: GamePage(), )); }

class GamePage extends StatelessWidget { const GamePage({super.key});

@override Widget build(BuildContext context) { return Scaffold( body: MultiBlocProvider( providers: [ // GameStatusBloc的建立 BlocProvider(create: (_) => GameStatusBloc()) ], child: SizedBox( width: MediaQuery.of(context).size.width, height: MediaQuery.of(context).size.height, child: const GameView()), ), ); } }

// class GameView GameWidget(game: SpaceGame(gameStatusBloc: context.read()) ```

然後再回去看看Game#onLoad方法,在Flame中可以通過FlameBlocProviderGameStatusBloc傳遞給子Component子Component可對此進行狀態監聽。這裡使用FlameMultiBlocProvider,支援多個provider。 ```dart @override Future onLoad() async { final ParallaxComponent parallax = await loadParallaxComponent( [ParallaxImageData('background.png')], repeat: ImageRepeat.repeatY, baseVelocity: Vector2(0, 25)); add(parallax);

await add(FlameMultiBlocProvider(providers: [ FlameBlocProvider.value( value: gameStatusBloc) ], children: [ player = Player( initPosition: Vector2((size.x - 75) / 2, size.y + 100), size: Vector2(75, 100)), EnemyCreator(), GameStatusController(), ])); } ```

上述程式碼可知,這裡的Component樹層級關係與之前有所不同 image.png

這樣在FlameMultiBlocProvider下的子Component就能監聽到GameStatusState的變化了。

監聽GameStatusState變化

繼續利用上面的遊戲開始事件為例,筆者在Player#onLoad中添加了一個進場效果,用的是之前的MoveEffect。 ```dart // class Player @override Future onLoad() async { 。。。

add(MoveEffect.to(Vector2(position.x, gameRef.size.y * 0.75), EffectController(duration: 1.5, curve: Curves.easeOutSine)) ..onComplete = () { gameRef.gameStart(); });

add(FlameBlocListener( listenWhen: (pState, nState) { return pState.status != nState.status; }, onNewState: (state) { if (state.status == GameStatus.playing) { _shootingTimer.start(); } else if (state.status == GameStatus.gameOver) { _shootingTimer.stop(); if (_bulletUpgradeTimer.isRunning()) _bulletUpgradeTimer.stop(); current = GameStatus.gameOver; } })); } `` - 進場效果**完成後**,會呼叫Game#gameStart(),這樣就與前面的邏輯形成閉環了,經過bloc的處理,GameStatusState就更新為playing了。 - 還記得這裡之前有一個Timer用於**定時發射子彈**嗎?之前的開啟和停止是依賴onMount/onRemove的,這裡就通過FlameBlocListener回撥的遊戲狀態決定了。 - 筆者將Player改成一個SpriteAnimationGroupComponent了,主要是方便作戰機Component被擊毀的效果,這個**與之前的Enemy`類似**就不多贅述了。【基於Flutter&Flame 的飛機大戰開發筆記】重構敵機

ps:之前的EnemyCreator定時生成的邏輯也是同理。

最後

本文主要記錄基於bloc管理飛機大戰的全域性狀態,相關邏輯參考Flame官方的例子:flame/packages/flame_bloc。後續會基於此狀態來新增遊戲面板的邏輯。