前言

前面根据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(),
        ),
      );
    });
  }
}

作用

来看看面板展现的作用吧

【基于Flutter&Flame 的飞机大战开发笔记】展示面板及重新开始菜单
  • 生命值面板在右下角三个小飞机,监听的是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的基本操作,这儿就不打开了。直接看看作用吧

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

最终

本文记录了飞机大战的面板展现与从头开端菜单。至此,整个游戏就适当完整了。相关逻辑参考Flame官方的比如:flame/packages/flame_bloc。