1. XRI 手柄交互到底在交互什么
先说个很多人刚接触 XRI 时的困惑:Unity XR Interaction Toolkit(下面我直接叫 XRI)里手柄到底是怎么跟场景里的物体发生关系的?你戴着头显,手里握着两个手柄,按下扳机键能抓起一个杯子,松开就掉下去,这中间隔了多少层逻辑?如果你脑子里对这件事没有一个清晰的结构图,后面调参、改交互逻辑的时候,大概率会被一堆组件搞到头大。
简单说,XRI 把“手柄交互”拆成了三个角色:输入源、交互器、可交互物体。输入源就是手柄本身,负责把你的物理按键、摇杆、扳机变成抽象事件;交互器决定“用什么方式去碰物体”,比如直接伸手抓还是用一道射线隔空点;可交互物体则是场景里所有能被碰、被抓、被按的东西。三个角色各司其职,互不越界,这是 XRI 整个框架的核心哲学。
我第一次接触这套东西的时候,最大的误区是想在一个脚本里把所有交互都写了:检测手柄按键、发射射线、判断碰撞、处理抓取、计算物体跟随……结果代码越写越长,后期任何一个小改动都得翻半天。后来我意识到,XRI 这套框架之所以设计成组件化,就是为了让你把“输入”“交互方式”“物体行为”完全解耦。你只需要挂好组件、配好参数,剩下的状态切换、事件回调、物理模拟,框架全给你兜着。
这一篇我重点聊手柄交互里最常用也最核心的几块:Direct Interactor、Ray Interactor、Socket Interactor,以及它们对应的 Interactable 组件。内容偏实战,我会把参数配置、常见坑位和我在实际项目里踩过的雷一起说清楚。上一篇已经把项目环境和 XR Origin 的搭建讲完了,没看过的可以先回去补课,下面我默认你已经有一个能跑起来、能看到手柄模型的 XRI 基础工程。
2. 交互器三兄弟:Direct、Ray、Socket
2.1 Direct Interactor:最像真实手感的抓取
Direct Interactor 的意思是“直接交互器”,说白了就是手柄本体的碰撞体去碰物体。你伸手过去,手柄模型的位置就是交互器的位置,手柄碰撞到了物体,你按下抓取键,物体就被抓起来了。
这个组件在 XRI 里的全称是XR Direct Interactor。它内部自带一个触发器(Trigger),默认配置下是一个球形碰撞体,半径可以调。你可以把它理解成“一只看不见的手”贴在手柄模型上,真正产生碰撞判定的是这个小球,而不是手柄的 3D 模型本身。
实际操作里,Direct Interactor 最适合做近距离、高精度的操作,比如拿起桌上的螺丝刀、捏住一个小药瓶、扳动控制台上的开关。原因很简单:它的交互判定完全基于物理碰撞,位置精确、手感真实,物体在手柄上的跟随也是基于刚体的运动学控制,不会有明显延迟。
但它有个天生的局限:手必须真能“够到”物体。如果你做的场景是那种大型设备巡检,人站在地上,手柄根本够不到高处的按钮,Direct Interactor 就完全没用了。这时候得靠下面这位。
2.2 Ray Interactor:隔空取物的万能射线
XR Ray Interactor是从手柄向前方发射一条射线,用射线的碰撞点来选中物体。按下抓取键,物体会被直接“吸”到你手边(或者原地看着你转),具体行为取决于你怎么配置。它是目前 XR 项目里用得最多的交互方式,没有之一。
Ray Interactor 的射线不是单纯的 Physics Raycast,它其实是一整条“交互判定通道”,包含了几种模式:直接射线、锚点射线、球体投射。默认的Line Type是Straight Line,也就是一条直直的红线;你也可以改成Projectile Curve,让射线像抛物线一样飞出去,适合那种需要“扔”一个交互点的场景,比如从地面指向高处。
为什么它这么常用?因为它把交互距离大大拉长了,用户站在原地就能操作几米外的物体,而且射线本身就带有视觉反馈——你看到一条线指过去,指到按钮上按钮高亮,指到杯子上杯子出现轮廓,用户不需要任何学习成本就知道“这个能碰”。
有个细节要注意:Ray Interactor 的射线命中物体之后,物体的默认响应是变成“悬停状态”,此时你按下选择键(通常是扳机),会触发selectEntered事件,物体才真正被抓取。如果你希望射线一碰到物体就自动抓取,需要关掉Select Action Trigger里的Toggle之类的手动触发,改成State模式并让按钮保持按住,这个后面我会展开说。
2.3 Socket Interactor:物体归位的插槽交互
Socket Interactor 和前两者不是同一类东西。它本身不主动去碰物体,而是提供一个“槽位”,等着其他物体被放进来的。比如一个工具架上有个空位,你把扳手拿起来再放到那个空位上,扳手会被槽位吸附住,位置和角度都根据你预设的锚点固定好。这就是XRSocketInteractor的典型场景。
它最常用的配置是配合XR Grab Interactable一起用:物体本身可以被手抓,也可以被塞进某个 Socket。另一个常见用法是装配线:一个零件被抓起来,对准另一个零件上的 Socket,往上一卡,两个物体就拼在一起。
Socket 的吸附逻辑很智能,默认有一个Socket Active开关,控制槽位是否启用;Reattach On Select Exit决定物体被拿出后,是否自动回到原 Socket,如果你做的是“拿走就回不来”的消耗品逻辑,这个选项必须关掉。还有一个Show Interactable Hover Meshes选项,开启后当物体靠近槽位时会显示一个半透明的“预览模型”,提示用户“放这儿就对了”,我强烈建议在装配类场景里打开它,体验提升非常明显。
3. 实操:十分钟搭出一套基础手柄交互
3.1 项目环境与 XRI 版本选型要点
开始之前,先确认你的工程环境。当前 XRI 的稳定版本线已经到 3.x,新版把很多旧功能的名称、命名空间都改了,网上大量教程还是基于 2.x 写的,你照着做很容易碰到“找不到组件”或者“脚本编译报错”的情况。所以第一步,先打开 Package Manager,找到XR Interaction Toolkit,看一眼你现在装的是哪个版本。
如果是新建项目,我建议直接上 3.x。3.x 最大的变化是全面转向 Input System,并且把XR Controller(基于设备)和XR Ray Interactor(基于交互)彻底分开,逻辑更清晰。如果你是接手老项目,还在用 2.x,也不用急着升,后面我会单独说迁移的问题。
包管理器里 XRI 还自带了几个 Starter Assets,里面包含预设好的输入动作资源(比如XRI Default Input Actions)和示例场景。我强烈建议你导入Starter Assets,里面有一套完整的 Input Action 配置,把手柄的扳机、摇杆、菜单键全都映射好了,省去你自己慢慢配置的麻烦。
3.2 配置 XR Origin 与手柄模型
你这会儿的 Hierarchy 里应该有一个XR Origin(老项目里叫XR Rig),它是整个 XR 系统的根节点。展开它,里面通常有Camera Offset、Camera、LeftHand Controller、RightHand Controller这几个子物体。
LeftHand Controller和RightHand Controller就是手柄交互的挂载点。在 3.x 里,每个手柄节点上面默认挂了一个XR Controller组件,它的职责是把真实手柄的位移、旋转、按键状态同步到虚拟手柄模型上。你需要在它下面再挂一个子物体,用来显示手柄模型。
选中RightHand Controller,右键 -> 3D Object -> 添加一个Sphere作为临时手柄模型,也可以把 SteamVR 或 OpenXR 包里自带的手柄模型拖进来。我平时调试阶段就用一个半透明的球凑合,逻辑跑通了再换正式模型,省事。
接下来是关键的挂载步骤:给RightHand Controller添加XR Ray Interactor组件。添加后它会默认创建一条射线可视化(一条红色直线)。再添加XR Direct Interactor,这时你会看到手柄节点上多了一个球形碰撞体。两个 Interactor 是可以共存的——远距离用射线,近距离用手直接摸,处理逻辑由 XRI 内部自动分配,不需要你写任何脚本。
最后,给手柄节点加上Line Visual(在 3.x 里是XR Interactor Line Visual),负责渲染射线外观。默认是一条红 / 蓝色线,你可以改颜色、宽度,甚至可以配置成只在悬停到物体上时才显示,这个对避免视觉干扰很有帮助。
3.3 给物体挂可交互组件的正确姿势
现在让场景里能抓的东西出现。创建一个 Cube,放到地上,位置随意。选中它,添加XR Grab Interactable组件——这就是让物体可以被抓取的核心。
很多人挂完组件直接运行,发现抓不了,或者抓到后物体乱飞。原因多半是没配好两个基础项:Movement Type和Colliders。
Movement Type有三个选项:
Instantaneous:物体瞬间跟随手柄,最快,但看起来硬邦邦,没有惯性。Velocity Tracking:用速度追随手柄,带点惯性,手感最平衡,我用的最多。Kinematic:完全由物理驱动,适合拿起来要和其他物体做物理互动的场景,比如抓起一个球砸向另一个球。
然后是 Colliders。XR Grab Interactable本身不会给物体加碰撞体,你需要自行确保物体上有一个 Collider,并且最好只保留一个有效的碰撞器。如果物体上挂着多个 Collider(比如你在 Cube 上同时加了 Box Collider 和 Mesh Collider),抓取判定会变得很诡异,射线碰到的位置和实际抓取点不一致,就会出现“明明指到了但抓不起来”的问题。
运行起来,用手柄射线指向 Cube,你会看到射线变成高亮色,按下扳机,Cube 就被“吸”到手边了。到这一步,最基础的手柄抓取闭环就跑通了。
4. 手柄交互参数调优:从“能用”到“好用”
4.1 Interactor 上的关键参数,一个一个说
很多新手挂在嘴边的“手感不对”,拆开看其实就是几个参数没调明白。拿XR Ray Interactor来说,有个Max Raycast Distance参数,默认是 30 米。30 米意味着你站在房间这一头,射线能指到另一头几十米开外的物体。听起来没什么问题,但实际项目中你大概率不想让射线无限远——距离越远,用户对“到底选中了没有”的判断就越模糊,误触概率越大。我一般调到 5~8 米,够用且清晰。
Ray Origin Transform参数决定射线的起点。默认是手柄节点本身,但如果你把射线起点改到手柄模型的前端(比如枪口位置),会让视觉上更自然,尤其做射击或激光笔式交互时,起点和模型不一致会显得很假。
Line Type里如果你的交互目标是地面上的物体,建议调成Projectile Curve,配合Max Endpoint Distance使用,让射线弯过去。这个在 2.5 米高的架子上取物很好用,因为直射线从眼睛高度出发,很难经过“向下指”的路径命中低处物体,弯曲射线则能把这个问题绕过去。
Select Action Trigger选项是我见大家吵得最多的一个。它有三个值:State、Toggle、Hold。
State:只要按住按钮就保持选中,松开就释放。Toggle:按一下选中,再按一下释放,类似于开关。Hold:按住选中,松开释放,和State很像但具体判定细节不同。
我个人的习惯:抓取用State,因为松开就放,符合直觉;菜单之类的功能用Toggle,按一次打开再按一次关闭,避免你一直举着手。这个选择没有绝对对错,但一定要跟你的视觉反馈配合好——按钮被按下时射线颜色变化、物体被抓取时轮廓高亮,这些反馈比参数本身对体验的影响更大。
4.2 Grab Interactable 手感调优:跟随、缓动与力量
物体跟不跟手,是手柄交互体验里最直观的一件事。XR Grab Interactable里有一组参数直接决定这个:Tracking Rotation Type和Tracking Position Type。
Tracking Position Type默认是Tracking,意思是被抓物体严格跟随手柄的世界坐标。这个在大多数场景里没问题,但如果你抓的是一个很重的物体(比如消防栓),瞬移式跟随就显得很假。我的做法是改成Smooth Follow,然后调整Smooth Position Amount到 5~10,让它带着一点缓动,看起来像是有重量感。
Tracking Rotation Type同理,默认Tracking是跟随手柄旋转。做装配类操作时,我建议改成Smooth Follow,因为手柄轻微抖动会让你辛辛苦苦对准的零件瞬间歪掉,加一点旋转缓动反而更容易精准对齐。
还有个隐藏参数很多人不知道:Attach Transform。物体被抓住后,其实不是“糊”在手柄碰撞体上,而是吸附在Attach Transform的位置。默认情况下这个是物体自身原点,所以如果你抓的是一个尺寸很怪的长条物体,抓起后它的中心点对在手柄中间,视觉上没问题;但你想要它像拿棍子一样握在一端,就必须新建一个空物体放到棍子一端,然后拖到Attach Transform里。这个技巧在做所有工具类物品时几乎必用。
如果出现抓取后物体一直抖动、偏离手柄的情况,多半是 Interactor 的碰撞体位置没对准Attach Transform,或者物体上还残留着其他没有禁用的 Collider。检查顺序:先看物体有没有多余的刚体和碰撞器,再看 Attach Transform 位置,最后才考虑调Smooth Amount。
4.3 交互反馈:你差的那一点“实感”
手柄交互和鼠标点击不一样,鼠标点击用户能看到光标、听到声音,手柄交互如果缺少反馈,用户会像在空气里摸东西。我在这里想专门说说视觉和触觉反馈的搭配。
XRI 3.x 里,XR Interactor Line Visual可以配置选中和悬停两种状态的射线颜色。默认悬停是白色,选中变蓝,实现起来零成本,但很有效。另外,XR Grab Interactable上有一个Interactable Events区域,里面列出了Hover Entered、Hover Exited、Select Entered、Select Exited等事件。你可以在 Inspector 里直接把这些事件拖到某个脚本的方法上,不需要写任何额外代码就能实现“抓起物体时播放音效”“放下时触发机关”这类逻辑。
触觉反馈是很多人忽略的。手柄马达的震动能给用户“我真的摸到了”的暗示。你可以在XR Controller组件上挂一个XR Controller (Action-based)的子组件,或者通过事件回调来触发Haptic Impulse。我通常在Select Entered事件里加一个 0.1 秒、振幅 0.5 的震动,成本极低,但用户在拿起东西那一刻的“手感”会好非常多。
5. 踩坑实录:这些坑我替你先踩了
5.1 射线乱飞、指向不准:Origin 没校准
很多项目跑起来后,手柄射线的方向偏了,或者手一动射线就跟飘了,半天对不准目标。这个问题绝大多数情况下不是 Interactor 的锅,而是XR Origin里的跟踪原点没校准。
XR Origin组件上有一个Tracking Origin Mode,两个选项:Device和Floor。
Device:以头显设备为原点,玩家站在哪里,哪里就是零点。Floor:以地面为原点,地面上方为 Y 轴正向。
如果你用的是坐姿体验(比如坐着看虚拟桌面),选Device没问题;但你要做的是站起来走动、从桌上拿东西,必须选Floor,否则手的高度会跟现实差一大截,射线直接指到天花板上。
还有一个很隐蔽的问题:Camera Offset下的Tracked Pose Driver组件必须正常启用。如果你的摄像机没有正确的姿态追踪,手柄的位置自然也是错的,射线当然跟着乱套。排查顺序永远是:先看头显追踪正不正常,再看手柄模型在不在手上,最后才怀疑射线参数。
5.2 抓取物体后穿模严重:Collider 才是元凶
有一个很典型的场景:你从桌上拿起一个杯子,结果杯子直接穿过了桌面,或者抓在手里时半截埋进手柄模型里。这里有两层原因,处理方式不同。
第一层是 Interactor 的碰撞体太大,导致物体被“隔空”抓起来,看起来像是还没摸到就吸走了。解决办法是把 Direct Interactor 的球形碰撞体半径调小,我一般调到 0.05~0.1 米,够触碰到又不会过于夸张。
第二层是物体本身的 Collider 和其他物体发生碰撞穿透。XRI 抓取时默认会把被抓物体变成 Kinematic(运动学刚体),这时候它不参与物理碰撞,所以你把它往桌子里按,它会直接穿过去。解决办法是不要过度依赖物理碰撞来阻止穿模,而是给被抓物体加一个XR Grab Interactable的Colliders白名单,明确告诉系统“只跟这个 Collider 发生关系”。这个方法在项目里几乎能解决八成的穿模问题。
5.3 多个交互器同时碰到物体:优先级冲突
项目做到后面,场景里往往同时有好几个手柄、射线、Socket。这时你会遇到一个奇怪的 bug:两个手柄的射线同时指到同一个物体,按抓取键却没有反应,或者物体在两手之间抖动。
原因是 XRI 默认只允许一个 Interactor 同时选中一个 Interactable。当两个 Interactor 同时尝试选中同一个物体时,系统会按照Interaction Layer Mask和Priority来决定谁有话语权。如果你的两个手柄配置完全一样,优先级相同,就会产生竞争。
解决办法有两个:要么在XR Grab Interactable的Interaction Layer Mask上把左右手分开,左手只能抓左手层,右手只能抓右手层;要么在 Interactor 组件上设置Priority,让右手优先。如果做的是双持武器的玩法,你需要用XR Multi Interactor或者手动处理selectEntered/selectExited事件,这个比较复杂,等以后单独写一篇。
5.4 升级 3.x 后老脚本全部失效
最后提醒一个关于版本升级的坑。2.x 的项目升到 3.x,最直接的冲击是命名空间和 API 的变化。2.x 里的XR Ray Interactor是同时负责输入和交互的,3.x 把它拆成了XR Controller+XR Ray Interactor,输入部分全部挪到XR Controller里。如果你的老项目里用代码获取过XR Ray Interactor的手柄输入按键状态,升级后大概率编译报错,因为按键相关的事件都在XR Controller上。
我升级时踩过的具体问题是自定义的 Grab 逻辑:原来写在OnSelectEntered里,升级后方法名变成了OnSelectEntered(SelectEnterEventArgs),参数类型变了,事件注册方式也从+=变成了UnityEvent驱动。这类改动不复杂,但分散在十几个脚本里,一个个改确实费时间。所以新项目直接用 3.x 起步才是正道。
6. 我的一点个人体会
手柄交互在 XR 项目里看着只是一个“拿起放下”的小功能,实际做进去才发现它是整个体验的骨架。你交互做得越扎实,后面做物体操作、UI 点击、场景传送才越有底气。我自己做了几个项目下来,最大的感受是:XRI 这套框架的组件化设计非常成熟,但它的学习门槛恰恰在于你需要理解“谁负责什么”,而不是急着写代码。
如果你刚开始接触,我不建议一上来就研究各种高级玩法,先把 Direct Interactor 抓杯子、Ray Interactor 隔空点按钮、Socket Interactor 物件归位这三个基础交互用熟。等这三个闭着眼睛都能配出来,再去碰传送、投掷、双手协作这些进阶内容,你会发现很多高级功能本质上只是对基础组件的组合和复用。
最后送一个小技巧:调试交互的时候,别老盯着手柄模型看,把视角切到 Scene 视图,打开XR Interaction Debugger窗口(Window -> XR -> Interaction Debugger)。它会把当前所有 Interactor、Interactable 的状态、悬停选中关系、Layer 匹配情况一屏列出来,比你在 Game 视图里盲猜高效太多。我几乎每次遇到交互逻辑问题,都是靠这个调试窗口定位到根因的。