【基于Flutter&Flame 的飞机大战开发笔记】利用bloc管理游戏状态

2,171 阅读4分钟

前言

继续来开发飞机大战,游戏内的基本构成都已经实现。剩下的就是面板功能了,譬如生命值、分数,还有之前一直没有实现的导弹道具。本文将记录如何利用bloc来做状态管理

笔者将这一系列文章收录到以下专栏,欢迎有兴趣的同学阅读:

基于Flutter&Flame 的飞机大战开发笔记

游戏中的bloc运用

由于本文重点不在理解开发模式,这里贴一篇文章来介绍一下bloc,可以帮助您对此开发模式理解得更通透一些。

Flutter 状态管理BLoC

在项目中需要添加依赖equatableflame_blocflutter_bloc

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

abstract class GameStatusEvent extends Equatable {
  const GameStatusEvent();
}

class GameStart extends GameStatusEvent {
  const GameStart();

  @override
  List<Object?> get props => [];
}

状态 GameStatusState

这里对游戏运行状态有一个枚举GameStatus的定义

enum GameStatus {
  initial, // 初始化
  playing, // 游戏中
  gameOver // 游戏结束
}

GameStatusState的定义包括生命值、分数、导弹道具数、游戏运行状态

class GameStatusState extends Equatable {
  final int score;
  final int lives;
  final GameStatus status;
  final int bombSupplyNumber;
  。。。

bloc层 GameStatusBloc

GameStatusBloc定义了接收到事件GameStart后,如何更新状态GameStatusState

class GameStatusBloc extends Bloc<GameStatusEvent, GameStatusState> {
  GameStatusBloc() : super(const GameStatusState.empty()) {
    。。。
    
    on<GameStart>((event, emit) {
      emit(state.copyWith(status: GameStatus.playing));
    });
    
    。。。
  }
}

这里是将游戏运行状态GameStatus更新为playing

GameStatusBloc的对象会被保存在Game中,当游戏开始时,就会调用Game#gameStart()将事件发送出去。ps:这里类名被修改成SpaceGame,与之前的文章有些不同。

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的控件来编写面板展示的逻辑,这个本文不涉及所以可暂不理会。

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<GameStatusBloc>(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<GameStatusBloc>())

然后再回去看看Game#onLoad方法,在Flame中可以通过FlameBlocProviderGameStatusBloc传递给子Component子Component可对此进行状态监听。这里使用FlameMultiBlocProvider,支持多个provider

@override
Future<void> onLoad() async {
  final ParallaxComponent parallax = await loadParallaxComponent(
      [ParallaxImageData('background.png')],
      repeat: ImageRepeat.repeatY, baseVelocity: Vector2(0, 25));
  add(parallax);

  await add(FlameMultiBlocProvider(providers: [
    FlameBlocProvider<GameStatusBloc, GameStatusState>.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

// class Player
@override
Future<void> 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<GameStatusBloc, GameStatusState>(
      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。后续会基于此状态来添加游戏面板的逻辑。