往期回顾:
Unity VR开发教程 OpenXR+XR Interaction Toolkit 2.1.1 (一) 安装和配置
Unity VR开发教程 OpenXR+XR Interaction Toolkit 2.1.1 (二) 手部动画
在之前的教程中,我们成功实现了用手柄来控制手部模型的姿势。这篇博客,我们一起来实现通过手柄控制人物的转向和移动。
使用的 Unity 版本: 2020.3.36
使用的 VR 头显: Oculus Quest 2
前期的配置:环境配置参考教程一,手部模型参考教程二
项目源码(持续更新):https://github.com/YY-nb/Unity_XRInteractionToolkit_Tutorial2022/tree/master
最终实现的效果:转动右手柄摇杆可以进行转向,转动左手柄摇杆可以进行移动,并且人物可以上楼梯。
效果图:
在一些 VR 游戏里,转动手柄的摇杆会瞬间转过一个角度,那么这个功能要怎么实现呢?
在 XR Interaction Toolkit 中,有一个 Locomotion System 专门处理人物的运动。我们可以在 Hierarchy 窗口中点击鼠标右键,如图所示选择 Locomotion System (Action-based):
可以看到新建了一个游戏物体,然后我们看看 Inspector 面板:
注:只要有 Locomotion System 和 Snap Turn Provider 这两个脚本就能实现瞬间转向,脚本挂载在任意游戏物体上都是可以的。
因为正常来说是一只手柄控制转向,另一只手柄控制移动。所以我们需要根据自己的需求将 Snap Turn Provider 脚本下的 Left Hand Snap Turn Action 和 Right Hand Snap Turn Action 的其中一个取消勾选。我这边是想让右手柄负责转向,所以把左手的 Action 禁用了:
那么现在可以运行游戏看看效果,当转动右手柄的摇杆时,视角会瞬间旋转一个角度:
(注:之后要实现上楼梯的功能,我这边先在场景中搭了一个简单的楼梯,也是作为旋转的参考物。另外我把 XR Origin 下的 LeftHand Controller 和 RightHand Controller 的 XR Ray Interactor 给禁用了,所以手不会发出射线。)
不错!此外,我们可以修改 Turn Amount 的值来调整瞬间转向的旋转角度:
有时候,我们不希望瞬间转向,而是能看到一段旋转的过程,我们就把这种需求叫做持续转向吧。XR Interaction Toolkit 也提供了相应的脚本,叫做 Continous Turn Provider,我们可以直接添加这个组件:
(注:选择 Action-based 的版本,这是官方推荐的版本,并且之后教程中用的都是 Action-based 的版本,它是基于 Input System,相比基于 XR Input Subsystem 的 Device-based,具有更高的灵活性,因为 Action-based 允许自由配置动作和按键的映射关系)
而 Turn Speed 就是持续转向时的速度,大家可以根据需求更改,感受一下。需要注意的是,如果要使用持续转向,就要把瞬间转向的脚本禁用掉。
添加 Continuous Move Provider 脚本:
根据需求我把右手的 Action 给禁用了。这时候能通过转动左手柄的摇杆实现持续移动:
可以发现此时有个问题:人物直接从楼梯中间穿了过去。这是因为此时我们还没设置人物的碰撞。说起移动过程中玩家的碰撞,我们可以使用 Unity 自带的一个脚本:Character Controller
因为 XR Origin 这个游戏物体代表了 VR 中的玩家自己,所以我们要在这个物体上添加 Character Controller 组件,然后调整一下 Center Y 和 Radius 的值:
现在,人物和楼梯间就有了碰撞,上楼梯的功能也就实现了。
但是这个时候其实还有一个问题,就是 Character Controller 的高度是固定的,不会随着头显的位置动态地调整高度和中心点,比如我想在现实中蹲下站立时,VR 中的人物碰撞体高度能进行动态地调整。好在 XR Interaction Toolkit 提供了一个脚本来解决这个问题:Character Controller Driver。它能在运动事件(包含持续移动、传送、转向)发生后根据 XR Origin 的位置调整 Character Controller 的高度。
我们在 Locomotion System 游戏物体上添加这个脚本。可能有人会有疑问:能在其他物体上添加这个脚本吗?比如把 Character Controller Driver 和 Character Controller 都挂载到 XR Origin 上。答案是可以的,只不过有一些注意点,稍后我会进行说明。
然后把 Continuous Move Provider 这个脚本(本教程中是 Locomotion System 这个物体)拖到 Locomotion Provider 这个变量上:
现在来回答之前的疑问。如果把 Character Controller Driver 这个脚本挂载到了其他物体上,那么我们就没法像上图展示的那样直接拖动脚本,而是只能把 Continuous Move Provider 这个脚本所属的游戏物体(本例中就是 Locomotion System 这个游戏物体)拖到 Locomotion Provider 上。
需要注意的是:
Continuous Move Provider , Teleportation Provider, 还有负责转向的 Snap Turn Provider 或者 Continuous Turn Provider 都是 Locomotion Provider 的子类,如果像上图展示的 Locomotion System 一样,挂载了两个及以上的 Locomotion Provider 脚本,那么就需要把 Continuous Move Provider 这个脚本移动至所有 Locomotion Provider 的最上方,如图所示:
这样,当我们把挂有多个 Locomotion Provider 脚本的物体拖给 Character Controller Driver 脚本的 Locomotion Provider 变量后,Character Controller Driver 会将组件位置处于更上方的 Continuous Move Provider 作为 Locomotion Provider。
当然,如果游戏中只需要持续移动的功能,完全可以移除 Teleportation Provider 脚本,这取决于大家自己的项目需求。
总结来说,添加了 Character Controller Driver 这个脚本后,需要确保 Continuous Move Provider 这个脚本赋给了 Character Controller Driver 的 Locomotion Provider 变量。
现在,我们试着运行程序。但是会发现仍然存在一个问题:在利用摇杆进行持续移动的过程中, Character Controller 的高度和中心会被动态调整。但是,在不动摇杆的情况下,Character Controller 并不会随着现实中头显的高度变化而发生改变。比如我只在现实中做蹲起的动作,游戏中 Character Controller 的高度和中心不会变化。
我们可以看一下 Character Controller Driver 的源码,顺藤摸瓜分析下其中的原理。
首先找到 UpdateCharacterController 这个方法,这就是改变 Character Controller 高度的核心代码。大概就是根据相机位置和 Character Controller 的参数去调整高度和中心。
那么这个方法在什么地方被调用呢?我们可以查看这个方法在什么地方被引用:
(1)获取属性时被调用(见下图),但是查看这个属性的引用时,发现它没有被引用的地方,所以至少在这个脚本里,这部分的代码不会被调用。因此这个属性的用处应该是在用户自定义的脚本里更新 Locomotion Provider。因为之前也有提到,Continuous Move Provider , Teleportation Provider, 还有负责转向的 Snap Turn Provider 或者 Continuous Turn Provider 都是 Locomotion Provider 的子类。我们目前是以 Contiunous Move Provider 作为 Locomotion Provider。之后可以通过这个属性进行 Locomotion Provider 的变更。
(2) Start 方法里被调用。这个也正常,初始化 Character Controller 的高度和中心。
(3)开始运动和结束运动时调用。
那么这两个方法又在什么地方被调用呢?我们继续查看引用,以 OnBeginLocomotion 为例:
可以看到这边是把更新 Character Controller 的方法绑定到开始运动的事件中,这个事件隶属于 LocomotionProvider。我们现在可以回看一下我之前写的 CharacterControllerDriver 脚本的作用:
能在运动事件(包含持续移动、传送、转向)发生后根据 XR Origin 的位置调整 Character Controller 的高度
所以更新 Character Controller 的方法除了初始化的时候被调用,大部分情况是只有运动事件发生时才会被调用的。但是,我们的需求是根据头显的高度实时更新 Character Controller 的高度和中心。所以,这个更新方法应该也要在 Unity 的 Update 方法里被调用。这里建议最好不要直接更改 XR Interaction Toolkit 的代码,为了有更好的拓展性,我们可以新建一个脚本去继承 CharacterControllerDriver,然后补充它的逻辑。
代码如下:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.XR.Interaction.Toolkit;
public class CustomCharacterControllerDriver : CharacterControllerDriver
{
void Update()
{
UpdateCharacterController();
}
}
直接在 Update 方法里调用 UpdateCharacterController 方法。然后我们移除原来的 CharacterControllerDriver 脚本,把这个新建的脚本加上,并且给 Locomotion Provider 赋值:
除此之外,大家可以根据自己的需求更改 Min Height 和 Max Height,它们分别代表 Character Controller 变化的最小高度和最大高度。在运行游戏的过程中,大家可以观察 Character Controller 的 Center 的数值,如果是动态更改的,那么就说明我们新建的脚本已经成功运行了。
那么到现在为止,我们的 Demo 就大功告成了,大家快去试试 VR 世界中的转向和持续移动吧!