一、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也在使用同一个资源,该资源会继续留在内存中。
核心流程:
使用配对 :每个需要加载资源的单元,都需遵循
ResLoader.Allocate()和Recycle2Cache()的配对使用规范。局部自治 :每个单元的
ResLoader实例负责记录其加载的所有资源,单元之间通过引用计数共享资源,互不干扰。自动回收 :当单元销毁时,只需调用
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回调,禁止手动轮询刷新
📋 快速检查清单
实现一个简单功能时,按此清单逐项完成:
-
- 创建
XxxModel,继承AbstractModel
- 创建
-
- 在 Model 中用
BindableProperty定义数据字段
- 在 Model 中用
-
- 创建
XxxCommand,继承AbstractCommand,在OnExecute中修改 Model
- 创建
-
- 创建
XxxApp,继承Architecture<XxxApp>,在Init中注册 Model
- 创建
-
- 创建
UIXxxPanel,继承UIPanel并实现IController
- 创建
-
- 在 Panel 中实现
GetArchitecture()返回XxxApp.Interface
- 在 Panel 中实现
-
- 在
OnInit中用GetModel<T>获取 Model 并注册数据监听
- 在
-
- 在
OnInit中绑定 UI 按钮事件,调用SendCommand<T>
- 在
-
- 创建启动器,调用
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 集中管理。
这条规则既保持了最简化版本的清晰度,又为你将来处理更复杂的业务逻辑预留了自然的扩展路径。

