【基於Flutter&Flame 的飛機大戰開發筆記】展示面板及重新開始選單

語言: CN / TW / HK

theme: cyanosis highlight: xcode


前言

前面基於bloc管理的全域性狀態已經成型。本文將會利用該資料基於Flutter的控制元件搭建遊戲的展示面板,以及關於重新開始遊戲的選單邏輯。

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

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

面板展示

還記得之前封裝的GameView嗎?這裡是全部邏輯,在GameWidget之上還有一層Stack,用於不同面板的展示。需要注意的是GameView父WidgetMultiBlocProvider,這樣它的子Widget才能獲取得到GameStatusBloc。 ``` class GameView extends StatelessWidget { const GameView({Key? key}) : super(key: key);

@override Widget build(BuildContext context) { return Stack( children: [ Positioned.fill( child: GameWidget( game: SpaceGame(gameStatusBloc: context.read()), overlayBuilderMap: { 'menu_reset': (_, game) { return ResetMenu(game: game as SpaceGame); } }, )), SafeArea( child: Stack(children: [ const Positioned(top: 4, right: 4, child: ScorePanel()), Positioned( bottom: 4, right: 4, left: 4, child: Row( children: const [ Expanded(child: BombPanel()), Expanded(child: LivePanel()), ], ), )], )) ], ); } } ```

生命值面板

單獨拿生命值面板來聊,其實就是常規的bloc模式,通過BlocBuilder來監聽GameStatusState,更新後會觸發builder方法重新重新整理ui。這個就是Flutter原生層面的知識點了。

需要注意的是,這裡用了Offstage對檢視進行隱藏和顯示,條件是上篇文章說的GameStatus遊戲的執行狀態。 ``` class LivePanel extends StatelessWidget { const LivePanel({super.key});

@override Widget build(BuildContext context) { return BlocBuilder( builder: (context, state) { int live = state.lives; return Offstage( offstage: state.status != GameStatus.playing, child: Wrap( spacing: 2, runSpacing: 5, alignment: WrapAlignment.end, children: List.generate( live, (index) => Container( constraints: const BoxConstraints(maxWidth: 35, maxHeight: 35), child: const Image( image: AssetImage('assets/images/player/life.png')), )).toList(), ), ); }); } } ```

效果

來看看面板展示的效果吧

Screenshot_2022-07-15-15-23-31-22_13914082904e1b7ce2b619733dc8fcfe.jpg

  • 生命值面板在右下角三個小飛機,監聽的是GameStatusState.lives
  • 還有兩個,一個是導彈道具,一個是計分。這兩個和上述的大同小異,這裡就不展開說了。
    • 導彈道具面板,在左下角,順帶一提之前沒有提及這個功能,它是在遊戲中通過道具獲得,和子彈補給同理。監聽的是GameStatusState.bombSupplyNumber
    • 計分面板,在右上角,擊落一艘敵機Component就會有相應的計分。監聽的是GameStatusState.score

GameOver與Replay

GameStatusController

戰機Component生命值到0時,使GameStatusState.status == GameStatus.gameOver。來看一下GameStatusBloc的處理邏輯。 // class GameStatusBloc on<PlayerLoss>((event, emit) { if (state.lives > 1) { emit(state.copyWith(lives: state.lives - 1)); } else { emit(state.copyWith(lives: 0, status: GameStatus.gameOver)); } }); 在上文中,我們Component樹中塞多了一個叫GameStatusController的東西。答案在這裡揭曉了,它是專門用於響應當遊戲執行狀態變化時介面變化的。 - 當GameStatusState.status == GameStatus.gameOver時,需要先暫停遊戲的執行時(還記得Flame有一個update方法回撥嗎?他是依賴執行時響應的)。然後展示GameOver選單。 - GameOver選單會展示分數和一個Replay按鈕。 - Replay按鈕點選後,會重新將GameStatusState.status == GameStatus.initial,此時恢復遊戲的執行時,與之前的遊戲開始邏輯形成閉環。 ``` class GameStatusController extends Component with HasGameRef { @override Future onLoad() async { add(FlameBlocListener( listenWhen: (pState, nState) { return pState.status != nState.status; }, onNewState: (state) { if (state.status == GameStatus.initial) { gameRef.resumeEngine(); gameRef.overlays.remove('menu_reset');

    if (parent == null) return;
    parent!.removeAll(parent!.children.where((element) {
      return element is Enemy || element is Supply || element is Bullet;
    }));
    parent!.add(gameRef.player = Player(
        initPosition:
            Vector2((gameRef.size.x - 75) / 2, gameRef.size.y + 100),
        size: Vector2(75, 100)));
  } else if (state.status == GameStatus.gameOver) {
    Future.delayed(const Duration(milliseconds: 600)).then((value) {
      gameRef.pauseEngine();
      gameRef.overlays.add('menu_reset');
    });
  }
}));

} } `` - 還是利用FlameBlocListener監聽GameStatusState的變化。 -GameStatus.gameOver時,通過gameRef.pauseEngine()可**暫停**遊戲的執行時。這裡的gameRef.overlays.add('menu_reset')會在檢視最上層新增一個選單。下面會講到。 -GameStatus.initial時,通過gameRef.resumeEngine()可**恢復**遊戲的執行時,並移除剛剛那個選單。順帶一提,這裡需要**移除部分Component**,譬如敵機Component、補給Component、子彈Component。還需要重新新增一個戰機Component`,因為之前那個已經被移除了。

GameOver選單

GameWidget提供一個overlayBuilderMap屬性,可以傳一個key-value。value為該檢視的builder方法。 GameWidget( game: SpaceGame(gameStatusBloc: context.read<GameStatusBloc>()), overlayBuilderMap: { 'menu_reset': (_, game) { return ResetMenu(game: game as SpaceGame); } }, ) 需要顯示和隱藏時就像上面一樣,呼叫add/remove方法。 ``` // 顯示 gameRef.overlays.add('menu_reset');

// 隱藏 gameRef.overlays.remove('menu_reset'); `` 選單類ResetMenu,由於都是Flutter`原生UI的基本操作,這裡就不展開了。直接看看效果吧

Record_2022-07-15-16-20-36_13914082904e1b7ce2b619733dc8fcfe_.gif

最後

本文記錄了飛機大戰的面板展示與重新開始選單。至此,整個遊戲就相當完整了。相關邏輯參考Flame官方的例子:flame/packages/flame_bloc