1. 为什么要在 Unity 里搞自动化代码生成
如果你在 Unity 项目里写过 UI 面板绑定、配置表读取、网络协议解析这类活儿,大概率经历过这样的场景:策划发来一份 Excel,你手动敲了几百行字段声明;UI 界面改了一版,你又得对着 Hierarchy 一个个拖引用、写GetComponent。这种重复劳动不仅枯燥,还特别容易出错——少写一个字段、拼错一个变量名,编译报错还算好的,有时候运行时才炸,排查起来更头疼。
Unity 自动化代码生成,说白了就是让编辑器替我们写代码。它通过读取某种“元数据”(比如 Excel、JSON、Prefab 结构、Proto 文件),按照预设模板批量产出 C# 脚本、绑定代码或序列化类。核心价值在于三点:消除手写重复代码、保证生成结果的一致性、把人为失误降到接近零。这套东西适合所有中大型 Unity 项目,尤其是 UI 复杂、配置表多、协议频繁变动的团队。哪怕你是一个人做小项目,只要涉及大量重复结构,用上它也能省下大把时间。
我前后在几个项目里落地过不同规模的生成方案,从最简单的 Prefab 绑定,到整套配置表 + 协议 + UI 的全链路生成。踩过的坑不少,也总结出一套相对稳妥的实践路径。下面就把这套东西拆开讲清楚,从设计思路到具体实现,再到问题排查,尽量让你看完就能动手搭一套。
2. 整体设计思路与方案选型
2.1 先想清楚:生成什么、从哪生成、生成到哪
动手写代码之前,有三个问题必须先定下来,否则后面越写越乱。
第一,生成什么。常见的生成目标有这么几类:数据类(配置表对应的 C# 类)、绑定类(UI 控件引用和事件绑定)、协议类(网络消息的序列化结构)、常量类(枚举、ID 映射)。不同目标的数据源和模板差异很大,建议一开始就分开模块,别混在一起。
第二,从哪生成。数据源决定了整个流程的复杂度。Excel 适合策划维护,但解析麻烦;JSON 好解析,但策划不一定愿意手写;Prefab 结构直接读 Hierarchy,适合 UI 绑定;Proto 文件适合协议。我的经验是:策划面向的数据用 Excel,程序内部的结构用代码或 JSON,UI 绑定直接读 Prefab。
第三,生成到哪。生成的文件一定要放在独立目录,比如Assets/Generated/,并且这个目录要能被一键清空重建。千万别把生成代码和手写代码混在一个文件里,否则下次生成就把你的手写逻辑覆盖了。正确的做法是:生成类用partial关键字,把自动生成的部分和手写扩展部分拆到两个文件。
2.2 为什么选模板引擎而不是字符串拼接
很多人第一版会用StringBuilder直接拼字符串,简单直接。但一旦模板变复杂,比如要处理缩进、条件分支、循环嵌套,拼接代码就会变成一坨难以维护的意大利面。我强烈建议用模板引擎,C# 生态里T4(Text Template Transformation Toolkit)或者轻量的Scriban都是不错的选择。
T4 是 Visual Studio 自带的,语法成熟,适合生成结构固定的 C# 代码;Scriban 是纯 C# 实现的模板引擎,语法接近 Liquid,运行时也能用,适合需要在 Unity 编辑器里动态生成的场景。选哪个取决于你的生成时机——如果是在编辑器里点个按钮生成,两者都行;如果要在构建流程里自动跑,Scriban 更灵活。
提示:模板文件本身也是需要维护的资产,建议和生成器代码放在一起,用版本管理跟踪。模板改动往往比生成逻辑改动更频繁。
2.3 生成时机的选择:手动、半自动还是全自动
生成时机有三种模式,各有适用场景。
手动触发:在编辑器菜单里加一个Tools/Generate/XXX,需要时点一下。适合生成频率不高、需要人工确认的场景,比如配置表生成。
资源导入触发:用AssetPostprocessor监听特定目录的文件变化,Excel 一保存就自动重新生成。适合策划频繁改表的项目,但要注意防止生成过程中触发递归导入。
构建前触发:在 CI 流程或IPreprocessBuildWithReport里跑生成,保证每次打包用的都是最新代码。这是最稳妥的做法,适合团队协作。
我的建议是组合使用:日常开发用资源导入触发,保证即时性;打包流程里再强制跑一遍,保证一致性。两者不冲突,后者是兜底。
2.4 目录结构与命名规范
一套清晰的目录结构能让后续维护轻松很多。我常用的结构是这样的:
Assets/ Editor/ CodeGenerator/ Generators/ # 各类生成器实现 Templates/ # 模板文件 Utils/ # 解析、IO 工具 Generated/ Config/ # 配置表生成结果 UI/ # UI 绑定生成结果 Protocol/ # 协议生成结果 Scripts/ Config/ # 配置表手写扩展(partial) UI/ # UI 手写逻辑命名上,生成类统一加后缀,比如配置类叫XXXConfig,UI 绑定类叫XXXPanel.Bind.cs。这样一眼就能看出哪些是生成的,哪些是手写的。生成文件头部一定要加注释标记,写明“此文件由工具自动生成,请勿手动修改”,避免不知情的同事直接改生成文件。
3. 核心细节解析与实操要点
3.1 配置表生成:从 Excel 到 C# 类
配置表生成是最常见的需求。策划用 Excel 维护数据,程序需要把它转成能在运行时快速读取的结构。整个流程分三步:解析 Excel、生成数据类、生成加载逻辑。
解析 Excel这块,Unity 里不能直接用Excel.Application(依赖 Office 环境,CI 上跑不了),推荐用EPPlus或NPOI这类纯托管库。EPPlus 对.xlsx支持好,API 也友好。解析时要注意几个细节:第一行通常是字段名,第二行是类型,第三行是注释,从第四行开始才是数据。这个约定要和策划对齐,写进规范文档。
生成数据类时,字段类型要做映射。Excel 里的int、string、float要映射到 C# 对应类型,数组类型用逗号分隔的字符串表示,生成时转成List<T>或数组。这里有个坑:Excel 里的空单元格读出来可能是null或空字符串,生成默认值时要小心处理,否则运行时反序列化会出问题。
生成加载逻辑有两种思路:一种是生成一个大的ConfigManager,把所有表都加载进来;另一种是每张表生成独立的加载方法。我倾向于后者,解耦更好,也方便按需加载。加载时用JsonUtility或自己写的二进制格式都行,小项目用 JSON 足够,大项目考虑二进制以提升读取速度。
3.2 UI 绑定生成:读 Prefab 结构自动写引用
UI 绑定生成的思路是:遍历 Prefab 的节点树,根据命名规则识别需要绑定的控件,然后生成对应的字段声明和查找代码。
命名规则是关键。我通常约定:需要绑定的节点以特定前缀开头,比如btn_表示按钮、txt_表示文本、img_表示图片。生成器遍历时,遇到这些前缀的节点就记录下来,生成一个同名的字段,并在初始化方法里用transform.Find或GetComponent赋值。
这里有个性能细节:transform.Find是按路径查找,节点层级深的时候比较慢。更好的做法是生成时记录完整路径,初始化时一次性查找并缓存。如果 Prefab 结构稳定,甚至可以在生成阶段就把引用关系写死,运行时直接赋值,省去查找开销。
注意:Prefab 结构变动后必须重新生成,否则字段和实际节点会对不上。建议在 Prefab 保存时触发重新生成,或者至少在打包前强制跑一遍。
3.3 协议生成:从定义文件到序列化类
网络协议这块,如果团队用 Proto 或自定义的协议定义文件,生成器可以把定义转成 C# 的序列化类。核心工作是解析定义文件,提取消息名、字段名、字段类型,然后按模板生成类。
字段类型映射要特别注意:协议里的int32、int64、string、bytes要映射到 C# 的int、long、string、byte[]。如果是变长数组或嵌套消息,生成逻辑会更复杂,需要递归处理。生成的类通常要实现序列化和反序列化方法,这部分可以统一用模板生成,避免手写。
一个容易忽略的点是版本兼容。协议字段增删后,旧客户端和新服务端可能对不上。生成时可以在类里加一个版本号字段,或者用可选的字段标记,方便做兼容处理。
3.4 模板设计:让生成代码像手写的一样
模板质量直接决定生成代码的可读性。好的模板生成的代码应该和手写的没区别——缩进正确、命名规范、注释齐全。几个实操要点:
第一,缩进用模板引擎控制,别在生成逻辑里手动拼空格。Scriban 和 T4 都支持自动缩进处理,用好了能省很多事。
第二,注释要生成。字段的注释可以从 Excel 的注释行或 Prefab 的节点名提取,生成到代码里。这样看代码时不用来回翻表。
第三,生成代码要能通过编译器的代码分析。比如partial类、#nullable指令、using语句都要正确处理。生成后最好跑一遍编译验证,有问题及时暴露。
第四,留出手写扩展点。生成类用partial,手写逻辑放到另一个文件。事件绑定、业务逻辑这些不该生成的内容,全部留给手写部分。
4. 实操过程与核心环节实现
4.1 搭建生成器骨架
先建一个基础的生成器接口,所有具体生成器都实现它:
public interface ICodeGenerator { string Name { get; } void Generate(); }然后建一个管理器,统一调度:
public static class CodeGenManager { private static readonly List<ICodeGenerator> Generators = new() { new ConfigGenerator(), new UIBindGenerator(), new ProtocolGenerator(), }; public static void GenerateAll() { foreach (var gen in Generators) { try { gen.Generate(); } catch (Exception e) { Debug.LogError($"[CodeGen] {gen.Name} 生成失败: {e}"); } } AssetDatabase.Refresh(); } }这个骨架的好处是:新增生成器只要实现接口并注册进去,互不影响;单个生成器出错不会中断整体流程,方便排查。
4.2 配置表生成器完整实现
以 EPPlus 为例,核心解析逻辑大概是这样:
public void Generate() { var excelFiles = Directory.GetFiles(ConfigPath, "*.xlsx"); foreach (var file in excelFiles) { using var package = new ExcelPackage(new FileInfo(file)); var sheet = package.Workbook.Worksheets[0]; var className = Path.GetFileNameWithoutExtension(file); var fields = ParseFields(sheet); var code = RenderTemplate(className, fields); WriteFile(className, code); } }ParseFields负责读取前三行(字段名、类型、注释),RenderTemplate用 Scriban 渲染模板。模板大概长这样:
// 此文件由工具自动生成,请勿手动修改 using System.Collections.Generic; public partial class {{ class_name }} { {{ for field in fields }} /// <summary>{{ field.comment }}</summary> public {{ field.type }} {{ field.name }}; {{ end }} }生成后,再写一个手写的partial类,加上加载和查询方法。这样生成部分只负责数据结构,手写部分负责业务逻辑,职责清晰。
4.3 UI 绑定生成器完整实现
UI 绑定生成器的核心是遍历 Prefab 节点树。用PrefabUtility.LoadPrefabContents加载 Prefab,然后递归遍历:
private void Traverse(Transform node, List<BindItem> items) { var name = node.name; if (name.StartsWith("btn_")) items.Add(new BindItem(name, "Button", GetPath(node))); else if (name.StartsWith("txt_")) items.Add(new BindItem(name, "Text", GetPath(node))); foreach (Transform child in node) Traverse(child, items); }GetPath记录从根节点到当前节点的完整路径,生成时用这个路径做查找。生成的绑定类大概是这样:
public partial class MainPanel { private Button btn_start; private Text txt_title; private void BindComponents() { btn_start = transform.Find("Root/btn_start").GetComponent<Button>(); txt_title = transform.Find("Root/txt_title").GetComponent<Text>(); } }手写部分在Awake里调用BindComponents,然后就可以直接用这些字段了。
4.4 参数计算与性能考量
生成器本身跑在编辑器里,性能不是大问题,但生成代码的运行性能要关注。几个关键点:
查找路径的缓存。transform.Find每次调用都有开销,如果面板打开频繁,建议在生成时就把路径转成索引,或者用GetComponentsInChildren一次性收集再匹配。实测下来,对于节点数超过 50 的面板,缓存路径能省下 30% 以上的初始化时间。
配置表的加载格式。JSON 可读性好但解析慢,二进制快但不可读。我的经验是:开发期用 JSON 方便调试,发布时转二进制。转换可以在生成阶段一并完成,生成 JSON 的同时生成一份二进制,运行时按环境选择。
生成文件的编译开销。生成代码多了以后,编译时间会变长。建议把生成代码单独放一个 Assembly Definition,这样改动生成代码时只重编这个程序集,不影响主工程。
4.5 接入构建流程
最后一步是把生成接入构建流程,保证打包时用的是最新代码。实现IPreprocessBuildWithReport:
public class CodeGenBuildPreprocessor : IPreprocessBuildWithReport { public int callbackOrder => 0; public void OnPreprocessBuild(BuildReport report) { CodeGenManager.GenerateAll(); } }这样每次打包前都会强制生成一遍,避免忘记手动生成导致的运行时错误。CI 上跑的时候,这个钩子同样生效,保证自动化流程的一致性。
5. 常见问题与排查技巧实录
5.1 生成代码编译报错怎么快速定位
生成代码报错是最常见的问题,通常有几类原因:模板语法错误、字段类型映射错误、命名冲突。排查时先看报错行号,定位到具体是哪个生成器、哪个模板出的问题。建议在生成时把每个文件的生成日志打出来,出错时能快速对应。
如果是类型映射错误,检查 Excel 里的类型写法和映射表是否一致。我遇到过策划把int写成Int的情况,映射表没做大小写兼容,直接生成了非法类型。后来在解析时统一转小写,问题就没了。
5.2 Prefab 结构变动后绑定失效
Prefab 改了节点名或层级,生成的绑定代码就找不到节点了。解决办法有两个:一是 Prefab 保存时自动触发重新生成,二是生成代码里加空引用检查,找不到节点时打警告而不是直接崩。
我倾向于两者都做。自动重新生成保证及时性,空引用检查作为兜底。警告日志里带上 Prefab 名和节点路径,排查起来很快。
5.3 生成文件被误改导致覆盖丢失
这个问题的根源是生成文件和手写文件混在一起。解决办法前面提过:生成类用partial,手写逻辑放另一个文件。另外在生成文件头部加醒目注释,并且在版本管理里把生成目录标记为“生成物”,提醒同事不要直接改。
如果团队里有人习惯直接改生成文件,可以在 CI 里加一个检查:重新生成一遍,如果文件有 diff 就报错,强制大家改模板而不是改生成结果。
5.4 常见问题速查表
| 问题现象 | 可能原因 | 排查方向 | 解决方法 |
|---|---|---|---|
| 生成代码编译报错 | 模板语法或类型映射错误 | 看报错行号对应模板 | 修正模板或映射表 |
| 运行时找不到节点 | Prefab 结构变动 | 检查节点路径 | 重新生成 + 空引用检查 |
| 生成文件被覆盖 | 手写逻辑混入生成文件 | 检查文件是否 partial | 拆分生成与手写文件 |
| 生成速度慢 | 遍历文件过多 | 看生成日志耗时 | 增量生成,只处理变动文件 |
| 打包后配置读不到 | 生成未接入构建流程 | 检查构建钩子 | 实现 IPreprocessBuildWithReport |
| 字段默认值异常 | 空单元格处理不当 | 检查解析逻辑 | 空值给默认值,别给 null |
5.5 几个踩坑心得
坑一:Excel 里的数字被识别成科学计数法。长数字(比如 ID)在 Excel 里容易变成1.23E+10,解析出来就不对了。解决办法是解析时按字符串读,再转成目标类型,别直接读数值。
坑二:生成时机和 AssetDatabase 刷新冲突。在AssetPostprocessor里生成后立刻Refresh,可能触发递归导入。解决办法是用EditorApplication.delayCall延迟刷新,或者加一个生成中的标志位防止重入。
坑三:模板里的换行符不一致。Windows 和 Mac 的换行符不同,模板里混用会导致生成代码格式混乱。统一用\n,生成时按平台转换。
坑四:生成代码的命名空间没处理。如果生成类需要放在特定命名空间下,模板里要显式声明,否则默认在全局命名空间,引用时会很麻烦。
6. 进阶扩展与个人体会
6.1 从生成代码到生成资源
代码生成跑通后,可以进一步扩展到资源生成。比如根据配置表自动生成 ScriptableObject 资产,或者根据 UI 配置自动生成 Prefab 骨架。思路是一样的:读元数据、套模板、产出资产。区别在于产出的是.asset或.prefab文件,需要用AssetDatabase.CreateAsset来写。
6.2 增量生成降低开销
项目大了以后,全量生成会越来越慢。可以做增量生成:记录每个源文件的修改时间或哈希值,只重新生成变动过的。实现上用一个 JSON 文件存生成记录,每次生成前对比,没变的跳过。实测下来,增量生成能把生成时间从几十秒降到几秒。
6.3 生成结果的可视化校验
生成完代码后,最好有个校验环节。比如配置表生成后,检查字段数量是否和 Excel 一致;UI 绑定生成后,检查每个绑定字段是否都能在 Prefab 里找到对应节点。校验不通过就报错,别让问题流到运行时。
6.4 我个人的一些体会
这套东西我前后迭代了好几版,最大的感受是:生成器的价值不在于省了多少行代码,而在于把“容易出错的手工活”变成了“可验证的自动化流程”。以前策划改表,程序要手动同步,改漏了就是线上事故;现在改完自动生成,编译期就能发现问题。
另一个体会是别追求一步到位。第一版就做个最简单的配置表生成,跑通了再逐步加 UI 绑定、协议生成。每加一个模块,先把模板和流程打磨稳定,再考虑下一个。贪多求全的结果往往是每个模块都半成品,维护起来更痛苦。
最后分享一个小技巧:生成器里加一个“预览模式”,不实际写文件,只把生成结果打印到控制台。调试模板的时候特别有用,改一行模板立刻能看到效果,不用反复生成文件再打开看。
这套方案后续还能往很多方向扩展,比如接入代码格式化工具保证生成代码风格统一,或者把生成器做成独立的命令行工具,脱离 Unity 编辑器也能跑。具体怎么走,看项目实际需求来定。