前言
继续来开发飞机大战,游戏内的基本构成都已经实现。剩下的就是面板功能了,譬如生命值、分数,还有之前一直没有实现的导弹道具。本文将记录如何利用bloc
来做状态管理。
笔者将这一系列文章收录到以下专栏,欢迎有兴趣的同学阅读:
游戏中的bloc运用
由于本文重点不在理解开发模式,这里贴一篇文章来介绍一下bloc,可以帮助您对此开发模式理解得更通透一些。
Flutter 状态管理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
:bloc
层,负责处理UI层传递过来的事件event
,并更新状态。ps:由于飞机大战暂时没有复杂逻辑,这里的处理基本都是收到一个事件然后更新一个状态。 -
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。后续会基于此状态来添加游戏面板的逻辑。