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(如 CommonPopUIToast 等),自动设置其父节点和渲染层级顺序

  • 生命周期触发 :依次调用面板的 OnInitOnOpenOnShow 方法

  • 数据传递 :如果 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:只能用 BindablePropertyRegister 回调,禁止手动轮询刷新


📋 快速检查清单

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

    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 集中管理。

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

相关推荐
摇滚侠3 小时前
Java 零基础全套教程,反射机制,笔记 187-188
java·开发语言·笔记
【云轩】4 小时前
如何设计一台能模拟电机的电子负载:一个硬件工程师的实战笔记
笔记·嵌入式硬件
可信计算5 小时前
X司民用无人机运行安全与合规培训手册
笔记
李子琪。5 小时前
Web漏洞-CSRF-CSRF防御 实验步骤
经验分享·笔记
小碗羊肉6 小时前
【Agent笔记 | 第四篇】Agentic RAG
笔记
小雨xs7 小时前
Vulnhub靶场DC-9 渗透测试笔记
笔记
whyTeaFo7 小时前
MIT 6.1810: xv6 book Chapter3: Page tables 笔记
笔记
東雪木8 小时前
JVM 与 Java 内存模型 专属复习笔记
java·jvm·笔记·java面试
kinl20189 小时前
Softmax Linear Units (SoLU)
笔记