前言
前面基于bloc
管理的全局状态已经成型。本文将会利用该数据基于Flutter的控件搭建游戏的展示面板,以及关于重新开始游戏的菜单逻辑。
笔者将这一系列文章收录到以下专栏,欢迎有兴趣的同学阅读:
面板展示
还记得之前封装的GameView
吗?这里是全部逻辑,在GameWidget
之上还有一层Stack
,用于不同面板的展示。需要注意的是GameView
的父Widget
为MultiBlocProvider
,这样它的子Widget
才能获取得到GameStatusBloc
。
class GameView extends StatelessWidget {
const GameView({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Stack(
children: [
Positioned.fill(
child: GameWidget(
game: SpaceGame(gameStatusBloc: context.read<GameStatusBloc>()),
overlayBuilderMap: {
'menu_reset': (_, game) {
return ResetMenu(game: game as SpaceGame);
}
},
)),
SafeArea(
child: Stack(children: [
const Positioned(top: 4, right: 4, child: ScorePanel()),
Positioned(
bottom: 4,
right: 4,
left: 4,
child: Row(
children: const [
Expanded(child: BombPanel()),
Expanded(child: LivePanel()),
],
),
)],
))
],
);
}
}
生命值面板
单独拿生命值面板来聊,其实就是常规的bloc
模式,通过BlocBuilder
来监听GameStatusState
,更新后会触发builder
方法重新刷新ui。这个就是Flutter
原生层面的知识点了。
需要注意的是,这里用了Offstage
对视图进行隐藏和显示,条件是上篇文章说的GameStatus
游戏的运行状态。
class LivePanel extends StatelessWidget {
const LivePanel({super.key});
@override
Widget build(BuildContext context) {
return BlocBuilder<GameStatusBloc, GameStatusState>(
builder: (context, state) {
int live = state.lives;
return Offstage(
offstage: state.status != GameStatus.playing,
child: Wrap(
spacing: 2,
runSpacing: 5,
alignment: WrapAlignment.end,
children: List.generate(
live,
(index) => Container(
constraints:
const BoxConstraints(maxWidth: 35, maxHeight: 35),
child: const Image(
image: AssetImage('assets/images/player/life.png')),
)).toList(),
),
);
});
}
}
效果
来看看面板展示的效果吧
- 生命值面板在右下角三个小飞机,监听的是
GameStatusState.lives
。 - 还有两个,一个是导弹道具,一个是计分。这两个和上述的大同小异,这里就不展开说了。
- 导弹道具面板,在左下角,顺带一提之前没有提及这个功能,它是在游戏中通过道具获得,和子弹补给同理。监听的是
GameStatusState.bombSupplyNumber
。 - 计分面板,在右上角,击落一艘
敌机Component
就会有相应的计分。监听的是GameStatusState.score
。
- 导弹道具面板,在左下角,顺带一提之前没有提及这个功能,它是在游戏中通过道具获得,和子弹补给同理。监听的是
GameOver与Replay
GameStatusController
当战机Component
的生命值到0时,使GameStatusState.status == GameStatus.gameOver
。来看一下GameStatusBloc
的处理逻辑。
// class GameStatusBloc
on<PlayerLoss>((event, emit) {
if (state.lives > 1) {
emit(state.copyWith(lives: state.lives - 1));
} else {
emit(state.copyWith(lives: 0, status: GameStatus.gameOver));
}
});
在上文中,我们往Component树
中塞多了一个叫GameStatusController
的东西。答案在这里揭晓了,它是专门用于响应当游戏运行状态变化时界面变化的。
- 当
GameStatusState.status == GameStatus.gameOver
时,需要先暂停游戏的运行时(还记得Flame
有一个update
方法回调吗?他是依赖运行时响应的)。然后展示GameOver
菜单。 GameOver
菜单会展示分数和一个Replay
按钮。Replay
按钮点击后,会重新将GameStatusState.status == GameStatus.initial
,此时恢复游戏的运行时,与之前的游戏开始逻辑形成闭环。
class GameStatusController extends Component with HasGameRef<SpaceGame> {
@override
Future<void> onLoad() async {
add(FlameBlocListener<GameStatusBloc, GameStatusState>(
listenWhen: (pState, nState) {
return pState.status != nState.status;
}, onNewState: (state) {
if (state.status == GameStatus.initial) {
gameRef.resumeEngine();
gameRef.overlays.remove('menu_reset');
if (parent == null) return;
parent!.removeAll(parent!.children.where((element) {
return element is Enemy || element is Supply || element is Bullet;
}));
parent!.add(gameRef.player = Player(
initPosition:
Vector2((gameRef.size.x - 75) / 2, gameRef.size.y + 100),
size: Vector2(75, 100)));
} else if (state.status == GameStatus.gameOver) {
Future.delayed(const Duration(milliseconds: 600)).then((value) {
gameRef.pauseEngine();
gameRef.overlays.add('menu_reset');
});
}
}));
}
}
- 还是利用
FlameBlocListener
监听GameStatusState
的变化。 GameStatus.gameOver
时,通过gameRef.pauseEngine()
可暂停游戏的运行时。这里的gameRef.overlays.add('menu_reset')
会在视图最上层添加一个菜单。下面会讲到。GameStatus.initial
时,通过gameRef.resumeEngine()
可恢复游戏的运行时,并移除刚刚那个菜单。顺带一提,这里需要移除部分Component
,譬如敌机Component、补给Component、子弹Component
。还需要重新添加一个战机Component
,因为之前那个已经被移除了。
GameOver菜单
GameWidget
提供一个overlayBuilderMap
属性,可以传一个key-value
。value为该视图的builder
方法。
GameWidget(
game: SpaceGame(gameStatusBloc: context.read<GameStatusBloc>()),
overlayBuilderMap: {
'menu_reset': (_, game) {
return ResetMenu(game: game as SpaceGame);
}
},
)
需要显示和隐藏时就像上面一样,调用add/remove
方法。
// 显示
gameRef.overlays.add('menu_reset');
// 隐藏
gameRef.overlays.remove('menu_reset');
菜单类ResetMenu
,由于都是Flutter
原生UI的基本操作,这里就不展开了。直接看看效果吧
最后
本文记录了飞机大战的面板展示与重新开始菜单。至此,整个游戏就相当完整了。相关逻辑参考Flame官方的例子:flame/packages/flame_bloc。