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 bloc
it for state management .
The author has included this series of articles in the following columns, and interested students are welcome to read:
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.
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 bloc
has designed several classes for the game state:
GameStatusBloc
:bloc
layer, which is responsible for processing what is passed from the UI layer事件event
and 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:
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中可以通过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。后续会基于此状态来添加游戏面板的逻辑。