RootScope
- 应用级单例服务
- 资源、配置、UI、声音、SDK、StageService
- 只在应用启动和关闭时创建/释放
- 不放 Act 临时数据,不放活动运行态
可渐进收口,也允许完全重来
当前项目已经有事实上的运行时结构:GContext 接全局系统, FishingStage 管捕鱼会话,AGameAct 管具体玩法。 如果基于现有项目演进,就把这些概念收口成接口边界;如果新框架允许重来, 就直接吸收 QFramework、Loxodon、uFrame、当前项目和新版 P0 的优点,重新设计职责分层。
核心不是引入新名词,而是把“谁拥有谁、谁释放谁、谁可以发起切换”固定下来。
这不是照搬某一个框架,而是把每个框架最能解决问题的部分拆出来,再避开它们在大型 Unity 项目里容易失控的部分。
吸收:状态变更有入口,业务行为命令化。
它提醒我们 UI 不应该到处直接改长期状态,写操作应通过 Command、UseCase 或 Service Method 进入。
避免:静态 Architecture 入口和单容器 Service Locator。
吸收:ServiceBundle 和 UI/Binding 管线拆分。
它提醒我们模块装配要成组出现,UI 绑定、Window、Messenger、Prefs 应该是外围模块,不是 Kernel。
避免:ApplicationContext 变成全能上下文。
吸收:可观察状态、命令流、Bind/Unbind 生命周期。
它提醒我们 UI 订阅必须有明确释放作用域,View 只响应 ViewModel,不直接操纵业务对象。
避免:全局容器创建 ViewModel、命名约定查找 prefab。
吸收:真实商业项目里的 Stage/Act 玩法生命周期。
GContext、FishingStage、AGameAct 证明项目真正需要的是主游戏会话和玩法模式切换,而不是单纯 MVVM。
避免:全局事件承载命令,Act 临时状态留在 Root。
吸收:Container Scope、GameFlow、FeatureContext、ResourceGroup。
它把五个失控点统一收束到一条生命周期链:创建 scope,进入 Feature,退出时释放资源和订阅。
避免:旧版 P0 那种横向大而全 Core。
| 对比对象 | 它主要解决的问题 | 对五个失控点的覆盖 | 新框架吸收什么 | 新框架不吸收什么 |
|---|---|---|---|---|
| QFramework | 业务分层、Command/Query、Model/System、事件和可绑定状态。 | 中:能约束状态变更和依赖方向,但不负责 Feature 生命周期和资源释放。 | 状态变更入口、上层驱动下层、Command/UseCase 思想。 | 静态 Architecture、单容器、把业务分层强塞进 Kernel。 |
| Loxodon | Unity MVVM、DataBinding、Window、ServiceBundle、Messenger。 | 中:能解决 UI 状态同步和模块装配,但不解决玩法切换和 Feature 生命周期。 | ServiceBundle 装配、Binding 管线拆分、Window 状态管理。 | 把 Prefs、Messenger、Binding、UI 全塞进 Core。 |
| uFrame | ViewModel 可观察状态、Signal 命令流、View Bind/Unbind。 | 局部:UI 生命周期有启发,但全局 Kernel 和命名约定会放大失控。 | BindingScope、UI Intent、View 订阅 ViewModel 的方向。 | 全局容器创建 ViewModel、Resources.Load 约定、ViewModel 框架对象化。 |
| 当前项目 | 真实商业项目里的全局服务接入、捕鱼 Stage、Act 玩法切换。 | 中:解决了能跑和能切,但依赖、事件、释放边界还偏软。 | GContext 的真实服务覆盖、FishingStage/AGameAct 的玩法生命周期经验。 | 全局 Publish 命令、全局 Resolve 一切、字符串 actId 作为长期协议。 |
| 新版 P0 | 最小纵向闭环:启动、Feature、UI、Data、资源、保存、退出。 | 高:用 Scope、FeatureContext、ResourceGroup、Disposables 直接对准五个失控点。 | 作为新框架主骨架:RootScope、FeatureHost、FeatureContext、Lifetime、Navigation、CoreModules。 | 旧版 P0 的大而全模块铺开、P3 生态能力提前进入核心。 |
如果允许完全重来,目标不是复刻当前项目,也不是让框架接管 App 启动;消费侧保留 CompositionRoot,Kernel 只提供机制。
| 层级 | 核心职责 | 解决的问题 | 主要参考 | 不负责什么 |
|---|---|---|---|---|
| CompositionRoot | 消费侧代码,负责创建 RootScope、注册项目服务、安装可选模块、选择初始 Feature。 | 解决不同项目启动流程差异巨大,不让框架硬编码 SDK、热更、登录、隐私弹窗等顺序。 | 真实商业项目启动经验、Loxodon ServiceBundle 的模块安装思想。 | 它不是 Kernel 类型;框架只提供 helper,不抢启动主控权。 |
| RootScope | 全局服务容器:Config、Save、Resource、UI、Audio、SDK Adapter,由消费侧创建和注册。 | 解决依赖到处找,但同时给依赖加生命周期边界。 | GContext、QFramework IOC、Loxodon ServiceContainer、新版 P0 Container。 | 不放 Act 临时数据,不承载活动运行态。 |
| FeatureHost / GameFlow | 创建 FeatureScope,串行执行 Load、Enter、Exit、Dispose。 | 解决玩法/页面流切换黑箱、失败清理、重复调用。 | 新版 P0 GameFlow、当前项目 FishingStage。 | 不关心 Feature 内部 UI 怎么刷新,不替 Feature 写业务。 |
| Feature / GameAct | 一个可进入、可退出、可释放的业务运行单元。 | 解决玩法资源、订阅、UI、临时状态归属不清。 | 当前项目 AGameAct、新版 P0 IFeature、uFrame Bind/Unbind。 | 不直接调用其他 Feature 内部对象。 |
| FeatureContext | 由多个能力接口组合出来:服务解析、资源组、生命周期、取消信号、启动参数。 | 解决资源和订阅释放不彻底,异步取消无统一入口。 | 新版 P0 FeatureContext、当前项目 CompositeDisposable/Addressables handle 经验。 | 不变成万能业务上下文,不默认暴露所有全局能力。 |
| Navigation | 提供 typed SwitchRequest / SwitchCommand / Navigator。 | 解决字符串 actId、全局 Publish 切 Act、来源不可追踪。 | QFramework Command、当前项目 UnloadActToNextAct、新版 P0 SwitchToAsync。 | 不做复杂路由生态,P0 只保证切换顺序和失败清理。 |
| State / Data | 纯 C# Data + Observable State + ViewModel/UseCase 写入入口。 | 解决 UI 随手改状态、状态事务边界不清。 | QFramework Model/Command、uFrame P<T>、Loxodon ViewModel、新版 P0 R3 Data。 | 不把响应式状态塞进 Kernel,不要求所有项目用同一套业务分层。 |
| CoreModules | Resource、UI、Save、Config、Audio、Events 等可选模块。 | 解决真实项目必需能力,但避免 Kernel 变厚。 | Loxodon 外围模块、新版 P0 模块清单、当前项目真实服务覆盖。 | 不强制所有项目一次性接入,不把热更/云存档/完整路由放进 P0。 |
前一版在定义“游戏 Core 应该有什么”,后一版在定义“最小可运行闭环必须怎么跑”。差异不只是模块多少,而是所有权从模糊变清楚。
旧版把 Core 看成一套横向基础设施合集:Game Loop、State Machine、Input、Scene、Event、 Data、Audio、UI、Save、Config、Time、Asset Loader 都在 Core 里。
新版把 Core 收敛为能跑通一个真实休闲游戏闭环的最小结构: Container、GameFlow、Feature、FeatureContext、ResourceGroup、UIManager、Save、Config、Audio。
框架不接管 App 启动。项目自己的 Bootstrap 决定 SDK、热更、登录、配置、首个 Feature 的顺序;FlowScope 只提供可组合的 Kernel 能力。
项目自己的入口。按项目需求初始化 SDK、热更、隐私、登录和配置。
消费侧创建 RootScope,安装 Resource/UI/Save/Audio 等模块和项目服务。
框架负责 Feature 切换、FeatureScope 创建、生命周期和失败清理。
业务 Feature 只消费自己声明的能力接口,不直接触碰全局大上下文。
public sealed class GameBootstrap : MonoBehaviour
{
private IFeatureHost _host;
private IServiceScope _root;
private async void Start()
{
_root = new ServiceContainer();
// 1. 消费侧决定安装哪些模块
new ResourceModule(addressablesBackend).Install(_root);
new UIModule(uiRoot).Install(_root);
new SaveModule(savePath).Install(_root);
// 2. 消费侧注册项目服务和数据
_root.RegisterInstance<IPlayerData>(new PlayerData());
_root.RegisterFactory<IFishingApi>(s => new FishingApi());
// 3. 框架只提供 FeatureHost 机制
_host = FlowScopeKernel.CreateFeatureHost(_root);
await _host.SwitchAsync<MainMenuFeature>(
FeatureSwitchRequest.Replace(), destroyCancellationToken);
}
}
| 装配方式 | 好处 | 缺点 | 建议用法 |
|---|---|---|---|
| 强类型显式装配 | 启动依赖一眼可见;重构安全;IL2CPP/AOT 风险低;模块顺序由项目掌控;测试时容易替换服务。 | 样板代码多;消费侧要懂模块顺序;模块多时 Bootstrap 会变长;不适合所有服务都手写注册。 | 用于核心模块、项目级服务、启动关键路径,例如 Resource、UI、Save、Audio、账号服务。 |
| 约定/扫描装配 | 接入快;业务类少写注册;适合大量同类对象,比如配置行、Panel、简单 UseCase。 | 依赖来源不明显;运行期错误更晚暴露;AOT/裁剪要额外处理;调试链路更隐蔽。 | 作为可选插件,不进入 Kernel 主路径;必须有日志、诊断和关闭开关。 |
| 混合装配 | 核心路径显式,重复注册自动化;兼顾可控性和开发效率。 | 需要文档说明哪些必须显式,哪些可以自动;否则团队会混用失控。 | 推荐默认:CompositionRoot 显式安装模块,模块内部可以用 Source Generator 或扫描补注册。 |
这里把当前项目、旧版 P0、新版 P0 放到同一张表里。判断标准不是“功能多不多”,而是能不能让责任和释放边界变硬。
| Unity 项目失控点 | 当前项目现状 | 旧版 P0 能否解决 | 新版 P0 能否解决 | 落到当前项目应收成什么 |
|---|---|---|---|---|
| 依赖到处拿 | GContext 能接起来,但容易变成全局 Service Locator。 | 部分:有模块清单,但缺少 Root/Feature scope 规则。 | 能:Container + 父子 Scope + 构造注入让依赖边界更显式。 | GContext 收为 RootScope;Act 临时依赖进入 ActScope。 |
| 生命周期没人负责 | FishingStage 和 AGameAct 有 Start/Stop,但资源、订阅、数据释放靠人工约定。 | 弱:模块都有验收,但没有统一 FeatureContext 所有权。 | 能:GameFlow 创建 scope、resources、disposables,并在切换/关闭时释放。 | FishingStage 创建 ActScope;AGameAct 只把资源和订阅挂进自己的 context。 |
| 流程切换变黑箱 | UnloadActToNextAct 是全局事件 + 字符串 actId,来源和并发不够清楚。 | 部分:State Machine / Scene Manager 提到切换,但偏通用。 | 能:GameFlow.SwitchToAsync 定义状态机、失败清理和重复调用行为。 | 用 IActNavigator / SwitchActCommand 收口 Act 切换,旧事件只做兼容转发。 |
| 状态被 UI 随手改 | 大量 Manager、DataCenter、Panel 可通过全局入口互相影响,状态变更入口不统一。 | 部分:Data Manager + Event System 有方向,但容易继续全局化。 | 能:R3 Data 纯 C#,ViewModel 驱动 UI,Feature 间用 Data 而不是互调内部对象。 | 跨 Act 持久状态放 Stage/Data;Act 内临时状态放 ActScope;UI 通过 ViewModel/UseCase 改状态。 |
| 资源和 UI 释放不彻底 | 有 Addressables handle、panel 清理、CompositeDisposable,但分散在 Stage、Act、Panel、Manager。 | 部分:Asset Loader / UI Framework 提到引用计数和面板栈,但没有统一挂载点。 | 能:IResourceGroup + UIPanel Unbind + FeatureContext.Disposables 形成释放链。 | AGameAct 退出时统一 Dispose ActScope,ActScope 释放资源组、订阅、Panel 绑定。 |
先做命名和职责收口,再做代码迁移。这样团队讨论时能对齐,不会一上来变成重构战役。
| 当前项目概念 | 收口后的名字 | 保留职责 | 要移走的职责 |
|---|---|---|---|
| GContext | RootScope / RootContext | 全局服务解析、应用级事件、应用级生命周期 | Act 临时数据、玩法内状态、命令式流程控制 |
| FishingStage | FishingStageHost | 捕鱼会话启动、Stage 级服务、Act 切换编排 | 具体玩法 UI 细节、具体活动资源、散落的数据初始化 |
| AGameAct | GameAct / FeatureInstance | 玩法进入退出、资源句柄、UI 面板、事件订阅、临时状态 | 全局服务注册、跨 Act 调度、其他玩法状态修改 |
| UnloadActToNextAct | IActNavigator / SwitchActCommand | 表达“我要切到哪个 Act”这个意图 | 不要再作为任意地方都能 Publish 的全局命令事件 |
| ActivityResolver | ActivityScope / ActivityFacade | 活动管理器访问、活动数据加载、活动有效性判断 | 不要让面板和 Act 到处直接穿透全局容器 |
参考 QFramework 的能力接口思想,但接口必须能复用。这里的接口不是空标签,而是挂在 Context 锚点上,通过扩展方法提供能力。
所有能力接口共享的锚点,避免每个接口都重复暴露一堆属性。
通过扩展方法复用服务解析和退出释放能力。
只有拿到导航能力的对象,才能请求 Feature 切换。
按需要实现能力接口;普通对象优先构造注入具体 Port。
public interface IFeatureContext
{
IServiceProvider Services { get; }
IFeatureLifetime Lifetime { get; }
IResourceGroup Resources { get; }
CancellationToken CancellationToken { get; }
FeatureArgs Args { get; }
}
public interface INavigableFeatureContext : IFeatureContext
{
IFeatureNavigator Navigator { get; }
}
public interface IHasFeatureContext<out TContext>
where TContext : IFeatureContext
{
TContext Context { get; }
}
public interface ICanGetService :
IHasFeatureContext<IFeatureContext> {}
public interface ICanUseLifetime :
IHasFeatureContext<IFeatureContext> {}
public interface ICanUseResources :
IHasFeatureContext<IFeatureContext> {}
public interface ICanRequestNavigation :
IHasFeatureContext<INavigableFeatureContext> {}
public static class FeatureCapabilityExtensions
{
public static T GetService<T>(this ICanGetService self)
=> self.Context.Services.GetRequiredService<T>();
public static void OnExitDispose(
this ICanUseLifetime self, IDisposable disposable)
=> self.Context.Lifetime.Add(disposable);
public static ValueTask<IResourceHandle<T>> LoadOwnedAsync<T>(
this ICanUseResources self, string key)
where T : class
=> self.Context.Resources.LoadAsync<T>(
key, self.Context.CancellationToken);
public static ValueTask SwitchToAsync<TFeature>(
this ICanRequestNavigation self,
FeatureSwitchRequest request)
where TFeature : IFeature
=> self.Context.Navigator.SwitchAsync<TFeature>(
request, self.Context.CancellationToken);
}
public interface IFeature
{
ValueTask LoadAsync(IFeatureContext context);
ValueTask EnterAsync(IFeatureContext context);
ValueTask ExitAsync(IFeatureContext context);
}
| 使用位置 | 推荐方式 | 原因 |
|---|---|---|
| Feature / GameAct | 可以实现 `ICanGetService`、`ICanUseResources`、`ICanUseLifetime`。 | Feature 本来就是框架生命周期对象,使用能力接口可以复用扩展方法。 |
| 需要切换流程的 Feature | 额外实现 `ICanRequestNavigation`,并接收 `INavigableFeatureContext`。 | 导航是高权限能力,不应默认给所有对象。 |
| UseCase / Command | 优先构造注入业务 Port,例如 `IPlayerWallet`、`IFishingSession`、`IFeatureNavigator`。 | 业务对象不应该为了拿框架能力而继承一堆接口;它应该声明自己的真实依赖。 |
| ViewModel / Presenter | 优先只拿只读状态和 UI Intent 输出口。 | 避免 ViewModel 直接 Resolve 服务或直接改全局 Data。 |
旧项目可以渐进收口,新框架可以完全重来。两条路共享同一套目标结构,区别只是先做适配还是直接按新接口实现。
明确 GContext 等价 RootScope,FishingStage 等价 StageHost,AGameAct 等价 FeatureInstance。团队先用同一套语言讨论。
在现有 GContext 外包一层 IRootScope,在 FishingStage 外包一层 IFishingStageHost,让旧代码继续 Resolve 和 Publish。
新增 IActNavigator,把切 Act 的来源集中到 SwitchAsync。旧事件先转发到 Navigator,后续新代码不再直接 Publish 切换事件。
FishingData、CollectingData、活动 Panel 订阅、Addressables handle 等玩法级对象进入 ActScope,ExitAsync 统一释放。
从 RootScope、FeatureHost、FeatureContext、Lifetime、Navigation、CoreModules 开始,不背旧命名和旧全局事件,只保留当前项目验证过的业务需求。
把 SubBoostrap 里的服务注册逐步拆成 ResourceBundle、UIBundle、ActivityBundle、FishingDataBundle,形成 Start/Stop 边界。
判断一次改动是否在正确方向上,就看它有没有让依赖、生命周期、切换入口更显式。