1. 先搞清楚框架怎么分工:Interactor、Interactable 与 Input Action 的三角关系
刚开始做 Unity XR Interaction Toolkit(后面统一叫 XRI)手柄交互的人,最容易犯的一个错误是:装好包、拖好预设体,就开始找"抓取"按钮在哪。实际上 XRI 把一次完整的交互拆成了三层,每一层各管一摊,你只改某一层,另外两层没配合好,结果就是白忙活。
第一层是 Interactor,可以理解成"手的抽象"。XRI 里有三类:XR Ray Interactor(射线手,负责隔空指)、XR Direct Interactor(直接手,负责贴脸碰)、XR Socket Interactor(槽位手,负责接住东西)。它们的共同点是都挂在玩家身上,靠手柄输入驱动。
第二层是 Interactable,也就是"能被交互的物体"。XRI 里最常见的是 XR Grab Interactable,挂在物体上之后,这个物体才"可被捡起"。脚本里还会管理它是可以抓取、可以远程抓取,还是只能被放到指定的槽位里。这一步很容易被忽略,因为交互器默认能看到场景里所有带 Collider 的物体,但物体没有 Interactable 组件时,射线穿过去也不会触发任何反馈。
第三层才是 Input Action,也就是手柄按钮输入。XRI 默认提供了一套 XRI Default Input Actions,里面预设了瞬移、抓取、旋转、转向等常用的输入映射。你需要在 XR Controller 组件的字段里把 Select Action、Activate Action、UI 点击等槽位指到这个 .inputactions 文件里对应的 Action 上。
这三层的关系可以类比成现实里的场景:你的手是 Interactor,杯子是 Interactable,而大脑发出的"伸手去拿"指令就是 Input Action。手到了杯子旁边,大脑也下了指令,这个动作才完整。如果手到了但大脑没指令(Input Action 没配对),或者大脑指令到了但手不在(Interactor 没激活),交互一定失败。
我在给项目排查手柄没反应的时候,第一步永远是去检查 Input Action 是否配对,第二步检查 Interactor 的 Interaction Layer Mask,第三步才看物体上的 Interactable 配置。顺序不要反,因为大部分"手柄能转但按钮无效"的问题,都出在 Input Action 没有被正确驱动上。
1.1 Interaction Layer Mask 是看不见的"过滤器"
XRI 里有一个很容易踩坑的设计:Interaction Layer Mask。它和 Unity 自带的 Layer 不是一回事,是 XRI 自己维护的一套掩码,用来决定 Interactor 能"看见"哪些 Interactable。
默认情况下,这个掩码是 Everything,也就是所有交互器能抓所有可交互物体,这在 Demo 里无所谓,但项目一复杂就出问题:射线想抓远处的遥控器,结果半路把桌上的打火机先吸附过来了;Socket 想接住电池,却把整个武器都吞了进去。
正确做法是在 Project Settings 的 XR Interaction Toolkit 分区里,自定义几层:比如 Player、Interactable、UI、SocketOnly。然后每个 Grab Interactable 勾选它属于哪一层,每个 Interactor 勾选它能够与哪一层发生交互。注意这里有个双向匹配的规则:交互器和物体的掩码必须互相包含,两边都满足才能交互。这个细节很容易被忽略,我见过不少"明明都配好了,但手指穿过去了没反应"的案例,最后查出来就是掩码不匹配。
1.2 版本差异:2.x 和 3.x 的配置位置完全不一样
还有一个经常让老项目翻车的地方:XRI 2.x 和 3.x(现在 Unity 6 里默认会拉到的版本)在配置方式上有比较大的变化。2.x 时代,很多组件字段是直接挂在 Interactor 上的;3.x 开始,XRI 把输入相关的入口收拢到了 XR Controller 和 Input Action Manager 里。
如果你是从老项目升级过来,会发现原来的 Ray Interactor Inspector 面板上少了很多输入字段,比如 Select Action、Activate Action 这些都被挪到了 Controller 脚本里。这个变化本身不算坏事,它让"输入设备"和"交互算法"解耦了,但升级时必须手动重新把 Action 拖到对应槽位上,否则手柄按键静悄悄。
另外,3.x 里 Interaction Manager 也变成了一个显式引用的组件,如果场景里的多个交互器没有共享同一个 Interaction Manager,它们之间是互相"看不见"的。特别是把两个预制体拼进一个场景时,最容易出现这种"射线各自为政"的诡异现象。
2. 射线交互:从瞬移、点按钮到隔空抓取,一条射线能做的三件事
射线交互是 VR 里手感最直观、也是使用频率最高的一类交互。很多人以为射线就是一条线加一个圆点,实际上 XRI 的射线交互能分成三种完全不同的应用场景:瞬移定位、UI 操作、隔空抓取。三条腿缺一不可,而且它们的配置细节各不相同。
2.1 先分清三种 Line Type,再谈手感
在 XR Ray Interactor 的 Inspector 里,有一个 Line Type 下拉框,里面有三个选项:Straight Line、Projectile Curve、Bezier Curve。这个选项直接决定射线的形态。
Straight Line 就是笔直一条线,适合瞬移瞄准、UI 点击这类需要精确指哪打哪的操作。Projectile Curve 是一条抛物线,适合模拟"扔出去"的感觉,比如往远处扔一个球、给某个区域投掷一个物体,也可以应用到瞬移上,让你能看到落点轨迹。Bezier Curve 可以做得更自由,它可以绕开中间的遮挡物,但这通常需要额外的控制点计算,普通项目里用得不多。
我之前在 Pico 4 上做了一个远程抓取的功能:玩家要隔空把一个零件从架子上取下来。如果只用 Straight Line,需要精确地瞄准一个很小的目标点,人的手稍微抖一下就偏了。后来改成 Projectile Curve 加一条宽松的抛物线,末端圆环(Reticle)跟着落点走,玩家"扔"一条线过去碰到零件就能抓,手感一下就好了很多。
还有个很容易被忽视的细节:Reticle,就是射线末端那个小圆环或者十字光标。如果 Ray Interactor 的 Reticle 字段是空的,玩家就不知道自己的"焦点"落在哪里,这在 UI 交互和瞬移里都很难受。正确做法是做一个带 XR Reticle 脚本的小圆标,拖到 Ray Interactor 的 Reticle 槽位上,射线末端就会跟着目标点移动。
2.2 瞬移不是"传送",而是整个 XR Origin 在移动
很多第一次做瞬移的人会想着去移动 Main Camera,这是错的。XRI 的标准做法是在 XR Origin 下挂一个 Teleportation Area,玩家用射线指到地面上,手柄按下瞬移键,系统会移动整个 XR Origin(包括地面、左右手柄和相机),而不是单独移动相机。这样做的原因是,单独移动相机会导致 Wrist 坐标系错乱,手柄视角和身体实际位置对不上,玩起来就晕。
如果你明确要让玩家瞬移到某个固定位置,比如一个椅子前、一扇门旁边,应该用 Teleportation Anchor,而不是 Area。Area 是"面可以随便传",Anchor 是"点固定落位"。我一般的大厅场景里两者混用:大厅本身是 Area,但每个展台前放一个 Anchor,保证玩家传到展台时,朝向和距离是对的。
瞬移还有一个常见问题:玩家传送后朝向飘了。这通常是因为右手摇杆同时驱动了移动和转向,瞬移触发的瞬间又碰了一下转向。XRI 在 Teleportation Area 上提供了 Teleport Direction 相关选项,可以选择"传送时是否带方向"以及方向参考哪里。一般建议把方向参考设置为"射线起点到落点的方向"或"头部朝向",不要用"手柄朝向",否则视角会非常不稳定。
注意:瞬移时移动的是 XR Origin,不是 Main Camera。如果你在 Main Camera 上额外挂了一个"摄像机跟随"脚本,让它去追 XR Origin,那必然会出现抖动和视角漂移。XRI 的场景结构里,Main Camera 本来就挂在 Camera Offset 下面,跟着 XR Origin 一起移动,不需要你再写任何跟随逻辑。
2.3 用射线点 UI:World Space UI 不被遮挡的设置
手柄射线点按钮和鼠标点按钮完全是两码事。鼠标是屏幕坐标的精确点击,手柄射线则是一个物理碰撞检测,它需要先决定"射线碰到的是哪一层",然后再把点击事件发到 UI 上。
这套流程在 XRI 里的实现是:场景里放一个 EventSystem,挂上 XR UI Input Module,然后把 XR Ray Interactor 的 Enable UI Interaction 勾上,同时把 Tracked Device 指向对应手柄的 XR Controller。这里有一个非常常见的坑:如果 Ray Interactor 没有勾选 Enable UI Interaction,它就只能和 3D 物体交互,UI 怎么点都没反应。
但更隐蔽的问题是 World Space UI 被 3D 物体挡住。比如你在场景里放了一块世界空间面板(World Space Canvas),面板前面正好有一根柱子,手柄射线从柱子后面穿过,结果事件被柱子截断了,UI 按钮点不到。解决办法是在 Canvas 的 Inspector 里调整 Blocking Objects 和 Blocking Mask。Blocking Objects 设成 All,Blocking Mask 设成"需要挡射线的那个层"(一般把柱子等 3D 物体单独放一层),这样 UI 就不会被无关物体抢走焦点。
还有一个小技巧:如果按钮的点击范围太小,手柄射线很难精准点中,不要在 UI 上到处放大按钮本身,而是给按钮的子物体加一个透明的 Image 组件来扩展点击热区。射线按到透明区域同样算点击,UI 视觉上还是一个精致的按钮。
2.4 隔空抓取:射线抓取什么情况下用、怎么调
隔空抓取用射线拿起远处的物体,是 VR 里很实用的能力,但手感和近处直接抓完全不同。实现上就是给物体挂上 XR Grab Interactable,然后让 Ray Interactor 的 Interaction Layer Mask 包含这个物体所在的层,射线指到物体上按抓取键就能吸附过来。
这里有个细节要注意:当一个物体同时可被 Direct Interactor(近处)和 Ray Interactor(远处)抓取时,玩家在靠近后,经常会发现距离很近却用不了直接抓取,因为射线优先把物体"锁定"了。解决办法有两个:一是在射线接近物体一定距离时主动禁用 Ray Interactor,二是在 Grab Interactable 上配置 Preferred Interactor 优先级,让它优先响应直接抓取。两种方案我都在项目里用过,建议先试试方案二,改配置比改逻辑简单得多。
3. 直接交互:为什么抓取总穿模、抛出像甩出去
近处抓取是所有手柄交互里最考验手感的部分。你伸手过去,握住一个杯子,拿起来,放下,看起来简单,但实际调起来会发现:要么手指还没碰到杯子杯子就"瞬移"到手心,要么抓住杯子之后一晃杯子就飞了,要么把杯子放到桌上时它直接穿进桌子。
这些问题大多可以归结到三个点:Attach Transform 没配、Movement Type 选错、刚体上的阻尼参数不合适。
3.1 抓取点(Attach Transform)决定物体"贴"在手的位置
先想象一个现实里的场景:你伸手去拿桌上的马克杯,手指是环住杯身握住它的,握点大概在杯身中间偏上。但在默认情况下,XRI 抓取物体时是拿物体的中心点贴向手柄的握持点,所以如果你想抓杯身,结果可能是杯子中心对着手心,杯口和杯底各露出一半,看起来很别扭。
解决办法是给每个 Grab Interactable 创建一个空的子物体,把它放到你期望的"握持位置",然后把这个子物体拖到 Attach Transform 槽位上。建议在制作物体时,就按"一个物体一个抓取点空物体"的约定来管理,毕竟枪支、瓶子、刀具这些物品的握持点各有不同,等你摆了一桌子物品再回头补,工作量很大。
还有一个容易被忽略的版本差异:在 XRI 3.x 的配置里,Attach Transform 的位置可以自动计算也可以手动指定。如果选择自动计算,物体的 Transform 中心就会被当作抓取点,等于没配。手动指定后,抓取时物体会平滑地把 Attach Transform 和手柄的 Attach Transform 对齐。
3.2 Movement Type:三种模式相当于三种"手的材质"
Grab Interactable 上的 Movement Type 有三个选项,初学者经常选默认值就不管了,但往往就是这里决定了物体"是死是活"。
Instantaneous 是瞬时改变物体位置,让它直接跳到抓取点。适合钥匙、遥控器、武器这类需要"到手即用"、不太在意惯性的物体。上手感就是"一按就吸附",但太重的物体用这个模式会显得假。
Velocity Tracking 是最接近物理跟手的模式。它把手柄运动速度、角速度记录下来,然后通过刚体驱动物体跟随手柄。这个模式的好处是物体可以抛出,并且在快速移动时不会卡顿;缺点是如果刚体质量、拖拽参数没调好,物体可能会抖动或者在空中乱飘。
Kinematic 模式下,物体会被设置为 kinematic 刚体(也就是不参与物理模拟),然后跟随手柄运动。它的优点是拿在手里非常稳定,不会因为碰撞抖动;缺点是抛出时需要用额外代码在释放瞬间把刚体切回非 kinematic 并手动赋予速度,不然物体就像被"冻结"在半空中。
这里给出我在 Pico 4 项目里的初始参数建议:
| 物品类型 | Movement Type | Mass | Drag | Angular Drag |
|---|---|---|---|---|
| 钥匙/小零件 | Instantaneous | 0.5 | 1 | 0.1 |
| 瓶子/球 | Velocity Tracking | 1.0 | 0.5 | 0.05 |
| 家具/大件 | Kinematic | 5.0 | 2 | 0.2 |
这些值不是标准答案,只是起点。最终还是要以"拿起、转动、放下是否自然"为判断标准去微调。
3.3 抛出效果:为什么扔出去像"甩"而不是"扔"
用 Velocity Tracking 模式抓一个球,伸出手快速一甩,球飞出去后应该获得一个和手部运动近似的速度。但默认情况下你会发现两种情况:要么球飞得很近,几乎像是"掉"出去;要么球脱手瞬间突然加速,飞得不可控。
问题出在 Velocity Damping 和 Angular Velocity Damping 这两个参数上。它们的作用是抑制速度的继承:阻尼越大,物体抛出后继承的手柄速度就越小;阻尼越小,继承的速度就越完整。
如果要实现"扔飞镖"那种轻快的抛出,建议把 Velocity Damping 调到 0.1~0.2,Angular Velocity Damping 调到 0.2 左右;如果要实现"放杯子到桌上"这种缓慢的放置动作,反而需要把阻尼调高一些,让物体在释放后不突然获得很大速度。你可以这样理解:阻尼本质上是一个"速度衰减器",它决定物体在离开手的瞬间,还保留了多少手的动能。
3.4 Socket 槽位:让物体精准放到位,而不是"掉"在附近
Socket 在 XRI 里的作用,是给场景里提供一个"指定位置接住物体"的交互器。经典用法是把盾牌放到背上的卡槽、把螺丝刀放回工具架、把弹夹装进枪里。
Socket Interactor 挂在空物体上,设置它的 Interaction Layer Mask,只接收特定层的物体。挂好之后,物体进入 Socket 的有效范围,只要满足掩码条件,它就会被吸附过去。
关于 Socket 有两个高频问题。第一个是物体放进去后旋转不对,比如钉子横着放进去了。解决办法依旧是给 Socket 设置一个 Attach Transform,调整它的旋转,让物体吸附时对齐到这个方向。第二个是 Socket 要在 Hover 的时候显示"这个位置可以放"的提示:Socket 的 Show Interactable Hover Meshes 字段默认是勾上的,会把当前悬停物体的网格显示出来当预览,如果项目里面有成百上千个可交互物体,建议把 Socket 的 Hover Mesh 显示关掉,可以明显降低每帧的绘制开销。
3.5 Interaction Layer Mask 的"双向匹配"规则
前面多次提到 Interaction Layer Mask,这里再说透一点。在 XRI 里,一个物体要能被抓取,必须满足两个条件同时成立:
一是 Interactor 的 Interaction Layer Mask 里要包含物体所在的 Interactable 层;二是 Interactable 的 Interaction Layer Mask 里要包含 Interactor 所在的层。两边都达成,交互才会建立。
这个规则导致的一个常见 bug 是:你创建了一个"Interactable"层,把物体都扔进去了,并在物件的 Grab Interactable 上勾选了这个层,但忘了检查手柄的 Ray Interactor 是不是也勾选了 Interactable 层。结果就是——手碰不到东西,因为两边的掩码不闭合。
调这种问题的时候,建议养成一个习惯:选中交互器,再看物体的 Inspector,把两个 Interaction Layer Mask 都点上确认一下,只盯一边永远看不出问题。
4. 手感补齐:震动反馈、手柄模型与自定义按钮
当你能抓起物体、能瞬移、能点 UI,这套手柄交互算"能用了"。但距离"像样"还差两件事:反馈和外观。VR 里玩家看不见自己的真实手腕,想要有"我真摸到了"的感觉,震动反馈至关重要;而手柄模型如果不替换,举起来看到的是一个方块,沉浸感瞬间没了。
4.1 震动反馈:短而有力的脉冲比持续震动更有效
XRI 里做震动的方式很简单,在 XR Base Controller(XR Controller 的父类)上调用 Send HapticImpulse 方法,传入振幅和时间即可。通常的做法是通过 Grab Interactable 的 Select Entered 事件,当物体被抓起来的时候触发一个脉冲。
下面是一个最简实现,挂在 Grab Interactable 物体上,让它在被抓取时触发震动:
using UnityEngine; using UnityEngine.XR.Interaction.Toolkit; public class GrabHapticFeedback : MonoBehaviour { [SerializeField] private XRBaseController leftController; [SerializeField] private XRBaseController rightController; [SerializeField] private float hapticAmplitude = 0.5f; [SerializeField] private float hapticDuration = 0.1f; private XRGrabInteractable grabInteractable; private void Start() { grabInteractable = GetComponent<XRGrabInteractable>(); grabInteractable.selectEntered.AddListener(OnGrabbed); } private void OnGrabbed(SelectEnterEventArgs args) { var interactor = args.interactorObject.transform; // 判断是哪只手抓的,然后触发对应控制器的震动 var controller = GetControllerByInteractor(interactor); if (controller != null) { controller.SendHapticImpulse(hapticAmplitude, hapticDuration); } } private XRBaseController GetControllerByInteractor(Transform interactor) { // 在这里用交互器的位置或其他标记判断左右手,返回对应控制器 return null; // 需要按项目实际结构实现 } }在真正开发时,判断左右手建议用 XR Input Device 的 characteristics 来区分,或者把左右手柄分别命名,然后从交互器反查。这个细节不处理好,就会出现"左手抓东西,右手在震"的情况。
震动的参数也很有讲究。我的经验是:拿取物体时振幅 0.3~0.5、时长 0.05~0.1 秒比较合适,既能让玩家明显感知,又不会震得发麻。持续震动的效果反而不好,手柄电机长时间高频率震动会让手很快疲劳,尽量只在"碰触、拿起、放下"这几个关键节点发短脉冲。
另外,很多 SDK 自带的手柄模型默认会抢占震动权限,比如 PICO 的控制器在某些模式下会自动处理震动。如果你发现代码里 SendHapticImpulse 调了没反应,先检查 XR Controller 上 Haptics 相关的初始化设置,再检查是不是 SDK 覆盖了震动事件。
4.2 手柄模型替换:让玩家在虚拟世界里"看见"自己的设备
如果没有做任何模型替换,XRI 默认的手柄会显示成一个简单的几何体。在 Pico 4 上开发时,玩家戴上头盔看到的是一对圆柱形手柄,而不是实际的 Pico 手柄形状,这会非常出戏。
替换模型的做法不复杂:准备一个包含正确手柄外观的预制体(可以用设备 SDK 自带模型,或者下载对应型号的 glTF),把它挂在 XR Controller 的 Model Prefab 字段下,XRI 会自动在运行时实例化这个模型,并跟随手柄的运动。
真正比较复杂的是骨骼绑定和动作驱动。比如 Pico 手柄在按下扳机键时,模型上的扳机部分应该有对应的动画。XRI 的 XR Controller Model 脚本可以监听手柄的 activate、select、uiPress 等输入动作,然后驱动模型上指定的骨骼节点。你需要确认模型资源里有对应的握持骨骼节点,然后在 Inspector 里把 Model 组件下方的手指/扳机映射槽位指向它们。
这里有个提醒:替换手柄模型后,一定要重新检查射线的起点。很多时候射线是从手柄模型头部发出的,模型换了之后如果锚点没对准,射线会从错误的位置射出,玩家看到射线从手指缝里穿出去,很奇怪。通常做法是保留一个名为 Attach/Anchor 的空子物体放在模型前端,作为射线起点。
4.3 自定义按钮:把"抓取"从手柄按键改成扳机键
XRI 默认的方案是:抓取用握持键(Grip),激活/点击用扳机键(Trigger)。但实际项目里经常要重新映射。比如有的游戏想把抓取放到扳机上,把瞬移放到 A 键;或者反过来,做成"按住扳机瞄准,松开扳机释放"。
要改这个,是在 .inputactions 文件里改绑定。打开 XRI Default Input Actions,找到 Grab 这个 Action,选中它,在 Binding 区域把绑定改成右手柄的 Trigger 按钮,然后保存并重新加载一次。改完之后要确认 XR Controller 上对应的 Select Action 变量引用的正是这个 Action。
这里最容易踩的坑是:很多人改了 .inputactions 文件,但忘了场景里正在引用的还是旧缓存,运行起来毫无变化。XRI 3.x 里同一个 Action 可以有不同的 Binding,你得确认当前设备实际触发的是哪条 Binding。最简单的调试方法是在 Inspector 里打开 Input Debugger,按下手柄按键看对应 Action 有没有数值变化,这样很快就能定位是绑定问题还是组件配置问题。
4.4 手柄和手势共存:从手柄玩法切到 MR 手势玩法
自从 Quest、Pico 这类一体机陆续开放手势追踪,很多项目都面临一个需求:玩家在菜单界面用手势操作,进入主玩法时切回手柄。这个切换在 XRI 里的本质就是:启用/禁用 Hand Interactor 和 XR Controller 对应的 Interactor。
我常用的做法很简单:在场景里同时保留手柄射线和手势射线,设置一个交互模式枚举,切换时把当前不需要的 Interactor 组件 Disable 掉,同时把 Input Action Manager 里的默认 Action Map 切到相应的映射。比如手势模式下把 Teleport 相关 Action 停用,否则玩家空手一挥就会瞬移,很难受。
这也解释了一个很多人问的问题:"unity mr切换vr"到底怎么实现。其实虚拟现实和设备 MR 模式的切换,更多是 XR 插件管理器层面的开关;但手柄和手势的切换,是 XRI 交互层的事情,两者分开处理,别混在一起。
5. Pico 4 等设备上的适配与排查:从"能跑"到"能交付"
最后这部分,我想围绕 Pico 4 这种国内一体机设备上跑 XRI 的实战问题,聊一聊我踩过的坑和现在的排查习惯。这些经验不限于 Pico,Quest 等设备也基本适用。
5.1 先定输入方案:OpenXR 还是设备 SDK,二选一别混用
现在在 Pico 4 上开发 XR 项目,主流方案有两种:一是用 Unity 的 OpenXR 插件加 XR Interaction Toolkit,配合 XR Plugin Management 来做;二是直接用 PICO Unity Integration SDK。两种方案各有适用场景,但千万不要两个混用。
混用的典型症状是:手柄模型出现两份、手柄坐标偏移、按钮事件被重复触发(按一次键,场景里响应了两次)。这是因为两套输入系统都在监听手柄。如果你只需要基础手柄追踪、瞬移、抓取、UI 点击这些 XRI 原生能力,推荐直接用 OpenXR 方案,干净省事;只有当项目需要用到 PICO 平台专属能力(比如头戴、面部追踪、特定平台商店接入)时,再引入 PICO SDK。
我现在的习惯是:能不开 PICO SDK 就不开,先纯 OpenXR + XRI 跑通核心玩法,等需要平台能力时再增量接入。这样调试的输入源少一个,出问题的时候好排查很多。
5.2 手柄交互失灵:我的标准排查链路
遇到"手柄能追踪但交互全没反应"这类问题,不要乱改配置,按照下面的顺序一条条排查,至少能解决九成的情况。
| 排查步骤 | 检查点 | 验证方法 |
|---|---|---|
| 1 | Input Action 是否绑定 | 打开 Input Debugger,按对应按钮看 Action 是否跳动 |
| 2 | XR Controller 是否驱动 | 检查 Select Action / Activate Action 字段是否引用了正确的 Action |
| 3 | Interactor 是否启用 | 确认 XR Ray Interactor 或 XR Direct Interactor 组件是 Enable 状态 |
| 4 | Interaction Layer Mask 是否闭合 | 同时检查交互器和物体的掩码是否互相包含 |
| 5 | 物体刚体是否缺失 | Grippable 物体必须要有 Rigidbody,且不要勾选错误的 Is Kinematic |
这套顺序是从"输入源头到最终物体"的链路来的。先保证手柄输入确实产生了 Action 值,再保证控制器把它转发给了交互器,然后保证交互器能"看见"物体,最后检查物体本身的物理状态。跳过任何一步都可能白忙。
举个例子,有个项目里抓取物体时偶尔失灵,排查发现是物体上的 Rigidbody 在启动时被代码设成了 Is Kinematic,导致 Grab Interactable 无法驱动刚体移动。这种问题在 Inspector 上看是看不出来的,必须打断点或者写日志才能定位。
5.3 射线断断续续、UI 点不到、传送线消失
射线交互的问题通常有三个现象:射线一会儿有一会儿没有、UI 点了没反应、瞬移的蓝色指示线不显示。
射线断断续续,多半是 Line Renderer 没有正确跟随射线末端。XRI 的 Ray Interactor 自带 Line Renderer 组件,但如果你手动加了一个新的 Line Renderer,两者会冲突。优先检查 Interaction Manager 里这个交互器是否被正确注册,以及 Reticle 有没有拖指定对象。
UI 点不到,先别急着改 UI 代码,按这个顺序看:射线有没有勾 Enable UI Interaction;场景里有没有 EventSystem 和 XR UI Input Module;Canvas 的 Event Camera 是不是指向玩家相机;Canvas 的 Blocking Mask 是不是设得太宽,把不该挡的东西也挡住了。尤其是第四个问题,很多人会把 Blocking Mask 设成 Everything,结果面板自带的一个小碰撞体把射线全部拦截了。
传送线不显示的问题,最常见的是 Teleportation Area 上面的 Interactable 层没被 Ray Interactor 的 Interaction Layer Mask 覆盖。瞬移是 XRI 内置的一种"交互",它同样遵循掩码规则。如果射线能看到普通物体但看不到地面,八成就是地面所在的层没勾上。
5.4 性能与稳定性:不为一时的手感埋性能的雷
手柄交互在性能上的开销,主要来自两个地方:Interactor 每帧对所有可达 Interactable 做 hover 检测,以及 Socket 的 Hover Mesh 预览。
在 XRI 里,每个 Interaction Manager 都会遍历场景中的可交互物体,去判断它们是否进入交互器的 hover 范围。物体数量多、又没有按层区分时,这个计算量会上升得很明显。建议把"场景里当前不需要交互的物体"所在的层,从交互器的掩码里剔除,等需要用的时候再动态开启。
Socket 的 Hover Mesh 也一样,它会在物体进入范围时动态生成预览网格。如果场景里有几十个 Socket,每个都在做预览,性能压力不小。实际交付时如果不需要预览提示,直接关掉这个开关就行;非要保留提示,建议只给关键交互点的 Socket 开。
我之前做过一个场景:墙上有 20 个武器槽位,每个槽位都是一个 Socket,每帧都在生成武器的预览网格,帧率掉了接近 15%。把这些 Socket 的 Show Interactable Hover Meshes 关掉之后,帧率马上回来了。交互逻辑没有任何改动,只是去掉了一部分实时视觉效果。
在手柄交互这件事上,我的经验是:别贪多,把手上的那一两件交互做到顺手,比把所有组件都摆上去更实用。你在项目里排查手柄问题时,如果能养成"先看 Input Action,再看 Interaction Layer Mask,最后看 Attach Transform"的习惯,很多诡异的问题从一开始就不会缠上你。