序文
航空機戦争を発展させ続け、ゲームの基本的な構成が実現されました。残りは、以前に実装されていないヘルス、スコア、ミサイル小道具などのパネル機能です。この記事では、状態管理に使用する方法について説明しますbloc
。
著者はこのシリーズの記事を次のコラムに含めており、興味のある学生は次の記事を読むことができます。
ゲームでのブロックの使用
この記事の焦点は開発モードを理解することではないので、この開発モードをより完全に理解するのに役立つブロックを紹介する記事があります。
依存関係を追加する必要があります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
このアイデアに基づいて、作者bloc
はゲームの状態に合わせていくつかのクラスを設計しました。
GameStatusBloc
:layer。UIレイヤーから渡されたものbloc
を処理し、状態を更新します。ps:航空機戦争には当面複雑なロジックがないため、ここでの処理は基本的にイベントを受信して状態を更新することです。事件event
GameStatusState
:状態。ここでは、ゲームのグローバル状態を表します。これには、現在、ヘルス、スコア、ゲーム状態(playing、gameover
...)などが含まれます。GameStatusEvent
:イベント。ゲームの開始、ゲームの終了、ヘルスの増減など、ゲームのグローバルイベントを表します。
例としてゲーム開始イベントを取り上げて、おおよそのデータフローがどのようになるかを確認します。
イベント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中可以通过FlameBlocProvider
将GameStatusBloc
传递给子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树层级关系与之前有所不同
这样在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。后续会基于此状态来添加游戏面板的逻辑。