14. 走进去,事情发生了
第 13 章那个开关挺好用:走过去,光标对准,按一下。但有些事情按按钮反而别扭。走进咖啡角,背景音乐自然响起;走出去,音乐淡下来。这里没有按钮可以按,玩家也没有动作要做。
事情该自己发生,靠玩家踏进了一片「区域」。
VRChat 里这一类被动触发依赖一种特别的 Collider:勾选了 Is Trigger 之后,它不再挡人,反过来变成一种检测装置。玩家穿过去时,Udon 脚本会收到一个事件通知。
这一章用它做一件事:在房间一角划一块「音乐区」,进去音乐响,出去音乐停。
两种常见做法(别混)
Section titled “两种常见做法(别混)”| 做法 | 典型设置 | 体验 |
|---|---|---|
| 全局 BGM | 场景里一个 AudioSource,Play On Awake 开,Spatial Blend = 0 | 进世界就开始响,全场音量差不多 |
| 区域触发(本章) | Trigger + OnPlayerTriggerEnter,AudioSource 放在区域内,由脚本 Play() / Stop() | 跨过区域边界才响,离开才停 |
很多公开世界用第一种。本章教第二种:Trigger 事件 + 局部声源。若目标是「一进地图就播、不分区」,不必用 Trigger,直接全局 AudioSource 即可。
Trigger 跟普通 Collider 的区别
Section titled “Trigger 跟普通 Collider 的区别”第 7 章用过普通 Collider:Floor 上挂一个 Box Collider,玩家踩上去停在表面。这是它的「实心」版。
Box Collider 有一个字段叫 Is Trigger。打勾之后,它的物理含义翻了个:
- 普通 Collider:物理实体。玩家撞上去会被挡住,球丢过去会反弹。
- Trigger:物理透明的检测体。玩家直接穿过去,但穿过的瞬间和离开的瞬间,挂在同一个物体上的脚本会收到事件通知。
形状还是那个形状,差别在「挡不挡人 / 通不通知」。一句话记忆:普通 Collider 管「碰得到」,Trigger 管「知道有人来过」。
第一步:在房间角落划一块音乐区
Section titled “第一步:在房间角落划一块音乐区”打开第二部那间小屋。Hierarchy 空白处右键 Create Empty,命名 MusicZone。位置摆在房间一角,比如:
Position 2.5, 1.0, 2.5Rotation 0, 0, 0Scale 1, 1, 1MusicZone 是一个空 GameObject,自身没有形状,得手工给它加一个 Collider。
选中 MusicZone,Inspector → Add Component → 搜 Box Collider,加上。
然后做关键的一步:勾上 Box Collider 上的 Is Trigger。
Box Collider 的 Size 调成 3, 2.2, 3,意思是一块 3 米见方、2.2 米高、能罩住玩家头顶的长方体区域。Scene 视图里会看到一圈绿色线框,那就是这块「音乐区」的边界。线框只有作者看得见,玩家进游戏后看不到。
Is Trigger 没勾时,这块 Collider 会像墙一样挡住玩家,OnPlayerTriggerEnter 也不会触发。事件不触发时,排查清单里先查这一项。
第二步:放一段背景音乐
Section titled “第二步:放一段背景音乐”MusicZone 旁边放一个 Audio Source 当声源。
可以选两条路:直接给 MusicZone 自己加 Audio Source,或者新建一个子物体专门放声音。这一章选简单的那条:
选中 MusicZone,Inspector → Add Component → 搜 Audio Source,加上。
这个组件刚加上是哑的。要给它一段音频。从手头的素材库找一段循环友好的环境音(咖啡馆白噪、雨声、轻爵士都行;时长 30 秒以上就够),拖进 Project 面板的 Assets/Audio/ 目录下(没这个目录就新建一个)。
把 Project 面板里的音频文件拖到 Inspector 上 Audio Source 的 AudioClip 字段。
下面这几个字段调一下:
AudioClip MyCafeLoop(拖入的音频文件)Play On Awake 勾选关闭Loop 勾选开启Volume 0.6Spatial Blend 1(完全 3D,离得远会变小)Play On Awake 关掉,意思是场景一加载不要立刻播。要等脚本说「现在播」才播。Loop 开着,让 30 秒的循环音不至于断成一截。
Spatial Blend 设为 1 时,声源在 MusicZone 物体位置,离声源远则小、近则大。若希望进区域后全场音量差不多(更像「跨进咖啡厅门槛就响 BGM」),把 Spatial Blend 改成 0,Trigger 仍负责何时 Play() / Stop(),只是声场不再随距离衰减。
第三步:写一段触发脚本
Section titled “第三步:写一段触发脚本”Assets/Scripts/ 下新建一个 U# Script,命名 MusicZone。
using UdonSharp;using UnityEngine;using VRC.SDKBase;
public class MusicZone : UdonSharpBehaviour{ public AudioSource musicSource;
public override void OnPlayerTriggerEnter(VRCPlayerApi player) { if (!player.isLocal) return;
if (musicSource != null && !musicSource.isPlaying) { musicSource.Play(); } }
public override void OnPlayerTriggerExit(VRCPlayerApi player) { if (!player.isLocal) return;
if (musicSource != null && musicSource.isPlaying) { musicSource.Stop(); } }}不到二十行,但比第 13 章那段稍微复杂一点。一个一个看:
using VRC.SDKBase;引入VRCPlayerApi,第三行的player参数就是这个类型。OnPlayerTriggerEnter(VRCPlayerApi player)是 VRChat 玩家进入 Trigger 时的事件。player形参代表那个进区域的人。if (!player.isLocal) return;这一行是关键。下面专门讲。musicSource != null是基本的判空保护。第 12 章讲过 UdonSharp 没有try-catch,引用为空会让事件在那一行中断。Play() / Stop()是 UnityAudioSource自带的方法,按字面意思工作。OnPlayerTriggerExit跟Enter镜像,玩家离开时调用。
挂脚本:把 MusicZone.cs 拖到 Hierarchy 里 MusicZone 物体上。Inspector 里出现 Music Source 字段(Audio Source 类型的引用槽),把同一个 MusicZone 物体上的 Audio Source 组件拖进去(直接拖物体本身也行,Unity 会自动抓出 AudioSource 组件)。
为什么要写 if (!player.isLocal) return;
Section titled “为什么要写 if (!player.isLocal) return;”OnPlayerTriggerEnter 这一类玩家事件有一个跟 Interact 不一样的特点:实例里只要有玩家进入 Trigger,所有人客户端上挂着这个脚本的物体都会被通知一次。
这跟 Interact 完全本地的行为不一样。如果不加 isLocal 判断,会发生这种事:
- A 走进音乐区。A 客户端调一次
OnPlayerTriggerEnter,参数是 A → 调Play(),A 听到音乐。 - 同一时刻 B 客户端也调一次
OnPlayerTriggerEnter,参数还是 A(因为 A 进区域了)→ B 也调了Play()。可 B 根本没进区域。 - B 听到音乐了。
加上 if (!player.isLocal) return; 之后,每个客户端只在「进区域的那个人就是自己」时才执行后面的逻辑。各人触发各人音乐,互不打扰。
桌面玩家自己测的时候没区别,但只要拉朋友进来就会立刻看出 isLocal 的重要。这条 if 看起来啰嗦,但漏了的话第二天会撞上「我朋友走进我的房间,我这边音乐自己响了」这种诡异现象。
第四步:Build & Test
Section titled “第四步:Build & Test”Build & Test,进世界。
走向 MusicZone 那块隐形区域,跨过线框那一刻,音乐响起。从远处走近时声音会从小到大,因为 Spatial Blend = 1 让 Audio Source 是 3D 衰减的。
走出去,音乐停。再走进,再响。
如果跑一圈下来音乐没响:
- 看 Console 有没有红字。
NullReferenceException大概率是Music Source引用槽是None。回 Inspector 拖一下。 - 没有红字也没声音 → 检查
Audio Source的AudioClip是不是None,或者Volume是不是被拉到 0。 - 走进区域听不到声音、走得很近才有 →
Spatial Blend = 1加上 Audio Source 的Min Distance / Max Distance太小。可以把Min Distance调到 1.5、Max Distance调到 8 试试。
不只能播音乐:进区域可以做任何事
Section titled “不只能播音乐:进区域可以做任何事”MusicZone 只是把 Trigger 和 AudioSource.Play() 接了起来。换上别的字段,OnPlayerTriggerEnter 里同样的几行结构能做完全不同的事:
public Light areaLight;public override void OnPlayerTriggerEnter(VRCPlayerApi player){ if (!player.isLocal) return; areaLight.enabled = true; // 进入区域才点亮}public GameObject hint;public override void OnPlayerTriggerEnter(VRCPlayerApi player){ if (!player.isLocal) return; hint.SetActive(true); // 进入区域弹出 UI 提示}public Transform respawnPoint;public override void OnPlayerTriggerEnter(VRCPlayerApi player){ if (!player.isLocal) return; Networking.LocalPlayer.TeleportTo(respawnPoint.position, respawnPoint.rotation); // 掉进虚空 → 自动传回安全点。第 16 章会专门讲 TeleportTo}骨架是同一份:玩家进入 → 检查是不是本地玩家 → 改一个状态 / 调一个方法。
OnPlayerTriggerStay 和 OnPlayerCollisionEnter
Section titled “OnPlayerTriggerStay 和 OnPlayerCollisionEnter”VRChat 还有几个相关的玩家事件,知道有就行,先不展开:
OnPlayerTriggerEnter 玩家进入 Trigger 的瞬间OnPlayerTriggerExit 玩家离开 Trigger 的瞬间OnPlayerTriggerStay 玩家停留在 Trigger 内时,每帧调一次OnPlayerCollisionEnter 玩家撞到普通 Collider(非 Trigger)瞬间OnPlayerCollisionExit 玩家与普通 Collider 分离瞬间OnPlayerCollisionStay 玩家与普通 Collider 接触期间,每帧调一次Stay 系列每帧调一次,性能开销显著高于 Enter / Exit。能用 Enter 和 Exit 解决就别用 Stay。比如「在区域内每秒扣 1 点血」用 Stay + 计时变量比直接每帧扣一次合理得多,第 12 章讲性能那一节展开过类似的取舍。
普通物体(不是玩家)穿过 Trigger 的事件叫 OnTriggerEnter / OnTriggerExit(Unity 标准事件,没有 Player 前缀),常用在「球进了得分框 / 钥匙碰到锁」这类场景。这一章按下不表,第 17 章做门时会再碰一面。
自己改三个版本
Section titled “自己改三个版本”把 MusicZone 当骨架,往里加东西:
- 加一个
public Light areaLight字段,进区域音乐响的同时把灯也点亮,离开同时灭。两个动作写在同一对 Enter/Exit 里就行。 - 把 Trigger 区域的 Box Collider 换成 Sphere Collider,做一个圆形声场,看看边界形状对体验的影响。
- 加一个
int playersInside字段,Enter时自加、Exit时自减,进 Build & Test 站进站出,在 Inspector 上观察这个数字怎么变。再思考:如果不写if (!player.isLocal) return;,这个数字会变成什么样?
一段被动交互的骨架
Section titled “一段被动交互的骨架”回头看,整个 Trigger 链条同样四件东西:
- 一个 GameObject + 一个
Box Collider(勾选Is Trigger),决定区域形状。 - 一段 UdonSharp,里头有
OnPlayerTriggerEnter/OnPlayerTriggerExit,加if (!player.isLocal) return;过滤。 - 通过
public字段引用到的目标对象(这一章是 AudioSource)。 - 一个
Play() / Stop() / SetActive()之类的具体动作。
跟第 13 章 Interact 的骨架几乎是同一套。差别只在事件名换了,触发方式从主动按变成被动撞。