语言支持

现在能编译哪些写法,以及 Udon 永远无法运行的那部分 C#。

更新于 2026-09-08

Udonite 能编译日常 C# 中相当大的一部分。下面列出的每一项都有测试覆盖:这些测试会汇编输出,并在真正的 Udon 虚拟机上运行。

能编译的部分

类型与成员

class struct interface enum record(需要一处声明) 嵌套类型 自定义类作为字段和局部变量 带约束的泛型 接口默认实现 属性和索引器 扩展方法 可选参数和命名参数 params outref 参数 元组和解构 作为值的 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 编译,所以这两者完全无法使用。静态字段必须是 constreadonly(见下文)。

继承

abstract abstract 成员 virtualoverride base. 调用 abstract behaviour

通过基类型的调用会到达最派生的重写,base.Method() 则正好上溯一层。普通类和挂在 GameObject 上的 behaviour 都是如此。

组件之间共享契约的办法是 abstract 基类,因为 Unity 无法序列化接口字段。参见接口

语句与表达式

控制流 switch 表达式 模式匹配 null 运算符 字符串插值 算术、位和移位运算符 标志枚举 数组和多维数组 范围和索引 Array 辅助方法 List<T> Dictionary<K,V> HashSet<T> Queue<T> Stack<T> LINQ 委托和事件 lambda 字符串和 StringBuilder 可空值类型 枚举解析与名称 try / finally throw 递归

控制流指 ifswitchforforeachwhiledobreakcontinue。模式匹配涵盖类型模式、is not nullis > 5 and < 10 这样的关系模式、属性模式和位置模式,以及 when 守卫。null 运算符是 ?.????=。插值字符串支持格式说明符,nameof 也能编译。范围与索引指 arr[^1]arr[1..3],以及字符串上的同样写法:text[1..3]text[^2..]text[..^1]typeof(T) 可以作为值使用:能存进局部变量、字段、参数和数组,能用 ==Equals 比较,也能调用 IsAssignableFromIsInstanceOfTypetypeof(T).Name.FullName 会在编译期折叠成字符串。Array 辅助方法有 SortFindFindAllFindLastExistsIndexOfLastIndexOfResizeCopyFillConvertAll

LINQ 可以用在数组、列表和字典上:SelectWhereOrderByThenByGroupByFirstAnyAllSumAverageMinMaxCountTakeSkipTakeWhileSkipWhileDistinctReverseConcatZipUnionIntersectExceptSequenceEqualToArrayToListToDictionaryRangeRepeat,以及它们的链式组合。传给 LINQ 以及 List<T>Array 辅助方法的 lambda 会在调用处内联展开。

委托包括 ActionFunc 字段、Action done = Cleanup; 这样的方法组、多播的 +=-=、C# 的 event 声明,以及指向另一个 behaviour 上 public 方法的委托。lambda 可以作为值使用;会捕获变量的 lambda 有一条限制,下文会说明。字符串有 FormatSplitJoinSubstringReplaceTrimPadLeft。可空值类型有 HasValueGetValueOrDefault。枚举有 ParseTryParseGetNamesGetValuesHasFlagtypeof(T).Name 也能编译。throw 会把消息写入日志并停止该 behaviour,因此可以当作断言使用;没有什么可以捕获。直接递归和相互递归都能编译,调用前后方法的变量会被保存到栈上。

Unity 与 VRChat

组件查找 InstantiateDestroy Transform 和物理 Mathf 和值类型 RandomTime 协程 Invoke 系列 asyncawait VRCPlayerApi Networking 拾取物和座椅 UdonBehaviour 引用 其他 behaviour 的 Unity 成员 Unity 和 SDK 类型的数组 Unity 对象类型之间的转换

组件查找指 GetComponent<T>() 及其变体、非泛型的 GetComponent(typeof(T))TryGetComponent<T>(out T),以及 SetActive。值类型指 Vector3QuaternionColor 等常见的 Unity 结构体。协程可以使用 yield return nullWaitForSecondsWaitUntil 和嵌套协程,用 StartCoroutine(Run())StartCoroutine(nameof(Run)) 启动,用 StopCoroutine(handle)StopAllCoroutines 停止。InvokeInvokeRepeatingCancelInvoke 可以用在 public 方法上。async 方法可以返回 voidTaskUniTaskUniTaskVoid,可以 await Task.Delay(...),也可以等待其他 async 方法。UdonBehaviour 引用带有 SendCustomEventSetProgramVariableGetProgramVariable<T>。另一个 behaviour 的 gameObjecttransformenabled 可以直接读写。Unity 类型、组件和 SDK 枚举的数组和其他字段一样使用。网络一页中的全部内容。

写入着色器和材质

VRCShader 全局值 material.SetFloat MaterialPropertyBlock EnableKeyword

UnityEngine.Shader 没有开放。全局值要通过 VRCShader.PropertyToID(name)VRCShader.SetGlobalFloatSetGlobalVectorSetGlobalColorSetGlobalMatrixSetGlobalTexture,以及它们各自的数组版本来写。没有 SetGlobalInt,因为 SDK 里就没有。

全局值的名字必须以 _Udon 开头。 不是这样的名字,VRChat 会忽略它,而且是悄悄忽略:调用成功, 着色器保持原来的值,日志里什么也没有。全局值不是材质属性,所以在着色器里要写在 Properties 块之外。

材质属性则很普通:按名字或按 id 的 SetFloatSetColorSetVectorGetColor, 以及 MaterialPropertyBlockEnableKeyword。请用 sharedMaterial 而不是 material—— 碰 material 会在运行时复制一份材质,而运行时材质除非登记在场景描述符上,否则会被从构建里剥掉。

2026-09-08 已在上传的世界里确认过两条路径,而不只是能编译。

Udon 永远无法运行的部分

这些会被 UDN0011 拒绝。提示信息会说明原因,并给出替代写法。

写法 原因,以及应该改写成什么
catch Udon 没有可以捕获的异常:出错时 behaviour 会直接停止。去掉 catch,改为先判断条件。tryfinally 可以编译,throw 会记录消息并停止该 behaviour。
可变的 static 字段 每个 behaviour 都有自己的堆,所以静态字段会变成「每个 behaviour 一份」,而不是共享的一份。请把它放在某一个 behaviour 的字段上再引用它,或者用一个没有存储的只读 static 属性(见单例)。
AwakeResetOnValidateOnGUIOnDrawGizmosOnApplicationQuitOnApplicationPauseOnApplicationFocusOnAudioFilterRead Udon 不会派发它们。Awake 里的工作请移到 Start;其余的要么只在编辑器中存在,要么属于应用程序而不是世界,而 OnApplicationQuit 想表达的通常是 OnPlayerLeft
CompareTaggameObject.tagFindGameObjectWithTag Udon 不暴露 tag。请比较层级或名称、直接持有引用,或使用 GameObject.Find
Camera.main 未暴露。请在 Inspector 里把摄像机赋给一个字段。
SendMessageIsInvoking 以及其他 Udon 没有的 MonoBehaviour 成员 协程、InvokeCancelInvoke 由 Udonite 转换实现;其余的没有能在世界里运行的形式。
Application.isPlayingisFocusedtargetFrameRateplatformTime.timeScaleAudioListener 未暴露。世界始终以实时速度运行。用 Networking.LocalPlayer.IsUserInVR() 区分 VR 与桌面,并逐个设置 AudioSource 而不是监听器。
PlayerPrefsResources.LoadSceneManagerUnityEventShaderInput.mousePositionnew GameObject()GameObject.CreatePrimitiveMesh.CombineMeshesTMP_Text.SetText 每条拒绝信息都会给出 VRChat 的做法:持久化用 PlayerData,资源用 Inspector 字段,场景加载改为 VRCPlayerApi.TeleportToUnityEvent 改为在 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。只要说清楚是哪种写法,通常一天就能补上。

出了点问题 重新加载