自动注册
+目标是少写重复装配代码。它只能在模块边界内工作,必须有 Marker、生命周期声明和诊断输出。
+From 491eb27cd25cacc1e0324f42b896b97f38ef8818 Mon Sep 17 00:00:00 2001 From: "JSD\\13999" <1399945104@qq.com> Date: Thu, 18 Jun 2026 15:11:42 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8F=90=E4=BA=A4=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core-contracts-five-rules-preview.html | 1091 +++++++++++++ ...urrent-project-core-evolution-summary.html | 1348 +++++++++++++++++ .../guides/loxodon-framework-core-analysis.md | 350 +++++ docs/guides/qframework-core-analysis.md | 501 ++++++ .../2026-06-12-uframe-mvvm-core-analysis.md | 473 ++++++ 5 files changed, 3763 insertions(+) create mode 100644 docs/guides/core-contracts-five-rules-preview.html create mode 100644 docs/guides/current-project-core-evolution-summary.html create mode 100644 docs/guides/loxodon-framework-core-analysis.md create mode 100644 docs/guides/qframework-core-analysis.md create mode 100644 docs/reviews/2026-06-12-uframe-mvvm-core-analysis.md diff --git a/docs/guides/core-contracts-five-rules-preview.html b/docs/guides/core-contracts-five-rules-preview.html new file mode 100644 index 0000000..6dcd01d --- /dev/null +++ b/docs/guides/core-contracts-five-rules-preview.html @@ -0,0 +1,1091 @@ + + +
+ + +五件事,五种责任
++ 新版 Core 的关键不是“多加几个抽象”,而是把 Unity 项目最容易失控的五件事拆成五种机制: + 自动注册只减少装配代码,构造注入暴露依赖,能力接口限制职责,Scope 管生命周期, + 诊断图让自动化可见。 +
+最新版不应该只是“一堆模块手动装起来”。它的全局概念是消费侧的 GameComposition,框架只把这份配方变成可运行、可诊断、可释放的 FlowScopeRuntime。
+| 概念 | +是谁拥有 | +负责什么 | +不负责什么 | +
|---|---|---|---|
| GameComposition | +消费侧 | +声明项目用了哪些模块、项目服务、首个 Feature、启动前置条件。 | +不执行 Feature 生命周期,不释放资源。 | +
| CompositionBuilder | +框架提供,消费侧调用 | +收集模块安装、服务注册、自动扫描规则和诊断信息。 | +不决定项目业务启动顺序。 | +
| FlowScopeRuntime | +框架创建,消费侧持有 | +持有 RootScope、FeatureHost、Diagnostics,作为运行时总句柄。 | +不变成全能 GContext,不允许业务随便从它 Resolve 一切。 | +
| FeatureHost | +框架 | +执行 Feature 切换、Scope 创建、生命周期和失败清理。 | +不写业务逻辑,不决定首个 Feature 是谁。 | +
它们不是并列名词,而是一条链。少写注册不等于隐藏依赖;自动化越多,诊断越要硬。
+目标是少写重复装配代码。它只能在模块边界内工作,必须有 Marker、生命周期声明和诊断输出。
+目标是让依赖可读、可测、可替换。不要把自动注册变成业务代码到处 Resolve。
+目标是限制职责权限。对象实现什么能力,才获得什么扩展方法;接口必须能复用。
+目标是明确谁拥有对象、谁释放对象。Feature 临时对象不能留在 Root。
+目标是让自动注册可审查。启动时能看见每个服务来自哪个模块、依赖谁、生命周期是什么。
+消费侧知道装了哪些模块,不让框架猜启动顺序。
+扫描本模块 Assembly 和 Marker,减少重复注册。
+类依赖在类型定义处暴露,不靠内部 Resolve。
+资源、订阅、临时状态进入本次 Feature 生命周期。
+自动化结果可见,启动失败能定位到模块和依赖链。
+消费侧仍然保留 CompositionRoot,但不再把注册散落在 MonoBehaviour 里。项目写一份 GameComposition,Bootstrap 只负责构建 Runtime 和进入首个 Feature。
+public sealed class GameBootstrap : MonoBehaviour
+{
+ private FlowScopeRuntime _runtime;
+
+ private async void Start()
+ {
+ var composition = new FishingGameComposition(
+ addressablesBackend,
+ uiRoot,
+ savePath);
+
+ _runtime = FlowScopeRuntime.Build(composition);
+
+ await _runtime.Features.SwitchAsync<MainMenuFeature>(
+ FeatureSwitchRequest.Replace(),
+ destroyCancellationToken);
+ }
+
+ private async void OnDestroy()
+ {
+ if (_runtime != null)
+ await _runtime.DisposeAsync();
+ }
+}
+ public sealed class FishingGameComposition : IGameComposition
+{
+ private readonly IResourceBackend _resourceBackend;
+ private readonly Transform _uiRoot;
+ private readonly string _savePath;
+
+ public FishingGameComposition(
+ IResourceBackend resourceBackend,
+ Transform uiRoot,
+ string savePath)
+ {
+ _resourceBackend = resourceBackend;
+ _uiRoot = uiRoot;
+ _savePath = savePath;
+ }
+
+ public void Configure(ICompositionBuilder app)
+ {
+ app.UseRoot(new ServiceContainer());
+
+ app.Install(new ResourceModule(_resourceBackend));
+ app.Install(new UIModule(_uiRoot));
+ app.Install(new SaveModule(_savePath));
+ app.Install(new FishingModule());
+
+ app.Services.RegisterInstance<IPlayerData>(new PlayerData());
+ app.Services.RegisterFactory<IFishingApi>(
+ s => new FishingApi());
+
+ app.Features.UseDefaultSwitchPolicy(FeatureSwitchPolicy.ReplaceOnly);
+ }
+}
+ 下面不是完整实现,而是接口和关键路径预览。重点是职责分离:自动注册、依赖、权限、生命周期、诊断各管一件事。
+public interface IGameComposition
+{
+ void Configure(ICompositionBuilder app);
+}
+
+public interface ICompositionBuilder
+{
+ IServiceRegistry Services { get; }
+ IFeatureRegistry Features { get; }
+ IDiagnosticOptions Diagnostics { get; }
+
+ void UseRoot(IServiceScope root);
+ void Install(IModule module);
+}
+
+public sealed class FlowScopeRuntime : IAsyncDisposable
+{
+ public IServiceScope Root { get; }
+ public IFeatureHost Features { get; }
+ public IDependencyDiagnostics Diagnostics { get; }
+
+ public static FlowScopeRuntime Build(IGameComposition composition)
+ {
+ var builder = new CompositionBuilder();
+ composition.Configure(builder);
+ return builder.BuildRuntime();
+ }
+}
+ public enum ServiceLifetime
+{
+ Root,
+ Stage,
+ Feature
+}
+
+[AttributeUsage(AttributeTargets.Class)]
+public sealed class FeatureServiceAttribute : Attribute
+{
+ public ServiceLifetime Lifetime { get; }
+
+ public FeatureServiceAttribute(
+ ServiceLifetime lifetime = ServiceLifetime.Feature)
+ {
+ Lifetime = lifetime;
+ }
+}
+
+public interface IModule
+{
+ void Install(IServiceRegistry services);
+}
+
+public interface IServiceRegistry
+{
+ void RegisterInstance<T>(T instance);
+ void RegisterFactory<T>(Func<IServiceProvider, T> factory);
+ IModuleScanBuilder Scan(Assembly assembly);
+}
+ public sealed class StartFishingUseCase
+{
+ private readonly IFishingSession _session;
+ private readonly IPlayerWallet _wallet;
+ private readonly IFeatureNavigator _navigator;
+
+ public StartFishingUseCase(
+ IFishingSession session,
+ IPlayerWallet wallet,
+ IFeatureNavigator navigator)
+ {
+ _session = session;
+ _wallet = wallet;
+ _navigator = navigator;
+ }
+
+ public async ValueTask ExecuteAsync(CancellationToken ct)
+ {
+ if (!_wallet.CanPay(_session.EntryCost))
+ return;
+
+ await _navigator.SwitchAsync<FishingFeature>(
+ FeatureSwitchRequest.Replace(), ct);
+ }
+}
+ public interface IFeatureContext
+{
+ IServiceProvider Services { get; }
+ IFeatureLifetime Lifetime { get; }
+ IResourceGroup Resources { get; }
+ CancellationToken CancellationToken { get; }
+}
+
+public interface INavigableFeatureContext : IFeatureContext
+{
+ IFeatureNavigator Navigator { get; }
+}
+
+public interface IHasFeatureContext<out TContext>
+ where TContext : IFeatureContext
+{
+ TContext Context { get; }
+}
+
+public interface ICanUseResources :
+ IHasFeatureContext<IFeatureContext> {}
+
+public interface ICanRequestNavigation :
+ IHasFeatureContext<INavigableFeatureContext> {}
+ public static class FeatureCapabilityExtensions
+{
+ public static ValueTask<IResourceHandle<T>>
+ LoadOwnedAsync<T>(
+ this ICanUseResources self,
+ string key)
+ where T : class
+ {
+ return self.Context.Resources.LoadAsync<T>(
+ key,
+ self.Context.CancellationToken);
+ }
+
+ public static ValueTask SwitchToAsync<TFeature>(
+ this ICanRequestNavigation self,
+ FeatureSwitchRequest request)
+ where TFeature : IFeature
+ {
+ return self.Context.Navigator.SwitchAsync<TFeature>(
+ request,
+ self.Context.CancellationToken);
+ }
+}
+ public sealed class FeatureHost : IFeatureHost
+{
+ private readonly IServiceScope _root;
+ private ActiveFeature _active;
+
+ public async ValueTask SwitchAsync<TFeature>(
+ FeatureSwitchRequest request,
+ CancellationToken ct)
+ where TFeature : IFeature
+ {
+ if (_active != null)
+ await _active.ExitAndDisposeAsync(ct);
+
+ var scope = _root.CreateScope();
+ var lifetime = new FeatureLifetime();
+ var resources = scope.GetRequiredService<IResourceService>()
+ .CreateGroup();
+
+ var context = new FeatureContext(
+ scope, lifetime, resources, ct, request.Args);
+
+ var feature = scope.GetRequiredService<TFeature>();
+ await feature.LoadAsync(context);
+ await feature.EnterAsync(context);
+
+ _active = new ActiveFeature(feature, context, scope);
+ }
+}
+ public sealed class MainMenuFeature :
+ IFeature,
+ ICanUseResources,
+ ICanRequestNavigation
+{
+ public INavigableFeatureContext Context { get; private set; }
+ IFeatureContext IHasFeatureContext<IFeatureContext>.Context
+ => Context;
+
+ public async ValueTask LoadAsync(IFeatureContext context)
+ {
+ Context = (INavigableFeatureContext)context;
+
+ var panel = await this.LoadOwnedAsync<GameObject>(
+ "UI/MainMenuPanel");
+
+ Context.Lifetime.Add(panel);
+ }
+
+ public ValueTask EnterAsync(IFeatureContext context)
+ => ValueTask.CompletedTask;
+
+ public ValueTask ExitAsync(IFeatureContext context)
+ => ValueTask.CompletedTask;
+}
+ 自动注册必须能回答三个问题:谁注册了它,它依赖谁,它属于哪个生命周期。
+public interface IDependencyDiagnostics
+{
+ RegistrationReport BuildRegistrationReport();
+ DependencyGraph BuildDependencyGraph(Type rootType);
+}
+
+public sealed record ServiceRegistration(
+ Type ServiceType,
+ Type ImplementationType,
+ ServiceLifetime Lifetime,
+ string ModuleName,
+ IReadOnlyList<Type> Dependencies);
+
+public sealed record RegistrationReport(
+ IReadOnlyList<ServiceRegistration> Services,
+ IReadOnlyList<string> Warnings);
+ FishingModule
+ IFishingSession -> FishingSession [Feature]
+ depends on:
+ IPlayerData
+ IFishingApi
+
+ IFishingEconomy -> FishingEconomy [Feature]
+ depends on:
+ IPlayerWallet
+ Tables
+
+MainMenuFeature
+ depends on:
+ IUIService
+ IResourceService
+ IFeatureNavigator
+
+Warnings
+ EventScratchPanel resolves PlayerData from RootScope.
+ ScratchTicketManager registered as Root but implements IDisposable.
+ 这套设计不是“自动注册万能”,也不是“能力接口万能”。每种机制只解决一个问题。
+| 机制 | +负责 | +不负责 | +容易误用 | +
|---|---|---|---|
| 自动注册 | +减少重复装配代码。 | +不负责说明业务依赖。 | +扫全项目、无 Marker、无生命周期、无诊断。 | +
| 构造注入 | +暴露类的真实依赖。 | +不负责减少注册代码。 | +构造函数塞进万能 Context 或 IServiceProvider。 | +
| 能力接口 | +限制对象能做什么。 | +不负责业务依赖注入。 | +让所有业务对象都实现框架接口。 | +
| Scope | +创建、隔离、释放生命周期对象。 | +不负责业务分层。 | +Feature 临时对象注册到 Root。 | +
| 诊断图 | +让自动注册结果可见。 | +不替代架构边界。 | +只在出错时打印,不在启动报告里展示。 | +
| 方案 | +消费侧感觉 | +好处 | +缺点 | +
|---|---|---|---|
| 旧 GContext | +全局入口很方便,哪里都能 Resolve 和 Publish。 | +接入快,适合业务快速堆功能。 | +依赖隐藏、生命周期不清、事件命令混用、自动发现难追踪。 | +
| 散装 Bootstrap | +消费侧能控制所有细节,但代码像一堆 new 和 Register。 | +启动顺序清楚,没有框架抢主控权。 | +缺少全局配方概念,项目越大越像脚本清单。 | +
| 新版 GameComposition | +消费侧声明一份项目配方,Bootstrap 只 Build Runtime。 | +既有全局概念,又不接管 App 启动;模块显式,模块内自动;可输出诊断图。 | +需要维护 Composition 规范;小项目会觉得比直接 new 多一层;Builder API 设计不好会变成新 DSL 负担。 | +
| GameComposition 成本 | +具体是什么意思 | +什么时候会出问题 | +规避方式 | +
|---|---|---|---|
| 维护 Composition 规范 | +团队必须约定哪些东西写在 Composition,哪些东西写在 Module,哪些东西留在 Feature 内部。 | +如果所有人都往 Composition 塞注册、初始化、业务判断,它会变成新的总控脚本。 | +Composition 只声明结构:安装模块、注册项目级服务、选择策略;业务初始化放 Feature 或项目 Bootstrap 步骤。 | +
| 小项目多一层 | +只有一个场景、几个服务、没有复杂 Feature 切换时,GameComposition 会显得比直接 new 多绕一步。 | +原型项目、Game Jam、小 Demo、一次性工具页面,会被框架流程拖慢。 | +提供 SimpleBootstrap 模式:允许直接 new ServiceContainer + FeatureHost;GameComposition 作为推荐,不作为强制。 | +
| Builder API 变 DSL 负担 | +如果 Builder 做太多语法糖,开发者需要学习一套框架专用语言,而不是普通 C#。 | +出现大量 `UseX().WithY().ForZ().When(...)` 链式调用,真实执行顺序反而不清楚。 | +Builder 只保留少量动词:`UseRoot`、`Install`、`Register`、`UsePolicy`、`EnableDiagnostics`。复杂逻辑回到普通 C#。 | +
可渐进收口,也允许完全重来
++ 当前项目已经有事实上的运行时结构: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 边界。
+判断一次改动是否在正确方向上,就看它有没有让依赖、生命周期、切换入口更显式。
+