Awake 里最先做的一件事

源码中 Awake() 的顺序很值得注意:

  1. 先 InitializeReferences()
  2. 再 base.Awake()

这意味着它先把自己依赖的核心组件找齐,再走基类初始化流程。这保证 attachTransform 在早期就准备好。

缺少核心组件时会自动补

InitializeReferences() 做三件事:

  • 远距离 Caster:farInteractionCaster 为空时,先找当前对象已有的 ICurveInteractionCaster,找不到就 AddComponent<CurveInteractionCaster>()。
  • 近距离 Caster:nearInteractionCaster 为空时,先找当前对象已有的 IInteractionCaster,但跳过同时也是 ICurveInteractionCaster 的组件——避免把远距 Caster 误当近距 Caster。最终找不到就 AddComponent<SphereInteractionCaster>()。
  • Attach Controller:interactionAttachController 为空时,先找 IInteractionAttachController,找不到就 AddComponent<InteractionAttachController>()。

最后还会执行:

attachTransform = interactionAttachController.GetOrCreateAnchorTransform();

附着锚点不是可有可无的,会在初始化阶段被明确创建或拿到。

运行时自动补虽然方便,但编辑器里不直观,Inspector 里看不清"现在到底是谁在起作用"。建议显式挂好并绑定引用,不要依赖自动补。

UI 交互不是单纯一个布尔值

enableUIInteraction 的 setter 不只是把布尔值改掉,还会调用:

RegisterOrUnregisterXRUIInputModule(m_EnableUIInteraction)

UI 交互开关不只是逻辑判断,还会影响和 XRUIInputModule 的注册关系。改这个值时,系统会同步处理 UI 注册状态。

OnEnable / OnDisable 做了什么

OnEnable:如果 UI 交互开启,执行 UpdateUIRegistration()。

OnDisable:

  • 取消向 XRUIInputModule 的注册
  • 如果启用了 UI Toolkit 世界空间支持,也会从对应系统中注销
  • 调用 InitializeInteractor() 清空本轮交互缓存

它对 UI 注册状态的管理比较完整,不是只在首次启动时注册一次就不管了。

每一帧的核心流程

在 PreprocessInteractor(UpdatePhase.Dynamic) 里,核心顺序是:

  1. InitializeInteractor()
  2. UpdateAnchor()
  3. DetermineSelectionRegion()
  4. EvaluateNearInteraction()
  5. EvaluateFarInteraction(newSelectionRegion)
  6. UpdateSelectionRegion(newSelectionRegion)
  7. 如启用 UI Toolkit 支持,再处理对应事件

这条顺序告诉我们:

  • 先更新附着点
  • 再判断当前是 Near 还是 Far
  • 再依次评估近距和远距命中

selectionRegion 是怎么判断的

DetermineSelectionRegion() 的逻辑很直接:

  • 如果当前没有选择,返回 None
  • 如果有选择,看 interactionAttachController.hasOffset
  • 有偏移就是 Far
  • 没偏移就是 Near

一个很关键的行为:

某个对象最开始可能是通过远距离选中的,但如果它被拉近到没有偏移,selectionRegion 会从 Far 变成 Near。

这也是官方文档里"Far 选中后可能变成 Near"的源码依据。

近距和远距的优先关系

近距逻辑

EvaluateNearInteraction() 里,如果满足下面任一条件,就不会继续:

  • 没开启近距探测
  • 当前已经有选中对象
  • 近距 Caster 没找到有效 Collider

近距逻辑更偏向"未选中时找手边目标",不是"选中了还一直靠近重新抢目标"的设计。

远距逻辑

EvaluateFarInteraction() 里有两个关键条件:

  • 如果没开远距探测,且当前也不是 Far 选中,就不更新远距
  • 如果近距已经先找到有效目标,则直接不处理远距

这说明:

同一帧里,如果近距先拿到有效目标,远距通常让路。

手边真有能抓的东西时,优先按近距处理——不要让远处射线抢走焦点。

它会比较 2D UI 命中和 3D 命中

远距逻辑不是只看 3D Collider。它还会:

  • 取当前 UI 的 RaycastResult
  • 计算 UI 命中距离
  • 计算 3D 命中距离
  • 比较两者谁更应该被处理

更容易理解地说:

它不是"只要打到 3D 就不管 UI",而是会在 UI 和 3D 之间做优先级比较。

对同时存在 World Space UI 与 3D 模型的 VR 场景很重要。

目前只支持一个有效目标

源码里有一个明确字段:

m_AllowMultipleValidTargets = false

注释写得很清楚:当前只支持一个有效目标,未来可能扩展。它内部虽然可能暂存多个候选,但对外主要按"单一有效目标"工作。

排序和过滤的一个细节

近距和远距目标注册时都会考虑 targetFilter。如果有 targetFilter,很多情况下排序会交给 filter;sortNearTargetsAfterTargetFilter 只适合"filter 本身不排序"的情况。

这个选项不是默认都该开,是给进阶过滤逻辑准备的。

关键结论

  1. 运行时确实会自动补默认组件,但项目里最好还是显式挂好,方便排查。
  2. UI 交互开关会影响注册状态,不只是变量值。
  3. 近距优先级通常高于远距。
  4. 远距逻辑会同时权衡 UI 命中和 3D 命中。
  5. selectionRegion 会随着对象被拉近而变化。

参考资料