[Flutter&Flameに基づく航空機戦争の開発ノート]ブロックを使用してゲームの状態を管理する

序文

航空機戦争を発展させ続け、ゲームの基本的な構成が実現されました。残りは、以前に実装されていないヘルス、スコア、ミサイル小道具などのパネル機能です。この記事では、状態管理に使用する方法について説明しますbloc

著者はこのシリーズの記事を次のコラムに含めており、興味のある学生は次の記事を読むことができます。

Flutter&Flameに基づく航空機戦争の開発ノート

ゲームでのブロックの使用

この記事の焦点は開発モードを理解することではないので、この開発モードをより完全に理解するのに役立つブロックを紹介する記事があります。

フラッター状態管理BLoC

依存関係を追加する必要がありますequatable、、flame_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はゲームの状態に合わせていくつかのクラスを設計しました。

  • GameStatusBloclayer。UIレイヤーから渡されたものblocを処理し、状態を更新しますps:航空機戦争には当面複雑なロジックがないため、ここでの処理は基本的にイベントを受信して​​状態を更新することです。事件event
  • 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。后续会基于此状态来添加游戏面板的逻辑。

おすすめ

転載: juejin.im/post/7120188013543424031