引言
Unity 的 XR Interaction Toolkit(XRI)解决的是「输入设备与三维对象之间的语义鸿沟」:手柄的扳机按下是硬件事件,而「拿起一个杯子」是业务语义。XRI 用 Interactor 与 Interactable 两层抽象把两者解耦,让同一个杯子既能被手柄射线选中,也能被手势抓取。
工程上容易踩的坑不在 API 本身,而在配置与生命周期:交互管理器没挂、层遮罩(Layer Mask)没配导致射线打不到、XR Origin 相机追踪模式选错导致视角不动、Input Actions 没绑定导致按键无响应。这些问题在 XRI 里都表现为「代码看起来对,但就是没反应」。
本文按「架构 → Origin → 管理器 → 交互器 → 可交互对象 → 射线与传送 → 抓取 → 手部 → UI → 输入绑定 → 多设备」的顺序展开,重点给出可复制的配置清单与代码。舒适度相关的设计原则在 VR 舒适度与晕动症工程对抗 里展开,引擎通用架构可参考 游戏引擎架构与 ECS 。
目录
- XRI 的定位与版本
- 核心架构
- XR Origin 与相机
- 交互管理器
- 交互器类型
- 可交互对象
- 射线交互与传送
- 抓取与投掷
- 手部追踪接入
- UI 交互
- Input Actions 绑定
- 多设备适配
- 权衡取舍
- 常见坑清单
- 小结
1. XRI 的定位与版本
XRI 是 Unity 官方维护的 XR 交互框架,替代了早期的 VRTK 等第三方方案。它本身不做渲染与追踪,那部分由 XR Plug-in Management(OpenXR、Oculus、OpenVR 等)负责。三层职责划分:
XR Plug-in Management → 设备接入与追踪(位姿、按键原始数据)
XR Interaction Toolkit → 交互抽象(射线、抓取、传送)
业务脚本 → 具体玩法
版本演进上要特别注意 2.x 与 3.x 的差异:3.0 起把输入完全迁移到 Input System(不再支持旧的 XR Input),并强化了手部追踪与 UI Toolkit 支持。新项目直接用 3.x,老项目升级需要重做输入绑定。
| 版本 | 输入系统 | 手部追踪 | UI 方案 |
|---|---|---|---|
| 2.x | 旧 Input / Input System 双轨 | 有限 | uGUI |
| 3.x | 仅 Input System | 完整 | uGUI + UI Toolkit |
2. 核心架构
XRI 的模型是「交互器主动寻找可交互对象」,而不是可交互对象监听输入:
XRInteractionManager
├── XRBaseInteractor(发出交互意图)
│ ├── XRRayInteractor(远处,射线)
│ ├── XRDirectInteractor(近处,碰撞体)
│ ├── XRSocketInteractor(吸附插槽)
│ └── XRGazeInteractor(注视)
└── XRBaseInteractable(接收交互)
├── XRGrabInteractable(可抓取)
├── XRSimpleInteractable(仅响应悬停/选择)
└── XRSocketInteractor 目标
配对由 XRInteractionManager 完成:每个 Interactor 有层遮罩与交互层,只有层级匹配且距离满足条件才会建立 Hover 关系,进而可升级为 Select。
理解这一点的价值在于:排障时先看层遮罩,再看管理器,最后才看脚本。
2.1 交互事件的触发时机
| 事件 | 触发条件 | 典型用途 |
|---|---|---|
| Hover Entered | 射线/碰撞命中且未选择 | 高亮、显示提示 |
| Hover Exited | 离开命中范围 | 取消高亮 |
| Select Entered | 按下选择键并锁定目标 | 抓取、点击 |
| Select Exited | 松开或目标失效 | 释放、投掷 |
| Activate | 按下副键(侧键) | 开枪、使用道具 |
| Focus Entered | 交互组内的焦点切换 | 多对象互斥高亮 |
事件有两套回调风格:XRBaseInteractable 的 UnityEvent(在 Inspector 里连)与接口实现(在代码里写)。同一对象不要混用,否则执行顺序不确定,容易出现「高亮被立刻取消」这类竞态。
3. XR Origin 与相机
XR Origin(旧名 XR Rig)是相机与手柄的父节点,负责把「追踪空间的局部位姿」映射到「场景世界坐标」:
XR Origin
├── Camera Offset (按设备类型设置高度偏移)
│ └── Main Camera (Tracking Origin Mode: Floor/Device)
├── LeftHand Controller (XRController + XRRayInteractor)
└── RightHand Controller
关键配置:
- Tracking Origin Mode:
Floor表示追踪原点在地面(推荐),Device表示在头显。选错会导致相机高度差 1.6 米。 - Camera Offset 的 Y:用
Floor时设为 0;用Device时设为用户身高,但身高因用户而异,因此推荐Floor。 - 相机不要手动改 Transform:XR 下相机位姿由追踪驱动,脚本改 Transform 会被覆盖。要移动玩家,移动 XR Origin 本身。
// 移动玩家:改 XR Origin,不要改相机
public void TeleportTo(Vector3 worldPos) {
var delta = worldPos - xrOrigin.Camera.transform.position;
delta.y = 0; // 只做水平移动
xrOrigin.transform.position += delta;
}
4. 交互管理器
XRInteractionManager 是全局单例式的协调者,场景里必须有且只有一个。它负责:
- 维护 Interactor 与 Interactable 的注册表。
- 每帧计算 Hover 与 Select 状态。
- 派发事件(
OnHoverEntered、OnSelectEntered等)。
常见故障:预制体(如 XR Origin 的变体)自带了管理器,场景里又放了一个,导致事件派发到错误实例。排查方法是搜索场景中所有 XRInteractionManager 组件。
// 事件注册的推荐写法:用接口而非具体类
public class HighlightOnHover : MonoBehaviour,
IXRHoverInteractable
{
public void OnHoverEntered(HoverEnterEventArgs args) { /* 高亮 */ }
public void OnHoverExited(HoverExitEventArgs args) { /* 取消 */ }
}
5. 交互器类型
四类交互器的适用场景:
| 交互器 | 触发方式 | 距离 | 适用 |
|---|---|---|---|
| XRRayInteractor | 射线命中 | 远(可设 30 m) | 菜单、远处选择 |
| XRDirectInteractor | 碰撞体接触 | 近(手部范围) | 直接抓取 |
| XRSocketInteractor | 区域吸附 | 固定点 | 插槽、工具架 |
| XRGazeInteractor | 注视 | 视线方向 | 眼动选择 |
5.1 射线交互器的关键参数
Line Type : Straight / Projectile / Bezier / Teleport
Max Raycast Distance : 默认 30,菜单类建议 10 以内(减少抖动)
Raycast Mask : 必须包含可交互对象的 Layer
Hit Closest Only : 建议勾选,避免穿透多层
Enable UI Interaction: 需要与 uGUI 交互时勾选
射线抖动是长距离瞄准的固有问题:手部微小抖动在 10 米外放大成几十厘米的偏移。缓解手段是加角度平滑(XRInteractorLineVisual 的平滑参数)或缩短射线。
6. 可交互对象
XRGrabInteractable 是最常用的可交互对象,关键参数决定手感:
| 参数 | 说明 | 建议值 |
|---|---|---|
| Movement Type | 运动方式 | Velocity Tracking(物理感)或 Kinematic |
| Track Position | 是否跟随位置 | 抓取物理物体时开 |
| Track Rotation | 是否跟随旋转 | 通常开 |
| Throw On Detach | 松手是否投掷 | 需要投掷时开 |
| Attach Transform | 抓取附着点 | 设为物体把手位置 |
| Interaction Layer Mask | 交互层 | 与交互器层匹配 |
Attach Transform 是最容易被忽略的参数:不设置时物体以自身原点附着到手部,抓一个长柄工具会出现「手握着刀尖」的怪异效果。正确做法是加一个空子物体标出握持点。
// 抓取时切换到持握姿态,松手恢复
public class GrabPoseSwitcher : MonoBehaviour,
IXRSelectInteractable
{
public void OnSelectEntered(SelectEnterEventArgs a) {
animator.SetBool("Holding", true);
}
public void OnSelectExited(SelectExitEventArgs a) {
animator.SetBool("Holding", false);
}
}
6.1 Socket 与吸附
XRSocketInteractor 实现「插入式」交互(工具架、卡槽、拼图),它的特殊之处是由插槽主动吸引物体:
配置要点:
Socket 上的 Interaction Layer Mask 要包含工具所在层
Show Socket Hover Mesh 用于显示吸附预览
Recycle Delay 控制取出后多久才能重新插入(防抖动)
Attach Transform 定义吸附后的位姿
常见问题:
吸附瞬间物体抖动 → 打开 "Use Dynamic Attach"
取出后立刻被吸回 → 增大 Recycle Delay 到 0.5~1 秒
吸附后物体穿透插槽 → 检查 Attach Transform 与碰撞体
Socket 与 Grab 的优先级冲突也要注意:物体既在插槽内又可被抓取时,需用交互层或 canSelect 回调限制,否则会出现「抓着东西却被插槽吸走」。
7. 射线交互与传送
传送是 VR 里最重要的舒适移动方式,XRI 提供了完整组件链:
Teleportation Area : 可传送的平面区域
Teleportation Anchor : 固定传送点(可指定落点朝向)
Teleportation Provider : 挂在 XR Origin 上,接收传送请求
XR Controller (Action-based) 绑定 "Teleport Mode" 与 "Select"
配置清单(漏一项就会失效):
- 地面物体挂
TeleportationArea且 Layer 在射线的 Raycast Mask 内。 - XR Origin 上挂
TeleportationProvider。 - 手柄的
XRRayInteractor上挂TeleportationProvider的配合脚本或使用XRController的 Teleport 模式。 - Input Actions 里绑定
Teleport Mode Activate与Teleport Select。
传送模式的工作流:
按下摇杆/按钮 → 进入传送模式(射线变抛物线)
瞄准有效落点 → 显示传送标记(可旋转朝向)
松开 → 触发 TeleportRequest → Provider 移动 XR Origin
抛物线射线比直线更易瞄准地面:XRI 的 Line Type = Projectile 提供重力曲线,配合 Velocity 参数可调弧度。
7.1 落点有效性校验
传送落点必须校验,否则玩家会被传进墙里或穿到地图外:
// 自定义落点校验:替换默认的 TeleportationArea 判定
public bool IsValidLanding(Vector3 point, float radius = 0.3f) {
// 用球体检测确认落点周围有足够空间
return !Physics.CheckSphere(point + Vector3.up * 0.1f,
radius,
obstructionMask,
QueryTriggerInteraction.Ignore);
}
还应校验地面法向:落点所在平面法向与竖直方向夹角超过 45 度时(斜坡或墙面)应判定无效,否则玩家会被传送到斜坡上并产生强烈不适。
8. 抓取与投掷
投掷手感取决于松手瞬间的速度传递:
// 让抓取物体保留手部速度(物理投掷)
grabInteractable.throwOnDetach = true;
grabInteractable.throwVelocityScale = 1.5f; // 增益,1.0 为真实
grabInteractable.throwAngularVelocityScale = 1.0f;
grabInteractable.useDynamicAttach = true; // 动态计算附着点
要点:
- Velocity Tracking 需要 Rigidbody,且物体的 Mass 要合理(0.1 到 2 kg),过重会让投掷距离骤减。
- 投掷速度增益:真实物理速度在 VR 里手感偏软,常加 1.3 到 1.8 的增益,但不能过高否则「一甩飞天」。
- 松手瞬间的速度采样:XRI 用最近若干帧的位姿差分估计速度,帧率低时会低估,因此必须保帧率。
- 抓取时的碰撞:抓取后关闭物体与手部的碰撞,否则物体会被手推开。
9. 手部追踪接入
XRI 3.x 提供手部追踪支持,但要注意能力差异:
// 检测手部追踪可用性
var hands = new List<XRHandSubsystem>();
SubsystemManager.GetSubsystems(hands);
bool handTrackingAvailable = hands.Count > 0 &&
hands[0].running;
手部追踪的三个工程现实:
- 捏合精度有限:小物体(小于 5 厘米)难以稳定抓取,需要放大交互体积。
- 无触觉反馈:手势抓取缺少力反馈,必须用视觉与音效补偿。
- 遮挡与出画:手出视野后追踪丢失,抓取会被强制中断,必须处理
OnSelectExited。
手眼协同的完整设计(含注视加捏合)见 手势识别与眼动追踪交互设计 。
10. UI 交互
XR 里的 UI 有三种做法:
| 方案 | 实现 | 优点 | 缺点 |
|---|---|---|---|
| World Space Canvas | uGUI 挂在三维空间 | 兼容性好 | 立体渲染下文字糊 |
| 射线 + Canvas | XRRayInteractor 勾 UI Interaction | 复用现有 UI | 需要射线精度 |
| UI Toolkit | 3.x 支持 | 性能好 | 生态较新 |
| Quad Layer | 平台特性 | 最清晰 | 需平台支持 |
文字清晰度是 XR UI 的核心痛点:立体渲染下同一像素被两只眼睛看,加上畸变与低 PPD,小字号完全不可读。经验规则是字号至少是手机上的 3 倍,并且用高对比度配色。
XR UI 可用性经验值:
最小可读字号 : 视角高度约 0.5° 以上(1 米处约 8.7 mm)
推荐交互目标尺寸 : 视角 3° 以上(1 米处约 52 mm)
按钮最小间距 : 视角 1° 以上,避免误触
文字与背景对比度 : 至少 4.5:1,建议 7:1
10.1 让射线能点中 uGUI
四步配置,缺一不可:
1. Canvas 的 Render Mode 设为 World Space,并缩放到合适尺寸
2. 在场景中加 EventSystem,且只保留一个
3. 给 Canvas 挂 XRUIInputModule(替换默认的 StandaloneInputModule)
4. XRRayInteractor 勾选 Enable UI Interaction,
并把 UI 所在 Layer 加入 Raycast Mask
XRUIInputModule 是必须的:默认输入模块不理解 XR 的射线与选择事件,会出现「射线明明指着按钮,点击却无反应」。
11. Input Actions 绑定
XRI 3.x 完全基于 Input System,交互通过 Input Action 名称映射:
默认 Action Map(XRI Default Input Actions):
XRI LeftHand / XRI RightHand
Select : 扳机 / 捏合
Activate : 侧键
UI Scroll : 摇杆
Teleport Mode : 摇杆按下或 A/X 键
Teleport Select : 扳机
Haptic : 触觉输出
自定义设备适配的做法是不改脚本,只换 Action Asset:为 Quest、Index、WMR 各做一套绑定,运行时按 InputSystem.devices 选择。
// 运行时切换绑定
var playerInput = GetComponent<PlayerInput>();
playerInput.actions = questActions; // 换 Action Asset 即可
12. 多设备适配
不同设备的输入能力差异必须用能力探测而非型号判断:
探测顺序:
1. 是否有手柄(XRController with devicePosition)→ 射线 + 抓取
2. 是否有手部追踪(XRHandSubsystem.running) → 手势交互
3. 是否有眼动(厂商扩展) → 注视选择
4. 都没有(Cardboard 类) → 注视 + 停留选择
常见做法是运行时启用/禁用对应的交互器,而不是打包不同版本。这样同一个包能在 Quest、PICO、Index 上运行。
性能侧的多设备适配同样重要:高端设备开注视点渲染与高分辨率,一体机降 LOD 与阴影,详见 一体机 XR 性能优化实战 。
12.1 主流设备能力对照
| 设备 | 手柄 | 手部追踪 | 眼动 | 备注 |
|---|---|---|---|---|
| Quest 3 | 有 | 支持 | 无 | 手部追踪需开启 |
| Quest Pro | 有 | 支持 | 支持 | 眼动与面部追踪 |
| PICO 4 | 有 | 支持 | 无 | 与 Quest 类似 |
| Vision Pro | 无 | 支持 | 支持 | 注视加捏合为默认 |
| Index | 有 | 无 | 无 | 精确指虎控制器 |
| Cardboard 类 | 无 | 无 | 无 | 仅注视停留 |
适配策略建议:把交互能力抽象成配置资产(是否启用射线、是否启用注视、选择键映射),运行时按探测结果加载,而不是在代码里写 if (device == "Quest") 这类硬编码。
13. 权衡取舍
- 射线与直接抓取:射线适合远处但抖动,直接抓取精准但受臂展限制,常两者共存。
- Kinematic 与 Velocity Tracking:前者稳定但无物理感,后者真实但可能穿模。
- 传送与平滑移动:传送不晕但破坏空间连续性,平滑移动连续但易晕,应同时提供。
- World Space Canvas 与 Quad Layer:前者兼容,后者清晰,需做降级。
- 手部追踪与手柄:手势免设备但精度低,游戏仍以手柄为主,演示用手势。
- 每设备一套场景与运行时切换:前者优化空间大,后者维护成本低,推荐后者。
14. 常见坑清单
- 场景里多个 XRInteractionManager:事件派发混乱,必须保证唯一。
- 层遮罩没配:射线打不到可交互对象,表现为「射线穿过去了」。
- 相机 Transform 被脚本修改:被追踪覆盖,应改 XR Origin。
- Tracking Origin Mode 选 Device:相机高度差 1.6 米,VR 应用应用 Floor。
- 忘挂 TeleportationProvider:传送区域配置正确也不生效。
- 没设 Attach Transform:抓长柄工具握在刀尖,手感怪异。
- 抓取后不关碰撞:物体被手部碰撞体推开,抖动不止。
- 投掷速度增益过高:物体一甩飞天,破坏物理预期。
- 手部出画不处理退出事件:物体卡在半空或跟随失效的手。
- UI 字号按手机标准设计:XR 里完全不可读,字号需放大 3 倍以上。
15. 小结
XRI 的工程主线是:搭好 XR Origin(Origin 用 Floor 模式)→ 确保唯一交互管理器 → 配好交互器与层遮罩 → 用 Attach Transform 定义抓取点 → 配全传送四件套 → 用 Input Actions 做多设备适配。它的抽象设计很干净,绝大多数问题都出在配置而非代码。
排障时建议按固定顺序:先查层遮罩,再查管理器实例数,再查 Tracking Origin 模式,最后看脚本事件注册。这四步能覆盖八成以上的「没反应」类问题。
舒适度是交互设计的隐藏约束,下一步建议读 VR 舒适度与晕动症工程对抗 ;如果关注头显上的渲染实现,读 Unreal VR 渲染管线与立体渲染 。
延伸阅读
- 手势识别与眼动追踪交互设计 — 手眼协同交互
- VR 舒适度与晕动症工程对抗 — 移动方式与舒适度
- 一体机 XR 性能优化实战 — 交互脚本的性能成本
- 游戏引擎架构与 ECS — 引擎架构与组件模型
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。