flame_bloc 组件化状态管理:Flame 游戏中的 Bloc Provider、Listener 与 Reader 全指南
【免费下载链接】flameA Flutter based game engine.项目地址: https://gitcode.com/GitHub_Trending/fl/flame
flame_bloc是 Flame 生态中连接 Bloc 为骨架,结合仓库内 flame_bloc 包源码、单元测试 与 完整示例工程,系统讲解FlameBlocProvider、FlameMultiBlocProvider、FlameBlocListener、FlameBlocListenable、FlameBlocReader五个核心 API 的用法、生命周期与底层实现,读完即可在自己的 Flame 游戏中落地 Bloc 状态管理。
背景:为什么游戏需要 Bloc
在复杂游戏中,玩家血量、背包、关卡分数等状态往往散落在多个Component里,互相直接引用容易造成耦合和难以定位的隐式修改。Bloc 通过将状态变更收敛到唯一入口(事件 → 状态),保证整棵组件树对状态变化有一致的认知。flame_bloc的价值在于:
- 贴近 flutter_bloc 的写法:与 Widget 树中的
BlocProvider/BlocListener使用习惯一脉相承,学习成本低; - 组件树原生集成:
FlameBlocProvider本身就是一个Component,可以像挂载其他组件一样挂载进FlameGame; - 生命周期自动管理:由 Provider 创建的 bloc 会随组件销毁而自动关闭,避免资源泄漏。
使用前置:添加依赖
要使用这些组件,首先在游戏项目的pubspec.yaml中声明依赖(参考 flame_bloc 的 pubspec.yaml):
dependencies: flame: ^1.0.0 flame_bloc: ^1.0.0 flutter_bloc: ^8.0.0然后导入库:
import 'package:flame_bloc/flame_bloc.dart';flame_bloc的公开入口 flame_bloc.dart 会导出src下全部组件,所以只需这一条 import。
FlameBlocProvider:向组件子树提供单个 Bloc
FlameBlocProvider是一个用于创建并向子组件提供 bloc 的Component。它的核心定位是依赖注入(DI)容器:让同一个 bloc 实例可以被组件子树内的多个Component共享,而不是各自 new 一份。
它有两种构造方式,语义完全不同,需要严格区分。
方式一:默认构造器create—— 创建并接管生命周期
FlameBlocProvider<BlocA, BlocAState>( create: () => BlocA(), children: [...], );create是一个返回BlocBase<S>的工厂函数,Provider 在构造时立即调用它创建 bloc 实例;- 生命周期规则:该 bloc 只活在 Provider 组件存活期间。从 flame_bloc_provider.dart 源码 可以看到,Provider 在
onRemove()回调里会判断_created标志,若为 true 则调用_bloc.close()自动关闭 bloc:
@override @mustCallSuper void onRemove() { super.onRemove(); if (_created) { _bloc.close(); } }这意味着当 Provider 从游戏组件树中移除(例如切换关卡销毁场景)时,由其创建的 bloc 会被自动释放,无需手动管理。
方式二:命名构造器.value—— 提供已有实例
FlameBlocProvider<BlocA, BlocAState>.value( value: blocA, children: [...], );value直接传入一个已存在的 bloc 实例;- 生命周期规则:此时
_created = false,Provider 在onRemove()中不会关闭该 bloc,关闭职责完全交给使用者。这一行为在 测试用例 中有明确验证:"don't dispose value bloc" 测试确认.value提供的 bloc 在 Provider 移除后isClosed仍为 false。
典型使用场景是:bloc 由 Widget 层(如flutter_bloc的BlocProvider)创建并持有,再注入到游戏组件树中共享。仓库的 example 工程 正是这种模式——Widget 层的MultiBlocProvider创建GameStatsBloc与InventoryBloc,随后通过构造参数传入SpaceShooterGame。
子组件的挂载
children参数会被立即add到 Provider 名下,成为其子组件:
void _addChildren(List<Component>? children) { if (children != null) { children.forEach(add); } }因此,凡是放在children里的组件以及它们的后代,都处于该 Provider 的"作用域"内,可以读取到这个 bloc。
FlameMultiBlocProvider:一次提供多个 Bloc
当一棵子树需要多个不同的 bloc 时,可以嵌套多个FlameBlocProvider,但更简洁的做法是使用FlameMultiBlocProvider,把多个 Provider 扁平化声明:
FlameMultiBlocProvider( providers: [ FlameBlocProvider<BlocA, BlocAState>( create: () => BlocA(), ), FlameBlocProvider<BlocB, BlocBState>.value( value: blocB, // 注意:`.value` 构造器没有 create 参数 ), ], children: [...], )注:原文示例中第二个 Provider 写的是
.value构造器搭配create参数,实际源码中FlameBlocProvider.value只接受value参数;如果你手头的是BlocB实例就传value: blocB,如果需要新建则用默认构造器的create: () => BlocB()。
从 flame_multi_bloc_provider.dart 源码 可以看到它的实现方式:构造函数会断言providers非空,然后把 providers 依次串联成一条组件链——后一个 Provider 作为前一个 Provider 的子组件:
var current = list.removeAt(0); while (list.isNotEmpty) { final provider = list.removeAt(0); current.add(provider); current = provider; } add(_providers.first); _lastProvider = current; _initialChildren?.forEach(add);children里的组件则全部挂到链条末端的_lastProvider下。这样一来,对于最内层的子组件而言,所有 bloc 都在它的祖先链上,任意一个都能被正确解析到。
FlameBlocListener:以组件方式监听状态变化
FlameBlocListener是一个可以监听 bloc 状态变化的Component,它把 flutter_bloc 中BlocListener的职责搬进了组件树。
FlameBlocListener<GameStatsBloc, GameStatsState>( listenWhen: (previousState, newState) { // 返回 true/false,决定是否调用 onNewState return newState.score != previousState.score; }, onNewState: (state) { // 基于新状态做响应,例如更新 HUD、播放音效 }, )参数说明(对照 flame_bloc_listener.dart 源码):
| 参数 | 类型 | 作用 | 默认行为 |
|---|---|---|---|
onNewState | void Function(S state) | 状态变化后的回调 | 必填 |
onInitialState | void Function(S state)? | 首次挂载时拿到初始状态的回调 | 可选,不传则不触发 |
listenWhen | bool Function(S previousState, S newState)? | 细粒度过滤,返回 true 才触发onNewState | 可选,缺省恒为 true |
bloc | B? | 显式指定要监听的 bloc 实例 | 可选,缺省时从祖先 Provider 解析 |
源码中listenWhen的默认实现:
@override bool listenWhen(S previousState, S newState) { return _listenWhen?.call(previousState, newState) ?? true; }使用建议:FlameBlocListener适合作为某个组件(如玩家、HUD)的子组件挂载,专责"监听-响应"逻辑,与渲染逻辑解耦。例如在玩家组件内挂载一个监听器来响应血量状态:
class Player extends PositionComponent { @override Future<void> onLoad() async { add( FlameBlocListener<PlayerHealthBloc, PlayerHealthState>( listenWhen: (prev, curr) => prev.health != curr.health, onNewState: (state) { flashRed(); // 受击闪红 playHitSound(); // 播放音效 }, ), ); } }FlameBlocListenable:用 mixin 让组件自己监听
如果你希望某个组件自身具备监听能力,而不是额外挂一个子组件,可以用FlameBlocListenablemixin:
class ComponentA extends Component with FlameBlocListenable<BlocA, BlocAState> { @override bool listenWhen(PlayerState previousState, PlayerState newState) { // 返回 true/false 决定是否调用 onNewState return previousState.hp != newState.hp; } @override void onNewState(PlayerState state) { super.onNewState(state); // 基于新状态执行逻辑 } }底层工作机制
从 flame_bloc_listenable.dart 源码 可以看到,mixin 在组件onMount()时完成三件事:
- 解析 bloc:优先使用显式设置的
bloc覆写;否则向上遍历祖先链,用ancestors().whereType<FlameBlocProvider<B, S>>()找到最近的 Provider,取其bloc。若找不到会触发 assert 提示'No FlameBlocProvider<$B, $S> available on the component tree'; - 初始化状态:把
bloc.state作为当前状态,并调用一次onInitialState(_state); - 订阅状态流:
_subscription = bloc.stream.listen(...),每次收到新状态时先调用listenWhen过滤,返回 true 才调用onNewState。
组件被移除时(onRemove),mixin 会执行_subscription.cancel()取消订阅,避免悬挂监听导致的泄漏。
三个可覆写回调
onInitialState(S state):挂载时携带初始状态调用一次,默认空实现;listenWhen(S previous, S new):状态过滤,默认返回 true;onNewState(S state):新状态响应入口,默认空实现。
显式指定 bloc
如果监听目标没有通过FlameBlocProvider提供(例如是从游戏外部传入的全局 bloc),可以用blocsetter 显式绑定,这在监听器源码中明确标注为"适用于 bloc 未经 Provider 提供的场景":
class ComponentA extends Component with FlameBlocListenable<BlocA, BlocAState> { ComponentA(BlocA bloc) { this.bloc = bloc; // 显式绑定,跳过祖先查找 } }注意 setter 有 assert 保护:一旦设置后不能再次更新('Cannot update the bloc instance once it has been set.')。
FlameBlocReader:只读访问当前状态
FlameBlocReader是一个更轻量的 mixin,适合只需要读取 bloc 当前状态或向它投递事件的组件,不建立监听关系:
class InventoryReader extends Component with FlameBlocReader<InventoryCubit, InventoryState> {} // 游戏内使用 final component = InventoryReader(); var state = component.bloc.state; // 读取当前状态 component.bloc.add(SomeEvent()); // 投递事件从 flame_bloc_reader.dart 源码 看,它在onLoad()时解析 bloc:
@override @mustCallSuper Future<void> onLoad() async { super.onLoad(); final providers = ancestors().whereType<FlameBlocProvider<B, S>>(); assert( providers.isNotEmpty, 'No FlameBlocProvider<$B, $S> available on the component tree', ); final provider = providers.first; _bloc = provider.bloc; }限制:一个组件只能带一个FlameBlocReader(每个组件只能解析一个 bloc 类型);同理,FlameBlocListenable也受此约束。如果需要同时读写多个 bloc,请组合多个组件或在组件树中分层。
典型场景:玩家组件用FlameBlocReader<PlayerStatsBloc, PlayerStatsState>读取自己的属性,并在受击时触发bloc.add(PlayerDamaged())——事件被 bloc 统一处理,状态变更可被其他监听组件感知,形成完整闭环。
生命周期与内存管理要点
把各 API 的生命周期规则汇总如下:
| API | 谁创建 bloc | 谁负责销毁 |
|---|---|---|
FlameBlocProvider(create:) | Provider 构造时 | ProvideronRemove()自动close() |
FlameBlocProvider.value | 使用者 | 使用者(Provider 不关闭) |
FlameBlocListenable/FlameBlocListener | 不创建 | 组件移除时自动cancel()订阅 |
FlameBlocReader | 不创建,也不订阅 | 无资源需要释放 |
其中 Provider 的自动关闭行为有测试佐证:在 flame_bloc_provider_test.dart 中,"dispose created blocs" 用例先ensureAdd一个create:的 Provider,调用removeFromParent()后断言provider.bloc.isClosed变为 true;而.value用例则断言isClosed保持 false。
实战组合示例
下面把三个 API 组合进一个完整的游戏场景:背包系统由 Cubit 管理,HUD 组件负责渲染,玩家组件负责交互。
class MyGame extends FlameGame { final inventoryBloc = InventoryCubit(); @override Future<void> onLoad() async { await add( FlameBlocProvider<InventoryCubit, InventoryState>.value( value: inventoryBloc, children: [ Player(), Hud(), ], ), ); } } // 玩家:只读状态 + 投递事件 class Player extends PositionComponent with FlameBlocReader<InventoryCubit, InventoryState> { void pickupItem(Item item) => bloc.add(ItemPicked(item)); } // HUD:挂载监听器响应背包变化 class Hud extends PositionComponent { @override Future<void> onLoad() async { await add( FlameBlocListener<InventoryCubit, InventoryState>( listenWhen: (prev, curr) => prev.items.length != curr.items.length, onNewState: (state) => redrawInventory(state), ), ); } }若还要同时管理玩家生命值,只需将FlameBlocProvider替换为FlameMultiBlocProvider,把InventoryCubit与PlayerHealthBloc两个 Provider 一并放入providers列表即可,children中的组件无需任何改动。
更多参考
- 完整 API 文档见 flame_bloc 概述;
- 可运行的示例工程位于 packages/flame_bloc/example,其游戏逻辑实现于 example/lib/src/game/game.dart,包含玩家、敌人、子弹、爆炸、背包与游戏统计的完整 Bloc 集成;
- 各组件对应的单元测试分别位于 test/src/ 目录,可作为 API 行为契约查阅。
【免费下载链接】flameA Flutter based game engine.项目地址: https://gitcode.com/GitHub_Trending/fl/flame
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考