项目结构
Udonite 会编译哪些脚本、程序集定义如何参与其中,以及怎样让代码不进入 Udon。
哪些脚本会变成程序
同时满足下面几点的类会变成一个 Udon 程序:
- 继承自
MonoBehaviour,无论是直接继承还是通过UdoniteBehaviour - 不是
abstract、不是泛型,也不嵌套在其他类型里 - 脚本位于
Assets/下的任意位置
每个符合条件的类都是一个根,每个根都会在同一个 GameObject 上得到属于自己的 Udon 程序。不需要注册,也没有哪个文件夹是特殊的:Assets/Scripts/Doors/Door.cs 里的脚本和 Assets/Door.cs 里的脚本,被找到的方式完全一样。
不符合条件的类不是错误。抽象基类、泛型辅助类、纯数据类和静态工具类都是 behaviour 会用到的普通 C#,只是它们本身不会变成程序。
程序集定义
用不用程序集定义,Udonite 的行为都一样。
如果项目里没有任何程序集定义,脚本会落在 Unity 默认的 Assembly-CSharp 中,Udonite 编译它们。加上一个 .asmdef,脚本就落在那个程序集里,Udonite 编译那一个。并不是其中一种才是正路、另一种算变通;唯一的规则是该程序集在 Assets/ 下有源码。
这件事比听上去更重要,因为程序集决定了你的代码能看见什么,有两方面:
引用。 一个 behaviour 可以使用它所在程序集引用的任何类型。如果你把一个世界拆成 World.Runtime 和 World.Interactables,而后者引用了前者,那么 World.Interactables 里的 behaviour 就能正常使用 World.Runtime 中的类型。
定义符号。 你的程序集编译时使用的脚本定义符号,也正是 Udonite 编译时使用的,因此 #if UNITY_EDITOR 和你自己的定义在这里的表现,和项目其他地方完全一致。
仅编辑器的程序集会被跳过。平台列表设为 Editor 的 .asmdef 装的是工具,不是世界逻辑,其中的内容不会被编译到 Udon。
分散在多个文件里
一个 behaviour 不必局限于一个文件。同一程序集内其他文件中的辅助类、枚举、接口和基类它都能使用,所以世界可以按需要拆成任意多个文件。
边界是程序集,不是文件。如果辅助类在另一个程序集里,behaviour 所在的程序集就需要引用它,这一点和 Unity 中其他地方一样。
包里的脚本
Packages/ 下的脚本不会被编译到 Udon。这是有意为之,而不是缺失。一个导入的包里可能有上百个 MonoBehaviour 类,写它们的时候根本不知道 Udon 的存在;编译它们只会让控制台被一堆拒绝信息淹没,而那些代码既不是你写的,你也改不了。
唯一的例外是 Udonite 自己附带的组件,比如 NetworkTransform。它们本来就是为了在世界里运行而做的,所以即使位于包中也会被编译。
如果你想在世界里用到某个包的 behaviour,可靠的做法是在 Assets/ 下写自己的 behaviour,再由它去驱动那个包的组件。
与 UdonSharp 共存
继承自 UdonSharpBehaviour 的类会被原样留给 UdonSharp 编译。Udonite 只认 MonoBehaviour 的类,因此两个编译器可以在同一个项目里工作,而不会争夺同一个脚本。
让代码不进入 Udon
Assets/ 下有些代码永远不该变成程序:编辑器工具、只在桌面端运行的第三方组件、你还没移植完的脚本。把它排除掉,Udonite 就不再编译它。
最快的办法是用复选框。打开 Udonite → Open Window,在 Overview 标签页的树里取消勾选那个脚本或文件夹。取消勾选一个文件夹会排除它下面的一切;把已排除的重新勾选上,会直接去掉那条排除,而不是留下一行禁用的记录。被排除的脚本或文件夹不会消失,而是在树里灰显;如果打开的场景里还有它的实例活着,会出现警告,因为那个实例马上就拿不到 Udon 程序了。同一个窗口里的 Diagnostics 和 Settings 标签页在快速开始里讲了。
不管哪个复选框,背后读写的都是同一个文件,所以手动编辑跟用复选框效果完全一样。排除项写在 ProjectSettings/UdoniteExclusions.txt 里,每行一条,顺序无关紧要。每行要么是一个程序集名,要么是一个资源 GUID:
# Udonite 排除项。每行一条;顺序不影响结果。
asm:SomeVendor.Runtime # 一个永远不会在世界里运行的包
guid:3c6e5249679282e459858775b10f38d0 # Assets/Editor/LevelBuilder.cs
asm: 接受一个程序集名。要整体排除一个第三方包时,通常用它就对了,因为 Unity 会把这样一个包编译成一个程序集。
guid: 接受 Unity 的 32 位十六进制数字,来自资源旁边的 .meta 文件。它可以指向单个脚本,也可以指向一个文件夹;指向文件夹时,其下的所有内容都会被排除。
用 GUID 是有意的。路径在有人移动文件的那一刻就不再匹配,而且是悄无声息地不再匹配,对这类规则来说这是最糟糕的失效方式。GUID 能在移动之后依然有效。同样的道理,行尾那句 # 备注也值得保留:万一某个 GUID 有一天解析不出来了,那句备注就是这一行含义的唯一记录。资源一旦被删除,下一次有什么东西读这个文件时——打开窗口,或者下一次编译——那一行就会自动被去掉,不会留下一个什么都指不到的 GUID。
文件夹的 GUID 靠手找很麻烦,Unity 自己的界面哪里都不显示它。从 Overview 里排除一个文件夹完全不需要输入 GUID,这才是把文件夹那一行写进文件的靠谱办法。
没有重新纳入这回事。不能先排除一个文件夹,再单独把里面的某一个文件重新纳入——要么在文件夹外面单独排除那个文件自己的 GUID,要么把它移出被排除的文件夹。
一个条目可以不删除就关掉:在前面加一个 !,比如 !asm:Cinemachine,这一行(连同它的备注)会留在文件里,但只要它这样写着就什么都不排除——Overview 里会把那个脚本显示为已包含,跟这一行不存在时一模一样。写 ! 是手动操作,Overview 不会替你做。但之后从 Overview 里再排除同一个脚本,会重新启用这一行而不是再写一条重复的,所以手动加的 ! 不需要重新打一遍 GUID 就能撤销——之后再把它纳入一次,这次就是把这一行直接去掉,而不是再禁用一次。
这里有两件事出错得够频繁,因而各有自己的代码。Udonite 读不懂的行会报告为 UDN0007,并附上它期望的格式。仍在调用被排除代码的 behaviour 会报告为 UDN0008,因为在世界里那个调用无处可去。
你在写代码时 Udonite 会说的话
有两件事会在编译之前告诉你,而且都出现在你已经在看的地方。
编辑器里的波浪线。 Udonite 附带一个 Roslyn 分析器,所以有几种拒绝会在你打字时就出现,而不是等编译之后:可变的 static 字段、catch 子句,以及 Udon 从不派发的 Unity 事件。它们带的是和编译器相同的 UDN 代码,所以搜出来的是一个答案,不是两个。
它们是警告,而且不会全部抓到。编辑器里干干净净并不代表程序能编译;只有真正编译过才知道。
空的 behaviour 引用。 当一个指向另一个 behaviour 的 public 字段没有连任何东西时,控制台会点名这个 behaviour 和这个字段:
Udonite: Switch on 'Door Controls' has an empty behaviour reference: 'target'.
Calling through one halts the behaviour for the rest of the session with nothing
logged, so wire it in the inspector, or make the field private if the script
assigns it itself.
只有指向别的 behaviour 的引用才会被报告。空的 GameObject 或 AudioSource 字段很常见,往往还是故意的;而一个 behaviour 引用的存在就是为了被调用,调用一个空的引用,正是那种不留任何痕迹的失败。
程序最终在哪里
每个程序都附加在对应 behaviour 所在的同一个 GameObject 上,上传时进入世界的就是它。你的 C# 留在项目里,供下一次编辑使用。除了平常那些,没有别的东西需要提交:每当 Unity 重新编译、每当场景被保存,程序都会重新生成。