Unity QFramework ResKit、UIKit 笔记

一、ResKit

1、简介

ResKit 是AssetBundle(AB包)基础之上的资源管理模块,它极大简化了 AB 包的复杂使用体验。



2、ResKit 关键设计:

① 模拟模式 ------ 彻底告别 频繁打包测试 ~

这是 Res Kit 最亮眼的设计:在Unity 编辑器下开启此模式后,加载资源 直接从项目的 Assets 目录读取,不加载真正的 AB 包。

换句话说,当你修改完预制体后,不需要再经历"打包"等待,直接点击运行就能在Unity中看到最新效果。 待真机发布时,打一次 AB 包进行验证,即可发布。

两种模式:

模式 适用阶段 加载源 特点
模拟模式 开发阶段 Assets目录 无需打包,修改即生效
真机模式 真机阶段 真正的 AssetBundle 每次改动需重新打包,接近真实环境

② 可寻址

可寻址 = 给资源一个唯一的 "名字"或"ID",你只要报出来,不用关心它具体堆放在哪里 ,系统就能帮你把它揪出来 。

传统的AB包,资源分布在各个包中,你要找某个资源,就需要先找到其所在的包,再加载包,再加载包中资源,流程复杂。

ResKit 实现了一套轻量级可寻址系统,通过 资源名 做唯一标识,让你直接 按"名"索"物",彻底简化加载流程。

注意 :请务必保证资源名称的**唯一性,**即使其源文件【后缀】不同(例如一张贴图和一个预制体,其名称也不能相同)


③ 自动管理依赖

传统 AB 包,假如 包 A中的(角色模型) 依赖了 包 B中的(材质贴图) ,没有加载包B,直接加载包 A 模型,就会因为内存中找不到包 B 数据,造成严重的引用缺失(画面变粉)。开发者必须自己写复杂的递归算法,先加载包 B,再加载包 A 角色模型,代码臃肿且易出错。

而 ResKit 彻底终结了这一痛苦。你只需要给出一个简单的"资源名称",ResKit 就会像拥有超能力一样,自动看穿它背后的所有依赖关系,自己在后台一层一层、顺着藤把所有关联资源全部加载进来。


④ 引用计数 + 自动生命周期管理

这就是 ResKit 能够防止内存泄漏(游戏越玩越卡)的核心秘密。它把"引用计数"和"自动生命周期"完美结合在了一起。

每个需要资源的脚本(如 MonoBehaviour 或 UI 界面)都会配一个 "私人采购员"(ResLoader) 。当脚本销毁(OnDestory)时,调用 Recycle2Cache() 就能让采购员把经手的东西全部释放掉。

释放掉? 如果别的脚本也用了这个资源怎么办?ResKit 引入了 **"数人头(引用计数)"**的机制:

只要有一个脚本在用这个资源,人头数就 +1 。哪怕其中一个脚本死掉了,它也只是把自己贡献的人头数 -1 。只要人头数不满足 0,这个资源就绝对稳稳地留在内存里,谁也不会被误删。

只有当所有用它的脚本都死了,人头数清零的那一瞬间,ResKit 才会把这个资源从内存里彻底释放。

这种"各人自扫门前雪 ,最后一人负责关灯"的设计,既保证了有线索可查,又做到了随生随灭,省心到了极致。

释放 ≠ 立即卸载

这里有个容易混淆的点需要区分:

概念 含义 说明
释放资源 解除当前 ResLoader 对资源的引用(引用计数 -1) Recycle2Cache() 做的是这个
卸载资源 真正从内存中删除资源对象 只有当引用计数变为 0 时,框架才会自动卸载

Recycle2Cache() 触发了释放流程 ,但并不保证资源会立即从内存中消失------如果还有其他 ResLoader 也在使用同一个资源,该资源会继续留在内存中。
核心流程:

  1. 使用配对 :每个需要加载资源的单元,都需遵循 ResLoader.Allocate() 和 Recycle2Cache() 的配对使用规范。

  2. 局部自治 :每个单元的 ResLoader 实例负责记录其加载的所有资源,单元之间通过引用计数共享资源,互不干扰。

  3. 自动回收 :当单元销毁时,只需调用 Recycle2Cache(),其绑定的 ResLoader 便会自动将所有记录的资源引用计数减一,引用计数归零的资源会被立即卸载。无需开发者进行复杂的手动管理。


⑤ 其他设计:
功能 说明
统一 API 一个 LoadSync / LoadAsync 搞定 Resources、AssetBundle、网络图片、本地文件等多种来源
代码生成 可一键生成常量,直接调用(类名.常量名),避免手写 "资源名称的字符串"
异步队列 支持批量异步加载,多个资源一次搞定
自定义扩展 可实现 IResCreator 扩展加载来源(如加密资源、Addressables 等)

统一API 说明:

资源来源 使用方式 关键点
AssetBundle(默认/推荐) mResLoader.LoadSync<GameObject>("Player") 直接传资源名,框架自动定位所属 AB 包
Resources 目录 mResLoader.LoadSync<Texture2D>("resources://TestTexture") 必须加 resources:// 前缀
网络图片 mResLoader.Add2Load("netimage:http://...", callback) 加 netimage: 前缀,必须异步加载
本地持久化路径 mResLoader.LoadSync<Texture2D>("localimage://path/to/file.png") 加 localimage:// 前缀,从 Application.persistentDataPath 读取
StreamingAssets 路径 mResLoader.LoadSync<TextAsset>("streaming://config.json") 加 streaming:// 前缀,只读
自定义来源 实现 IResCreator 接口 可扩展加密资源、Addressables 等其他来源


3、需要关注的方面

传统 AB 包的问题:资源冗余:

在原生 AB 包机制下,如果 包 A 和 包 B 都引用了一张**不在任何包中的** 贴图 icon.png,默认打包时,这张贴图会被分别打入两个 AB 包中。结果就是:

  • 包体变大(同一份资源存了两份)

  • 内存冗余(分别加载两个包时,同一张贴图会在内存中出现两份)

解决这个问题的传统方式是:将共享资源单独打成一个 AB 包,让 A 和 B 都依赖这个共享包。

ResKit 不会自动帮你 "拆出" 上述的共享贴图 icon.png 到一个AB包,

  • 和传统AB包一样的机制

  • 正确做法:将共享资源(贴图、材质、字体等)单独打一个包,ResKit 会自动处理跨包依赖。



4、ResKit 实践:

目标: 将一个预制体资源,用ResKit加载到场景。

体验: 资源标记、模拟模式、可寻址、加载API

① 场景中新建一个Cube,拖拽Assets下成为预制体,删除场景中的Cube对象
② 右键Assets中Cube预制体,标记资源

资源标记:支持标记资源 或文件夹,一份标记一个包。

③ 新建一个脚本,在Start方法写入:
cs 复制代码
    void Start()
    {
        ResKit.Init(); // 全局初始化一遍
        ResLoader resLoader = ResLoader.Allocate(); //申请一个ResLoader
        var v = resLoader.LoadSync<GameObject>("Cube");//同步加载
        GameObject.Instantiate(v);//克隆一份到场景中
    }
④ 将脚本挂载到场景对象上
⑤ 设置为 "模拟模式" (菜单栏 > QFramework > Preferences > ResKit 设置/编辑器 > 模拟模式)
⑥ 运行场景,可以看到Cube加载到场景中了

执行 【打AB包】 ,切换到【真机模式】,运行场景,即是真机环境的体验。

(AB包处于StreamingAssets文件夹下)


点击【生成代码(资源常量名)】,即可发现资源名称的常量,最好直接【调用常量】替代【直接写字符串】




二、UIKit

1、简介

原生的 UGUI 仅仅是一套基础 UI 组件库------它能提供按钮、图片、文本框等元素,但没有"界面"这一业务层面的抽象,在代码里,一个界面只是一个由若干 **GameObject**拼凑成的预制体。

UIKit 就可以看作一个 "UI 开发框架" ,它是在 UGUI 这个"组件库"的基础上,引入了真正面向业务开发的 "界面" 这一上层概念,并围绕它提供了一整套 管理与开发解决方案。

概念 UGUI (原生) UIKit (框架)
抽象层次 组件层。 关注按钮、图片、文本如何渲染与响应点击。 界面层。 关注一个完整的"界面"如何被打开、关闭、传递数据和管理生命周期。
核心概念 GameObject, Button, Image 等 UIPanel (界面) 、UIElement (子控件)、UIData (参数)

它本质上是对 UGUI 的一种面向业务的上层封装,让你从"如何用 UGUI 拼出一个界面并让它工作"的繁琐细节中解放出来,转而专注于"这个界面要完成什么业务功能"这一更有价值的事情上。

它的核心理念是让每个界面只负责展示数据和监听用户输入 ,界面之间互相独立且可独立测试。



2、UIKit 工作流程

UIKit 默认使用 ResKit资源模块,核心工作流程可以概括为:

一、资源组织层:一界面一预制体

  • 每个 UI 界面(UIPanel)对应一个独立的 .prefab 预制体文件

  • 每个预制体挂载一个对应的 UIPanel 脚本组件(如 UIMyPanel.cs)

  • 预制体存放在项目的 Assets 目录下 或 AssetBundle 包中

二、资源加载层:基于 ResKit 的寻址加载

步骤 说明
1. 唯一标识 每个界面通过其 类型 (如 UIMyPanel)或 资源名 (UIPanel.DefaultResName)作为唯一标识
2. 资源寻址 调用 UIKit.OpenPanel 将界面标识传递给 ResKit
3. 资源加载 ResKit 根据标识,从 Resources、AssetBundle 或网络等位置加载对应的预制体资源
4. 开发支持 开发阶段开启 模拟模式 ,ResKit 直接从 **Assets**目录读取源文件,无需打包 AB 包

三、界面实例化层:生成并显示到场景

  • 资源加载完成后,UIKit 执行以下操作:

  • 实例化 :Instantiate 预制体,生成可交互的 GameObject

  • 层级管理 :根据面板预设的 UILevel(如 Common、PopUI、Toast 等),自动设置其父节点和渲染层级顺序

  • 生命周期触发 :依次调用面板的 OnInit → OnOpen → OnShow 方法

  • 数据传递 :如果 OpenPanel 时传入了 UIPanelData,框架会自动赋值给面板的 mData 属性

四、界面管理层:单例策略与生命周期

UIKit 内部通过字典缓存了已打开的面板实例,确保每个 UIPanel 类型在同一时刻只有一个活动实例:

  • 再次调用 OpenPanel<T>() 时,如果面板已存在,则只会将其置前,而不会重复创建

  • 调用 ClosePanel<T>() 时,框架会销毁实例并从缓存中移除

还记得我们前面刚聊过的 ResKit 引用计数 吗?UIKit 把它和 UI 的生命周期做了完美的深度捆绑 。当你调用 UIKit.ClosePanel<T>() 关闭并销毁这个界面时,UIKit 会在后台默默地自动帮你调用一句 mResLoader.Recycle2Cache()。

这种设计既避免了界面冲突,又允许面板在关闭时释放内存,兼顾了唯一性与资源效率。

五、开发体验层:独立测试与代码生成

  • 独立测试 :通过 UIPanelTester 组件,可在任意场景中单独运行某个界面,无需启动整个游戏

  • 代码生成 :右键预制体即可自动生成 UIPanel 脚本;所有控件借助**Bind 组件** 自动绑定,省去手写 transform.Find

工作流程:

步骤 说明
1. 标记控件 在需要引用的 UI 控件上挂载 Bind 组件
2. 智能识别 Bind 组件自动扫描挂载的 GameObject,识别出合适的组件类型(如 Button、Text、Image 等),无需手动指定
3. 路径追踪 框架记录每个 Bind 控件相对于面板根节点的路径(如 "TopBar/BtnStart")
4. 生成代码 右键点击预制体 → 选择 @UIKit - Create UI Code,自动生成两个文件

生成的文件:

文件 说明
MyPanel.cs 用户逻辑区域,开发者在此编写交互逻辑
MyPanel.Designer.cs 自动生成区域(禁止修改),包含所有控件的自动绑定代码

3、UIKit 的 MVC 架构

(笔记仅供参考,来自deepseek)

核心对象(5个)

对象 角色 必须继承/实现 核心职责
Model 数据层 AbstractModel 存储数据,用BindableProperty实现可观察
Command 命令层 AbstractCommand 封装修改数据的业务逻辑
Architecture 架构层 Architecture<T> 注册所有模块,全局单例
UIPanel 视图+控制器 UIPanel + IController 监听数据变化,响应用户操作
GameLauncher 启动器 MonoBehaviour 初始化架构,打开面板

一个示例:简单加法计数器

实现一个 UIPanel ------ UICounterPanel,上面放:

  • 一个显示数字的Text(取名txtNumber)

  • 一个"+1"按钮(取名btnAdd)

  • 一个清零按钮(取名btnClear)

点击 +1按钮,总计数+1,显示在Text上,点击清零按钮,总计数归0,显示在Text上。

cs 复制代码
using QFramework;
using UnityEngine;
using UnityEngine.UI;

// ==================== 1. Model层(数据) ====================
public class CounterModel : AbstractModel
{
    public BindableProperty<int> Count { get; } = new BindableProperty<int>(0);
    
    protected override void OnInit()
    {
        Count.Value = 0;
    }
}

// ==================== 2. Command层(修改数据的业务逻辑) ====================
public class AddCountCommand : AbstractCommand
{
    protected override void OnExecute()
    {
        var model = this.GetModel<CounterModel>();
        model.Count.Value++;
    }
}

public class ClearCountCommand : AbstractCommand
{
    protected override void OnExecute()
    {
        var model = this.GetModel<CounterModel>();
        model.Count.Value = 0;
    }
}

// ==================== 3. Architecture层(全局架构) ====================
public class CounterApp : Architecture<CounterApp>
{
    protected override void Init()
    {
        this.RegisterModel(new CounterModel());
    }
}

// ==================== 4. UIPanel层(Controller + View) ====================
public class UICounterPanel : UIPanel, IController
{
    // UI控件引用(需要在Unity Editor中拖拽赋值)
    public Text txtNumber;
    public Button btnAdd;
    public Button btnClear;
    
    // 实现IController接口要求的方法
    public IArchitecture GetArchitecture()
    {
        return CounterApp.Interface;
    }
    
    protected override void OnInit(IUIData uiData = null)
    {
        // 获取Model并监听数据变化(数据 → UI)
        var model = this.GetModel<CounterModel>();
        model.Count.Register(newValue =>
        {
            txtNumber.text = newValue.ToString();
        }).UnRegisterWhenGameObjectDestroyed(gameObject);
        
        // 绑定按钮事件(UI → 数据)
        btnAdd.onClick.AddListener(() =>
        {
            this.SendCommand<AddCountCommand>();
        });
        
        btnClear.onClick.AddListener(() =>
        {
            this.SendCommand<ClearCountCommand>();
        });
    }
}

// ==================== 5. 启动器(游戏入口) ====================
public class GameLauncher : MonoBehaviour
{
    void Start()
    {
        CounterApp.Interface.Init();  // 初始化架构
        UIKit.OpenPanel<UICounterPanel>();  // 打开面板
    }
}

基于这个计数器案例,我们提炼出UIKit实现MVC模式的核心对象 和抽象规则。


🎯 核心对象(5个)

对象 角色 必须继承/实现 核心职责
Model 数据层 AbstractModel 存储数据,用BindableProperty实现可观察
Command 命令层 AbstractCommand 封装修改数据的业务逻辑
Architecture 架构层 Architecture<T> 注册所有模块,全局单例
UIPanel 视图+控制器 UIPanel + IController 监听数据变化,响应用户操作
GameLauncher 启动器 MonoBehaviour 初始化架构,打开面板

📐 抽象规则(5条)

cs 复制代码
规则1:Model 规则
csharp

public class XxxModel : AbstractModel
{
    // 数据字段必须用 BindableProperty<T> 包装
    public BindableProperty<T> Data { get; } = new BindableProperty<T>(默认值);
    
    protected override void OnInit() { /* 可选:初始化数据 */ }
}
规则2:Command 规则
csharp

public class XxxCommand : AbstractCommand
{
    // 可选:构造函数传参
    public XxxCommand(参数) { /* 保存参数 */ }
    
    protected override void OnExecute()
    {
        var model = this.GetModel<目标Model>();
        // 修改 model 的数据
        model.XXX.Value = 新值;
    }
}
规则3:Architecture 规则
csharp

public class XxxApp : Architecture<XxxApp>
{
    protected override void Init()
    {
        // 在此注册所有 Model(必须)
        this.RegisterModel(new XxxModel());
        // 可选:注册 System、Utility
    }
}
规则4:UIPanel 规则
csharp

public class UIXxxPanel : UIPanel, IController
{
    // 1. 必须实现 GetArchitecture()
    public IArchitecture GetArchitecture() => XxxApp.Interface;
    
    // 2. 在 OnInit 中完成初始化和绑定
    protected override void OnInit(IUIData uiData = null)
    {
        // 2.1 获取 Model 并监听数据变化
        var model = this.GetModel<XxxModel>();
        model.Data.Register(OnDataChanged).UnRegisterWhenGameObjectDestroyed(gameObject);
        
        // 2.2 绑定 UI 事件,发送 Command
        btnXxx.onClick.AddListener(() => this.SendCommand<XxxCommand>());
    }
    
    private void OnDataChanged(T newValue) { /* 更新 UI 显示 */ }
}
规则5:启动规则
csharp

public class GameLauncher : MonoBehaviour
{
    void Start()
    {
        XxxApp.Interface.Init();           // 初始化架构
        UIKit.OpenPanel<UIXxxPanel>();     // 打开面板
    }
}

🔄 数据流向规则(单向闭环)

text

复制代码
用户操作 UI
    ↓
UIPanel 发送 Command (SendCommand<T>)
    ↓
Command 修改 Model (model.Data.Value = xxx)
    ↓
Model 的 BindableProperty 自动触发回调
    ↓
UIPanel 收到通知,刷新 UI

关键约束:

  • UI → 数据:只能用 SendCommand,禁止直接修改 Model

  • 数据 → UI:只能用 BindableProperty 的 Register 回调,禁止手动轮询刷新


📋 快速检查清单

实现一个简单功能时,按此清单逐项完成:

    1. 创建 XxxModel,继承 AbstractModel
    1. 在 Model 中用 BindableProperty 定义数据字段
    1. 创建 XxxCommand,继承 AbstractCommand,在 OnExecute 中修改 Model
    1. 创建 XxxApp,继承 Architecture<XxxApp>,在 Init 中注册 Model
    1. 创建 UIXxxPanel,继承 UIPanel 并实现 IController
    1. 在 Panel 中实现 GetArchitecture() 返回 XxxApp.Interface
    1. 在 OnInit 中用 GetModel<T> 获取 Model 并注册数据监听
    1. 在 OnInit 中绑定 UI 按钮事件,调用 SendCommand<T>
    1. 创建启动器,调用 XxxApp.Interface.Init() 和 UIKit.OpenPanel<>


假设需求升级:每次加分时,如果当前分数超过历史最高分,就更新最高分。

这个逻辑涉及两个数据:当前分数 和 历史最高分,属于"业务规则",需要引入 【System】 。

cs 复制代码
// ==================== Model 层 ====================
public class CounterModel : AbstractModel
{
    public BindableProperty<int> CurrentCount { get; } = new BindableProperty<int>(0);
    public BindableProperty<int> HighestScore { get; } = new BindableProperty<int>(0);
    
    protected override void OnInit()
    {
        CurrentCount.Value = 0;
        HighestScore.Value = 0;
    }
}

// ==================== System 层(无状态业务逻辑) ====================
public class ScoreSystem : AbstractSystem
{
    protected override void OnInit() { }  // System 通常没有初始化数据
    
    // 核心业务逻辑:增加分数并更新最高分
    public void AddScore(int increment)
    {
        var model = this.GetModel<CounterModel>();
        int newValue = model.CurrentCount.Value + increment;
        model.CurrentCount.Value = newValue;
        
        // 业务规则:更新最高分
        if (newValue > model.HighestScore.Value)
        {
            model.HighestScore.Value = newValue;
        }
    }
    
    public void ResetScore()
    {
        var model = this.GetModel<CounterModel>();
        model.CurrentCount.Value = 0;
        // 注意:重置时不重置最高分,这是业务规则
    }
}

// ==================== Command 层(薄薄一层,转发给 System) ====================
public class AddScoreCommand : AbstractCommand
{
    private int increment;
    public AddScoreCommand(int inc) { increment = inc; }
    
    protected override void OnExecute()
    {
        // Command 不再直接操作 Model,而是调用 System
        this.GetSystem<ScoreSystem>().AddScore(increment);
    }
}

public class ResetScoreCommand : AbstractCommand
{
    protected override void OnExecute()
    {
        this.GetSystem<ScoreSystem>().ResetScore();
    }
}

// ==================== Architecture 层(注册 System) ====================
public class CounterApp : Architecture<CounterApp>
{
    protected override void Init()
    {
        this.RegisterModel(new CounterModel());
        this.RegisterSystem(new ScoreSystem());  // ⭐ 注册 System
    }
}

// ==================== UIPanel 层(不变) ====================
public class UICounterPanel : UIPanel, IController
{
    public Text txtCurrent;
    public Text txtHighest;
    public Button btnAdd;
    public Button btnReset;
    
    public IArchitecture GetArchitecture() => CounterApp.Interface;
    
    protected override void OnInit(IUIData uiData = null)
    {
        var model = this.GetModel<CounterModel>();
        
        // 监听当前分数变化
        model.CurrentCount.Register(v => txtCurrent.text = v.ToString())
            .UnRegisterWhenGameObjectDestroyed(gameObject);
        
        // 监听最高分变化
        model.HighestScore.Register(v => txtHighest.text = v.ToString())
            .UnRegisterWhenGameObjectDestroyed(gameObject);
        
        btnAdd.onClick.AddListener(() => this.SendCommand(new AddScoreCommand(1)));
        btnReset.onClick.AddListener(() => this.SendCommand(new ResetScoreCommand()));
    }
}

🔄 引入 System 后的数据流

text

复制代码
用户点击 +1
    ↓
UIPanel 发送 AddScoreCommand(1)
    ↓
Command 调用 this.GetSystem<ScoreSystem>().AddScore(1)
    ↓
ScoreSystem.AddScore() 执行业务逻辑
    ├── 修改 model.CurrentCount
    └── 必要时修改 model.HighestScore
    ↓
BindableProperty 自动通知 UI 刷新

📊 何时需要 System?

场景 是否需要 System 理由
修改单个 Model 的简单字段 ❌ 不需要 Command 直接操作足够
同时修改多个 Model ✅ 需要 需要协调逻辑
包含复杂业务规则(如最高分判定) ✅ 需要 规则集中管理
需要跨多个 UIPanel 共享的业务逻辑 ✅ 需要 System 是全局唯一的
需要调用外部服务(网络、文件等) ✅ 需要 System 适合做封装

💎 总结

在最简单的计数器案例中,System 层没有体现,因为需求太简单。

但当你需要处理跨数据、跨模块的业务逻辑 时,System 就是必选项了。它的核心价值是:让 Command 变薄,把业务规则抽离到可复用的 System 中。


在加入 System 层后,我对之前的规则做了升级,整理出一套包含 System 的简化版抽象规则。


🎯 核心对象(6个)

对象 角色 必须继承/实现 核心职责
Model 数据层 AbstractModel 存储数据,用BindableProperty实现可观察
System 业务逻辑层 AbstractSystem 无状态业务规则(跨Model、跨模块)
Command 命令层 AbstractCommand 薄封装,调用System或直接修改Model
Architecture 架构层 Architecture<T> 注册所有模块,全局单例
UIPanel 视图+控制器 UIPanel + IController 监听数据变化,响应用户操作
GameLauncher 启动器 MonoBehaviour 初始化架构,打开面板

📐 升级后的抽象规则

cs 复制代码
规则1:Model 规则(不变)
csharp

public class XxxModel : AbstractModel
{
    public BindableProperty<T> Data { get; } = new BindableProperty<T>(默认值);
    protected override void OnInit() { }
}
规则2:System 规则(新增)
csharp

public class XxxSystem : AbstractSystem
{
    // System 通常没有成员变量(无状态)
    
    protected override void OnInit() { /* 可选:初始化,但不能依赖具体数据值 */ }
    
    // 业务逻辑方法:可操作多个 Model,可包含复杂规则
    public void DoBusinessLogic(参数)
    {
        var modelA = this.GetModel<ModelA>();
        var modelB = this.GetModel<ModelB>();
        // 修改 model 数据,包含业务规则判断
    }
}
核心约束:

✅ 可以调用 GetModel<T>() 获取并修改多个 Model

✅ 可以调用 GetSystem<T>() 调用其他 System

❌ 不能有状态成员变量(所有数据都在 Model 中)

❌ 不能持有 UI 引用

规则3:Command 规则(简化版)
csharp

// 简单场景:Command 直接操作 Model
public class SimpleCommand : AbstractCommand
{
    protected override void OnExecute()
    {
        this.GetModel<XxxModel>().Data.Value = 新值;
    }
}

// 复杂场景:Command 转发给 System
public class ComplexCommand : AbstractCommand
{
    protected override void OnExecute()
    {
        this.GetSystem<XxxSystem>().DoBusinessLogic(参数);
    }
}
核心原则:Command 应该是薄薄的一层,只做转发或极简操作。

规则4:Architecture 规则(升级版)
csharp

public class XxxApp : Architecture<XxxApp>
{
    protected override void Init()
    {
        // 注册所有模块(顺序无严格要求)
        this.RegisterModel(new XxxModel());
        this.RegisterSystem(new XxxSystem());  // ⭐ 注册 System
        // this.RegisterUtility(new XxxUtility()); // 可选
    }
}
规则5:UIPanel 规则(不变)
csharp

public class UIXxxPanel : UIPanel, IController
{
    public IArchitecture GetArchitecture() => XxxApp.Interface;
    
    protected override void OnInit(IUIData uiData = null)
    {
        // 监听 Model 变化
        var model = this.GetModel<XxxModel>();
        model.Data.Register(OnDataChanged).UnRegisterWhenGameObjectDestroyed(gameObject);
        
        // 发送 Command(可能经过 System,也可能直接改 Model)
        btn.onClick.AddListener(() => this.SendCommand<XxxCommand>());
    }
}
规则6:启动规则(不变)
csharp

public class GameLauncher : MonoBehaviour
{
    void Start()
    {
        XxxApp.Interface.Init();
        UIKit.OpenPanel<UIXxxPanel>();
    }
}

🔄 两条数据流路径

场景 数据流 适用情况
简单路径 UI → Command → Model 单字段修改,无业务规则
复杂路径 UI → Command → System → Model 多字段协调、业务规则判断

📋 决策树:要不要加 System?

text

复制代码
需要修改数据吗?
    ├── 只改一个 Model 的简单字段 → 不需要 System,Command 直接操作 Model
    └── 满足以下任一条件 → 需要 System
            ├── 同时修改多个 Model
            ├── 包含条件判断或计算规则(如最高分判定)
            ├── 需要被多个 Command 复用
            └── 需要跨多个 UIPanel 共享

🆚 升级前后对比

维度 无 System(最简化版) 有 System(升级版)
核心对象数 5个 6个
Command 厚度 直接改 Model 薄转发层
业务逻辑位置 分散在各个 Command 集中在 System
适用复杂度 简单 UI 交互 复杂业务规则
代码复用性 低 高

💎 一句话总结

简单操作用 Command 直改 Model,复杂规则抽到 System 集中管理。

这条规则既保持了最简化版本的清晰度,又为你将来处理更复杂的业务逻辑预留了自然的扩展路径。

相关推荐
an12321216 小时前
ARM嵌入式学习笔记:SPI通信协议详解
arm开发·笔记·学习
Because_of_Her19 小时前
dfs-洪水填充技巧
笔记·算法·深度优先·洪水填充
xzal129 小时前
GESP Python笔记
笔记·python·算法
周玉奎先生10 小时前
CGM系列水泥基灌浆料型号参数解析及安庆重点工程应用匹配
经验分享·笔记·零售
海海不掉头发10 小时前
软件项目管理学习笔记-从软件危机到敏捷与DevOps
笔记·学习·devops
M78佐菲12 小时前
ARM学习笔记(12)
linux·arm开发·笔记·嵌入式硬件·学习
奇思妙想聪明勤奋的小羊13 小时前
ai-agent-book第五章:Coding Agent 与通用 Agent 学习笔记
人工智能·笔记·学习
优化Henry13 小时前
BBU与RRU光链路详解:从双纤一收一发到单纤双向
网络·笔记·学习·5g·tdd
yi01113 小时前
DAY17: LeetCode 139|单词拆分:从“把单词删掉”到用 DP 记录合法切口
人工智能·笔记·python·算法·leetcode·动态规划
sunoo-22914 小时前
【嵌入式 ARM 学习第十二天】SPI 裸机驱动 + I2C 触摸屏驱动 + 三大串行协议深度对比
arm开发·笔记·vscode·学习·imx6ull