跳转到内容

16. 坐下来,照镜子,走过去

约 50 分钟 难度:新手 动手章

走进任何一个 VRChat 休闲世界,几乎都会找到这三件东西:一把能坐下歇歇腿的椅子、一面给玩家照自己 avatar 的镜子、一道按一下或走过去就能跳到下一片区域的传送门。三件东西单看都不大,凑齐之后房间从「能站着看的空间」升级成「能停下来、能看到自己、能换地方」的房间。

这一章把三个配件一次做完,每个都用最薄的一层逻辑。前几章的事件骨架还在,这次把它们接到 VRChat 三个不同的内置组件上。

打开第二部那间小屋。这一章会在场景里加:

  • 房间一角放一把椅子(一个 Cube 就够)。
  • 一面墙边立一块镜子(一个 Quad)。
  • 一个传送区域(隐形 Trigger Collider)和一个传送目的点(空 GameObject)。

为了少在 Hierarchy 里翻找,建议先建一个空 GameObject 当容器:

Hierarchy
├─ ...(已有的物体)
└─ Fixtures (新建的空 GameObject,Position 0,0,0)
├─ Chair
├─ MirrorRoot
└─ Teleporter

Fixtures 用来装这一章的三个配件,方便整体启用 / 禁用。Hierarchy 的整理对后期项目找东西非常关键,第 8 章铺过的 Assets/ 目录建议跟这边的 Hierarchy 容器思路是一致的。

第一节:用 VRCStation 做一把能坐的椅子

Section titled “第一节:用 VRCStation 做一把能坐的椅子”

VRCStation 是 VRChat 让玩家「被固定在某个位置和姿势」的官方组件。底下的载具、电影院座位、餐桌椅子,都是它。

Fixtures 下右键 Create Empty,命名 Chair。Position 摆到房间一角,比如 2.5, 0, -2.5

Chair 下右键 3D ObjectCube,命名 Chair_Seat,Transform:

Position 0, 0.45, 0
Rotation 0, 0, 0
Scale 0.6, 0.05, 0.6

得到一块 60 厘米见方、5 厘米厚、悬在地面 45 厘米高的薄板。这就是椅面。要做得更像椅子,再加一块靠背 Cube,形状自由。

选中 Chair(不是 Chair_Seat),Add Component → 搜 VRC Station,加上。

Inspector 里 VRC Station 的字段:

Player Mobility Immobilize # 坐着不能再走
# (选项:Mobile / Immobilize / Immobilize For Vehicle)
Can Use Station From Station False # 已经坐在另一把上时还能不能切到这把
Disable Station Exit False # 玩家可以自行起身
Animation Controller None # 自定义坐姿动画的 Override Controller,这一章用不到
Station Enter Player Location None # 待会儿拖一个 Transform 进来
Station Exit Player Location None
Seated True # 应用坐姿动画

Seated 勾上之后 VRChat 会用坐姿姿势放玩家。要做的是「立着的传送阵」那种 Station 时才把它关掉。

接下来定位「玩家坐下后落点在哪里、面朝哪边」。Chair 下右键 Create Empty,命名 EnterPoint。Transform:

Position 0, 0.40, 0 # 椅面之上一点点(玩家 root,默认在脚下)
Rotation 0, 0, 0 # 蓝色 Z 轴指向玩家面朝的方向
Scale 1, 1, 1

把 Hierarchy 里的 EnterPoint 拖到 VRC StationStation Enter Player Location 字段。Station Exit Player Location 留空,VRChat 会让玩家在 Enter Point 附近起身。

给椅面加 Collider 和 Interaction Text

Section titled “给椅面加 Collider 和 Interaction Text”

玩家走到椅子前要能瞄准椅面。Chair_Seat 自带 Box Collider,保留即可。

但 VRCStation 默认不会把椅面的 Collider 当成「可点击区域」。它需要的是 VRC Station 同一物体上有 Collider。

最稳的做法是把 Chair_Seat 上的 Box Collider 复制配置过去:

  1. 选中 ChairAdd ComponentBox Collider
  2. Center 设为 0, 0.45, 0Size 设为 0.6, 0.05, 0.6,跟 Chair_Seat 上的盒子重合。
  3. 保留 Chair_Seat 自身的 Collider 不删,那一份继续负责物理碰撞(玩家撞到椅面会停)。

VRC Station 下面会有一个隐藏的 Interaction Text 字段(从 SDK 内部拿,不在 Inspector 顶部显示)。装上 VRC Station 时如果出现一个 Udon Behaviour 占位,里面也有 Interaction Text。把它填上:

Sit

Build & Test 进世界。走到椅子前,光标对准椅面,头顶提示 Sit。Interact 一次,玩家被放进椅面上方,坐姿生效,视角也固定。再 Interact 一次起身。

如果坐下去之后脚穿透椅面,回 Unity 把 EnterPoint 的 Y 拉高一点,多调几次直到看着自然。VR 里坐姿的视角高度比桌面下不一样,最终位置以拉朋友实际进 VR 测试为准。

VRCStation 提供两个事件让作者在玩家坐下 / 起身时做点事:

public override void OnStationEntered(VRCPlayerApi player) { ... }
public override void OnStationExited(VRCPlayerApi player) { ... }

跟第 14 章的玩家事件一样,这两个会在所有玩家客户端都触发,要按需求加 if (player.isLocal) return;if (!player.isLocal) return; 过滤。

一个常见小用法:玩家坐上去之后把这把椅子标记为「占用」,别人就别再点了。

ChairOccupancy.cs
using UdonSharp;
using UnityEngine;
using VRC.SDKBase;
public class ChairOccupancy : UdonSharpBehaviour
{
public override void OnStationEntered(VRCPlayerApi player)
{
// 所有人客户端都会执行:这把椅子在所有人眼里都失效
DisableInteractive = true;
}
public override void OnStationExited(VRCPlayerApi player)
{
DisableInteractive = false;
}
}

DisableInteractiveUdonSharpBehaviour 自带的字段,true 时这个脚本所在物体的 Interact / Sit 提示和点击都失效。这一段不需要每个客户端都同步状态。只要每个客户端各自把「看到那把椅子上有人」翻译成「我也别点」,这件事在视觉上就成立了。多人同步层面会不会出现「两个人同时点同一把椅子」的极端情况,第六部展开。

第二节:用 VRCMirrorReflection 做一面镜子

Section titled “第二节:用 VRCMirrorReflection 做一面镜子”

VRChat 玩家进世界喜欢做的第二件事就是找镜子。镜子的实现简单,但镜子的性能成本是房间里所有元素中最高的之一,做的时候要考虑 ON/OFF 切换和反射层级。

FixturesCreate Empty 命名 MirrorRoot,下面再 3D ObjectQuad,命名 Mirror。Quad 的好处:默认朝 Z+ 方向,不需要再旋转 -90 度,反射方向不会上下颠倒。

Mirror 摆到墙边,比如:

Position -3.5, 1.6, 0
Rotation 0, 90, 0 # 让镜面朝向房间中央
Scale 2, 2, 1 # 2 米宽、2 米高

具体角度看墙的位置调整。镜面的法线方向(Quad 的 +Z)要朝向房间,玩家站在房间里才能看到自己。

选中 MirrorAdd Component → 搜 VRC Mirror Reflection,加上。

Inspector 里几个核心字段:

m_DisablePixelLights False # true 关掉镜中点光,省性能
m_ReflectLayers Everything # 反射哪些层
Mirror Resolution Auto # Auto 跟随屏幕分辨率,最高 2048×2048
Custom Skybox None # 镜中天空盒可以单独设置

m_ReflectLayers 是这一章最该改的东西。默认值 Everything 会把场景所有层都渲一遍进镜子,相当于场景被画了两次。改成只反射 Player 层(角色和 avatar)已经够大多数「对镜整理 avatar」的需求,性能负担瞬间下降一大截。

镜子是 VRChat 公认最贵的对象之一。社区里几乎所有讲性能优化的文章都会强调:默认关闭,玩家想用时再开

做法很简单:取消勾选 Mirror 物体顶部的复选框(让它默认 inactive),再用一个 Interact 按钮控制开关。

Fixtures 下放一个小 Cube 当镜子开关,挂上一段几乎跟第 13 章一模一样的脚本:

MirrorSwitch.cs
using UdonSharp;
using UnityEngine;
public class MirrorSwitch : UdonSharpBehaviour
{
public GameObject mirrorObject;
public override void Interact()
{
if (mirrorObject == null) return;
mirrorObject.SetActive(!mirrorObject.activeSelf);
}
}

挂在小 Cube 上,把 Mirror 物体拖到 Mirror Object 槽。Interaction Text镜子开关Mirror

进世界后镜子默认是关的,墙上看到一块 Quad(带个默认材质)。走到 MirrorSwitch 前 Interact 一次,Quad 变成镜面,能看到自己 avatar。再 Interact 一次关掉。

如果开了之后镜面是黑的:

  • m_ReflectLayers 拉回 Everything 看是不是层级过滤过了。
  • Mirror Resolution 在 Quest 上不要拉到 Unlimited,会直接卡住。
  • Quad 的法线方向是不是朝向房间,旋转角度不对会让玩家站在镜子背面。

第三节:用 TeleportTo 做一个走过去就传送的门

Section titled “第三节:用 TeleportTo 做一个走过去就传送的门”

第 14 章用过 OnPlayerTriggerEnter。这一章把进区域的事件改成「传送玩家到另一个位置」。

摆两个点:起点的 Trigger 和终点的 Marker

Section titled “摆两个点:起点的 Trigger 和终点的 Marker”

FixturesCreate Empty 命名 Teleporter

Teleporter 下:

  1. Create Empty 命名 EntryZone。这是触发区。Add ComponentBox Collider,勾选 Is TriggerSize 设为 1.2, 2.2, 1.2,做一块走到门口大小的隐形区域。EntryZone 的 Position 摆在房间靠近「想离开」的位置,比如 0, 1.1, -3.8

  2. Create Empty 命名 Destination。这是目的点。Position 摆到想送玩家到的位置,比如二楼平台 0, 4.5, 5,Y 轴 Rotation 调成玩家落地后该面朝的方向(比如 0, 180, 0 让玩家面朝相反方向)。

如果还没有「二楼平台」这种地方,把 Destination 临时摆在房间另一头(同样的 Y 高度)就能测。

Assets/Scripts/ 下新建 Teleporter.cs

Teleporter.cs
using UdonSharp;
using UnityEngine;
using VRC.SDKBase;
public class Teleporter : UdonSharpBehaviour
{
public Transform destination;
public override void OnPlayerTriggerEnter(VRCPlayerApi player)
{
if (!player.isLocal) return;
if (destination == null) return;
Networking.LocalPlayer.TeleportTo(destination.position, destination.rotation);
}
}

挂到 EntryZone 上,把 Destination 物体拖到 Destination 字段。

逐行扫一眼:

  • if (!player.isLocal) return; 第 14 章讲过:玩家事件在所有客户端都被通知。只有「进区域的就是自己」时才执行传送,否则会出现「别人进区域,自己被传走」的诡异现象。
  • Networking.LocalPlayer 拿到当前客户端的本地玩家。TeleportToVRCPlayerApi 上的方法,第一个参数是目标位置(Vector3),第二个是目标朝向(Quaternion)。
  • 直接传 destination.positiondestination.rotation,等于「以这个 GameObject 的位姿为准」。

TeleportTo 还有一个三参数重载,第三个参数是 VRC_SceneDescriptor.SpawnOrientation,控制传送后玩家朝向的对齐方式。两参数版本够用。

进世界,走向 EntryZone 那块隐形区域,跨过线框那一刻视角瞬间切到 Destination。再走回来不会被反向传送(因为那块区域只在前一个位置,不在 Destination)。

要做「双向门」,复制一份 Teleporter、再做一个反向 EntryZone,目的点指回原来的位置。两个 Teleporter 各管各的方向。

OnPlayerTriggerEnter 换成 Interact,传送区域变成传送按钮:靠近 → 看到提示 → 按一下才传走。要想做「传送门只在玩家主动选择时才生效」,这个版本更合适:

public override void Interact()
{
if (destination == null) return;
Networking.LocalPlayer.TeleportTo(destination.position, destination.rotation);
}

注意:用 Interact 时不需要 isLocal 过滤,第 13 章讲过 Interact 完全本地,按下的就是当前客户端。

从开局到现在,房间里多了:

  • 一把能坐的椅子 Chair,挂着 VRCStation
  • 一面默认关、按钮开的镜子 Mirror,挂着 VRCMirrorReflection
  • 一个能传到另一处的隐形区域 EntryZone,挂着自己写的 Teleporter

每一件单独的接线方式都跟前几章一模一样:一个 Collider(决定瞄准 / 触发区域),一段脚本(决定调什么),一个引用槽(决定操作哪个对象)。

挑一个或几个改:

  • 给椅子加一个事件:玩家坐下时灯光稍微暗一点,离座时恢复(在 OnStationEntered 里调 Light.intensity)。
  • 给镜子开关加一段「按住 Interact 五秒切高画质 / 短按切开关」的逻辑。提示:用第 12 章 SendCustomEventDelayedSeconds 的延时事件思路,再补一个 int qualityLevel 字段。
  • Teleporter 改成 OnPlayerTriggerExit 触发:人走出区域才传走,做「门口的告别」。

第二个改起来最磨人,做完后回头看 SDK 自带的镜子 Prefab 会更顺眼。

回到第四部反复出现的那个骨架:

  • 一个 GameObject + Collider(实体 / Trigger 看用途),决定瞄准 / 触发的范围。
  • 一个 SDK 自带组件(VRCStation / VRCMirrorReflection / 不需要也行)做实际「重活」。
  • 一段薄薄的 UdonSharp(开关镜子、传送、改 DisableInteractive)做「控制」。
  • 一组 public 字段引用具体目标(座椅 EnterPoint、镜子物体、传送终点)。

每一章的「动作」在变,骨架在 stay 一致。下一章把这个骨架推到稍微难一点的地方:一扇门要记得自己是开还是关,按一下变开、再按一下变关。脚本第一次需要「记忆」。