Unity VR开发教程 OpenXR+XR Interaction Toolkit (六)手与物品交互(触摸抓取)
Posted YY-nb
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Unity VR开发教程 OpenXR+XR Interaction Toolkit (六)手与物品交互(触摸抓取)相关的知识,希望对你有一定的参考价值。
文章目录
- 📕教程说明
- 📕VR 交互的类型
- 📕发起交互的对象(Interactor)
- 📕可交互的对象(Interactable)
往期回顾:
Unity VR开发教程 OpenXR+XR Interaction Toolkit (一) 安装和配置
Unity VR开发教程 OpenXR+XR Interaction Toolkit (二) 手部动画
Unity VR开发教程 OpenXR+XR Interaction Toolkit (三) 转向和移动
Unity VR开发教程 OpenXR+XR Interaction Toolkit (四) 传送
Unity VR开发教程 OpenXR+XR Interaction Toolkit (五) UI
交互一般需要两个对象:一个是可交互的对象(Interactable),一个是发起交互的对象(Interactor,一般是玩家自己)。本系列教程中的传送功能也是交互的一种方式,可传送的地面是可交互的对象,手部发出的传送射线是发起交互的对象。而这篇教程将要介绍的是如何在 VR 世界中直接用双手与物品进行交互,此时物品是可交互的对象,手是发起交互的对象。
📕教程说明
使用的 Unity 版本: 2020.3.36
使用的 VR 头显: Oculus Quest 2
教程使用的 XR Interaction Toolkit 版本:2.1.1(此教程尽量考虑了向上兼容,如果有过期的地方,欢迎大家指出)
前期的配置:环境配置参考教程一,手部模型参考教程二。本篇教程的场景基于上一篇教程搭建的场景进行延伸。
项目源码(持续更新):https://github.com/YY-nb/Unity_XRInteractionToolkit_Tutorial2022/tree/master
最终实现的效果:
📕VR 交互的类型
VR 中的交互一般分为三种类型:
- Hover (悬停) :一般指的是发起交互的对象停留在可交互对象的交互区域。以手与物品交互为例,假设物品表面为可交互区域,当手触摸到物品(悬停在物品的可交互区域),则视为触发了 Hover。
- Grab(抓取):这个概念好理解,就是把物品抓起来。
- Use(使用):“使用” 是基于 “抓取” 的。有时候我们可以继续使用正在抓取的物体,触发它的一些特性。比如抓取一把枪视为 Grab,然后按下枪的扳机发射子弹则视为 Use。
接下来我会详细讲解这三种类型的用法。那么首先我们需要拥有可交互对象和发起交互的对象,让交互的条件成立,然后再具体实现交互的类型。
📕发起交互的对象(Interactor)
⭐XR Direct Interactor 脚本
因为本教程场景中的 XR Origin 沿用了上一篇 UI 教程中的游戏物体,所以我们先回顾一下 XR Origin 目前的结构:
类似的,我们很容易想到在 LeftHand Controller 和 RightHand Controller 下创建子物体,然后添加 XR Controller (Action-based) 脚本(关闭 Enable Input Tracking)和专门负责抓取的脚本。
为了让双手成为发起交互的对象,我们需要用到 XR Direct Interactor 脚本。
但是,如果把子物体上 XR Controller 的 Tracking 关闭,同个物体上的 XR Direct Interactor 将失去作用。这是因为抓取的时候也是需要判断手部的姿态,然而关闭了追踪与其产生了矛盾。
因为作为父物体的 LeftHand/RightHand Controller 上的 XR Controller 开启了追踪,所以我们只能将 XR Direct Interactor 挂载到父物体上。
XR Interaction Toolkit 2.3.0 及以上版本可以采用的做法:
2.3.0 版本也可以用上面介绍的方法,不过它新增了一个 XR Interaction Group 脚本(https://docs.unity3d.com/Packages/com.unity.xr.interaction.toolkit@2.3/manual/xr-interaction-group.html),可以使我们 XR Origin 的层级看起来更有条理。使用方法也可以参考开发包中的样例场景,我这边再简单说明一下。
如果要使用 XR Interaction Group,我们就可以在 LeftHand Controller 和 RightHand Controller 下分别创建 Direct Interactor 子物体,将 XR Direct Interactor 和 Sphere Collider 移到这个子物体上,由 Direct Interactor 物体单独处理手部的交互,层级看起来更加清晰:
我们在作为父物体的 LeftHand Controller 和 RightHand Controller 上分别添加 XR Interaction Group 脚本,然后在 Starting Group Members 添加 Interactor:
这就相当于把原来与发起交互有关的两个组件从父物体挪到了子物体上,由父物体的 XR Interaction Group 统一控制子物体上的 Interactor。可能有的小伙伴会有疑问:为什么这时子物体无需添加 XR Controller 呢?
因为父物体的 XR Controller 绑定的 InputAction 和手部发起交互的 Input Action 是一样的。并且父物体的 XR Interaction Group 充当了 Interactor 的管理者,让原本父物体上的 XR Controller 统一控制组内的 Interactor。有了 XR Interaction Group 脚本,我们就可以用一个物体的 XR Controller 集中控制多个物体的 Interactor。
不过像传送这种 XR Controller 绑定的 Input Action 和父物体略微有些不同的情况,就单独在子物体上添加 XR Controller(Action-based),并且关闭 Enable Input Tracking,让它不受 XR Interaction Group 管理。
⭐添加可交互区域
XR Direct Interactor 需要一个 Trigger 类型的碰撞体,作为可交互的区域,并且这个碰撞体需要和 XR Direct Interactor 挂载到同一个游戏物体上。因此我这边直接在 LeftHand Controller 和 RightHand Controller 上分别添加一个 Sphere Collider,调整 Radius,并且把 Is Trigger 勾选。那么当可交互对象进入这个可交互区域后,就可以进行交互。
📕可交互的对象(Interactable)
首先我们创建一个可交互的物品,我这边用红色方块来表示。
⭐添加刚体
因为手与物品的交互基本上是基于物理效果的,所以我们要为可交互的物体添加刚体。
⭐XR Simple Interactable 脚本
最简单的可交互脚本就是 XR Simple Interactable,但是它没有自带抓取的功能。
我们在方块上添加 XR Simple Interactable 脚本:
虽然 XR Simple Interactable 的官方文档没有说明需要添加刚体,但是实测后发现物体有了刚体后,XR Simple Interactable 脚本才会生效。
另外要注意的是,挂载可交互脚本的游戏物体还需要一个碰撞体。游戏运行后,这个碰撞体会自动赋给 XR Simple Interactable 的 Colliders 数组。
⭐Interactable Events
为了让 XR Simple Interactable 的效果可视化,我们可以添加几个功能:
1)当手触碰到方块时,方块的颜色发生改变。而这个功能也是 VR 交互方式中很常见的 Hover,即悬停在物品的可交互区域。
2)触碰到方块后,按下手柄 Grip 键,方块的颜色变成蓝色。
3)触碰到方块后,按住手柄的 Grip 键,再按下手柄的 Trigger 键,方块的颜色变回红色。
以上的三个功能也分别模拟了 VR 中常见的三种交互方式:悬停(Hover)、抓取(Grab)、使用(Use)。但是因为 XR Simple Interactable 脚本的局限性,我们不能真正将物品抓起来,而只能模拟 “抓取” 发生的事件。
这时候就要用到 XR Simple Interactable 脚本中的 Interactable Events,里面包含了交互时会发生的一些事件,可以看到里面有一个 Hover Entered,也就是开始悬停在可交互区域触发的事件。然后我们可以手动设置更改方块的材质,我这边想让方块被触碰后变成黄色。
(也许有小伙伴会注意到 First Hover Entered。它的作用是只能某个手占用了相应的事件,比如一只手 Hover 的时候,另一个手过来 Hover 就不会触发这个事件。后面我们会看到的 First Select Entered 也是类似的道理)
然后,我们要实现的第二个和第三个功能分别对应了 Select Entered 和 Activated 这两个事件。同样地,我们在 Inspector 面板中手动绑定事件:
看到 Select 和 Activate,是不是觉得有些眼熟呢?我们打开 XR Interaction Toolkit 中自带的输入配置文件 XRI Default Input Actions:
可以看到 XRI LeftHand Interaction 或者 XRI RightHand Interaction 下就有 Select 和 Activate。Select 动作绑定的是 “按下 Grip 键” 这个操作,Activate 动作绑定的是 “按下 Trigger 键” 这个操作。
而 Interaction Events 中的 Select 和 Activate 使用的就是 XRI Default Input Actions 配置文件中的这两个动作,并且要注意的是:Activate 动作必须要以 Select 动作的发生为前提,Interactable Events 中的所有事件都是以“与可交互对象发生了交互”为前提。因此,当我们的手触碰到方块后按下手柄的 Grip 键,视为发生了 Select 动作,触发了 “方块的颜色变成蓝色” 这个事件;在手触碰到方块后按下手柄的 Grip 键的前提下,继续按下 Trigger 键,视为发生了 Activate 动作,触发了 “方块的颜色变成红色” 这个事件。
这时候,也许有人会有疑问:在上一篇 UI Demo 中,我们也有用到 Activate 这个动作,作用是按下 Trigger 键与 UI 进行交互。虽然 XR Controller 中的 Select Action 绑定的是 Select 这个动作,正常来说得先按下Grip 键才能触发 Select 动作。可是实际上当 UI 射线射到 UI 上时,射线的颜色变成了白色,说明此时已经进入了选中的状态。我们为什么不需要先按下 Grip 键呢?
这是因为 Canvas 上的 Tracked Device Graphic Raycaster 脚本的特性。这个脚本能让 UI 被射线响应,当射线射到 UI 上时,自动进入选中的状态,也就是触发了 Select 动作。然后在 Select 动作发生的前提下,我们按下 Trigger 键就能与 UI 进行交互。因此 UI Demo 中的 Activate 也是以 Select 为前提。
⭐XR Grab Interactable 脚本
给可交互的物体添加上这个脚本,就能实现真正的抓取。因为抓取对应的是 XRI Default Input Actions 配置文件中的 Select 动作,而 Select 动作绑定的是 “按下手柄的 Grip 键” 这个操作,所以当手部靠近可交互物体时,按下手柄的 Grip 键就能抓取物体。
注:要想使用 XR Grab Interactable 脚本,必须给物体添加刚体组件。不过即使之前没有刚体,添加 XR Grab Interactable 脚本也会自动给物体加上刚体。
⚡Movement Type(Instantaneous, Kinematic,Velocity Tracking )
在 XR Grab Interactable 脚本中,比较重要的是三种 Movement Type:Instantaneous, Kinematic 和 Velocity Tracking
为了以示区分,我这边创建了三个挂载 XR Grab Interactable 脚本的方块,红色方块对应 Instantaneous,黑色方块对应 Kinematic,绿色方块对应 Velocity Tracking
Instantaneous:
物体的移动位置和姿态完全跟随了 Interactor(手)的移动。它是通过在每一帧更新 Position 和 Rotation 进行移动,所以看上去物体跟随手部的移动是几乎没有延迟的。但是这种移动方式没有运用物理刚体的效果(即使物体上有刚体)。此时抓取的物体会穿过带有碰撞体的桌子,并且和其他刚体方块的碰撞效果也不是基于物理的。
Kinematic:
通过 Kinematic Rigidbody(运动刚体)进行移动,跟随手部移动的过程中会有一些延迟。移动过程中物体不受力和碰撞的作用。所以此时物体触碰其他碰撞体不会受到反作用力,比如物体还是能穿过带有碰撞体的桌子。但是可以对其他刚体(Kinematic Rigidbody 除外)施加物理效果,比如此时移动的 Kinematic 方块能够推动其他放置在桌子上的方块。
Velocity Tracking:
通过设置刚体的速度和角速度进行移动。跟随手部移动的过程中会有一些延迟。移动过程中带有刚体的物理效果,比如会和带有碰撞体的桌子发生碰撞,也可以对其他刚体产生力的效果。
⚡Attach Transform 抓取点
XR Grab Interactable 脚本中有一个 Attach Transform 变量可以赋值,作为物体的抓取点。如果没有赋值,将默认以物体的 position 作为抓取点。
这个变量有时候很有用。比如抓取一把枪,那么物品的抓取点应该位于枪柄上。
现在我们造一把简易的枪,来看看 Attach Transform 怎么使用,当然大家也可以用自己的模型资源。
然后添加碰撞体,刚体以及 XR Grab Interactable 脚本。这里我先把 Movement Type 设为 Instantaneous。此时如果运行程序,会发现抓取枪的抓取点不是我们想要的样子。
这时候,Attach Transform 就派上了用场。我们可以在枪的游戏物体下创建一个子物体,叫做 Attach Point。
然后将 Attach Point 赋给 Attach Transform:
接下来我们运行程序,动态调整 Attach Point 的 Position 和 Rotation,直到手能握住枪柄,复制 Attach Point 的 Transform组件,退出程序后将其粘贴至 Attach Point 原来的 Transform 组件。
现在,抓取点的效果看起来稍微好了一点,大家也可以自行优化。但是这里还有一个问题,刚刚的 Attach Point 对应的是右手的抓取点,如果我用左手抓取枪,会发现 Attach Point 的位置是不对的。具体的解决方法我会放在下一个部分的 “优化一:左右手抓取” 进行详细说明。
值得注意的是,仅设置 Attach Transform 只是粗略地抓取。因为它在物理效果上显得不是很真实,可以看到物体和手之间还是有穿模的现象。如果要实现精细地抓取,会稍微麻烦一点,今后也会出相应的教程进行详细说明。
关于 XR Grab Interactable 脚本的其他变量设置,大家可以参考官方文档:
https://docs.unity3d.com/Packages/com.unity.xr.interaction.toolkit@2.1/manual/xr-grab-interactable.html
⚡代码实现 Use 功能(制作简易手枪)
接着上面枪的例子,我们要怎么实现抓起枪后按下手柄 Trigger 键进行射击呢?这个功能实际上就是 VR 交互中的 Use 功能。
联想 XR Simple Interactable 和 Interactable Events 的部分,因为 XR Grab Interactable 脚本中也有一模一样的 Interactable Events,所以我们可以在 Inspector 面板中绑定 Activate 触发时的事件。不过,现在我想展示如何用代码来进行 Interactable Events 事件的绑定。
🔍核心脚本
我们创建一个脚本,叫做 GunController,把它挂载到枪的游戏物体上。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.XR.Interaction.Toolkit;
public class GunController : MonoBehaviour
public GameObject bullet;
public Transform spawnPoint;
public float fireSpeed = 40;
void Start()
XRGrabInteractable grabbable = GetComponent<XRGrabInteractable>();
grabbable.activated.AddListener(FireBullet);
private void FireBullet(ActivateEventArgs arg)
GameObject spawnBullet = Instantiate(bullet,spawnPoint.position,spawnPoint.rotation);
spawnBullet.GetComponent<Rigidbody>().velocity = spawnPoint.forward * fireSpeed;
Destroy(spawnBullet,5);
其中最重要的部分就是获取 XR Grab Interactable 中的 activated 事件,然后通过 AddListener 绑定事件触发的函数。
🔍制作子弹(碰撞检测方式设为 Continous Dynamic)
然后我们可以创建一个子弹的 Prefab(需要刚体和碰撞体):
这里有个小坑需要注意一下,就是我们最好要把子弹 Rigidbody 的 Collision Detection 设为 Continous Dynamic,否则因为子弹是高速运动的,有时候会检测不到和刚体的碰撞,造成子弹直接从物体中间穿过去。
🔍制作子弹发射位置
再创建一个枪的子物体,叫做 Spawn Point,作为子弹生成的位置。该物体的 z 轴箭头方向对应子弹发射的方向。
然后把子弹和 Spawn Point 赋给 Gun Controller:
这时候,枪的功能就制作好了。我们试着运行程序:
大功告成!😊
🔍优化一:左右手抓取(判断哪只手与物体交互)
到目前为止,我们还有一个问题没有被解决,就是枪的 Attach Transform 的为止只适用于右手的抓取。我们希望左右手的抓取点都是正确的,但是枪的 XR Grab Interactable 脚本只能有一个 Attach Transform。那么我们其实可以动态地去切换左右手对应的 Attach Transform。
首先,我们要准备好左右手对应的 Attach Point,作为切换用的 Attach Transform:
然后,我们需要修改之前写的 GunController 脚本 ( 方法一):
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.XR.Interaction.Toolkit;
public class GunController : MonoBehaviour
public GameObject bullet;
public Transform spawnPoint;
public float fireSpeed = 40;
private Transform leftHandAttachPoint;
private Transform rightHandAttachPoint;
private XRGrabInteractable grabbable;
void Start()
leftHandAttachPoint = transform.Find("LeftHand Attach Point");
rightHandAttachPoint = transform.Find("RightHand Attach Point");
grabbable = GetComponent<XRGrabInteractable>();
grabbable.selectEntered.AddListener(ChangeAttachTransform);
grabbable.activated.AddListener(FireBullet);
private void ChangeAttachTransform(SelectEnterEventArgs arg)
Transform interactor = arg.interactorObject.transform;
if (interactor.name == "LeftHand Controller")
grabbable.attachTransform = leftHandAttachPoint;
else if (interactor.name == "RightHand Controller")
grabbable.attachTransform = rightHandAttachPoint;
private void FireBullet(ActivateEventArgs arg)
GameObject spawnBullet = Instantiate(bullet,spawnPoint.position,spawnPoint.rotation);
spawnBullet.GetComponent<Rigidbody>().velocity = spawnPoint.forward * fireSpeed;
Destroy(spawnBullet,5);
核心思想就是给 XR Grab Interactable 的 Select Entered 事件绑定事件触发的函数,通过判断是哪一个 Interactor 来决定切换成哪一个 Attach Transform。
刚刚我们将左右手切换抓取的功能写在了枪的控制器中,但是这样代码的耦合性可能会比较高,因为除了枪,可能还会有其他的物体可以用左右手切换抓取。如果想要让左右手抓取的脚本更为通用,我们可以新建一个脚本继承 XR Grab Interactable (方法二):
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.XR.Interaction.Toolkit;
public class XRGrabInteractableTwoAttach : XRGrabInteractable
public Transform leftAttachTransform;
public Transform rightAttachTransform;
protected override void OnSelectEntered(SelectEnterEventArgs args)
if(args.interactorObject.transform.CompareTag("Left Hand"))
attachTransform = leftAttachTransform;
else if(args.interactorObject.transform.CompareTag("Right Hand"))
attachTransform = rightAttachTransform;
base.OnSelectEntered(args);
核心思想类似,我们是重写了 XR Grab Interactable 当中的 OnSelectEntered 方法,它会在 Select Entered 事件触发时被调用。
然后,我们把 Gun 上的 XR Grab Interactable 脚本替换成 XRGrabInteractableTwoAttach,而之前 GunController 当中和左右手抓取相关的代码就可以删除了。
另外,我这边将左右手 Attach Transform 的赋值改为了拖动赋值,并且用 Tag 来判断是哪一个 Interactor,所以不要忘了在编辑器中进行赋值,并且给 LeftHand Controller 和 RightHand Controller 加上 Tag 哦。
最终效果:
🔍优化二:防止传送射线误触发远距离抓取(Interaction Layer Mask)
如果大家的项目中沿用了传送的功能,那么会发现一个 BUG:当向前推动手柄摇杆发射传送射线的时候,如果射线刚好射到可交互的物品上,这个物品会 “附着”在射线上,跟随射线移动,效果就和抓取一样,我们称这种现象为远距离抓取,它是由射线来控制抓取,这一部分我会在下一篇教程中进行详细说明。
因为 XR Grab Interactable 的抓取对应的是 Select 这个动作,而激活传送射线也是对应 Select ,所以此时我们是误打误撞触发了远距离抓取,因此我们要想办法让传送射线不触发远距离抓取。实现方式也很简单,我们需要对之前传送的相关组件进行一些设置。
首先我们找到负责发射传送射线的 XR Ray Interactor 脚本,将关注点放到 Interaction Layer Mask 上。
Interaction Layer Mask 相当于交互的过滤器,表示交互的层级。当发起交互的对象(Interactor) 和可交互的对象(Interactable)的 Interaction Layer Mask 至少有一个是相同的,那么它们是可以被交互的。
XR Ray Interactor 作为 Interactor,它的层级是 Everything。然后我们找到作为 Interactable 的 Teleport Area,可以看到它的层级是 Default。
因为 Default 包含在 Everything 中,所以满足 Interactor 和 Interactable 的 Interaction Layer Mask 至少有一个是相同的条件,即二者可以交互。
我们再看手与物品交互过程中的 XR Direct Interactor 和 XR Grab Interactable,它们的层级也分别是 Everything 和 Default。所以物品的 Default 也包含在传送射线的 Everything 中,传送的射线是能够与物品进行交互的。
那么我们可以单独为传送设置一个层级。我们点击 XR Ray Interactor 的 Interaction Layer Mask,再选择 Add Layer,我们新建一个 Teleport 层级:
然后将与传送有关的 XR Ray Interactor 和 Teleport Area 的 Interaction Layer Mask 改成 Teleport:
然后把 XR Direct Interactor 的 Interaction Layer Mask 改为 Default,这样用于抓取的 Interactor 和 Interactable 的层级是相同的,不会对其他的交互方式产生干扰。当然,我们也可以为抓取单独设立一个 Interaction Layer Mask。重要的是搞清楚 Interaction Layer Mask 的作用。
现在再次运行程序,这个 BUG 就消失啦!此时传送射线就只能和 Teleport Area 进行交互。😊
⚡其他功能一:将与物体接触的地方作为抓取点(Dynamic Attach)
我们之前介绍的抓取功能通过设置 Attach Transform,确定了物体被抓取后的位置与朝向。可以发现无论从物体上的哪个部位进行抓取,被抓取后的姿态都是相同的。
有时候,我们并不想要这种抓取方式,而是希望手能直接抓在抓取的部位上。XR Grab Interactable 脚本也提供了这个功能。
现在,我创建一个细长的 Cube,作为该功能的测试物体。
添加碰撞体,刚体。然后添加 XR Grab Interactable 脚本,勾选其中的 Use Dynamic Attach :
勾选后会自动跳出 Match Position (匹配抓取时的 Position),Match Rotation(匹配抓取时的 Rotation),Snap To Collider(抓在物体的碰撞体上)。这些选项可以根据需求进行更改。
效果:
⭐XR Tint Interactable Visual 脚本
这个脚本可以挂载到可交互对象上,当发起交互的对象(Interactor)悬停(Hover)或者选中(Select 动作触发,对应 Input System 中设置的 Select 映射关系,一般和 “按下手柄 Grip 键” 进行绑定)可交互的对象时,能够暂时改变可交互对象的颜色。
调整 Tint Color 能够设置想要改变的颜色;勾选 Tint On Hover 能够在 Hover 的时候改变颜色;勾选 Tint On Selection 能够在 Select 的时候改变颜色。
Unity VR开发教程 OpenXR+XR Interaction Toolkit 传送
文章目录
![](https://image.cha138.com/20230404/99a44642edd442788f83c2796f02a0ee.jpg)
往期回顾:
Unity VR开发教程 OpenXR+XR Interaction Toolkit (一) 安装和配置
Unity VR开发教程 OpenXR+XR Interaction Toolkit (二) 手部动画
Unity VR开发教程 OpenXR+XR Interaction Toolkit (三) 转向和移动
上一篇教程中,我们学习了如何用手柄来控制转向和持续移动。在 VR 应用中,除了持续移动还有另一种比较常见的移动方式,那就是 “传送”,即让玩家直接到达想要传送的地点 。相较于持续移动,传送给人带来的晕动感不会那么强烈。本篇教程,我们一起来学习如何实现传送功能。
📕教程说明
使用的 Unity 版本: 2020.3.36
使用的 VR 头显: Oculus Quest 2
教程使用的 XR Interaction Toolkit 版本:2.1.1
前期的配置:环境配置参考教程一,手部模型参考教程二
项目源码(持续更新):https://github.com/YY-nb/Unity_XRInteractionToolkit_Tutorial2022/tree/master
最终实现的效果:向前推动手柄的摇杆然后释放,实现人物的传送。
📕添加触发传送的脚本
传送其实也是交互的一种。而交互的过程一般需要两个对象,一个是可交互的对象(Interactable),一个是发起交互的对象(Interactor,一般是玩家自己)。传送的过程也是如此,需要可传送的区域和触发传送的人。现在,我们先来介绍与触发传送相关的脚本。
Locomotion System:可以直接控制场景中的 XR Origin(在 VR 中就是我们自己的身体),从而实现 VR 中人物的运动。
Teleportation Provider:负责传送功能。
XR Ray Interactor:通过射线检测实现与物体的远距离交互,在传送时可以配合可视化的射线使用。
Line Renderer:用于渲染传送时的射线
XR Interactor Line Visual:搭配 Line Renderer 和 XR Ray Interacter,使准备传送的射线可视化。
另外 XR Controller(Action-based) 这个脚本也是必备的,它用于跟踪手柄的姿态和处理手柄输入和动作的绑定。而我们的传送功能也是通过手柄的按键触发的。(这个系列中的每一篇教程都会用到它,这篇教程再次强调它是因为待会儿要对这个脚本上的一些设置做些修改)
如果你跟着此系列教程的前三篇做了一遍,那么这些脚本其实就已经添加好了。我这里直接将上一篇教程中的大部分场景沿用下来,此时 Hierarchy 窗口如下图所示:
不过这里还是再简要地提一下:最底下的 Locomotion System 游戏物体可以直接在 Hierarchy 面板中的 XR 下创建。
添加了这个游戏物体就会自带 Teleportation Provider 和 Locomotion System 脚本。
而 XR Controller,XR Ray Interactor,Line Renderer 和 XR Interactor Line Visual 脚本在 XR Origin 下的 LeftHand Controller 和 RightHand Controller 上。
虽然这些脚本可以让 Unity 自动帮我们创建好,但是还是希望大家能够记住需要用到哪些脚本,以及这些脚本大致的作用,以便日后能够根据我们的需求灵活使用。
最后,我们还需要对 XR Ray Interactor 脚本进行一些操作:
在 Inspector 面板中将 Keep Selected Target 取消勾选。否则当你选中了一个传送区域后,你无法切换到另一个传送区域。
接下来,我们来介绍一下传送需要的可交互对象。
📕添加传送区域脚本
⭐Teleportation Area 脚本
我们可以在地面(在本教程的场景中是 Plane)添加 Teleportation Area 脚本。
注:地面必须要有碰撞体,且碰撞体不能设为 Is Trigger,否则无法检测到传送区域
如果没有给此脚本的 Colliders 手动赋值,那么它会找到任意一个子物体(包括自己)的碰撞体,将该碰撞体用于检测是否选中了传送区域,如果触发传送的射线射到了碰撞体上,则视为选中了传送区域。
这时候我们可以运行一下程序。可以看到手部会发出一条直线。当直线射到地面上时,射线颜色会变成白色,因为这时候我们选中了传送区域;当直线没有射到地面上,射线的颜色会变成红色。
按下手柄的 Grip 键,传送就会生效。至于为什么是按下 Grip 键才能传送,在稍后的 “向前推动手柄摇杆实现传送” 这一部分会进行详细地说明。
⭐Teleportation Anchor 脚本
Teleportation Anchor 可以理解成传送的一个目标点,它能使玩家传送到一个指定的位置和角度。
为了演示,我创建了一个新的 Plane,然后创建一个空物体叫做 “Teleportation Anchor",作为传送目标点,然后创建一个它的 Cylinder 子物体来表示可传送的区域,并且将 Cylinder 的碰撞体用于判断是否选中了传送区域。接着给 Teleportation Anchor 游戏物体添加 Teleportation Anchor 脚本。
为了演示能传送到特定角度的功能,我将传送目标点的旋转角度调了一下,让它 z 轴角度和相机 z 轴角度不相同。
相较于 Teleportation Area,Teleportation Anchor 多了一个重要的变量:Teleport Anchor Transform。这个变量就决定了传送的目标点是哪一个,默认是这个脚本所挂载的那个游戏物体,当然你也可以自定义一个传送点。
这个时候我们试着运行程序,可以发现,当手部的射线射到我们设定的圆柱体上时,射线颜色才会变成白色,说明我们选中了传送区域。然后我们按下手柄的 Grip 键,我们就会被传送到圆柱体的那个位置,准确来说就是我们提前设定的 Teleportation Anchor 的位置(可能会有一点点的偏差),尝试多次传送到 Teleportation Anchor,每次传送的位置都是非常相近的,至少都能传送到我们指定的区域。
不过我们可以发现,虽然每次传送的 Position 是相近的,但是传送后人物面朝向的角度却还是传送前面朝向的那个角度。而我们的需求是传送后面朝向指定的角度,也就是传送后眼睛看向的方向和 Teleportation Anchor 的 z 轴所指向的方向一样,那这要怎么实现呢?
回到 Teleportation Anchor 脚本,找到 Match Orientation,改成 Target Up and Forward,意思是传送到目标点后的角度以目标点正上方为 y 轴,目标点正前方为 z 轴,这就与我们的需求匹配了。
现在我们再运行一下程序看看效果:
现在就成功传送到了特定的位置,特定的角度。
📕向前推动手柄摇杆实现传送
值得注意的是,目前是只有按下手柄的 Grip 键,传送才会生效。那么是在什么地方定义了 “按下 Grip 键开始传送” 呢?
我们来看 XR Controller 这个脚本(以左手为例),原因就出在这个 Select Action 当中:
我们打开这个 Reference,可以看到 Select 这个动作绑定的是 “Grip 键按下” 这个操作:
但是,按照大多数 VR 游戏的习惯,传送一般是向前推动手柄摇杆的时候被激活,然后释放摇杆进行触发,那么要怎么更改成这种方式来触发传送呢?
其实在 Input Action Asset 里,就有相关的动作。我们找到 XRI LeftHand Locomotion 的 Teleport Select 或者 Teleport Mode Activate,这两个动作都绑定了 Primary2DAxis,它表示的是摇杆的坐标位置(把摇杆的推动范围看作一个坐标系,不动摇杆的时候摇杆位于原点,推动摇杆后摇杆在 x 和 y 轴上的位置会发生偏移。
它们的区别可以看界面最右边的 Binding Properties
Teleport Select:
Teleport Mode Activate:
可以观察上面两幅图用红框标出的部分,最显著的区别是这个 Directions。
Teleport Select 是 Everything,意为摇杆推向任何方向都能激活传送。
Teleport Mode Activate 是 North,意为摇杆向北(向前)推才能激活传送。
因为我们想要通过向前推动摇杆来激活传送,所以我们将要选择的是 Teleport Mode Activate。然后,我们把 XR Controller 中的 Select Action 和 Select Action Value 都换成 Teleport Mode Activate:
接下来可以运行程序,选中了传送区域后,向前推动摇杆,再松开摇杆,人物就能够传送了!😊
📕让传送的射线变成曲线
在大多数游戏中,传送时的射线是曲线,或者是贝塞尔曲线。我们可以更改 XR Ray Interactor 脚本的配置来实现这个需求。
我们可以选择 Projectile Curve 或者 Bezier Curve,它们都能实现曲线的效果,只不过底层原理会有些不同。我这边选择 Projectile Curve,然后将 Velocity 减小到了 8,Velocity 越大,曲线射到的距离就越远。感兴趣的小伙伴也可以调试一下曲线的其他数值以及贝塞尔曲线的各个数值,调出适合自己的曲线。
然后运行程序,可以看到射线变成了曲线:
📕在射线末端添加辅助瞄准区域
英文的说法是添加 Reticle,Reticle 有十字线、瞄准线的意思,大家可以联想瞄准镜的样子。
在大部分 VR 游戏中,传送的射线末端一般会有一个圆形的区域,也就是这个 Reticle,用于辅助瞄准,这样让传送看起来更直观。
那么我们就来简单地制作一个 Reticle。我创建一个 Cylinder 来表示 Reticle,然后更改缩放值,移除掉它的碰撞体。
⭐法一:在 XR Interactor Line Visual 脚本上添加 Reticle
然后把这个游戏物体或者它的预制体拖到 XR Interactor Line Visual 的 Reticle 当中。注意,一个 Reticle 物体只能绑定一个 XR Interactor Line Visual 脚本,如果想让两只手的传送都具有 Reticle,需要把这个 Reticle 复制一份,然后将复制的 Reticle 拖给另一边手柄控制器的 XR Interactor Line Visual 脚本中。
因为大部分 VR 游戏是只用一边手柄来触发传送,所以我这里先规定左手负责传送,因此我把右手的传送功能关闭,只需把 XR Ray Interactor 脚本关闭就行了。那么我们来看一下实际的效果:
射线末端这个圆形的区域,就是我们自己添加的 Reticle。
⭐法二:在 Teleportation Area 或者 Teleportation Anchor 脚本上添加 Reticle
如图所示, 将我们准备好的 Reticle 赋到 Custom Reticle 处,最终呈现的效果和法一是一样的:
📕实现向前推动摇杆才能显示传送射线
大部分 VR 游戏中,只有向前推动手柄的摇杆,才会显示传送的射线,然后释放手柄的摇杆,传送射线消失。
但是目前为止我们的 Demo 是在程序一运行的时候就会显示传送的射线。不过因为射线是由 XR Ray Interactor 控制的,所以改进的思路其实也比较简单,就是我们可以自定义一个脚本,去控制 XR Ray Interacter 脚本的开启和关闭。因为射线的打开和关闭是实时检测的,所以我们应该每一帧去判断手柄是否触发了传送,以及一个传送的过程是否结束。
2023.1.18更新:
此时 XR Interaction Toolkit 已经更新到了 2.3.0 pre 版本。经尝试,控制 XR Ray Interacter 脚本的开启和关闭的思路已经失效,即使能控制射线的打开和关闭,但是无法触发传送。因此,为了能向上兼容,我们换一种思路:去控制 XR Ray Interacter 脚本所在游戏物体的显示和隐藏。
因为输入的动作是基于 Input System的,所以需要一些 Input System 方面的知识。
脚本代码如下:
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.XR.Interaction.Toolkit;
public class TeleportationController : MonoBehaviour
public InputActionProperty m_teleportModeActivate;
public InputActionProperty m_teleportModeCancel;
private InputAction teleportModeActivate;
private InputAction teleportModeCancel;
public XRRayInteractor teleportInteractor;
void Start()
teleportModeActivate = m_teleportModeActivate.action;
teleportModeCancel = m_teleportModeCancel.action;
EnableAction();
private void OnDestroy()
DisableAction();
void Update()
if (CanEnterTeleport())
SetTeleportController(true);
return;
if (CanExitTeleport())
SetTeleportController(false);
return;
private void SetTeleportController(bool isEnable)
if (teleportInteractor != null)
teleportInteractor.gameObject.SetActive(isEnable);
private bool CanEnterTeleport()
bool isTriggerTeleport = teleportModeActivate != null && teleportModeActivate.triggered;
bool isCancelTeleport = teleportModeCancel != null && teleportModeCancel.triggered;
return isTriggerTeleport && !isCancelTeleport; //判断是否触发传送且没有按下取消传送的键
private bool CanExitTeleport()
bool isCancelTeleport = teleportModeCancel != null && teleportModeCancel.triggered;
bool isReleaseTeleport = teleportModeActivate != null && teleportModeActivate.phase == InputActionPhase.Waiting;
return isCancelTeleport || isReleaseTeleport; //判断是否按下取消传送的键或者释放了之前推动的摇杆
private void EnableAction()
if (teleportModeActivate != null && teleportModeActivate.enabled)
teleportModeActivate.Enable();
private void DisableAction()
if (teleportModeActivate != null && teleportModeActivate.enabled)
teleportModeActivate.Disable();
脚本解释:
- Input Action Asset 中有一个 Teleport Mode Cancel 的动作,绑定的是“按下 Grip 键”这个操作,也就是按下手柄的 Grip 键可以取消传送,我们也将这个功能加入脚本。
- 触发传送(显示传送射线)的条件:向前推动手柄摇杆,并且没有按下 Grip 键
- 不显示传送射线的条件:按下 Grip 键或者手柄的摇杆处于原始的位置(向前推动摇杆并且释放最终也是回到了原始位置)
因为是控制游戏物体的显隐,所以 Teleportation Controller 脚本和 XR Ray Interactor 脚本不能挂载到同一个物体上。否则游戏物体隐藏时无法调用 Teleportation Controller 的功能。
因此,我们稍微改变一下 XR Origin 游戏物体的层级结构:
首先在 LeftHand Controller 或者 RightHand Controller 下创建 Teleport Interactor 物体(这取决于你设定哪只手负责传送,我这边为了日后拓展,干脆先在两边上都添加上这个物体,实际上我设定的是左手负责传送),先把 Teleport Interactor 物体隐藏掉,它的显示与隐藏会由我们刚写的 Teleport Controller 脚本进行控制。然后将之前教程中与传送有关的脚本挪到 Teleport Interactor 游戏物体上。
接下来我们要在 LeftHand Controller 上添加两个脚本(因为我这边设定左手负责传送),如下图所示:
解释一下,就是把刚刚写的 Teleportation Controller 脚本挂载到 LeftHand Controller 上,将 Input Action Asset 里的 Teleport Mode Activate 和 Teleport Mode Cancel 赋上。把手部的模型作为 LeftHand Controller 的子物体。需要注意的是,手部模型的父物体必须要有 XR Controller (Action-based) 脚本,且要开启 Enable Input Tracking 。因为需要用到设备输入的游戏物体需要 XR Controller 脚本的支持,并且打开 Tracking 才能正确追踪手柄控制器的姿态,从而让作为子物体的手部模型能跟着手柄运动,所以我们需要给 LeftHand Controller 物体额外添加一个 XR Controller(Action-based)脚本,其中的动作配置先用默认的就行,与之前负责传送的 XR Controller 不同。
这个时候,LeftHand Controller 和 Teleport Interactor 都有 XR Controller (Action-based) 脚本,如果我们运行程序,会发现传送射线的起始点位置不对。这是因为父子物体上的 XR Controller 都开启了 Tracking 导致的冲突。因此如下图所示,我们需要点击子物体 Teleport Interactor 上的 XR Controller (Action-based),把 Enable Input Tracking 取消勾选。
而且以后在使用其他功能的时候,只要父子物体都有挂载 XR Controller (Action-based) 脚本,我们都需要关闭子物体 XR Controller (Action-based) 上的 Enable Input Tracking
注:我们在 Teleportation Controller 脚本中赋值的两个 Input Action Reference 作用仅仅是为了显示和隐藏射线,真正触发传送的还是 Teleport Interactor 物体上 XR Controller (Action-based) 中的 Select Action(我们之前已经设置好了)
然后我们可以运行一下程序,这时候,只有向前推动摇杆才会显示传送的射线。
但是,此刻需要关注一下选用了哪种方式在射线末端添加 Reticle。如果选用的是在 XR Interactor Line Visual 脚本上添加 Reticle,那么你会发现这个 Reticle 在没有触发传送的时候会固定在场景中的某个位置,而我们希望的是和传送射线一样,只有向前推动摇杆的时候显示 Reticle。
其中一种方式是修改脚本,获取 Reticle 的引用,然后在 SetTeleportController 方法中去控制 Reticle 的显示和隐藏。
不过,如果选用的是在 Teleportation Area 或者 Teleportation Anchor 脚本上添加 Reticle,无需修改脚本。只要把 Reticle 做成预制体赋给 Custom Reticle,在射线选中可传送区域后,就会在可传送区域的碰撞体表面生成 Reticle。
但是此时还有一个小问题,如果你照着这篇教程制作了 Teleportation Anchor,就会发现射到圆柱体表面的时候也会显示 Reticle
这看起来有一些奇怪,因为传送的 Reticle 一般是显示在传送区域的上表面。不过,这个圆柱体只是我在本篇教程的前半段演示用的。在实际的开发中, Teleportation Anchor 一般都是地面上的某一块区域,碰撞体也应该是高度非常小的一块,这个圆柱体也一般是透明的,类似于光柱的效果。因此在实际的项目中需要调试 Teleprtation Anchor 所检测的碰撞体的高度,然后 Reticle 能够显示在目标区域的上表面。
我这边将原来的圆柱体改成方体,因为方体的碰撞体更好调试一点。那么方体的上表面面积就代表了用于演示 Teleportation Anchor 的可传送区域。
最终效果:
📕美化传送射线的方法
如果你想美化传送射线,可以更改 XR Interactor Line Visual 和 Line Renderer 脚本的配置。
先来看 XR Interactor Line Visual 脚本:
常用的有调整射线宽度(Line Width),选中传送区域时射线的颜色(Valid Color Gradient),未选中传送区域时射线的颜色(Invalid Color Gradient)
而 Line Renderer 是 Unity 中原有的组件,每个设置具体是什么意思可以参考官方文档。不过在美化传送射线上经常用的是更改射线的材质,如果你想让射线的材质效果看起来更好看,可以自定义材质,然后更改下图中标出的这个部分:
那么本篇教程的内容就到此为止啦!大家快去制作属于自己的传送功能吧!🌹
以上是关于Unity VR开发教程 OpenXR+XR Interaction Toolkit (六)手与物品交互(触摸抓取)的主要内容,如果未能解决你的问题,请参考以下文章
Unity VR开发教程 OpenXR+XR Interaction Toolkit (六)手与物品交互(触摸抓取)
Unity VR开发教程 OpenXR+XR Interaction Toolkit 手部动画
Unity 使用OpenXR和XR Interaction Toolkit 开发 HTCVive(Vive Cosmos)
Unity开发OpenXR | 使用 OpenXR 制作一款简单VR示例场景 的全过程详细教程,包含两个实战案例。
Unity开发OpenXR |使用 OpenXR 添加一个运动系统,实现传送抓取功能 的简单VR示例场景 的全过程详细教程