foreword
The previous management- based bloc
global state has taken shape. This article will use this data to build a display panel for the game based on Flutter's controls , as well as the menu logic for restarting the game .
The author has included this series of articles in the following columns, and interested students are welcome to read:
panel display
Remember the packaging from earlier GameView
? Here is all the logic, GameWidget
and there is a layer above Stack
it for the display of different panels. It should be noted that it GameView
is父Widget
MultiBlocProvider
so that it 子Widget
can be obtained 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()),
],
),
)],
))
],
);
}
}
health panel
Talking with the health panel alone is actually a regular bloc
mode. By BlocBuilder
listening GameStatusState
, the method will be triggered builder
to refresh the UI after the update. This is Flutter
the knowledge point at the original level.
It should be noted that here is used Offstage
to hide and displayGameStatus
the view, the condition is the running state of the game mentioned in the previous article .
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(),
),
);
});
}
}
Effect
Let's take a look at the effect of the panel display
- There are three small planes in the lower right corner of the health panel, monitoring is
GameStatusState.lives
. - There are two more, one for missile props and one for scoring. These two are similar to the above , and will not be discussed here.
- 导弹道具面板,在左下角,顺带一提之前没有提及这个功能,它是在游戏中通过道具获得,和子弹补给同理。监听的是
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
, by resuminggameRef.resumeEngine()
the game's runtime and removing the menu just now. By the way, there are parts to be removed here , eg . One needs to be added again because the previous one has been removed .Component
敌机Component、补给Component、子弹Component
战机Component
GameOver Menu
GameWidget
Provide a overlayBuilderMap
property, you can pass one key-value
. value is the builder
method of this view.
GameWidget(
game: SpaceGame(gameStatusBloc: context.read<GameStatusBloc>()),
overlayBuilderMap: {
'menu_reset': (_, game) {
return ResetMenu(game: game as SpaceGame);
}
},
)
When you need to show and hide, just like above, call the add/remove
method.
// 显示
gameRef.overlays.add('menu_reset');
// 隐藏
gameRef.overlays.remove('menu_reset');
The menu class ResetMenu
, since it is Flutter
the basic operation of the native UI, will not be expanded here. Just see the effect
at last
This article documents the panel display and restart menu for Aircraft Wars. At this point, the entire game is quite complete. For related logic, refer to the official example of Flame: flame/packages/flame_bloc .