FlowScope / 当前商业项目 Core 对照总结

可渐进收口,也允许完全重来

集百家之长,重组一套清晰的游戏 Core。

当前项目已经有事实上的运行时结构:GContext 接全局系统, FishingStage 管捕鱼会话,AGameAct 管具体玩法。 如果基于现有项目演进,就把这些概念收口成接口边界;如果新框架允许重来, 就直接吸收 QFramework、Loxodon、uFrame、当前项目和新版 P0 的优点,重新设计职责分层。

最终收成什么样

核心不是引入新名词,而是把“谁拥有谁、谁释放谁、谁可以发起切换”固定下来。

RootScope

来自:GContext.container
  • 应用级单例服务
  • 资源、配置、UI、声音、SDK、StageService
  • 只在应用启动和关闭时创建/释放
  • 不放 Act 临时数据,不放活动运行态

StageScope

来自:FishingStage
  • 捕鱼会话级状态和服务
  • 奖励队列、活动总控、地图会话、StageData
  • 负责创建和销毁 ActScope
  • 接收 typed navigation command,不裸听所有全局事件

ActScope

来自:AGameAct
  • 具体玩法的 UI、资源、订阅和临时状态
  • FishingAct、BuildAct、活动 Act 都是同一模型
  • 进入时注册,退出时自动释放
  • 不把自己的临时数据留在 RootScope

新框架参考了什么

这不是照搬某一个框架,而是把每个框架最能解决问题的部分拆出来,再避开它们在大型 Unity 项目里容易失控的部分。

QFramework

吸收:状态变更有入口,业务行为命令化。

它提醒我们 UI 不应该到处直接改长期状态,写操作应通过 Command、UseCase 或 Service Method 进入。

避免:静态 Architecture 入口和单容器 Service Locator。

Loxodon

吸收:ServiceBundle 和 UI/Binding 管线拆分。

它提醒我们模块装配要成组出现,UI 绑定、Window、Messenger、Prefs 应该是外围模块,不是 Kernel。

避免:ApplicationContext 变成全能上下文。

uFrame

吸收:可观察状态、命令流、Bind/Unbind 生命周期。

它提醒我们 UI 订阅必须有明确释放作用域,View 只响应 ViewModel,不直接操纵业务对象。

避免:全局容器创建 ViewModel、命名约定查找 prefab。

当前项目

吸收:真实商业项目里的 Stage/Act 玩法生命周期。

GContext、FishingStage、AGameAct 证明项目真正需要的是主游戏会话和玩法模式切换,而不是单纯 MVVM。

避免:全局事件承载命令,Act 临时状态留在 Root。

新版 P0

吸收: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。

两版 P0 Core 对比

前一版在定义“游戏 Core 应该有什么”,后一版在定义“最小可运行闭环必须怎么跑”。差异不只是模块多少,而是所有权从模糊变清楚。

旧版 P0:通用 Core 模板

旧版把 Core 看成一套横向基础设施合集:Game Loop、State Machine、Input、Scene、Event、 Data、Audio、UI、Save、Config、Time、Asset Loader 都在 Core 里。

  • 解决的是“一个通用游戏框架应该覆盖哪些系统”。
  • 优点是视野完整,能帮团队盘点 Unity 项目常见模块。
  • 问题是 P0 太宽,容易把 Event、Scene、Data、UI、Save 都做成全局系统。
  • 它更像框架蓝图,不像可立即验收的最小交付。
Game Loop State Machine Event System Scene Manager Asset Loader

新版 P0:最小纵向切片

新版把 Core 收敛为能跑通一个真实休闲游戏闭环的最小结构: Container、GameFlow、Feature、FeatureContext、ResourceGroup、UIManager、Save、Config、Audio。

  • 解决的是“启动、进入 Feature、打开 UI、响应数据、切换/退出并释放”。
  • 优点是所有权清楚:GameFlow 管编排,Feature 管业务,Context 管资源和订阅。
  • 明确不做全局 EventBus、完整 UI 路由、多资源后端、复杂存档生态。
  • 它更像可测试、可落地、可迁移到当前项目的核心运行时。
Container Scope GameFlow FeatureContext ResourceGroup R3 Data

消费侧怎么用

框架不接管 App 启动。项目自己的 Bootstrap 决定 SDK、热更、登录、配置、首个 Feature 的顺序;FlowScope 只提供可组合的 Kernel 能力。

GameBootstrap

项目自己的入口。按项目需求初始化 SDK、热更、隐私、登录和配置。

RootScope + Modules

消费侧创建 RootScope,安装 Resource/UI/Save/Audio 等模块和项目服务。

FeatureHost

框架负责 Feature 切换、FeatureScope 创建、生命周期和失败清理。

MainMenuFeature

业务 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 锚点上,通过扩展方法提供能力。

IHasFeatureContext

所有能力接口共享的锚点,避免每个接口都重复暴露一堆属性。

ICanGetService / ICanUseLifetime

通过扩展方法复用服务解析和退出释放能力。

ICanRequestNavigation

只有拿到导航能力的对象,才能请求 Feature 切换。

Feature / UseCase / ViewModel

按需要实现能力接口;普通对象优先构造注入具体 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。

收导航:替换 UnloadActToNextAct

新增 IActNavigator,把切 Act 的来源集中到 SwitchAsync。旧事件先转发到 Navigator,后续新代码不再直接 Publish 切换事件。

建 ActScope:临时状态不进 Root

FishingData、CollectingData、活动 Panel 订阅、Addressables handle 等玩法级对象进入 ActScope,ExitAsync 统一释放。

新框架路径:直接按边界实现

从 RootScope、FeatureHost、FeatureContext、Lifetime、Navigation、CoreModules 开始,不背旧命名和旧全局事件,只保留当前项目验证过的业务需求。

共同目标:拆 Bundle

把 SubBoostrap 里的服务注册逐步拆成 ResourceBundle、UIBundle、ActivityBundle、FishingDataBundle,形成 Start/Stop 边界。

边界规则

判断一次改动是否在正确方向上,就看它有没有让依赖、生命周期、切换入口更显式。

要做

  • Root 只放全局服务。
  • Stage 只管会话和 Act 编排。
  • Act 拥有自己的资源、UI、订阅和临时状态。
  • 切 Act 走 typed command / navigator。
  • 服务注册按 Bundle 成组出现。

不要做

  • 不要把所有东西继续塞进 GContext。
  • 不要让任意 UI 直接 Publish 切 Act。
  • 不要让 Act 临时数据常驻全局容器。
  • 不要用字符串 actId 作为长期主协议。
  • 不要在没有职责边界和验收切片前盲目重写。