[Development Notes for Aircraft Wars Based on Flutter&Flame] Using bloc to manage game state

foreword

Continue to develop the aircraft war, the basic composition of the game has been realized. The rest are panel functions, such as health, score, and missile props that have not been implemented before . This article will document how to use blocit for state management .

The author has included this series of articles in the following columns, and interested students are welcome to read:

Development Notes for Aircraft Wars Based on Flutter&Flame

The use of bloc in the game

Since the focus of this article is not to understand the development mode, here is an article to introduce bloc, which can help you understand this development mode more thoroughly.

Flutter state management BLoC

Need to add dependencies equatable, flame_bloc,flutter_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

Based on the idea, the author blochas designed several classes for the game state:

  • GameStatusBloc: bloclayer, which is responsible for processing what is passed from the UI layer 事件eventand updating the state . ps: Since there is no complicated logic in the aircraft war for the time being, the processing here is basically to receive an event and then update a state.
  • GameStatusState: State, here represents the global state of the game , which currently includes health, score, game state ( playing、gameover...), etc.
  • GameStatusEvent: Events, which represent the global events of the game, such as the start of the game, the end of the game, the increase or decrease of health, etc.

Take the game start event as an example to see how the approximate data flow goes:image.png

Event GameStatusEvent

Define an event as the start of the game , inherited fromGameStatusEvent

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

class GameStart extends GameStatusEvent {
  const GameStart();

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

State 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。后续会基于此状态来添加游戏面板的逻辑。

Guess you like

Origin juejin.im/post/7120188013543424031