【基于Flutter&Flame 的飞机大战开发笔记】展示面板及重新开始菜单

前言

前面基于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<GameStatusBloc>()),
          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<GameStatusBloc, GameStatusState>(
        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<SpaceGame> {
  @override
  Future<void> onLoad() async {
    add(FlameBlocListener<GameStatusBloc, GameStatusState>(
        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

猜你喜欢

转载自juejin.im/post/7120516724151025672