1. XRI 手柄交互到底在交互什么先说个很多人刚接触 XRI 时的困惑Unity XR Interaction Toolkit下面我直接叫 XRI里手柄到底是怎么跟场景里的物体发生关系的你戴着头显手里握着两个手柄按下扳机键能抓起一个杯子松开就掉下去这中间隔了多少层逻辑如果你脑子里对这件事没有一个清晰的结构图后面调参、改交互逻辑的时候大概率会被一堆组件搞到头大。简单说XRI 把“手柄交互”拆成了三个角色输入源、交互器、可交互物体。输入源就是手柄本身负责把你的物理按键、摇杆、扳机变成抽象事件交互器决定“用什么方式去碰物体”比如直接伸手抓还是用一道射线隔空点可交互物体则是场景里所有能被碰、被抓、被按的东西。三个角色各司其职互不越界这是 XRI 整个框架的核心哲学。我第一次接触这套东西的时候最大的误区是想在一个脚本里把所有交互都写了检测手柄按键、发射射线、判断碰撞、处理抓取、计算物体跟随……结果代码越写越长后期任何一个小改动都得翻半天。后来我意识到XRI 这套框架之所以设计成组件化就是为了让你把“输入”“交互方式”“物体行为”完全解耦。你只需要挂好组件、配好参数剩下的状态切换、事件回调、物理模拟框架全给你兜着。这一篇我重点聊手柄交互里最常用也最核心的几块Direct Interactor、Ray Interactor、Socket Interactor以及它们对应的 Interactable 组件。内容偏实战我会把参数配置、常见坑位和我在实际项目里踩过的雷一起说清楚。上一篇已经把项目环境和 XR Origin 的搭建讲完了没看过的可以先回去补课下面我默认你已经有一个能跑起来、能看到手柄模型的 XRI 基础工程。2. 交互器三兄弟Direct、Ray、Socket2.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 ControllerXR 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 视图里盲猜高效太多。我几乎每次遇到交互逻辑问题都是靠这个调试窗口定位到根因的。