11. 刚好够用的 C#
第 10 章把变量、判断、循环、函数、类用做菜的方式过了一遍。这一章把那些朴素思路落到 C# 的真实写法上。
C# 整套语言能写到非常深,但 VRChat 世界用得上的语法只是其中一小块。这一章只讲那一小块。学得太多反而绕路:第 12 章会指出哪些 C# 特性 UdonSharp 不支持,提前学了也用不上。
一段最小脚本长什么样
Section titled “一段最小脚本长什么样”VRChat 世界里的脚本几乎都是这个骨架:
using UdonSharp;using UnityEngine;
public class LampToggle : UdonSharpBehaviour{ public Light targetLight;
public override void Interact() { targetLight.enabled = !targetLight.enabled; }}这段不到十行的脚本就能让按钮控制一盏灯。先不深究每一行,整体认识一下:
using ...在文件顶上引一些工具,类似菜谱开头写「需要烤箱、电饭锅」。public class LampToggle : UdonSharpBehaviour定义了一个类,名字叫LampToggle。冒号后面的UdonSharpBehaviour表示这个类要装到 GameObject 上当行为脚本。public Light targetLight;声明了一个变量。Light是类型,targetLight是名字。public让这个变量在 Unity Inspector 里可见,可以用拖拽的方式指过去。Interact()是一个方法,名字是 VRChat 规定好的。玩家走过来按 Use 键时,VRChat 会替作者调用这个方法。- 方法体里那行
targetLight.enabled = !targetLight.enabled;把灯的开关状态翻转。
短短一段 C#,已经把第 10 章谈到的东西全用上了:类、变量、方法、判断(! 是「取反」)。下面把每一块拆开。
三件 C# 的语法基本功
Section titled “三件 C# 的语法基本功”在拆变量、控制流之前,先把三件最朴素的语法物理过一遍。如果这三件没扫过,一些莫名其妙的红字会把人卡住。
分号 ; 结束一条语句。int score = 0; 末尾那个分号是必须的,少一个就报错。同一段代码里换行不重要,分号才决定一条语句到哪里结束。
大括号 { ... } 圈出一组语句。类的内容、方法的内容、if 块的内容、for 块的内容,都用一对大括号括起来。括号必须成对,少一个右括号编译器会一路报错到文件末尾。Unity 里写代码时编辑器会自动配对,大概率不会漏。
注释 // 和 /* ... */。// 后面到行尾的内容是给人看的、编译器忽略:
int score = 0; // 玩家分数,每分一格// 整行只是说明文字,不会被执行/* ... */ 用来注释多行:
/* 这一段先放着不跑,调试时再开 targetLight.enabled = false;*/调试时常用注释临时屏蔽一两行代码,比删掉再写回去稳。
C# 每个变量都得有个类型。这跟 Python、JavaScript 这种「动态类型」不一样,在 C# 里写变量时必须告诉编译器它是什么。
VRChat 世界里高频出现的几种类型:
| 类型 | 装什么 | 例子 |
|---|---|---|
int | 整数 | int score = 0; |
float | 小数(带小数点) | float speed = 1.5f; |
bool | 真或假 | bool isOpen = false; |
string | 一段文字 | string playerName = "alice"; |
Vector3 | 三维坐标 | Vector3 spawnPoint = new Vector3(0, 0.2f, 0); |
GameObject | 一个 Unity 物体 | public GameObject door; |
Light | 一盏灯组件 | public Light mainLight; |
Transform | 物体的位置朝向 | public Transform target; |
Material | 一份材质 | public Material redMat; |
写 float 数值时末尾要加 f,比如 1.5f。不加的话编译器会按 double(更精确的小数类型)解读,赋值给 float 变量时会报错。
声明变量的语法非常稳定:
类型 名字 = 初始值;初始值可以省略,省略后基本类型会拿到默认值(数字是 0,bool 是 false,引用类型是 null)。null 这个值后面会反复碰到,它表示「这个引用还没指向任何东西」。
C# 项目通行的命名约定:类名、方法名、public 字段用大驼峰(PlayerHealth、OpenDoor),private 字段和局部变量用小驼峰(isOpen、currentScore)。本书里的代码统一这套写法。约定不是硬性规则,但跟主流不一致的命名会让其他读者迷惑,也容易跟 Unity API 的命名风格打架。
字段、属性和局部变量
Section titled “字段、属性和局部变量”类里写在大括号最外层的变量叫字段(field),方法里写在大括号内层的变量叫局部变量(local variable)。
public class Counter : UdonSharpBehaviour{ public int score; // 字段:整个类都能访问 private int internalState; // 字段:但外部访问不到
public override void Interact() { int delta = 1; // 局部变量:只在这个方法里有效 score = score + delta; }}字段的访问修饰符常见三种:
public:外部类可以访问,Unity Inspector 也会显示。private:只有自己类内部能访问,Inspector 默认看不到。[SerializeField] private:外部类访问不到,但 Inspector 能看见。VRChat 项目里推荐这种写法,既不污染外部接口,又能在 Inspector 里调参数。
[SerializeField] private float openSpeed = 2f;C# 还有一种叫「属性」(property)的东西,长得像方法又像字段。VRChat 项目里很少自己写属性,用到的多是 Unity API 提供的,比如 transform.position。这一章先不展开。
控制流:if / else / for / while
Section titled “控制流:if / else / for / while”// if 单分支if (score >= 10){ door.SetActive(false); // 隐藏门}
// if / elseif (isDay){ sun.intensity = 1f;}else{ sun.intensity = 0.1f;}
// if / else if / elseif (score < 0){ label.text = "异常";}else if (score < 10){ label.text = "继续努力";}else{ label.text = "通关";}条件表达式里常用的比较符号:
| 写法 | 含义 |
|---|---|
== | 相等 |
!= | 不相等 |
< <= > >= | 小于、小于等于、大于、大于等于 |
&& | 且 |
|| | 或 |
! | 取反 |
注意「相等」是两个等号 ==,单个等号 = 是赋值。这是最常踩的笔误之一。
循环最常用的是 for:
for (int i = 0; i < 10; i++){ Debug.Log("打印第 " + i + " 次");}for 三段分别是:起始(int i = 0)、继续条件(i < 10)、每轮结束做的事(i++,即 i 加 1)。
while 用在「直到某个条件满足为止」:
while (door.transform.position.y < 3f){ door.transform.Translate(Vector3.up * 0.01f);}VRChat 世界写脚本时 while 用得比 for 少很多,多数情况下 for 配合数组就够。
方法的完整写法是这样:
返回类型 方法名(参数列表){ 方法体; return 返回值;}具体例子:
// 没有返回值,没有参数public void OpenDoor(){ door.SetActive(false);}
// 有参数,没有返回值public void SetScore(int newScore){ score = newScore; UpdateLabel();}
// 有参数,有返回值public int Add(int a, int b){ return a + b;}void 表示「没有返回值」。return 用来把结果交还给调用方,调用方可以接着用:
int total = Add(3, 5); // total 现在是 8VRChat 脚本里有几个特殊方法名是 Unity 和 VRChat 帮调的:
| 方法名 | 调用时机 |
|---|---|
Start() | 物体出现在场景里之后调用一次 |
Update() | 每一帧调用一次(桌面端约 60 次/秒,VR 模式约 90 次/秒) |
Interact() | 玩家按 Use 键交互时调用 |
OnPlayerJoined(VRCPlayerApi player) | 有玩家进入这个房间实例 |
OnPlayerLeft(VRCPlayerApi player) | 有玩家离开 |
第四部会反复用到这几个。这一章先认识到:作者不主动调用它们,是 VRChat 在合适的时机替作者调用。作者的工作是定义「被调用时该做什么」。
写法上有一个细分要点先打个伏笔:上面这五个里,Start 和 Update 是 Unity 标准生命周期方法,写定义时不写 override;Interact / OnPlayerJoined / OnPlayerLeft 是 UdonSharpBehaviour 自己定义的虚方法,必须写 public override。第 12 章会专门讲这条规则的来历。
把脚本说出来:Debug.Log
Section titled “把脚本说出来:Debug.Log”写脚本时最常用的「工具方法」是 Debug.Log。它把一段文字打到 Unity 编辑器的 Console 面板里:
public override void Interact(){ Debug.Log("按钮被按了"); score = score + 1; Debug.Log("现在分数是 " + score);}按下 Use 键之后,Unity 顶上的菜单 Window → General → Console 打开的那个面板里会看到刚才打出来的字。这是「程序到底跑没跑、跑到哪一步、变量是什么值」最有效的检查手段。
不知道脚本有没有挂上 → Start 里写一句 Debug.Log("我活了")不知道按钮按了没 → Interact 里写一句 Debug.Log("被按了")不知道变量是什么值 → 在那一行前后写 Debug.Log("score = " + score)这一招在做完第四部里几个练习之前不会显眼,撞上第一个「按钮按下去没反应」的问题时立刻有用。
Unity 在每次保存 .cs 文件时都会重新编译一次,编译错误第一时间出现在 Console 里。改几行保存一次和写完一大段再保存,错误暴露的时机会差很多。
类是 C# 组织代码的基本单位。VRChat 世界里每个 .cs 文件通常装一个类,类继承自 UdonSharpBehaviour:
using UdonSharp;using UnityEngine;
public class DoorController : UdonSharpBehaviour{ [SerializeField] private float openHeight = 3f; [SerializeField] private float closeHeight = 0f; [SerializeField] private float speed = 2f;
private bool isOpening;
public override void Interact() { isOpening = !isOpening; }
private void Update() { float targetY = isOpening ? openHeight : closeHeight; Vector3 pos = transform.position; pos.y = Mathf.MoveTowards(pos.y, targetY, speed * Time.deltaTime); transform.position = pos; }}这段脚本能做出一扇按 Use 键开关、开合带动画的门。它把上面讲过的东西全用了一遍:
- 字段(带
SerializeField):让作者在 Inspector 里调参数。 Interact()方法:响应玩家按 Use 键。Update()方法:每帧推动门朝目标高度移动。- 三元表达式
isOpening ? openHeight : closeHeight:等同于if (isOpening) ... else ...的简写。 - 调用 Unity 提供的 API:
Mathf.MoveTowards、Time.deltaTime、transform.position。
读不懂每一行没关系,第四部第 17 章会专门做一扇能开的门。这里先看到「类长这样、字段长这样、方法长这样」就够了。
引用其他物体
Section titled “引用其他物体”VRChat 脚本里高频要做一件事:让脚本控制别的物体。比如「按这个按钮,开那个门」,按钮和门是两个不同的 GameObject。
最稳的做法是定义一个 public 字段或 [SerializeField] private 字段,然后在 Unity Inspector 里把目标物体拖进去:
public class ButtonOpensDoor : UdonSharpBehaviour{ [SerializeField] private GameObject targetDoor;
public override void Interact() { targetDoor.SetActive(false); }}挂上脚本后 Inspector 会出现一个 Target Door 槽位,把场景里的门拖进去就行。
按钮(挂 ButtonOpensDoor 脚本) └─ Target Door 槽 ────► 门 GameObject这种「在 Inspector 里连线」的工作流是 Unity 风格脚本最舒服的部分。它免去在代码里写一长串 GameObject.Find("..."),也不会因为名字改了就找不到。
这一章学到的东西
Section titled “这一章学到的东西”只列学过的,方便回忆:
语法物理 ; 结束语句, { } 圈块, // 注释变量声明 类型 名字 = 初始值;访问修饰符 public / private / [SerializeField] private基本类型 int / float / bool / stringUnity 类型 Vector3 / GameObject / Transform / Light / Material控制流 if / else if / else / for / while方法 public void 方法名() { ... }特殊方法 Start / Update / Interact / OnPlayerJoined / OnPlayerLeft类 public class XXX : UdonSharpBehaviour { ... }引用别的物体 在字段里声明,Inspector 里拖调试 Debug.Log("..."),看 Unity Console这些就是 VRChat 世界写脚本的工具盘。第四部用到的所有交互,几乎都是把这些拼一拼。
C# 这门语言比这要大得多,还有泛型、异步、反射、属性、委托、事件、LINQ 这些。它们大多在 UdonSharp 里要么不能用、要么有奇怪的限制。这是写 VRChat 脚本和写普通 C# 项目最明显的分叉。理解到这里,C# 这套语法的最小可用集就齐了。