您的位置:首页 > 移动开发 > Unity3D

用 Unity 和 HTC Vive 实现高级 VR 机制(1)

2017-08-04 15:36 471 查看
原文:Advanced VR Mechanics With Unity and the HTC Vive Part 1

作者:Eric Van de Kerckhove

译者:kmyhy

VR 从来没有这样时髦过,但是游戏不是那么好做的。为了提供真实的沉浸式体验,游戏内部机制和物理必须让人觉得非常、非常的真实,尤其当你在和游戏中的对象进行交互的时候。

在本教程的第一部分,你会学习如何创建一个可扩展的交互系统,并在系统中实现多种抓取虚拟物品的方式,并飞快地将它们扔出去。

学完本教程后,你可以拥有几个灵活的交互系统并可以用在你自己的 VR 项目中。

注意:本教程适合于高级读者,不会涉及如何添加组件、创建新的游戏对象脚本或者 C# 语法这样的东西。如果你需要提升自己的 Unity 技能,请先阅读我们的 getting started with Unity introduction to Unity Scriptin,然后在阅读本文。

开始

在本教程中,你将必须具备下列条件:

安装好 Unity 5.6.0f3(或以上)

一套带手柄的、安装好、电源开启,准备就绪的 HTV View

如果你之前没有用过 HTC Vive,你可以去看我们之前的 HTC Vive tutorial,以了解如何在 Unity 中使用 HTC Vive。HTC Vive 是目前最好的头戴式显示器之一,它所支持的 room-scale 功能提供了精彩的沉浸式体验。

下载开始项目,解压缩,用 Unity 打开项目文件夹。

在项目窗口中看一下目录结构:



分别介绍如下:

Materials: 场景中用到的材质。

Models: 本文用到的所有模型。

Prefabs: 目前只有一个预制件,用于关卡中随处可见的柱子。

Scenes:游戏画面和灯光数据。

Scripts: 有几个现成的脚本;你自己的脚本也会放到这里。

Sounds: 弓箭射出的声音。

SteamVR: 放置 SteamVR 插件及其相关脚本、预制件和示例。

Textures: 包含了几乎所有模型都共用的纹理(为了效率),以及 book 对象的纹理。

打开 Scenes 文件夹下的 Game 场景。

看一下 Game 视图,你会发现场景中缺少了相机:



在下一节,我们来解决这个问题,添加必要的东西,让 HTC Vive 能够工作。

场景设置

将 SteamVR\Prefabs 目录中将 [CameraRig] 和 [SteamVR] 预制件拖进结构视图。



摄像机现在应该是在地上,但要将它放在木塔上。将 [CameraRig] 的 position 修改为 (X:0, Y:3.35, Z:0) 。现在 Game 视图应该是这个样子:



保存场景,按 Play 按钮试一下是否顺利。四处逛逛,起码用一支手柄试试看能够看到游戏中的控制器。

如果手柄不工作,别担心!在写到此处的时候,最新版的 SteamVR 插件(版本 1.2.1)在 Unity 5.6 中有一个 bug,导致手柄的动作没有被注册。

要解决这个问题,选择 [CameraRig]/Camera (head) 下选择的 Camera (eye),然后为它添加一个 SteamVR_Update_Poses 组件:



这个脚本手动修改手柄的位置和角度。再次运行这个场景,问题解决了。

在编写任何脚本之前,看一下项目中的这几个 tag:



这几个 tag 允许我们更加容易判断哪种种对象发生碰撞或者触象。

交互系统:InteractionObject

交互系统允许场景中的玩家和物理用一种灵活的、模块化的方式进行交互。替代为每个对象和控制器编写重复的代码,你将编写几个类给其它脚本进行继承。

第一个脚本是 RWVR_InteractionObject 类;所有能够被交互的对象都应该从此类继承。这个基类中包含了几个基本的变量和方法。

注意:为了避免和 SteamVR 创建冲突或者便于搜索,本文中所有 VR 脚本都使用 RWVR 前缀。

新建文件夹 Scripts/RWVR。新建类 RWVR_InteractionObject。

打开这个脚本,删除 Start() 和 Update() 方法。

添加下列变量,就在类声明的下方:

protected Transform cachedTransform; // 1
[HideInInspector] // 2
public  RWVR_InteractionController currentController; // 3


你可能会看到报错 “RWVR_InteractionController couldn’t be found”。目前请忽略它,后面我们会创建这个类。

上面代码分别解释如下:

为了改善性能,将 tranform 值缓存。

这个属性使下面的变量在检视器窗口中不可见,哪怕它是public 的。

当前对象正在交互的手柄。后面我们会用到这个手柄。

保存脚本,回到编辑器。

在 RWVR 下面新建一个 C# 文件 RWVR_InteractionController。打开它,删除 Start() 和 Update() 方法,保存。

打开 RWVR_InteractionObject ,之前的错误消失。

注意:如果错误仍然存在,关闭代码编辑器,点一下 Unity,然后再次打开脚本。

在刚刚添加的变量后面新增 3 个方法:

public virtual void OnTriggerWasPressed(RWVR_InteractionController controller)
{
currentController = controller;
}

public virtual void OnTriggerIsBeingPressed(RWVR_InteractionController controller)
{
}

public virtual void OnTriggerWasReleased(RWVR_InteractionController controller)
{
currentController = null;
}


这 3 个方法会在手柄的扳机按下、按住和放开时调用。当手柄被按下时,controller 被赋值,当它释放时,controller 被移除。

所有方法都是虚方法,它们将在更复杂的脚本中覆盖,以便它们能使用这些控制器回调方法。

在 OnTriggerWasReleased 方法后新增方法:

public virtual void Awake()
{
cachedTransform = transform; // 1
if (!gameObject.CompareTag("InteractionObject")) // 2
{
Debug.LogWarning("This InteractionObject does not have the correct tag, setting it now.", gameObject); // 3
gameObject.tag = "InteractionObject"; // 4
}
}


分别解释如下:

缓存 transform 以改善性能。

检查 InteractionObjet 是否有指定的 tag 值。如果没有,执行 if 后面的代码。

在检视器中输出一个警告,告诉开发者忘记设置 tag。

及时设置 tag,以便对象能够像我们期望的工作。

这个交互系统严重依赖于 InteractionObject 和控制器的 tag 来区分特殊对象和其它对象。忘记设置 tag 是很可能的,所以我们专门为这个编写了脚本。这是一种“失效保险”的设计。小心使得万年船。

最后,在 Awake() 方法后添加方法:

public bool IsFree() // 1
{
return currentController == null;
}

public virtual void OnDestroy() // 2
{
if (currentController)
{
OnTriggerWasReleased(currentController);
}
}


这些方法分别负责:

一个公有的 Boolean 方法,表示当前对象是否正在被控制器所用。

当对象被销毁,将它从当前控制器(如果有的话)中释放。这有助于解决一些莫名其妙的问题。

爆粗脚本,打开 RWVR_InteractionController。

现在它还是空的。我们马上会充实它!

交互系统: Controller

控制器脚本是最重要的部分,因为它是玩家和游戏之间的直接联系。尽可能地接受输入并返回用户正确的反馈很重要。

首先,在类声明下面添加变量:

public Transform snapColliderOrigin; // 1
public GameObject ControllerModel; // 2

[HideInInspector]
public Vector3 velocity; // 3
[HideInInspector]
public Vector3 angularVelocity; // 4

private RWVR_InteractionObject objectBeingInteractedWith; // 5

private SteamVR_TrackedObject trackedObj; // 6


分段解释如下:

保存对手柄尖端的引用。后面我们会添加一个透明的球,表示你能够到触摸的位置以及距离你可以够到的地方有多远:



手柄的可见对象。上图中白色的部分。

手柄的速度和方向。可以用于计算当你做抛掷时物体如何飞出。

手柄的角度,在抛掷时计算物体的移动也会用到它。

手柄当前正在交互的 InteractionObjecdt 对象。用它来向当前对象发送事件。

用于获得真实手柄的引用。

继续在下面添加:

private SteamVR_Controller.Device Controller // 1
{
get { return SteamVR_Controller.Input((int)trackedObj.index); }
}

public RWVR_InteractionObject InteractionObject // 2
{
get { return objectBeingInteractedWith; }
}

void Awake() // 3
{
trackedObj = GetComponent<SteamVR_TrackedObject>();
}


代码解释如下:

这个变量通过 trackedObj 获得了一个对真实 SteamVR 手柄的引用。

返回和手柄进行交互的 InteractionObjecdt。对这个对象进行再次封装,是为了对其他类保持只读。

最后,保持一个和当前控制器相绑定的 TrackedObject 组件的引用,以便后面用到。

然后是这个方法:

private void CheckForInteractionObject()
{
Collider[] overlappedColliders = Physics.OverlapSphere(snapColliderOrigin.position, snapColliderOrigin.lossyScale.x / 2f); // 1

foreach (Collider overlappedCollider in overlappedColliders) // 2
{
if (overlappedCollider.CompareTag("InteractionObject") && overlappedCollider.GetComponent<RWVR_InteractionObject>().IsFree()) // 3
{
objectBeingInteractedWith = overlappedCollider.GetComponent<RWVR_InteractionObject>(); // 4
objectBeingInteractedWith.OnTriggerWasPressed(this); // 5
return; // 6
}
}
}


这个方法从控制器的碰撞体的某个范围内查找 InteractionObject。一旦找到一个,就将赋给 objectBeingInteractedWith。

代码解释如下:

创建一个碰撞体的数组,保存 OverlapSpherer() 方法找到的所有碰撞体,查找的位置和 scale 是 snapColliderOrigin,这是一个透明球体,如上图所示,我们后面会添加它。

遍历整个数组。

如果找到的碰撞体 tag 值等于 InteractionObject,同时它又是自由的,继续。

保存碰撞体的 RWVR_InteractionObject 在 objectBeingInteractedWidth。

调用 objectedBeingInteractedWith 的 OnTriggerWasPressed 方法,将当前控制器传递给它。

退出循环,完成查找。

新增方法,调用刚刚的这个方法:

void Update()
{
if (Controller.GetHairTriggerDown()) // 1
{
CheckForInteractionObject();
}

if (Controller.GetHairTrigger()) // 2
{
if (objectBeingInteractedWith)
{
objectBeingInteractedWith.OnTriggerIsBeingPressed(this);
}
}

if (Controller.GetHairTriggerUp()) // 3
{
if (objectBeingInteractedWith)
{
objectBeingInteractedWith.OnTriggerWasReleased(this);
objectBeingInteractedWith = null;
}
}
}


代码非常简单:

当扳机被按下时,调用 CheckForInteractionObject() 方法,说明有可能发生了一次交互。

当扳机被按住时,同时有一个对象被抓住时,调用这个对象的 OnTriggerIsBeingPressed()。

当扳机被松开,同时有一个对象被抓住时,调用这个对象的 OnTriggerWasReleased() 方法,并停止交互。

这些检查确保玩家的所有输入都能被传递到正在和他们交互的 InteractionObject 对象。

添加两个方法,记录控制器的速度和角速度:

private void UpdateVelocity()
{
velocity = Controller.velocity;
angularVelocity = Controller.angularVelocity;
}

void FixedUpdate()
{
UpdateVelocity();
}


FixedUpdate() 以固定帧率调用 UpdateVelocity() ,后者更新 velocity 和 angularVelocity 变量。然后,你会将这两个值传递给一个刚体,以确保扔出去的东西能够更真实的移动。

有时候需要隐藏手柄,以确保体验更加浸入式,避免遮住视线。再添加两个方法:

public void HideControllerModel()
{
ControllerModel.SetActive(false);
}

public void ShowControllerModel()
{
ControllerModel.SetActive(true);
}


这些方法简单地启用或禁用代表了控制器的 GameObject。

最后加入这两个方法:

public void Vibrate(ushort strength) // 1
{
Controller.TriggerHapticPulse(strength);
}

public void SwitchInteractionObjectTo(RWVR_InteractionObject interactionObject) // 2
{
objectBeingInteractedWith = interactionObject; // 3
objectBeingInteractedWith.OnTriggerWasPressed(this); // 4
}


代码解释如下:

这个方法造成了控制器中的压电线型驱动器(这个词不是我编造的)振动多次。它振动的时间越长,震动感就越强烈。它的强度是 1-3999。

这个方法将激活的 InteractionObject 换成参数指定的对象。

将指定的 InterationObject 变成激活状态。

在新的 InteractionObject 对象上调用 OnTriggerWasPressed() 方法,并传入当前控制器。

保存脚本,回到编辑器。为了让控制器按照我们的想法工作,还需要做一些调整。

在结构视图中选中两个控制器。它们都是[ CameraRig ]的子对象。



给它们各添加一个刚体。这允许它们使用固定连接,并和其它物体进行交互。

反选 Use Gravity,勾选 Is Kinematic。控制器不需要受物理的影响,因为在真实世界中,它们被你抓在手上。

将 RWVR_Interaction 控制器组件提交给两个手柄。我们待会要配置它。



展开 Controller(left),右键点击它,选择 3D Object > Sphere,为它添加一个球体。



选中球体,命名为 SnapOrigin,按 F 键让它在场景视图中居中。你会在地板中央看到一个巨大的白色半球体。



设置它的 Position 为 (X:0, Y:-0.045, Z:0.001) ,Scale 设为 (X:0.1, Y:0.1, Z:0.1)。这会将球放到控制器的前端。

删除 Sphere Collider 组件,因为物理检查通过代码进行。

最后,将它的 Mesh Renderer 修改为 Transparent 材质,让球体透明。



复制 SnapOrigin,将 SnapOrigin(1) 拖到 Controller(right)上,变成右手柄的子对象。命名为 SnapOrigin。

最后一步是创建控制器,使用它们的模型和 SnapOrigin。

选择并展开 Controller(left),将它的 SnapOrigin 子对象拖到 Snap Collider Origin 一栏中,将 Model 拖到 Controller Model 一栏。



在 Controller(right) 上重复同样的动作。

现在来放松一下!打开手柄电源,运行这个场景。

将手柄举到头盔前面,看看球体是否能够看见并和控制器粘在一起。



测试完后,保存场景,准备进入交互系统的使用!

用交互系统抓取物体

你可能看到附近有这些东西:



你只能看着它们,但无法把它们拿起来。你最好尽快解决这个问题,否则你怎么去读我们那本精彩的 Unity 教程呢?:]

为了和这些刚体进行交互,你需要创建一个新的 RWVR_InteractionObject 子类,用它来实现抓和扔的功能。

在 Scripts/RWVR 目录下创建新的 c# 脚本,名为 RWVR_SimpleGrab。

用代码编辑器打开它,删除里面的 Start() 和 Update() 方法。

将这一句:

public class RWVR_SimpleGrab : MonoBehaviour


修改为:

public class RWVR_SimpleGrab : RWVR_InteractionObject


这样这个类就继承了 RWVR_InteractionObject,后者提供了获得控制器输入的钩子,这样它就能对输入进行适当的处理。

在类声明下面声明几个变量:

public bool hideControllerModelOnGrab; // 1
private Rigidbody rb; // 2


很简单:

一个标志,用于表示控制器模型是否应该在该物体被拿起时隐藏。

为了性能和简单起见,缓存了刚体组件。

在变量声明之后添加方法:

public override void Awake()
{
base.Awake(); // 1
rb = GetComponent<Rigidbody>(); // 2
}


调用基类的 Awake() 方法。这会缓存对象的 Transform 组件并检查 InteractionObject 的 tag 是否赋值。

保存刚体组件,以便后面使用。

然后是一些助手方法,用于将对象用 FixedJoint 附着在手柄上,或者从手柄上放开。

在 Awake() 方法后面添加:

private void AddFixedJointToController(RWVR_InteractionController controller) // 1
{
FixedJoint fx = controller.gameObject.AddComponent<FixedJoint>();
fx.breakForce = 20000;
fx.breakTorque = 20000;
fx.connectedBody = rb;
}
private void RemoveFixedJointFromController(RWVR_InteractionController controller) // 2
{
if (controller.gameObject.GetComponent<FixedJoint>())
{
FixedJoint fx = controller.gameObject.GetComponent<FixedJoint>();
fx.connectedBody = null;
Destroy(fx);
}
}


这两个方法分别用于:

这个方法接收一个控制器作为参数,然后创建一个 FixedJoint 组件添加到手柄上,配置这个连接,使它不是那么容易掉,最后连接上当前的 InteractionObjecdt。在连接上添加一个力是为了防止用户将对象移过其他坚固的物体上,否则可能导致一些奇怪的物理问题。

将参数指定的控制器的 FixedJoint 组件(如果有的话)断开。所连接的对象将被删除,然后销毁 FixedJoint。

写完这些方法,我们可以实现来自于基类的几个 OnTrigger 方法,以处理用户输入。首先添加 OnTriggerWasPressed() 方法:

public override void OnTriggerWasPressed(RWVR_InteractionController controller) // 1
{
base.OnTriggerWasPressed(controller); // 2

if (hideControllerModelOnGrab) // 3
{
controller.HideControllerModel();
}

AddFixedJointToController(controller); // 4
}


这个方法在玩家按下扳机抓住一个对象时添加 FixedJoint 连接。代码分为几个阶段:

覆盖基类的 OnTriggerWasPressed() 方法。

如果 hideControllerModelOnGrab 标志为 true,隐藏控制器模型。

添加一个 FixedJoint 到控制器。

最后一步是添加 OnTriggerWasReleased() 方法:

public override void OnTriggerWasReleased(RWVR_InteractionController controller) // 1
{
base.OnTriggerWasReleased(controller); //2

if (hideControllerModelOnGrab) // 3
{
controller.ShowControllerModel();
}

rb.velocity = controller.velocity; // 4
rb.angularVelocity = controller.angularVelocity;

RemoveFixedJointFromController(controller); // 5
}


这个方法移除参数指定的控制器的 FixedJoint,将控制器的速度传递给刚体,以实现真实的抛掷效果。代码解释如下:

覆盖基类的 OnTriggerWasReleased() 方法。

调用基类方法解绑控制器。

如果 hideControllerModelOnGrab 标志为 true,再次显示控制器模型。

将控制器的速度和角速度传递给对象的刚体。这样当你放开对象时,对象会表现出真实的行为。例如,如果你扔出一个球,你会将手柄从后向前做一个抛物线动作。球应当获得旋转和向前的力,就像是在真实世界中你将动能传递给它一样。

删除 FixedJoint。

保存脚本,返回编辑器。

骰子和书在 Prefabs 文件夹中都有相应的预制件。在项目视图中打开这个文件夹:



选择 Book 和 Die 预制件,将 RWVR_Simple Grab 组件添加到二者。同时开启 Hide Controller Model。

保存场景运行游戏。尝试拿起几本书或骰子,扔到一边。



在下一节,我将介绍另一种抓取对象的方法:吸附。

拿起对象和吸附对象

在手柄所在的位置和角度拿起东西是可以的,但有时候将手柄吸附到物体的某个位置可能更有用。例如,如果用户看到一只枪,当他们拿起枪时会希望枪被指向右边。这就是 snapping (吸附)的意思。

为了吸附对象,你需要创建另外一个脚本。在 Scripts/RWVR 目录创建新的 C# 脚本,命名为 RWVR_SnapToController。用代码编辑器打开它,删除 Start() 和 Update() 方法。

将这句:

public class RWVR_SnapToController : MonoBehaviour


改成:

public class RWVR_SnapToController : RWVR_InteractionObject


这允许脚本具备所有 InteractionObject 的功能。

添加变量声明:

public bool hideControllerModel; // 1
public Vector3 snapPositionOffset; // 2
public Vector3 snapRotationOffset; // 3

private Rigidbody rb; // 4


一个标志,表示手柄模型是否要在玩家抓住对象时隐藏。

当抓住对象时添加的位置。该对象默认会用这个位置吸附到手柄上。

同上,只是这个变量用于表示角度。

引用了对象的刚体组件。

然后增加方法:

public override void Awake()
{
base.Awake();
rb = GetComponent<Rigidbody>();
}


和 SimpleGrab 脚本一样,覆盖了基类的 Awake() 方法,然后保存刚体组件。

接下来是几个助手方法,这才算是这个脚本的肉戏。

添加如下方法:

private void ConnectToController(RWVR_InteractionController controller) // 1
{
cachedTransform.SetParent(controller.transform); // 2

cachedTransform.rotation = controller.transform.rotation; // 3
cachedTransform.Rotate(snapRotationOffset);
cachedTransform.position = controller.snapColliderOrigin.position; // 4
cachedTransform.Translate(snapPositionOffset, Space.Self);

rb.useGravity = false; // 5
rb.isKinematic = true; // 6
}


这个方法和 SimpleGrab 脚本中的方法不同,它不使用 FixedJoint 连接,而是将它自己作为控制器的子对象。也就是说控制器和所吸附的对象是无法被外力所打断的。在这个教程中,这种方式会很稳定,但在你自己的项目中你更应该采取 FixedJoint 连接。

代码解释如下:

接收一个控制器参数,用于连接它。

将对象的 parent 设置为该控制器。

让对象的方向和控制器保持一定的偏移。

让对象的位置和控制器保持一定的偏移。

关闭重力,否则它会从你的手上掉落。

开启运动学特征。当附着到手柄上后,这个对象不会受福利引擎的影响。

现在来添加放开对象的方法:

private void ReleaseFromController(RWVR_InteractionController controller) // 1
{
cachedTransform.SetParent(null); // 2

rb.useGravity = true; // 3
rb.isKinematic = false;

rb.velocity = controller.velocity; // 4
rb.angularVelocity = controller.angularVelocity;
}


这个方法简单地将对象从父对象中解除,重置刚体并应用控制器的速度。详细解释一下:

方法参数指定要松开对象的控制器。

将对象的父对象解开。

重新打开重力,并再次使对象再次变成非运动学的。

应用控制器的速度给对象。

覆盖如下方法以实现 snapping 操作:

public override void OnTriggerWasPressed(RWVR_InteractionController controller) // 1
{
base.OnTriggerWasPressed(controller); // 2

if (hideControllerModel) // 3
{
controller.HideControllerModel();
}

ConnectToController(controller); // 4
}


代码非常简单:

覆盖 OnTriggerWasPressed(),以添加吸附逻辑。

调用机类方法。

如果 hideControllerModel 标志为 true,隐藏控制器模型。

将对象连接到控制器。

然后是 release 方法:

public override void OnTriggerWasReleased(RWVR_InteractionController controller) // 1
{
base.OnTriggerWasReleased(controller); // 2

if (hideControllerModel) // 3
{
controller.ShowControllerModel();
}

ReleaseFromController(controller); // 4
}


同样十分简单:

覆盖 OnTriggerWasReleased() 方法。

调用基类的方法。

如果 hideControllerModel 标志为 true,重新显示手柄的模型。

将对象从控制器上放开。

保存脚本返回编辑器。从 Prefabs 目录中将 RealArrow 预制件拖到结构视图。



选择 arrow,设置它的 position 为 (X:0.5, Y:4.5, Z:-0.8)。它会悬浮在石板上方:



在结构视图中,将 RWVR_Snap To Controller 组件附加到箭支上,这样你就可以和它交互,同时将它的 Hide Controller Model 设为 true。最后点击检视器窗口上方的 Apply 按钮,将修改应用到该预制件。



对于这个对象,不需要修改 offset,默认它的握持部位就可以了。

保存并运行场景。抓住箭支,然后扔出去。唤醒你内心野兽吧!



注意,箭支握在手上的位置总是固定的,不管你如何拿起它。

本教程的内容就到此为止了,试玩一下游戏,感受一下交互中的变化。

结束

此处下载最终项目。

在本教程中,你学习了如何创建可扩展的交互系统,你已经通过这个交互式系统找出了几种抓取物品的方法。

在第二部分的教程中,你将学习如何扩展这个系统,制作一套功能完备的弓和箭,以及一个功能完备的背包。

如果你想学习更多关于用 Unity 编写杀手游戏,请阅读我们的Unity Games By Tutorials

在这本书中,你将创建 4 个完整的游戏:

一个 twin-stick 射击游戏

一个第一人称射击游戏

一个塔防游戏(带 VR 支持!)

一个 2D 平台游戏

学完这本书后,你将能够编写自己的游戏运行在 Windows、macOS、iOS及更多平台。

本书完全针对 Unity 初学者,将他们的 Unity 技能升级到专家水准。本书假设你有一定的编程经验(任何语言)。

感谢你阅读本教程!如果有任何意见和建议,请留言!
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  unity VR htc-Vive