理解章 C:代码是怎么跑起来的
第 10 到 12 章学了写脚本的语法和一组让人头疼的限制。这一章不教新写法,回头把那条没说清的链条补上:写下的那段字,到底怎么变成屏幕上灯亮的那一帧画面。
理解这条链条之后,前面那些「为什么不能这么写」「为什么这条 API 调不到」的问题会自然有答案。
一段脚本要走过的几道关
Section titled “一段脚本要走过的几道关”设想最简单的一段脚本:
public override void Interact(){ targetLight.enabled = true;}从写下这几行字,到玩家在 VRChat 里按下按钮看见灯亮,中间这段路实际有四五道关。一道一道走一遍。
[ 1. 写在 Unity 里的 .cs 文件 ] │ │ UdonSharp 编译器 ▼[ 2. Udon Assembly 字节码 + ProgramAsset ] │ │ Unity 打包过程把它塞进场景资源里 ▼[ 3. .vrcw 世界包 ] │ │ 上传到 VRChat CDN,玩家客户端下载 ▼[ 4. Udon 虚拟机在玩家电脑上加载 ] │ │ 运行时按帧执行 ▼[ 5. Udon VM 调用 Unity 引擎 API ] │ │ Unity 负责真正改 Light 状态 ▼[ 6. 屏幕上灯亮了 ]每一步都不是凭空过的。下面把第 1 到第 5 步分别看清。
第 1 步:写下来的字
Section titled “第 1 步:写下来的字”最开始的那段 C# 是给人读的。public override void Interact() 这一行能让其他作者看懂这段代码在做什么、什么时候会被执行、它属于哪个类。
但电脑读不懂这堆字。每个英文单词对它来说只是字符序列。要让它跑起来,必须先翻译成它认识的形式。
第 2 步:UdonSharp 编译器把它翻译成字节码
Section titled “第 2 步:UdonSharp 编译器把它翻译成字节码”打开 Unity 编辑器、导入 UdonSharp 包之后,每次保存 .cs 文件,UdonSharp 编译器会自动跑一遍。它做的事大致是:
- 解析:把那段字按 C# 语法规则切成树形结构,识别出哪是类、哪是方法、哪是赋值。
- 检查:用到的每个名字(变量、方法、类型)能不能在白名单里找到?方法签名对不对?类型匹不匹配?
- 生成:把检查通过的代码翻译成一段 Udon Assembly。
Udon Assembly 是 VRChat 自己设计的字节码格式。它的指令是「把变量 A 的值压栈」「调用某个 API」「跳到第 17 行」这种非常基础的动作,每条都很简单。一行 C# 通常对应几条到十几条 Udon Assembly。
编译产物会被存进 Unity 项目里的一个特殊资产文件,叫 UdonProgramAsset(图标是个紫色小卷轴)。每个 UdonSharpBehaviour 类对应一个 ProgramAsset。
第 3 步:打包进世界
Section titled “第 3 步:打包进世界”按下 Build & Test 或 Build & Publish 时,Unity 会把整个场景(GameObject、组件、材质、贴图、模型、音频、Udon 程序集)打包成一个 .vrcw 文件。
这个文件本质上是 Unity 的 AssetBundle 加上 VRChat 的元数据。它带着场景里每个物体的位置、属性、材质引用,以及每个 UdonSharpBehaviour 对应的 Udon 字节码。
Build & Test 模式下,这个文件被存到玩家本机的 VRChat 缓存目录,启动 VRChat 之后能在「最近上传的世界」里看到。Build & Publish 模式会把它发到 VRChat 的 CDN,玩家通过世界搜索就能找到。
到这一步,写下的那段 C# 已经被压缩进了一个二进制包里。
第 4 步:玩家客户端加载
Section titled “第 4 步:玩家客户端加载”玩家在 VRChat 里进入这个世界时,客户端做的事大致是:
- 从 CDN 下载
.vrcw(如果本地缓存没有)。 - 让 Unity 加载里面的场景资源,按预设的位置摆好所有 GameObject。
- 给每个挂了 UdonSharpBehaviour 的 GameObject 启动一个 Udon 虚拟机实例,把对应的 Udon 字节码装进去。
- 调用每个虚拟机里的
Start()方法(如果定义了的话)。
到这一步,世界出现在玩家眼前。每个挂了脚本的物体身边,都有一个小型虚拟机在等下一条指令。
第 5 步:Udon VM 运行字节码
Section titled “第 5 步:Udon VM 运行字节码”玩家走过来按 Use 键。Unity 检测到交互发生,VRChat 的接入层告诉对应的 Udon 虚拟机:「调一下你那段叫 Interact 的代码。」
虚拟机找到 Interact 方法在字节码里的入口,开始一条条执行:
LOAD_VARIABLE targetLightGET_PROPERTY Light.enabledPUSH_BOOL trueCOPY (顶部值放回 enabled)RETURN执行到「调用 Light.enabled = true」这一条时,虚拟机翻出白名单,确认这是允许的调用,然后真的去 Unity 引擎那边把那盏灯的 enabled 设为 true。下一帧 Unity 渲染场景时,那盏灯就亮了。
整个过程中,每一条字节码 Udon VM 都比对一次白名单,确认可以执行才执行。这是沙箱真正在发挥作用的地方。
几个心智上的换算
Section titled “几个心智上的换算”理解了这条链路,几件事会更容易对上号。
为什么 Update 里写复杂逻辑会让世界卡。 Update 一秒会被调几十到上百次(桌面端约 60 次/秒,VR 模式约 90 次/秒),每帧虚拟机都要把整段字节码再走一遍。每条指令比直接 C# 慢很多(按 VRChat 官方的量级估算,是 200 到 1000 倍那一档),循环里写得越复杂,越容易把帧率拖下来。需要在 Update 里反复用的值,能缓存到字段就缓存。
为什么 LINQ、async、reflection 这些用不了。 它们要么依赖白名单外的 API,要么生成的字节码模式 UdonSharp 编译器还没支持。沙箱和编译器同时在拦。
为什么 public 字段能在 Inspector 里调。 字段值会被 Unity 序列化到场景文件里,而不是写死在字节码里。打包成 .vrcw 时一起带过去,虚拟机启动时把场景里存的值塞回字段。Inspector 里改这些值,是在改场景文件,下次加载时虚拟机会拿到新的值。
为什么 [UdonSynced] 字段能在多人之间同步。 这是 Udon 虚拟机的另一项扩展能力。后面第六部会展开。
把链路压缩成一句话
Section titled “把链路压缩成一句话”整段链路压缩到一句话:
写的 C# 被翻译成 Udon 字节码,被 VRChat 客户端里的 Udon 虚拟机一条条执行,必要时由虚拟机调 Unity API 改场景。
再看自己写过的 targetLight.enabled = true,可以从这行字追到 Inspector 字段、编译、虚拟机执行、Unity 把灯点亮。