项目地址:
Unity 项目里,UI 界面通常会有大量节点绑定代码。
比如按钮、文本、图片、列表、输入框:
bash
protected myUGUIButton mButtonClose;
protected myUGUIText mTextTitle;
protected myUGUIImage mImageIcon;
然后在初始化时找到这些节点:
bash
newObject(out mButtonClose, "ButtonClose");
newObject(out mTextTitle, "TextTitle");
newObject(out mImageIcon, "ImageIcon");
这种代码手写很麻烦。
Prefab 改一次,脚本也要跟着改。
所以很多项目都会做 UI 代码生成工具。
但 UI 自动生成有一个很现实的问题:
bash
生成代码确实方便
但不能把手写业务逻辑覆盖掉
否则工具就不是提高效率,而是在制造风险。
一、最危险的做法
最简单的 UI 代码生成方式是:
bash
根据 Prefab 扫描节点
生成一个完整 C# 文件
覆盖原来的脚本文件
这种方式实现最简单。
但问题也最大。
因为一个界面脚本里通常不只有节点绑定。
还会有业务逻辑:
bash
protected override void init()
{
base.init();
mButtonClose.setClickCallback(onCloseClick);
mButtonBuy.setClickCallback(onBuyClick);
}
protected void onCloseClick()
{
close();
}
protected void onBuyClick()
{
// 购买逻辑
}
如果生成器每次都覆盖整个文件,那这些手写代码也会被覆盖。
结果就是:
bash
Prefab 一改
重新生成代码
业务逻辑没了
这种工具不敢频繁用。
开发者会越来越不信任它。
二、真正需要自动生成的部分
UI 脚本里并不是所有代码都需要自动生成。
真正适合自动生成的,通常只有两类:
bash
成员变量声明
节点绑定代码
例如成员变量:
bash
protected myUGUIButton mButtonClose;
protected myUGUIText mTextTitle;
protected myUGUIImage mImageIcon;
以及节点绑定:
bash
newObject(out mButtonClose, "ButtonClose");
newObject(out mTextTitle, "TextTitle");
newObject(out mImageIcon, "ImageIcon");
这些代码和 Prefab 结构强相关。
Prefab 节点变了,它们就应该跟着变。
但点击回调、数据刷新、界面逻辑不应该由生成器覆盖。
这些是业务代码。
所以 MyFramework 的做法是:
bash
自动生成结构代码
手写业务逻辑保留
三、自动生成区域
要做到这一点,生成器不能直接覆盖整个文件。
它只能替换固定的自动生成区域。
示例结构类似这样:
bash
public class UILogin : LayoutScript
{
//-------------------自动生成开始-------------------
protected myUGUIButton mButtonLogin;
protected myUGUIText mTextAccount;
protected myUGUIInput mInputPassword;
//-------------------自动生成结束-------------------
protected override void assignWindow()
{
base.assignWindow();
//-------------------自动绑定开始-------------------
newObject(out mButtonLogin, "ButtonLogin");
newObject(out mTextAccount, "TextAccount");
newObject(out mInputPassword, "InputPassword");
//-------------------自动绑定结束-------------------
}
protected override void init()
{
base.init();
mButtonLogin.setClickCallback(onLoginClick);
}
protected void onLoginClick()
{
// 手写登录逻辑
}
}
生成器只处理这两段:
bash
自动生成开始 -> 自动生成结束
自动绑定开始 -> 自动绑定结束
其他地方一律不动。
四、重新生成时发生什么
当 Prefab 改动后,重新执行 UI 代码生成。
生成器会做几件事:
bash
读取原来的 C# 文件
找到自动生成区域
重新生成成员变量
替换成员变量区域
找到自动绑定区域
重新生成 newObject 代码
替换绑定区域
保留其他所有手写代码
所以开发者可以放心写:
bash
protected override void init()
{
base.init();
mButtonClose.setClickCallback(onCloseClick);
mButtonBuy.setClickCallback(onBuyClick);
}
重新生成时,这些代码不会被动到。
生成器只更新和 Prefab 结构相关的部分。
五、为什么不能只生成一个新文件
也有人会选择生成一个单独文件。
例如:
bash
UILogin.Generated.cs
UILogin.cs
用 partial class 拆开。
这个方案也能避免覆盖手写代码。
但 MyFramework 里更偏向直接在同一个文件中保留自动区域。
原因是 UI 脚本本身是一个界面类。
成员变量、绑定代码、业务逻辑放在同一个文件里,查看和调试更直接。
只要生成器严格只替换标记区域,就能同时满足两个要求:
bash
代码集中
不会覆盖手写逻辑
这种方式对项目成员也更直观。
打开一个界面脚本,就能看到这个界面的节点绑定和业务逻辑。
六、生成器必须守规则
只替换自动区域听起来简单,但工具必须严格遵守规则。
不能因为某个标记没找到,就直接重建整个文件。
正确做法应该是:
bash
文件不存在
创建完整模板
文件存在且标记完整
只替换标记区域
文件存在但标记缺失
报错
停止生成
标记缺失时,直接覆盖文件是很危险的。
因为这说明文件可能被手动改坏了,或者不是当前生成器生成的文件。
这时应该让开发者处理问题,而不是由工具猜测。
自动化工具最重要的不是"能生成"。
而是"不会破坏已有代码"。
七、节点变动时的价值
UI Prefab 在开发过程中经常变。
比如:
bash
新增一个按钮
删除一个文本
改一个节点名
某个 Image 改成 RawImage
列表结构调整
子窗口拆分
如果没有代码生成,每次都要手动同步脚本。
很容易出现:
bash
Prefab 里节点已经改名
代码里字段还是旧名字
Prefab 里删除了节点
代码里还在绑定
Prefab 里新增了按钮
代码里忘了声明成员
UI 自动生成能减少这些重复工作。
但前提是重新生成足够安全。
如果重新生成会覆盖业务代码,那开发者就不敢频繁使用。
MyFramework 的自动区域替换,就是为了让重新生成变成常规操作。
Prefab 改了,就重新生成。
不用担心手写逻辑被清掉。
八、成员变量和业务逻辑分离
自动生成区域里只放结构代码。
例如:
bash
protected myUGUIButton mButtonClose;
protected myUGUIButton mButtonBuy;
protected myUGUIText mTextPrice;
业务逻辑放在自动区域外:
bash
protected override void init()
{
base.init();
mButtonClose.setClickCallback(onCloseClick);
mButtonBuy.setClickCallback(onBuyClick);
}
protected void refreshPrice()
{
mTextPrice.setText(getPriceText());
}
这样职责很清楚。
bash
生成器负责:
节点字段
节点绑定
子窗口创建
控件类型同步
开发者负责:
按钮回调
数据刷新
界面打开逻辑
界面关闭逻辑
网络回包处理
生成器不应该理解业务。
它只应该理解 UI 结构。
九、为什么这很适合 Unity
Unity UI 的问题是 Prefab 和 C# 脚本天然分离。
Prefab 里有节点树。
C# 里有成员变量。
两边必须保持同步。
手动同步成本很高。
尤其是复杂界面:
bash
几十个按钮
几十个文本
多个列表
多个子窗口
多个动态节点
如果每个节点都手写查找和声明,代码量很大。
而且这些代码没有多少业务价值。
自动生成可以把重复劳动交给工具。
开发者只关注真正的界面逻辑。
十、重新生成后的代码可读性
UI 代码生成不能只追求"能用"。
生成出来的代码还应该可读。
因为 UI 脚本是经常被打开的。
MyFramework 的思路不是把所有东西都隐藏起来,而是生成普通 C# 代码。
例如:
bash
newObject(out mButtonClose, "ButtonClose");
newObject(out mTextTitle, "TextTitle");
newObject(out mImageIcon, "ImageIcon");
这种代码可以直接看懂。
出问题时,也方便断点调试。
工具生成代码不是为了让代码不可见。
而是为了减少重复手写。
十一、和运行时查找的区别
还有一种做法是运行时按字符串查找节点:
bash
getObject("ButtonClose");
getObject("TextTitle");
需要用时再查。
这种方式减少了声明字段。
但问题是:
bash
运行时才知道节点是否存在
字符串散落在业务代码里
重命名不安全
重复查找也容易变乱
自动生成成员变量的好处是:
bash
节点集中声明
绑定集中执行
业务代码直接使用成员变量
节点缺失可以在初始化时统一报错
业务代码里不需要到处写路径字符串。
这更适合中大型 UI 工程。
十二、设计边界
自动生成区域替换也有边界。
它要求开发者遵守规则:
bash
不要手动修改自动生成区域
不要删除自动区域标记
业务逻辑写在标记区外
Prefab 节点改动后重新生成
生成器也要遵守规则:
bash
只改标记区域
标记缺失时报错
不要覆盖整个文件
不要替开发者生成业务逻辑
双方规则清楚,工具才稳定。
十三、这个设计解决的不是代码量
UI 自动生成表面上是在减少代码量。
但更重要的是减少同步成本。
真正的问题不是少写几行代码。
而是:
bash
Prefab 变了
脚本必须同步变
同步过程不能破坏手写逻辑
所以 MyFramework 的 UI 自动生成重点不是"生成多少代码"。
而是:
bash
哪些代码可以交给工具
哪些代码必须留给开发者
工具修改范围必须可控
自动区域替换就是这个边界。
总结
Unity UI 自动生成最大的问题,不是怎么生成字段和绑定代码。
这些都不难。
真正的问题是:
bash
重新生成时,怎么不覆盖手写业务逻辑
MyFramework 的做法是把 UI 脚本分成两类区域:
bash
自动生成区域
成员变量声明
节点绑定代码
手写业务区域
初始化逻辑
点击回调
数据刷新
网络处理
生成器每次只替换自动生成区域。
自动区域之外的代码原样保留。
这样 Prefab 改动后可以反复重新生成 UI 代码,而不用担心业务逻辑被覆盖。
这也是 UI 代码生成工具真正能在项目里长期使用的前提。
