语言支持
现在能编译哪些写法,以及 Udon 永远无法运行的那部分 C#。
Udonite 能编译日常 C# 中相当大的一部分。下面列出的每一项都有测试覆盖:这些测试会汇编输出,并在真正的 Udon 虚拟机上运行。
能编译的部分
类型与成员
class
struct
interface
enum
record(需要一处声明)
嵌套类型
自定义类作为字段和局部变量
带约束的泛型
接口默认实现
属性和索引器
扩展方法
可选参数和命名参数
params
out 和 ref 参数
元组和解构
作为值的 typeof
局部函数
静态辅助类
record:位置构造、with、值相等性、ToString、解构。但你需要先在自己的脚本里加上一处声明:每个 record 和每个 init 访问器都会编译成 init-only 属性,而 Unity 编译世界代码所用的 .NET 版本,其类库里没有 System.Runtime.CompilerServices.IsExternalInit。没有它,Unity 自己的编译器会在 Udonite 运行之前就以 CS0518 中断构建:
namespace System.Runtime.CompilerServices
{
internal static class IsExternalInit { }
}
record struct 以及作用于结构体的 with 属于 C# 10,而 Unity 以 C# 9 编译,所以这两者完全无法使用。静态字段必须是 const 或 readonly(见下文)。
继承
abstract 类
abstract 成员
virtual 与 override
base. 调用
abstract behaviour
通过基类型的调用会到达最派生的重写,base.Method() 则正好上溯一层。普通类和挂在 GameObject 上的 behaviour 都是如此。
组件之间共享契约的办法是 abstract 基类,因为 Unity 无法序列化接口字段。参见接口。
语句与表达式
Array 辅助方法
List<T>
Dictionary<K,V>
HashSet<T>
Queue<T>
Stack<T>
LINQ
委托和事件
lambda
字符串和 StringBuilder
可空值类型
枚举解析与名称
try / finally
throw
递归
控制流指 if、switch、for、foreach、while、do、break 和 continue。模式匹配涵盖类型模式、is not null、is > 5 and < 10 这样的关系模式、属性模式和位置模式,以及 when 守卫。null 运算符是 ?.、?? 和 ??=。插值字符串支持格式说明符,nameof 也能编译。范围与索引指 arr[^1]、arr[1..3],以及字符串上的同样写法:text[1..3]、text[^2..]、text[..^1]。typeof(T) 可以作为值使用:能存进局部变量、字段、参数和数组,能用 == 和 Equals 比较,也能调用 IsAssignableFrom 和 IsInstanceOfType。typeof(T).Name 和 .FullName 会在编译期折叠成字符串。Array 辅助方法有 Sort、Find、FindAll、FindLast、Exists、IndexOf、LastIndexOf、Resize、Copy、Fill 和 ConvertAll。
LINQ 可以用在数组、列表和字典上:Select、Where、OrderBy、ThenBy、GroupBy、First、Any、All、Sum、Average、Min、Max、Count、Take、Skip、TakeWhile、SkipWhile、Distinct、Reverse、Concat、Zip、Union、Intersect、Except、SequenceEqual、ToArray、ToList、ToDictionary、Range、Repeat,以及它们的链式组合。传给 LINQ 以及 List<T>/Array 辅助方法的 lambda 会在调用处内联展开。
委托包括 Action 和 Func 字段、Action done = Cleanup; 这样的方法组、多播的 += 和 -=、C# 的 event 声明,以及指向另一个 behaviour 上 public 方法的委托。lambda 可以作为值使用;会捕获变量的 lambda 有一条限制,下文会说明。字符串有 Format、Split、Join、Substring、Replace、Trim 和 PadLeft。可空值类型有 HasValue 和 GetValueOrDefault。枚举有 Parse、TryParse、GetNames、GetValues 和 HasFlag,typeof(T).Name 也能编译。throw 会把消息写入日志并停止该 behaviour,因此可以当作断言使用;没有什么可以捕获。直接递归和相互递归都能编译,调用前后方法的变量会被保存到栈上。
Unity 与 VRChat
Instantiate 和 Destroy
Transform 和物理
Mathf 和值类型
Random 和 Time
协程
Invoke 系列
async 和 await
VRCPlayerApi
Networking
拾取物和座椅
UdonBehaviour 引用
其他 behaviour 的 Unity 成员
Unity 和 SDK 类型的数组
Unity 对象类型之间的转换
组件查找指 GetComponent<T>() 及其变体、非泛型的 GetComponent(typeof(T))、TryGetComponent<T>(out T),以及 SetActive。值类型指 Vector3、Quaternion、Color 等常见的 Unity 结构体。协程可以使用 yield return null、WaitForSeconds、WaitUntil 和嵌套协程,用 StartCoroutine(Run()) 或 StartCoroutine(nameof(Run)) 启动,用 StopCoroutine(handle) 或 StopAllCoroutines 停止。Invoke、InvokeRepeating、CancelInvoke 可以用在 public 方法上。async 方法可以返回 void、Task、UniTask 或 UniTaskVoid,可以 await Task.Delay(...),也可以等待其他 async 方法。UdonBehaviour 引用带有 SendCustomEvent、SetProgramVariable 和 GetProgramVariable<T>。另一个 behaviour 的 gameObject、transform 和 enabled 可以直接读写。Unity 类型、组件和 SDK 枚举的数组和其他字段一样使用。网络一页中的全部内容。
写入着色器和材质
VRCShader 全局值
material.SetFloat 等
MaterialPropertyBlock
EnableKeyword
UnityEngine.Shader 没有开放。全局值要通过 VRCShader.PropertyToID(name) 和
VRCShader.SetGlobalFloat、SetGlobalVector、SetGlobalColor、SetGlobalMatrix、
SetGlobalTexture,以及它们各自的数组版本来写。没有 SetGlobalInt,因为 SDK 里就没有。
全局值的名字必须以 _Udon 开头。 不是这样的名字,VRChat 会忽略它,而且是悄悄忽略:调用成功,
着色器保持原来的值,日志里什么也没有。全局值不是材质属性,所以在着色器里要写在 Properties 块之外。
材质属性则很普通:按名字或按 id 的 SetFloat、SetColor、SetVector、GetColor,
以及 MaterialPropertyBlock 和 EnableKeyword。请用 sharedMaterial 而不是 material——
碰 material 会在运行时复制一份材质,而运行时材质除非登记在场景描述符上,否则会被从构建里剥掉。
2026-09-08 已在上传的世界里确认过两条路径,而不只是能编译。
Udon 永远无法运行的部分
这些会被 UDN0011 拒绝。提示信息会说明原因,并给出替代写法。
| 写法 | 原因,以及应该改写成什么 |
|---|---|
catch |
Udon 没有可以捕获的异常:出错时 behaviour 会直接停止。去掉 catch,改为先判断条件。try/finally 可以编译,throw 会记录消息并停止该 behaviour。 |
可变的 static 字段 |
每个 behaviour 都有自己的堆,所以静态字段会变成「每个 behaviour 一份」,而不是共享的一份。请把它放在某一个 behaviour 的字段上再引用它,或者用一个没有存储的只读 static 属性(见单例)。 |
Awake、Reset、OnValidate、OnGUI、OnDrawGizmos、OnApplicationQuit、OnApplicationPause、OnApplicationFocus、OnAudioFilterRead |
Udon 不会派发它们。Awake 里的工作请移到 Start;其余的要么只在编辑器中存在,要么属于应用程序而不是世界,而 OnApplicationQuit 想表达的通常是 OnPlayerLeft。 |
CompareTag、gameObject.tag、FindGameObjectWithTag |
Udon 不暴露 tag。请比较层级或名称、直接持有引用,或使用 GameObject.Find。 |
Camera.main |
未暴露。请在 Inspector 里把摄像机赋给一个字段。 |
SendMessage、IsInvoking 以及其他 Udon 没有的 MonoBehaviour 成员 |
协程、Invoke 和 CancelInvoke 由 Udonite 转换实现;其余的没有能在世界里运行的形式。 |
Application.isPlaying、isFocused、targetFrameRate、platform、Time.timeScale、AudioListener |
未暴露。世界始终以实时速度运行。用 Networking.LocalPlayer.IsUserInVR() 区分 VR 与桌面,并逐个设置 AudioSource 而不是监听器。 |
PlayerPrefs、Resources.Load、SceneManager、UnityEvent、Shader、Input.mousePosition、new GameObject()、GameObject.CreatePrimitive、Mesh.CombineMeshes、TMP_Text.SetText |
每条拒绝信息都会给出 VRChat 的做法:持久化用 PlayerData,资源用 Inspector 字段,场景加载改为 VRCPlayerApi.TeleportTo,UnityEvent 改为在 Inspector 里连接的 public 方法,全局着色器属性用 VRCShader,指针位置改用头部追踪数据,实例化预制体或模板对象,网格在编辑器里合并,以及使用 text 属性。 |
| 指向普通对象(而非 behaviour)方法的委托,以及放在 Unity 会序列化的 public 字段中的委托 | 两者都无法带进世界里。 |
| 超出创建它的方法之后仍然保留的捕获型 lambda(赋值给字段、作为返回值、作为参数传出) | 被捕获的变量位于 behaviour 唯一的堆上,因此保存下来的闭包在下次调用时会看到过时的状态。不捕获的 lambda 可以随意保存;捕获型的,只要作为局部变量在同一方法内调用就没问题。 |
checked 运算 |
Udon 的运算符 extern 在溢出时不会抛出,而且 Udon 根本没有可抛的异常。请在运算之前先检查操作数。 |
| 在 Inspector 序列化类型为本程序所编译的类的字段(含数组) | 这些实例只在程序运行期间存在,Unity 既没有东西放进 Inspector,也没有东西可保存,该字段到了世界里会是空的。请把它设为 private 或加上 [NonSerialized],然后在 Start 里填充。这说的是字段而不是类型:类本身作为局部变量、参数和 private 字段都能正常使用。基元类型、Unity 类型、SDK 枚举和 behaviour 引用的字段与数组都没有问题。 |
经由委托的递归(UDN0012) |
直接递归和相互递归都能编译。如果调用环中有一次是通过委托发生的,编译器就没有确定的目标可以为其保存现场,因此请在调用环里直接调用该方法。 |
暂时还不支持
这些会被 UDN0002 拒绝。它们是 Udonite 的缺口,而不是 Udon 的限制;有人提出需求的通常会被补上。
- 在循环内声明的捕获型 lambda。
如果某个拒绝挡住了你,请带上那一行拒绝信息提交 issue。只要说清楚是哪种写法,通常一天就能补上。