系列 · 为 UITK 写一套模板系统 第 1 / 7 篇

在 Unity UITK 里,我为什么给自己造了一套模板系统

起点:我为什么会盯上这件事

这几年我在 Unity 里做过不少编辑器工具:行为树编辑器、角色编辑器、多面板配置界面。它们都跑在 Unity Editor 里,UI 层选择的是 UITK——Unity 2022 内置的新一代 UI 系统,用 UXML(类 XML 的标记语言)描述结构、USS 描述样式、事件系统处理交互,心智上和网页的 HTML / CSS / DOM 很接近。

UITK 比老一代的 IMGUI 舒服得多,但真开始写编辑器界面时,我还是会反复撞到几类老问题:

一句话:UITK 给了我「文档结构 + 样式 + 事件」的底层,但「把数据变成界面、把界面变回数据」这件事,还是要我一次次自己拼。

背景:这是一个从兴趣里长出来的实验

先交代一下这个系列的背景:我做了 10+ 年游戏开发,最近几年又大量接触 Web 技术栈。Vue 的模板与响应式、TypeScript 的类型系统、前后端工程里那些「小而锐利」的设计,我都越来越喜欢。

这种喜欢没有停留在 Web 里。回到 Unity 编辑器写工具界面时,我总会反复想到一件事:能不能也用更声明式的方式描述界面,把增量交给框架,把复杂度关进编译器?

所以这套系统从一开始就不是某条业务需求直接推出来的结果,而是一个从个人兴趣里长出来的实验。我想看看,那些我在 Web 世界里喜欢的设计哲学,到了 UITK / C# 这块地基上,究竟能长成什么样。它的骨架也因此天然带着 Web 的印记,比如「编译前端与运行时分工」「语法糖在生成期消化、运行时保持最小模型」「所有权与生命周期显式化」。本系列后面会反复回到这几条线。

为什么是 Vue:一张对照表,和它背后的一套分工

如果你长期写 Vue,前面那一列痛点会立刻弹出熟悉的解法:

痛点Vue 3 的答案我们想要的东西
样板构树模板编译authoring 里写结构,编译器负责变成树
手动同步响应式系统值变了,依赖它的文本/属性自动更新
粗粒度刷新声明式结构更新 + keyed identity用条件/循环声明结构变化,并尽量保住已有节点身份
生命周期分散生命周期阶段 + 副作用清理明确的挂载/完成/卸载阶段与统一清理边界

尤其吸引我的是 Vue 3 那种「模板先编译、运行时再消费」的分工。{{ }} 插值、v-if / v-for、v-model、slot 都不是魔法,而是一个编译前端 + 运行时的组合:前者把 authoring 变成可消费的编译产物,后者据此建立绑定、驱动结构更新与内容投影。

于是问题就变成:这些概念能不能平移到 Unity 编辑器里,用 C# + UXML 重新做一遍?

我的做法:给 UITK 叠一层「编译前端 + 运行时」

我没有发明新的底层,整套东西完全长在 UITK 之上,大致分成三段:

  1. Authoring(编写侧):还是 UXML 文件,但允许在里面写模板指令——纯文本子节点、{{ }} 插值、v-if、v-for、on-click、v-model、ref、v-slot。
  2. 编译前端(生成期):编辑器导入时,一套收集器(collector)解析 UXML,做语法糖规范化(比如把裸文本变成 ui:Label)、表达式编译、结构蓝图生成,并把产物落盘成生成资产与元数据索引。
  3. 运行时(构造期装配 + 挂载后激活):模板组件最终落在 TemplateElement 这层;构造时按类型查索引、加载自己的生成 UXML / USS、克隆成树,挂载稳定后再由指令编译器建立响应式绑定,并由完成链驱动生命周期。

整套系统的最短链路,是一条再简单不过的链:类 → 模板资源 → UI。最小形态像这样:

// 一个「空壳」模板组件类 —— 没有任何构树代码
public class MyTemplateComponent : TemplateElement
{
    public new class UxmlFactory : UxmlFactory<MyTemplateComponent, UxmlTraits> { }
    public new class UxmlTraits : VisualElement.UxmlTraits { }
}

对应地,界面写在 authoring 的 UXML 里——类只声明自己的身份,剩下的接驳工作全部交给编译前端与运行时。对组件使用者来说,通常只需要拿到最外层那个模板容器实例;从它往下的静态树生成、以及模板里声明的自定义子组件实例化,都会在模板系统内部递归发生。后面你会看到,真正的运行时链也不是单点动作,而是先装配出静态树,再在稳定挂载点接上绑定与结构语义。

取舍的原点:它首先是一个编辑器侧实验

先划清边界:这套系统不是给游戏运行时做 UI 的。玩家看到的 HUD、菜单属于 Runtime UI,是另一条线。它的使用场景只有一类:编辑器工具。

这个坐标几乎解释了后面的大部分取舍:编辑器界面相对小,迭代快,服务的是内容生产侧的工作流,所以我可以接受编译前端、生成资产和元数据索引这类机制层的存在,让 authoring 侧获得更自然的表达。很多设计决定——为什么敢做生成期规范化、为什么指令编译要后置到稳定挂载点——都能从这里一路推回去。

系列路线图

篇章沿「实现」这条线展开:

#主题讲什么
00前言(本篇)背景与动机
01Hello World:第一个模板组件最小模板组件、最小 UXML、最小宿主窗口,以及第一张无 USS 的运行截图
02继承链:模板组件由哪四层组成运行时基座逐层拆解:从 VisualElement 到 TemplateElement
03组件配方:UXML / C# / USS 如何拼成一个组件三块资产、根组件实例化后的自动装配、为什么这样设计
04嵌套组件:父模板里如何声明子组件父模板里声明子组件,递归装配如何开始成立
05模板 token 规格:authoring 语言的契约token 全集、语义契约与管线归属
06token 开始生效:先看几类最小结果先看一个 token 版的 hello world:文本插值、事件和值变化、条件分支怎样开始落到界面上
07+更多接下来会继续进入表达式抽象、collector / metadata、运行时分层,以及最后的整体回看

这篇先把背景、动机和实验边界讲清楚;后面的文章再一步步回到实现细节。

本文在 AI 协作下完成。