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 @@ + + + + + + FlowScope Core 五条约束预览 + + + +
+
+
FlowScope Kernel Contract Preview
+ +
+ +
+
+

五件事,五种责任

+

自动注册不是黑箱,接口也不是装饰。

+

+ 新版 Core 的关键不是“多加几个抽象”,而是把 Unity 项目最容易失控的五件事拆成五种机制: + 自动注册只减少装配代码,构造注入暴露依赖,能力接口限制职责,Scope 管生命周期, + 诊断图让自动化可见。 +

+
+ + +
+ +
+
+

核心思想

+

最新版不应该只是“一堆模块手动装起来”。它的全局概念是消费侧的 GameComposition,框架只把这份配方变成可运行、可诊断、可释放的 FlowScopeRuntime。

+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
概念是谁拥有负责什么不负责什么
GameComposition消费侧声明项目用了哪些模块、项目服务、首个 Feature、启动前置条件。不执行 Feature 生命周期,不释放资源。
CompositionBuilder框架提供,消费侧调用收集模块安装、服务注册、自动扫描规则和诊断信息。不决定项目业务启动顺序。
FlowScopeRuntime框架创建,消费侧持有持有 RootScope、FeatureHost、Diagnostics,作为运行时总句柄。不变成全能 GContext,不允许业务随便从它 Resolve 一切。
FeatureHost框架执行 Feature 切换、Scope 创建、生命周期和失败清理。不写业务逻辑,不决定首个 Feature 是谁。
+
+ +
+ 所以它不是回到 AppKernel 接管一切,也不是散装 Bootstrap。消费侧用一个 CompositionRoot 声明全局结构; + 框架把这份声明转成 Runtime,并保证依赖、权限、生命周期和诊断。 +
+
+ +
+
+

五条约束

+

它们不是并列名词,而是一条链。少写注册不等于隐藏依赖;自动化越多,诊断越要硬。

+
+ +
+
+

自动注册

+

目标是少写重复装配代码。它只能在模块边界内工作,必须有 Marker、生命周期声明和诊断输出。

+
+
+

构造注入

+

目标是让依赖可读、可测、可替换。不要把自动注册变成业务代码到处 Resolve。

+
+
+

能力接口

+

目标是限制职责权限。对象实现什么能力,才获得什么扩展方法;接口必须能复用。

+
+
+

Scope

+

目标是明确谁拥有对象、谁释放对象。Feature 临时对象不能留在 Root。

+
+
+

诊断图

+

目标是让自动注册可审查。启动时能看见每个服务来自哪个模块、依赖谁、生命周期是什么。

+
+
+ +
+
+ install + 显式安装模块 +

消费侧知道装了哪些模块,不让框架猜启动顺序。

+
+
+ scan + 模块内自动注册 +

扫描本模块 Assembly 和 Marker,减少重复注册。

+
+
+ inject + 构造函数声明依赖 +

类依赖在类型定义处暴露,不靠内部 Resolve。

+
+
+ scope + 进入 FeatureScope +

资源、订阅、临时状态进入本次 Feature 生命周期。

+
+
+ report + 输出诊断图 +

自动化结果可见,启动失败能定位到模块和依赖链。

+
+
+
+ +
+
+

消费侧用法

+

消费侧仍然保留 CompositionRoot,但不再把注册散落在 MonoBehaviour 里。项目写一份 GameComposition,Bootstrap 只负责构建 Runtime 和进入首个 Feature。

+
+ +
+
+
+ GameBootstrap + 干净入口 +
+
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();
+    }
+}
+
+ +
+
+ FishingGameComposition + 消费侧全局配方 +
+
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);
+    }
+}
+
+ +
+
+ FeatureHost + Scope 管生命周期 +
+
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);
+    }
+}
+
+ +
+
+ Feature 示例 + 能力接口实际用法 +
+
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;
+}
+
+
+ +
+ 这里的能力接口只建议给 Feature、GameAct 这类框架生命周期对象使用。 + UseCase、Command、ViewModel 不应该为了拿框架能力而实现一堆接口,它们更适合构造注入业务 Port。 +
+
+ +
+
+

诊断图

+

自动注册必须能回答三个问题:谁注册了它,它依赖谁,它属于哪个生命周期。

+
+ +
+
+
+ 诊断接口 + 自动注册不黑箱 +
+
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#。
+
+ +
+ 判断 GameComposition 是否值得引入,看项目是不是已经有跨模块服务、多个 Feature、资源释放、UI 生命周期和启动诊断需求。 + 如果只是一个小 Demo,直接 Bootstrap 更合适;如果是长期 Unity 项目,Composition 能把“项目全局结构”从散落代码里抽出来。 +
+
+ + +
+ + diff --git a/docs/guides/current-project-core-evolution-summary.html b/docs/guides/current-project-core-evolution-summary.html new file mode 100644 index 0000000..c7af33c --- /dev/null +++ b/docs/guides/current-project-core-evolution-summary.html @@ -0,0 +1,1348 @@ + + + + + + 当前项目 Core 演进总结 + + + +
+
+
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。
LoxodonUnity MVVM、DataBinding、Window、ServiceBundle、Messenger。:能解决 UI 状态同步和模块装配,但不解决玩法切换和 Feature 生命周期。ServiceBundle 装配、Binding 管线拆分、Window 状态管理。把 Prefs、Messenger、Binding、UI 全塞进 Core。
uFrameViewModel 可观察状态、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,不要求所有项目用同一套业务分层。
CoreModulesResource、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 绑定。
+
+
+ +
+
+

现有概念映射

+

先做命名和职责收口,再做代码迁移。这样团队讨论时能对齐,不会一上来变成重构战役。

+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
当前项目概念收口后的名字保留职责要移走的职责
GContextRootScope / RootContext全局服务解析、应用级事件、应用级生命周期Act 临时数据、玩法内状态、命令式流程控制
FishingStageFishingStageHost捕鱼会话启动、Stage 级服务、Act 切换编排具体玩法 UI 细节、具体活动资源、散落的数据初始化
AGameActGameAct / FeatureInstance玩法进入退出、资源句柄、UI 面板、事件订阅、临时状态全局服务注册、跨 Act 调度、其他玩法状态修改
UnloadActToNextActIActNavigator / SwitchActCommand表达“我要切到哪个 Act”这个意图不要再作为任意地方都能 Publish 的全局命令事件
ActivityResolverActivityScope / 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 作为长期主协议。
  • +
  • 不要在没有职责边界和验收切片前盲目重写。
  • +
+
+
+
+ + +
+ + diff --git a/docs/guides/loxodon-framework-core-analysis.md b/docs/guides/loxodon-framework-core-analysis.md new file mode 100644 index 0000000..ddb229b --- /dev/null +++ b/docs/guides/loxodon-framework-core-analysis.md @@ -0,0 +1,350 @@ +# Loxodon Framework Core 分析 + +> 分析对象: +> +> 分析日期:2026-06-12 + +## 结论 + +Loxodon Framework 的 Core 本质上是一个 **Unity MVVM + DataBinding 运行时核心**,而不是覆盖完整游戏生命周期的游戏框架内核。 + +它的核心价值集中在: + +- 用 `Context` 和 `ServiceContainer` 提供全局/局部上下文与轻量服务注册。 +- 用 `BindingServiceBundle` 装配 DataBinding 所需的 Parser、Converter、SourceProxy、TargetProxy、Binder。 +- 用 `ViewModelBase`、`ObservableObject`、Observable Collection 支撑 ViewModel 状态变化。 +- 用 `UIView`、`Window`、`WindowManager` 提供 UI 视图、窗口状态和转场管理。 +- 用 `Messenger`、`Command`、`Async`、`Prefs`、`Localization` 等模块补齐 MVVM UI 开发所需的基础设施。 + +因此,Loxodon 更适合作为 FlowScope 的 **UI / Binding / ServiceBundle 外围模块参考**,不适合作为 FlowScope Kernel 的边界模板。 + +## Core 模块拆解 + +### Context + +源码位置: + +- +- + +`Context` 维护静态 `ApplicationContext` 和按 key 注册的多个 `Context`,每个 Context 内部有: + +- `IServiceContainer` +- 属性字典 +- 可级联查询的 parent context + +`ApplicationContext` 在 `Context` 基础上补了: + +- `IMainLoopExecutor` +- Global Preferences +- User Preferences + +这个设计方便应用层快速拿到全局能力,但也说明 Loxodon 的 Core 并不是纯机制内核。它把主线程执行器和 Prefs 这类应用级能力直接放进了全局上下文。 + +### ServiceContainer + +源码位置: + +- +- +- + +`ServiceContainer` 是轻量服务定位器,支持: + +- 按 `Type` 注册对象或 factory。 +- 按 `string name` 注册对象或 factory。 +- `Resolve()` / `Resolve(string)` 解析服务。 +- `Unregister()` / `Unregister(string)` 注销服务。 + +`ServiceBundle` 是模块装配入口: + +```csharp +public interface IServiceBundle +{ + void Start(); + void Stop(); +} +``` + +`AbstractServiceBundle` 把容器传给 `OnStart` / `OnStop`,由具体 bundle 负责注册和注销一组服务。 + +这个模式对 FlowScope 有参考价值:模块可以用显式 `Start/Stop` 完成能力注册,避免散落的静态初始化。 + +### Binding + +源码位置: + +- +- + +Binding 是 Loxodon Core 最厚、最核心的部分。 + +`BindingServiceBundle` 会装配: + +- `PathParser` +- `ExpressionPathFinder` +- `ConverterRegistry` +- `ObjectSourceProxyFactory` +- `SourceProxyFactory` +- `TargetProxyFactory` +- `BindingFactory` +- `StandardBinder` + +然后注册: + +- `IBinder` +- `IBindingFactory` +- `IConverterRegistry` +- `IExpressionPathFinder` +- `IPathParser` +- `INodeProxyFactory` +- `ISourceProxyFactory` +- `ITargetProxyFactory` + +这说明 Loxodon 的 Binding 不是简单的反射赋值,而是一条可扩展管线: + +```text +BindingDescription + -> Path / Expression + -> SourceProxy + -> Converter + -> TargetProxy + -> Binding +``` + +README 也强调该框架围绕 DataBinding 做了性能优化:减少装箱、降低 GC、通过动态委托或静态织入降低反射成本。 + +### ViewModel / Observable + +源码位置: + +- +- +- + +`ViewModelBase` 继承 `ObservableObject`,核心职责是: + +- 封装属性变更。 +- 触发 `PropertyChanged`。 +- 可选通过 `IMessenger` 广播 `PropertyChangedMessage`。 + +典型路径是: + +```text +ViewModel property set + -> Set + -> field updated + -> RaisePropertyChanged + -> optional Messenger.Publish(PropertyChangedMessage) + -> Binding updates target UI +``` + +这是标准 MVVM 的数据驱动 UI 模型。 + +### View / Window + +源码位置: + +- +- +- + +`UIView` 负责 Unity UI 基础行为: + +- `RectTransform` +- `CanvasGroup` +- `Visibility` +- Enter / Exit Animation +- Enable / Disable event + +`Window` 在 `UIView` 基础上增加: + +- `WindowType` +- `windowPriority` +- `WindowState` +- activated / dismissed / visibility changed event +- state broadcast + +`WindowManager` 管理窗口集合、当前窗口、可见窗口和转场执行。 + +这部分是 UI 框架能力,不是游戏 Feature 生命周期管理。 + +### Messaging / Command / Async / Prefs + +源码位置: + +- +- +- +- + +这些模块服务于 MVVM/UI 工程化: + +- `Messenger`:按消息类型和 channel 做发布订阅。 +- `RelayCommand`:给 UI 事件绑定命令。 +- `AsyncResult` / `ProgressResult` / `CoroutineTask`:把异步、协程、进度结果统一成可等待对象。 +- `Preferences`:默认基于 `PlayerPrefs`,支持全局和用户偏好。 + +这些能力有实用价值,但它们放在 Core 中会让 Core 变厚。对 FlowScope 来说,应当拆成独立模块或适配层,而不是进入 Kernel。 + +## 架构优点 + +### 1. MVVM UI 工程化成熟 + +Binding、ViewModel、Command、Window、Messenger 形成了完整闭环。对于 UI 重、状态同步复杂、需要数据绑定的 Unity 项目,Loxodon 能显著减少手写 UI 刷新代码。 + +### 2. Binding 管线拆得足够细 + +`SourceProxy`、`TargetProxy`、`Converter`、`PathParser`、`Binder` 的拆分使绑定系统具备较强扩展性。不同 UI 系统或不同数据源可以通过 proxy/factory 接入,而不是改主流程。 + +### 3. ServiceBundle 模式简单可控 + +`Start/Stop` 注册/注销一组服务,适合插件化装配。相比全局静态初始化,这种方式更容易表达模块边界。 + +### 4. 有性能意识 + +README 中明确强调减少 value type 装箱、减少 GC、优化绑定性能。这说明 Loxodon 的设计目标不是单纯追求抽象,而是面向 Unity UI 高频更新场景。 + +## 架构局限 + +### 1. Core 边界偏厚 + +`ApplicationContext` 直接提供主线程执行器和 Preferences,`Preferences` 默认落到 `PlayerPrefs`,`Messenger.Default` 也带有全局静态倾向。 + +这些设计让使用更方便,但会让 Core 逐步吸收平台、存储、消息、UI 等应用级能力。 + +### 2. 不是完整游戏框架内核 + +Loxodon Core 没有把以下能力作为主轴: + +- 游戏 Feature 生命周期 +- 模块启动顺序 +- 资源后端边界 +- 配置表加载边界 +- 存档版本迁移 +- 热更新策略 +- 场景/流程切换策略 + +它的插件生态里有 Addressable、Data、XLua、ILRuntime 等扩展,但 Core 本体的主轴仍然是 MVVM/DataBinding。 + +### 3. 对小团队很省事,对强边界内核有侵蚀风险 + +如果项目目标是快速做 UI 驱动业务,Loxodon 的全局上下文和内置 Prefs 会很方便。 + +但如果目标是 FlowScope 这种机制型内核,直接照搬会让 Kernel 同时承担 UI、Prefs、Messaging、Async、Binding 等职责,边界会变得越来越难控。 + +## 对 FlowScope 的借鉴建议 + +### 可以借鉴 + +1. **ServiceBundle 的模块装配方式** + + FlowScope 的 P1/P2 模块可以采用类似显式装配模式: + + ```text + ModuleBundle.Start(container) + -> register contracts + -> register default implementations + -> register adapters + + ModuleBundle.Stop(container) + -> unregister contracts + -> release module-owned state + ``` + +2. **Binding 管线的分层思想** + + 如果 FlowScope 后续需要 UI Binding 或 Editor Binding,可以参考 Loxodon 的拆分: + + ```text + Source + Target + Path + Converter + Binder + BindingContext + ``` + + 但这应该作为 UI 扩展模块,而不是 Kernel 机制。 + +3. **ViewModelBase 的属性变更封装** + + `Set` 模式可以降低 ViewModel 代码重复,也能统一 PropertyChanged 和消息广播策略。 + +4. **Window 状态模型** + + `Window` / `WindowManager` 对弹窗、页面、转场、可见性和激活状态有参考价值,可用于 FlowScope UI 模块设计。 + +### 不建议照搬 + +1. **不要把 Prefs 放进 Kernel** + + FlowScope 的 Save / Prefs / PlayerPrefs 应保持在 Save 模块或 Unity Adapter 中。 + +2. **不要把 Messenger 作为 Kernel 默认通信机制** + + 消息总线容易变成隐式耦合通道。Kernel 应保持 Feature 生命周期和 Slot/Policy 机制清晰,事件系统应作为 P1/P2 模块单独设计。 + +3. **不要把 UI Window 放进 Kernel** + + UI 是 Feature/Module 的外围能力。Kernel 只需要管理 Feature 的启动、替换、停止策略,不应知道 Window 栈。 + +4. **不要用全局 ApplicationContext 承载所有能力** + + FlowScope 更适合显式 Bootstrap + Container + FeatureContext。全局上下文可以作为上层便利入口,但不应成为 Kernel 的事实中心。 + +## 与 FlowScope Kernel 边界的关系 + +FlowScope 当前更适合保持: + +```text +Kernel + -> Container + -> AppKernel + -> StartupPipeline + -> FeatureHost + -> FeatureInstance + -> FeatureContext + -> FeatureLifetime + -> FeatureSlot + -> FeaturePolicy +``` + +Loxodon 的能力更适合放在: + +```text +FlowScope.UI + -> ViewModel + -> Binding + -> Window / Panel + -> Command + +FlowScope.Events + -> Messenger / EventBus + +FlowScope.Save.Unity + -> PlayerPrefs adapter + +FlowScope.Runtime.Extensions + -> ServiceBundle helpers +``` + +换句话说,Loxodon 对 FlowScope 的启发是:**外围模块可以做厚,Kernel 必须保持薄。** + +## 最终判断 + +Loxodon Framework 的 Core 是一个成熟的 Unity MVVM/UI 运行时。它适合解决 UI 数据绑定、ViewModel 状态传播、窗口管理和 UI 事件命令化问题。 + +但它不是 FlowScope Kernel 的直接模板。对 FlowScope 来说,最值得吸收的是: + +- `ServiceBundle` 式模块装配。 +- Binding 管线拆分方式。 +- ViewModel 属性变更封装。 +- Window 状态管理经验。 + +最需要避免的是: + +- 把 Prefs、UI、Messaging、Binding 一起塞进 Core。 +- 用全局上下文替代显式生命周期。 +- 让 Kernel 从机制层滑向应用便利层。 + diff --git a/docs/guides/qframework-core-analysis.md b/docs/guides/qframework-core-analysis.md new file mode 100644 index 0000000..b278533 --- /dev/null +++ b/docs/guides/qframework-core-analysis.md @@ -0,0 +1,501 @@ +# QFramework Core 细读与 FlowScope 借鉴边界 + +更新时间:2026-06-12 + +参考源码: + +本文分析 QFramework 单文件 Core 的架构设计、适用范围、优缺点,以及它对 FlowScope Kernel / CoreModules 的可借鉴部分。本文不是选型结论,也不是实现计划;它用于后续讨论 FlowScope 业务架构层、事件层、状态层时作为参考。 + +## 1. 总结 + +QFramework Core 的价值不在于提供完整商业游戏运行时,而在于用很少的类型固定 Unity 业务代码的依赖方向。 + +它的核心模型可以概括为: + +```text +Architecture + -> IOCContainer + -> Model / System / Utility + -> Command / Query + -> Event / BindableProperty + +Controller / MonoBehaviour + -> SendCommand / SendQuery + -> GetModel / GetSystem + -> RegisterEvent +``` + +QFramework Core 解决的是 Unity 项目里常见的“表现层乱改状态、MonoBehaviour 互相引用、业务逻辑没有入口、通知链路散落”的问题。它不是资源框架、UI 框架、热更框架、构建框架,也不负责 Feature、场景、流程或模块生命周期编排。 + +对 FlowScope 来说,QFramework Core 最值得借鉴的是: + +- 状态变更必须有入口。 +- 上层可以调用下层,下层不直接引用上层。 +- 下层通过事件或可观察状态通知上层。 +- Command / Query 是短生命周期行为对象,不承载长期状态。 +- 用能力接口限制对象可访问的架构能力。 + +不适合直接照搬的是: + +- 静态全局 `Architecture.Interface` 入口。 +- 强制 `Model / System / Utility / Command` 成为 Kernel 业务分层。 +- 没有 RootScope / FeatureScope 的单容器模型。 +- 没有 Feature 级生命周期和释放边界。 +- 把 Event / BindableProperty 作为所有项目默认基础设施。 + +## 2. Core 范围 + +QFramework.cs 里的核心组成大致分为以下几组。 + +| 组 | 类型 | 职责 | +| --- | --- | --- | +| 架构根 | `IArchitecture`, `Architecture` | 注册和获取 Model / System / Utility,执行 Command / Query,发送和注册事件 | +| 表现层协议 | `IController` | 通常由 MonoBehaviour 实现,负责接入架构,不直接持有业务状态 | +| 业务逻辑层 | `ISystem`, `AbstractSystem` | 放跨表现层共享的业务逻辑 | +| 数据层 | `IModel`, `AbstractModel` | 放状态和数据操作 | +| 工具层 | `IUtility` | 放外部能力适配,例如存储、SDK、序列化 | +| 行为对象 | `ICommand`, `ICommand`, `IQuery` | 封装写操作和读操作 | +| 能力接口 | `ICanGetModel`, `ICanSendCommand` 等 | 用接口组合限制对象能做什么 | +| 事件系统 | `TypeEventSystem`, `EasyEvent` | 按类型注册、发送、反注册事件 | +| 可绑定状态 | `BindableProperty` | 值变化时通知订阅者 | +| 容器 | `IOCContainer` | 按类型注册和解析实例 | + +这个范围说明 QFramework Core 更接近“业务代码架构层”,而不是“应用 Kernel”。它把代码写法约束住,但不管理完整应用运行期。 + +## 3. Architecture + +`Architecture` 是 QFramework 的架构根。它使用静态字段保存当前架构实例,`Interface` 首次访问时触发初始化。 + +典型启动过程: + +1. 创建 `T : Architecture` 实例。 +2. 调用子类实现的 `Init()`。 +3. 在 `Init()` 中注册 Model、System、Utility。 +4. 执行 `OnRegisterPatch`。 +5. 初始化所有未初始化的 Model。 +6. 初始化所有未初始化的 System。 +7. 将架构标记为已初始化。 + +反初始化过程: + +1. 调用 `OnDeinit()`。 +2. 对已初始化的 System 调用 `Deinit()`。 +3. 对已初始化的 Model 调用 `Deinit()`。 +4. 清空容器。 +5. 清空静态架构实例。 + +这个设计很适合中小项目:入口少、初始化简单、读源码很快。它的问题也同样明确:全局静态入口会自然演化成 Service Locator,业务对象很容易绕开组合根直接拿依赖。 + +FlowScope 不应该照搬这个入口。FlowScope 的 Kernel 需要显式 `AppKernel`、`RootScope`、`StartupPipeline` 和 `FeatureHost`,让启动顺序、依赖边界和关闭顺序都可测试、可替换、可释放。 + +## 4. Model / System / Utility + +QFramework 的分层规则可以简化理解为: + +```text +Controller -> Command / Query -> System / Model -> Utility +Model / System -> Event / BindableProperty -> Controller +``` + +### Model + +`IModel` 表示数据层。它可以: + +- 持有状态。 +- 调用 Utility。 +- 发送事件。 +- 初始化和反初始化。 + +它不应该直接引用 Controller,也不应该直接操作 UI。 + +### System + +`ISystem` 表示共享业务逻辑层。它可以: + +- 获取 Model。 +- 获取 Utility。 +- 获取其他 System。 +- 注册事件。 +- 发送事件。 +- 初始化和反初始化。 + +System 适合放跨 UI、跨场景、跨表现层复用的业务逻辑。它比 Model 更偏行为,比 Controller 更偏领域。 + +### Utility + +`IUtility` 是最薄的一层,只是一个标记接口。它通常承载外部能力,例如: + +- 本地存储。 +- 网络 SDK。 +- 平台 SDK。 +- JSON 序列化。 +- 时间服务。 + +Utility 本身不被强制注入 Architecture,这也意味着它更像纯工具或外部适配器。 + +### 对 FlowScope 的启发 + +FlowScope Kernel 不应该规定 `Model / System / Utility`。这是项目业务架构选择,不是 Kernel 机制。 + +但 FlowScope 可以在 CoreModules 或推荐模板里提供类似规则: + +- Feature 内部状态归 Feature 自己或 Feature-scoped state service。 +- 跨 Feature 长期状态归 Root-scoped service。 +- 外部能力通过接口注册到 RootScope。 +- UI 和 MonoBehaviour 不直接写长期状态,而是调用 UseCase、Command 或 Service 方法。 + +## 5. Command / Query + +QFramework 用 Command 作为状态变更入口,用 Query 作为读操作入口。 + +Command 的特征: + +- 短生命周期。 +- 执行前被注入 Architecture。 +- 可获取 Model / System / Utility。 +- 可发送事件。 +- 可继续发送其他 Command 或 Query。 +- 不保存长期状态。 + +Query 的特征: + +- 短生命周期。 +- 执行前被注入 Architecture。 +- 主要读取 Model / System。 +- 返回查询结果。 + +这个设计的好处是把“写操作”从 Controller 中拿出来。Controller 不再到处写: + +```csharp +model.Coins.Value += 10; +``` + +而是写: + +```csharp +this.SendCommand(new AddCoinCommand(10)); +``` + +这样状态变化可以被搜索、测试和复用。 + +对 FlowScope 来说,这个思想很值得吸收,但名称和形态不应进 Kernel。Kernel 不应该强制所有状态变更都叫 Command。项目可以选择: + +- `Command` +- `UseCase` +- `Action` +- `Service Method` +- `Application Service` + +Kernel 只需要保证依赖边界和生命周期,不需要规定业务行为对象的命名体系。 + +## 6. 能力接口组合 + +QFramework 一个很好的设计点是能力接口组合。 + +例如: + +- `ICanGetModel` +- `ICanGetSystem` +- `ICanGetUtility` +- `ICanSendCommand` +- `ICanSendEvent` +- `ICanRegisterEvent` +- `ICanSendQuery` + +这些接口本身几乎没有实现,实际能力通过扩展方法提供。对象只有实现了对应接口,才获得对应扩展方法。 + +这带来的好处是: + +- Controller 能做的事和 Model 能做的事不同。 +- Model 可以发事件,但不能随意发 Command。 +- Command 可以访问 Model/System/Utility,但不变成长期服务。 +- 架构规则体现在类型系统里,而不是只写在文档里。 + +这个方向对 FlowScope 有参考价值。FlowScope 如果后续做业务架构层,可以避免提供一个万能 `FeatureContext`。更好的方式是拆小能力: + +```text +ICanResolveService +ICanPublishEvent +ICanReadState +ICanExecuteUseCase +ICanRegisterLifetime +``` + +但这些应属于 P1/P2 业务层或扩展层。Kernel 的 `FeatureContext` 仍应保持最小,只暴露 Scope、Lifetime、CancellationToken、Handle 等机制概念。 + +## 7. TypeEventSystem + +QFramework 的 `TypeEventSystem` 是一个按事件类型分发的轻量事件系统。 + +它的核心机制: + +```text +TypeEventSystem + -> EasyEvents + -> Dictionary + -> EasyEvent +``` + +发送事件时按 `EasyEvent` 类型取出事件对象并触发。注册事件时如果没有对应事件对象,就创建一个。 + +优点: + +- 使用简单。 +- 类型安全。 +- 适合 UI 刷新和轻量业务通知。 +- 反注册模型清晰。 + +风险: + +- 没有作用域隔离。 +- 没有事件优先级。 +- 没有异步派发协议。 +- 没有错误隔离。 +- 没有事件链路追踪。 +- 大型项目里容易变成全局黑箱。 + +FlowScope 可以借鉴“下层发事件,上层订阅”的方向,但 EventBus 不应进入 Kernel 主依赖。更合理的位置是 `FlowScope.Events` 或 P1 Events 模块: + +```text +Kernel + 不知道 EventBus + +P1 Events + 注册 IEventBus 到 RootScope + 提供 FeatureLifetime 可登记的订阅句柄 + 提供同步或异步事件策略 +``` + +这样 Feature 可以使用事件,但 Kernel 不被事件模型绑死。 + +## 8. BindableProperty + +`BindableProperty` 是一个可观察值对象。它在 `Value` 变化时触发 `EasyEvent`,并提供: + +- `Register` +- `RegisterWithInitValue` +- `UnRegister` +- `SetValueWithoutEvent` +- 自定义比较器 + +它适合表达 UI 绑定和简单状态监听,例如金币、生命值、进度条、开关状态。 + +优点: + +- 简单直接。 +- UI 订阅体验好。 +- `RegisterWithInitValue` 避免 UI 初始刷新遗漏。 +- 对中小项目足够实用。 + +风险: + +- 比较器是静态泛型级别配置,使用时要小心全局影响。 +- 复杂状态容易被拆成大量可绑定字段,导致状态事务边界不清。 +- 不适合表达复杂异步流、错误流、完成流。 +- 如果到处暴露可写 `BindableProperty`,仍然会出现状态乱改。 + +FlowScope 可以把类似能力放到状态或 UI 扩展模块,但不应放进 Kernel。Kernel 只负责 Feature 生命周期;状态响应式属于业务层或表现层。 + +## 9. IOCContainer + +QFramework 的 `IOCContainer` 很小: + +- `Register(T instance)` 按 `typeof(T)` 注册。 +- `Get()` 按 `typeof(T)` 获取。 +- `GetInstancesByType()` 枚举所有兼容类型实例。 +- `Clear()` 清空。 + +优点: + +- 极易理解。 +- 调试成本低。 +- 足够支撑 QFramework 的 Model/System/Utility 注册。 + +不足: + +- 没有构造函数注入。 +- 没有 Scope。 +- 没有生命周期释放。 +- 没有 `IDisposable` / `IAsyncDisposable` 管理。 +- 没有多绑定。 +- 没有 named/keyed service。 +- 没有循环依赖检测。 +- 没有线程安全。 + +FlowScope 可以借鉴“按类型注册和解析”的最小体验,但实现必须比 QFramework 的容器多一层生命周期能力: + +```text +RootScope + 长生命周期服务 + +FeatureScope + Feature 实例和本次启动参数 + 先查本地,再回退 RootScope + +FeatureLifetime + 释放运行期句柄、订阅、资源组、临时对象 +``` + +这也是 FlowScope 不能直接采用 QFramework IOC 的核心原因。 + +## 10. 生命周期对比 + +| 维度 | QFramework Core | FlowScope Kernel 目标 | +| --- | --- | --- | +| 应用入口 | 静态 `Architecture.Interface` | 显式 `AppKernel` | +| 启动流程 | 首次访问时初始化 | `StartupPipeline` 顺序执行 | +| 依赖容器 | 单个 `IOCContainer` | `RootScope + FeatureScope` | +| 业务运行单元 | Model/System 长驻 | Feature 实例 | +| Feature 生命周期 | 不负责 | `LoadAsync / EnterAsync / ExitAsync` | +| 释放边界 | Model/System Deinit,容器 Clear | `FeatureLifetime + FeatureScope + RootScope` | +| 异步关闭 | 不突出 | Kernel 必须处理 async stop/shutdown | +| 运行位置 | 不负责 | `FeatureSlot + FeaturePolicy` | + +QFramework 的生命周期适合“一个游戏一个 Architecture,若干长驻 Model/System”。FlowScope 的目标是“一个应用 Kernel 管多个 Feature 实例,每个 Feature 有独立依赖边界和释放边界”。 + +这两个方向不冲突,但层级不同。QFramework 更像可选的业务组织方式,FlowScope Kernel 更像运行机制。 + +## 11. 与完整框架的横向比较 + +| 维度 | QFramework Core | UnityGameFramework | ET Framework | TinaX | FlowScope 应吸收 | +| --- | --- | --- | --- | --- | --- | +| 核心定位 | 轻量业务分层 | 客户端模块全家桶 | 双端大型联网运行时 | 服务化 Package 框架 | 小 Kernel + 可选模块 | +| 启动入口 | Architecture | GameEntry / Procedure | Scene / Fiber / Actor | Core service bootstrap | AppKernel / StartupPipeline | +| 资源系统 | Core 不负责 | 内置 Resource | 可接入包和热更链路 | VFS | P1 Resources 模块 | +| UI | Core 不负责,UIKit 在工具生态 | 内置 UI 模块 | 通常通过包扩展 | UIKit | P1 UI 模块 | +| 事件 | TypeEventSystem | Event 模块 | 消息/事件体系 | Event/System service | P1 Events 模块 | +| 状态变更 | Command | 模块 API / Procedure | Entity/System/消息 | Service API | 可选 Command/UseCase | +| 工程流水线 | 不负责 | 部分覆盖 | 强工具链 | Package 化 | Tools/Package 后续模块 | + +QFramework 在这张表里的优势是轻量和清晰。它不应该被拿来和 UGF 的资源/UI/Procedure 覆盖度硬比,也不应该被要求承担 BDFramework 的热更构建流水线职责。 + +## 12. 对 FlowScope 的建议 + +### 12.1 Kernel 不吸收 QFramework 分层 + +FlowScope Kernel 继续保持: + +```text +AppKernel +StartupPipeline +FeatureHost +Container +FeatureLifetime +FeatureSlot +FeaturePolicy +IFeature +FeatureContext +FeatureHandle +``` + +Kernel 不加入: + +- `IModel` +- `ISystem` +- `IUtility` +- `ICommand` +- `IQuery` +- `BindableProperty` +- `TypeEventSystem` +- `IController` + +原因是这些属于业务架构风格,不是 Feature 编排机制。 + +### 12.2 CoreModules 可吸收状态变更规则 + +FlowScope 可以在后续 CoreModules 或 template 中提供推荐规则: + +```text +View / MonoBehaviour + -> Command / UseCase / Service Method + -> State Service / Feature Local State + -> Event / Observable + -> View / Presenter / Controller +``` + +这能吸收 QFramework 的优点,同时避免 Kernel 被业务范式锁死。 + +### 12.3 Events 模块可参考 TypeEventSystem,但要补足边界 + +如果 FlowScope 做 P1 Events,不建议只复制 `TypeEventSystem`。至少要明确: + +- 事件作用域:Root-scoped 还是 Feature-scoped。 +- 订阅句柄如何进入 FeatureLifetime。 +- 事件处理异常是否聚合、记录或继续派发。 +- 是否支持异步事件。 +- 是否允许跨 Feature 事件。 +- 是否提供调试追踪。 + +QFramework 的 TypeEventSystem 可作为最小同步事件模型参考,但不是最终模块规格。 + +### 12.4 State 模块可参考 BindableProperty,但不要裸露可写状态 + +如果 FlowScope 提供类似 BindableProperty 的能力,建议区分: + +```text +IReadonlyState +IMutableState +StateWriter +StateChanged +``` + +UI 层优先拿只读接口,写入通过 UseCase/Command/Service。这样比直接把 `BindableProperty` 暴露给所有人更稳。 + +### 12.5 业务模板可以提供 QFramework 风格 + +可以考虑后续提供一个可选模板: + +```text +FlowScope.BusinessArchitecture + - ICommand + - ICommand + - IQuery + - IUseCase + - IStateService + - IEventPublisher +``` + +但这应是项目选择,不是 Kernel 强制。 + +## 13. 风险清单 + +如果直接照搬 QFramework Core 到 FlowScope,主要风险是: + +1. Kernel 变成业务架构框架,失去机制最小化。 +2. `Architecture.Interface` 和 `RootScope` 形成双全局入口。 +3. `IOCContainer` 无法表达 FeatureScope 生命周期。 +4. Command/Event 进入 Kernel 后,P1/P2 模块会被迫统一业务风格。 +5. EventBus 变成隐式跨 Feature 通道,削弱 FeatureHost 的显式编排边界。 +6. BindableProperty 进入底层后,UI/状态响应式会反向污染 Kernel。 + +规避方式: + +- Kernel 只保留生命周期、依赖边界、Feature 编排。 +- QFramework 的分层规则只进入文档、模板或可选模块。 +- Event/State/Command 都作为 CoreModules 或扩展层设计。 +- FeatureContext 不提供全能业务能力。 + +## 14. 结论 + +QFramework Core 是一个优秀的轻量 Unity 业务架构样本。它用很少代码表达了三条关键规则: + +- 上层驱动下层。 +- 状态变更有入口。 +- 下层通过通知影响上层。 + +FlowScope 应吸收这些规则,但不能把 QFramework 的 Core 形态直接变成自己的 Kernel。FlowScope Kernel 的目标更底层:它负责应用启动、依赖 Scope、Feature 生命周期、运行槽和释放链。QFramework 风格更适合作为 FlowScope 的可选业务层、示例模板或 P1/P2 模块设计参考。 + +最终边界建议: + +```text +FlowScope.Kernel + 只负责机制 + +FlowScope.CoreModules + 提供 Events / State / Config / Save / Resources / UI 等模块 + +FlowScope.BusinessTemplate.QFrameworkLike + 可选提供 Command / Query / State / Event 的业务写法参考 +``` + +这样既能保留 QFramework 的轻量分层优点,又不会牺牲 FlowScope 当前“Kernel tiny, modules optional”的主方向。 diff --git a/docs/reviews/2026-06-12-uframe-mvvm-core-analysis.md b/docs/reviews/2026-06-12-uframe-mvvm-core-analysis.md new file mode 100644 index 0000000..561003a --- /dev/null +++ b/docs/reviews/2026-06-12-uframe-mvvm-core-analysis.md @@ -0,0 +1,473 @@ +# uFrame MVVM Core 分析 + +日期:2026-06-12 + +## 结论 + +uFrame MVVM 的核心价值不在它的 Kernel、IOC 启动或代码生成器,而在运行时 MVVM 的几个机制: + +- `ViewModel` 持有可观察状态。 +- `P` 把 ViewModel 属性包装成可订阅的数据流。 +- `Signal` 把 UI 操作表达成命令流。 +- `ViewBase` 通过 `Bind / Unbind` 管理订阅生命周期。 +- `ViewService` 负责把 View 与 ViewModel 对接。 + +但这套 MVVM 被 uFrame 自身的全局 Kernel、全局容器、命名约定、`Resources.Load` 和设计器生成代码强绑定。它可以作为反面边界样本和局部机制参考,不适合作为 FlowScope Kernel 或 UI 模块的直接原型。 + +FlowScope 可以吸收“可观察状态 + 命令流 + 绑定生命周期作用域”,但不应吸收它的全局 IOC 查找、ViewModel 自动解析、Controller 命名约定和资源自动定位。 + +## 源码范围 + +本次分析基于 `uFrame/uFrame.MVVM` 当前公开仓库源码,重点阅读: + +- [`ViewModel.cs`](https://github.com/uFrame/uFrame.MVVM/blob/master/uFrame.MVVM/Source/Runtime/ViewModels/ViewModel.cs) +- [`ModelPropertyBase.cs`](https://github.com/uFrame/uFrame.MVVM/blob/master/uFrame.MVVM/Source/Runtime/ViewModels/ModelPropertyBase.cs) +- [`Signal.cs`](https://github.com/uFrame/uFrame.MVVM/blob/master/uFrame.MVVM/Source/Runtime/ViewModels/Signal.cs) +- [`ViewBase.cs`](https://github.com/uFrame/uFrame.MVVM/blob/master/uFrame.MVVM/Source/Runtime/Views/ViewBase.cs) +- [`View.cs`](https://github.com/uFrame/uFrame.MVVM/blob/master/uFrame.MVVM/Source/Runtime/Views/View.cs) +- [`Controller.cs`](https://github.com/uFrame/uFrame.MVVM/blob/master/uFrame.MVVM/Source/Runtime/Controllers/Controller.cs) +- [`ViewService.cs`](https://github.com/uFrame/uFrame.MVVM/blob/master/uFrame.MVVM/Source/Runtime/Services/ViewService.cs) +- [`MVVMKernelExtensions.cs`](https://github.com/uFrame/uFrame.MVVM/blob/master/uFrame.MVVM/Source/Runtime/MVVMKernelExtensions.cs) +- [`ViewBindings.cs`](https://github.com/uFrame/uFrame.MVVM/blob/master/uFrame.MVVM/Source/Runtime/Bindings/ViewBindings.cs) +- [`ViewResolver.cs`](https://github.com/uFrame/uFrame.MVVM/blob/master/uFrame.MVVM/Source/Runtime/Views/ViewResolver.cs) + +## 运行时结构 + +uFrame MVVM 的运行时可以拆成五组对象: + +```text +ViewModel 层 + ViewModel + P + ModelCollection + Signal + ViewModelCommand + +View 层 + ViewBase + View + ViewComponent + ViewBindings + +创建与连接层 + Controller + ViewService + ViewResolver + +绑定层 + Binding + ModelPropertyBinding + ModelViewPropertyBinding + ModelViewModelCollectionBinding + +全局基础设施 + uFrameKernel.Container + IEventAggregator + SystemService / SystemServiceMonoBehavior +``` + +它不是一个独立、纯粹的 MVVM runtime。`ViewModel`、`View`、`Controller` 和 `ViewService` 都会触碰 uFrame Kernel 或全局容器。 + +## ViewModel + +`ViewModel` 是所有生成 ViewModel 的基类。它承担了状态、绑定、序列化、事件发布、销毁通知等多种职责。 + +关键职责: + +- 通过 `FillProperties` 暴露属性元信息。 +- 通过 `FillCommands` 暴露命令元信息。 +- 持有 `Bindings`,用于释放订阅。 +- 持有 `Identifier`,用于全局容器按 id 查找。 +- 通过 `PropertyChanged` 通知属性变化。 +- `Dispose` 时发布 `ViewModelDestroyedEvent`。 + +问题是它太重。一个干净的 ViewModel 应该主要表达 UI 状态和 UI 意图,但这里的 `ViewModel` 混入了: + +- 序列化协议。 +- `IBindable`。 +- `IDisposable`。 +- 全局事件聚合器。 +- binding 容器。 +- 引用计数。 +- controller wiring hook。 + +这会让 ViewModel 从“状态对象”变成“框架对象”。长期项目里,这种设计会让测试、复用和生命周期判断都变复杂。 + +## P + +`P` 是 uFrame MVVM 最值得看的部分。它把单个属性封装成 observable property: + +```csharp +public class P : ISubject, IObservableProperty, ISimpleNotifyPropertyChanged +``` + +它提供: + +- `Value`:当前值。 +- `LastValue`:上一次值。 +- `ChangedObservable`:只在值变化时通知。 +- `ObjectValue`:非泛型访问入口。 +- `Owner`:所属 ViewModel。 +- `PropertyName`:属性名。 +- `ToComputed`:根据其他 observable property 计算派生值。 + +View 侧可以订阅 `P`,属性变化时刷新 UI。这个机制适合 Unity UI,因为它避免了大量手动 `Refresh()` 和每帧轮询。 + +可取点: + +- 属性变化可以显式订阅。 +- 派生属性可以声明依赖。 +- View 绑定时可以立即应用当前值。 +- IDisposable 可以挂到绑定作用域统一释放。 + +风险点: + +- `Value` setter 没有先比较新旧值,任何赋值都会触发通知。 +- `Owner` 让 property 反向知道 ViewModel。 +- `P` 同时实现 subject、property、notify,职责偏多。 +- `ChangedObservable` 依赖 `Value` 和 `LastValue` 的外部状态,语义不够直观。 + +FlowScope 如果吸收这个思路,应改成更小的 `ObservableProperty`: + +```text +只负责保存值、比较值、发布变化、释放订阅。 +不要知道全局 Kernel。 +不要知道 View。 +不要知道资源或 Controller。 +``` + +## Signal + +`Signal` 用来表达 ViewModel 命令: + +```csharp +public class Signal : ISubject, ISignal + where TClass : IViewModelCommand, new() +``` + +它的行为是: + +1. View 或绑定层构造 command。 +2. 调用 `Signal.OnNext(command)`。 +3. `Signal` 把 `Sender` 设置为当前 ViewModel。 +4. 执行本地 `Action`。 +5. 推送给订阅者。 + +这个方向是好的:UI 操作不应该直接调用业务系统,而应该变成可观察的 command 或 intent。 + +可取点: + +- UI 输入被建模为显式命令。 +- 命令可以订阅、组合、测试。 +- View 与业务处理之间不需要直接互相引用。 + +风险点: + +- command 的 sender 被 `Signal` 自动写入,有隐式副作用。 +- 命令流最终仍然容易接到全局 EventAggregator。 +- 生成器会为每个命令生成强约定代码,手写维护成本高。 + +FlowScope 可以吸收 command stream,但建议命名为更中性的 `UIIntent` / `ViewAction` / `FeatureCommand`,并放在 feature scope 内,不进入 Kernel core。 + +## ViewBase / View + +`ViewBase` 是 View 与 ViewModel 的连接点。它提供: + +- `ViewModelObject`:当前绑定的 ViewModel。 +- `Bind()`:用户编写订阅逻辑。 +- `PreBind()`:生成器插入绑定逻辑。 +- `AfterBind()`:绑定完成后的 hook。 +- `Unbind()`:释放绑定。 +- `AddBinding()`:把 disposable 放到当前 View 的绑定列表。 +- `BindOnStart`:是否启动时自动绑定。 +- `DisposeViewModelOnDestroy`:View 销毁时是否销毁 ViewModel。 + +`View` 只是给 `ViewBase` 加了泛型 Model 访问: + +```csharp +public abstract class View : ViewBase where TModel : ViewModel, new() +``` + +可取点: + +- View 的绑定逻辑有明确生命周期。 +- 订阅统一挂到 binding scope。 +- View 只响应 ViewModel 的状态,不需要每帧刷 UI。 +- View 销毁时可以集中释放订阅。 + +问题点: + +- `ViewBase.KernelLoaded()` 会调用 `uFrameKernel.Container.Inject(this)`。 +- `OnDestroy()` 依赖 `uFrameKernel.IsKernelLoaded` 和全局事件。 +- View 的 `Bindings` 实际挂在 `ViewModelObject.Bindings[ViewId]` 上,View 和 ViewModel 的生命周期互相缠住。 +- `References` 引用计数用于判断 ViewModel 是否销毁,复杂且脆弱。 + +FlowScope 可以借鉴 `Bind / Unbind / AddBinding`,但绑定容器应属于 View 或 feature-local binding scope,而不是塞进 ViewModel。 + +## Controller + +uFrame 的 `Controller` 是特殊的 `SystemService`,主要负责创建和初始化 ViewModel。 + +流程大致是: + +```text +Controller.Create(identifier) + -> CreateEmpty(identifier) + -> RegisterViewModel(vm, identifier) + -> Initialize(vm) + -> Publish ViewModelCreatedEvent +``` + +问题在于 `CreateEmpty` 内部直接注册到全局容器: + +```text +uFrameKernel.Container.RegisterViewModel(vm, identifier) +``` + +`MVVMKernelExtensions.CreateViewModel(type)` 又会通过 ViewModel 类型名去全局容器找 Controller: + +```text +Resolve(type.Name) +``` + +这是强命名约定 + 服务定位器。它表面上把 ViewModel 创建集中到了 Controller,实际上把依赖关系藏进全局容器和类型名字符串。 + +FlowScope 不应该吸收这套 Controller 模式。更合适的是: + +```text +Feature 明确创建 ViewModel。 +Feature 明确创建/绑定 View。 +FeatureContext 提供本 feature 需要的服务。 +UI module 不通过全局容器按名字找 Controller。 +``` + +## ViewService / ViewResolver + +`ViewService` 负责: + +- 监听 `InstantiateViewCommand`。 +- 根据 ViewModel 找 View prefab。 +- 实例化 View。 +- 在 View 创建时补齐 ViewModel。 +- View 销毁时移除记录。 +- ViewModel 销毁时从全局容器移除实例。 + +`ViewResolver` 默认根据 ViewModel 类型名找 prefab: + +```text +PlayerViewModel -> Player -> Resources.Load("Player") +``` + +这是老式 Unity 框架常见做法,但不适合 FlowScope: + +- 资源路径是隐式命名约定。 +- 默认绑定到 `Resources.Load`。 +- 不支持现代资源后端的显式 handle、group、release。 +- 不利于 Addressables / YooAsset / AssetBundle / 自定义资源系统适配。 +- View 的创建和 ViewModel 的创建混在同一个服务里。 + +FlowScope 如果需要 UI 模块,可以把它拆成: + +```text +IViewFactory + 负责创建 View,不负责创建 ViewModel。 + +IViewBinder + 负责绑定和解绑。 + +IUIViewRegistry 或 IViewCatalog + 负责显式注册 view key 到 prefab/address。 + +IResourceService adapter + 负责真实资源加载和释放。 +``` + +## Binding + +uFrame 的 binding 层包括: + +- `Binding` +- `ModelPropertyBinding` +- `ModelViewPropertyBinding` +- `ModelViewModelCollectionBinding` +- `ViewBindings` 扩展方法 + +最实用的是 `ViewBindings.BindProperty`: + +```text +立即用当前值刷新一次 UI。 +订阅后续变化。 +把 IDisposable 加入 bindable 的绑定列表。 +``` + +这是 FlowScope 可以吸收的重点: + +```csharp +bindingScope.Bind(vm.Title, value => titleText.text = value); +``` + +但不要吸收它的反射绑定和生成器绑定作为 core。反射/生成器可以是工具层能力,不应该成为 UI runtime 的基本前提。 + +## 与 FlowScope Kernel 的关系 + +uFrame MVVM 不应该进入 FlowScope Kernel core。 + +FlowScope Kernel 已经明确应保持机制小核: + +```text +Container +AppKernel +StartupPipeline +FeatureHost +FeatureInstance +FeatureContext +FeatureLifetime +FeatureSlot +FeaturePolicy +``` + +MVVM 属于更高层的 UI module 或 feature-local module。Kernel 最多提供 feature 生命周期和上下文,不提供: + +- ViewModel 基类。 +- View 自动绑定。 +- UI prefab 查找。 +- 命令总线。 +- 全局 ViewService。 +- 全局事件聚合器。 +- 全局 IOC 注入 View。 + +推荐边界: + +```text +FlowScope.Kernel + 只负责 Feature 生命周期和 Slot/Policy 机制。 + +FlowScope.UI + 提供 ViewModel、ObservableProperty、BindingScope、ViewBinder。 + +FlowScope.Resources + 提供资源加载接口和后端适配。 + +Game Feature + 自己声明 ViewModel、View、Binder、Command handler。 +``` + +## 可吸收清单 + +可以吸收: + +- `ObservableProperty` 思路。 +- `ObservableCollection` 思路。 +- UI command / intent stream。 +- `Bind / Unbind` 生命周期。 +- `IDisposable` binding scope。 +- View 订阅 ViewModel,ViewModel 不直接操作 View。 +- View 创建和 ViewModel 绑定分离。 +- 集合绑定生成子 View 的思路,但要显式管理资源释放。 + +不应吸收: + +- `uFrameKernel.Container` 全局创建 ViewModel。 +- `Container.Inject(view)`。 +- 按 `ViewModel` 类型名查找 `Controller`。 +- 按 `ViewModel` 类型名 `Resources.Load` prefab。 +- ViewModel 持有 View 绑定列表。 +- ViewModel 引用计数决定销毁。 +- 全局 EventAggregator 作为 UI command 默认通道。 +- 代码生成器作为 runtime 必需条件。 +- ViewModel 同时承担序列化、绑定、事件、销毁、controller hook。 + +## FlowScope 可采用的最小 UI MVVM 形态 + +建议的最小形态: + +```text +ObservableProperty + 保存值并发布变化。 + +BindingScope + 持有 IDisposable 列表,Dispose 时统一释放。 + +IViewModel + 标记 UI 状态对象,不依赖 Kernel。 + +IViewBinder + Bind(view, vm, scope) + Unbind 由 scope.Dispose 承担。 + +UIView + MonoBehaviour,只持有 Unity UI 引用和当前 ViewModel。 + +UICommand + feature-local command,不走全局事件总线。 +``` + +示意: + +```csharp +public sealed class BindingScope : IDisposable +{ + private readonly List _items = new(); + + public T Add(T item) where T : IDisposable + { + _items.Add(item); + return item; + } + + public void Dispose() + { + foreach (var item in _items) + item.Dispose(); + + _items.Clear(); + } +} +``` + +```csharp +public sealed class MainMenuViewModel +{ + public ObservableProperty Title { get; } = new("Main Menu"); + public Subject StartGame { get; } = new(); +} +``` + +```csharp +public sealed class MainMenuBinder +{ + public void Bind(MainMenuView view, MainMenuViewModel vm, BindingScope scope) + { + scope.Bind(vm.Title, value => view.TitleText.text = value); + scope.Add(view.StartButton.OnClickAsObservable() + .Subscribe(_ => vm.StartGame.OnNext(new StartGameIntent()))); + } +} +``` + +关键点是:这里没有全局容器查找,没有 `Resources.Load` 约定,没有 Kernel 注入 View,也没有 ViewModel 引用计数。 + +## 最终判断 + +uFrame MVVM 的局部机制比它的 Kernel 更有参考价值,但必须拆开看。 + +真正值得保留的是: + +```text +状态可观察 +操作命令化 +绑定有生命周期 +View 不直接写业务 +``` + +必须避免的是: + +```text +全局容器创建一切 +命名约定连接一切 +ViewModel 变成框架对象 +资源加载隐藏在 ViewResolver +UI 模块污染 Kernel core +``` + +对 FlowScope 来说,uFrame MVVM 最好的用途不是成为模板,而是提醒我们:UI MVVM 应该是 Kernel 之上的可选模块,且必须保持 feature-local、显式绑定、显式资源、显式生命周期。