原文:HTC Vive Tutorial for Unity
作者: Eric Van de Kerckhove
译者:kmyhy
HTC Vive 是一个虚拟现实头盔,由 HTC 和 Valve 公司制造。它提供一种在虚拟世界中的浸入式体验,而不是屏幕头像。
如果你是一个 Unity 开发者,在虚拟现实游戏中使用 HTC Vive 非常简单——你可以认为 HTC Vive 和 Unity 是天生一对。
在这篇 HTC Vive 教程中,你会学习如何在 Unity 游戏中集成 HTC Vive。包括:
在本文最后,你将对未来体验有一个粗略的了解。让我们开始吧!
注:每个人在戴着头戴式显示器都会对运动和旋转产生不同的反应。如果你是第一此穿戴此类设备,当感觉不适时请放松并深呼吸。大部分人很快就会适应 VR。开头几次如果你不适应请不要着急——它很快就会过去。
在正式开始学习之前,你必须拥有下列条件:
确认 HTC Vive 已经打开并连接!
下载开始项目。解压缩到任意目录并用 Unity 打开。在项目窗口中看一眼文件夹:
每个文件夹都和对应的资源一一对应:
看一看场景视图,按 play 按钮运行游戏:
这里不会有太多内容,因为场景中还没有加入 VR 控制。你需要将 SteamVR 添加到项目中,以便将 Vive 连接到 Unity。
SteamVR SDK 是一个由 Valve 提供的官方库,以简化 Vive 开发。当前在 Asset 商店中是免费的,它同时支持 Oculus Rift 和 HTC Vive。
打开 Asset 商店,在顶部工具栏中选择 Window > Asset Store:
等商店页面加载完,在搜索栏中输入 StreamVR 并回车。上下滚动浏览搜索结果,点击 StreamVR Plugin,会打开它的商店页面:
点击 Download 按钮,然后静静等待。等下载完成,你看到导入包对话框。
点击右下角的 Import,导入包:
等导入完成,你会看到下列提示:
点击 I Made a Backup 按钮,让编辑器对脚本进行预编。几秒后会看到这个窗口:
这是 SteamVR 插件的界面。它会列出一些编辑器设置,这些设置能够提升性能和兼容性。
当你打开一个新项目并导入 SteamVR 时,你会在这里看到几个选项。因为开始项目已经优化过,这里我们只需要禁用解析度对话框(resolution dialog)即可。点击 Accept All 按钮,执行所有推荐的修改。关闭 Asset 商店回到场景视图。在项目窗口中,我们现在多了一个新文件夹 SteamVR:
打开这个文件夹,看一眼内容。我们会从 Prefabs 文件中添加一个 VR GameObjects 到场景中。
同时选中 [CameraRig] 和 [SteamVR] ,将它们拖到结构窗口:
[SteamVR] 负责几件事情。它在玩家打开系统菜单并将物理刷新率和绘图系统进行同步时让游戏自动暂停。它还负责处理“房间规模 VR 动作”的平滑。在检视器面板中查看属性:
[CameraRig] 更有趣,因为它控制着 Vive 头盔和控制器。选择 [CameraRig] ,在检视器面板中设置它的位置为 (X:0, Y:0, Z:-1.1),将摄像机放到桌子后面。
从结构视图中删除主摄像,因为这会干扰 [CameraRig] 和它的相机。
打开手柄,查看屏幕。拿起手柄,四处移动。你会看到在场景视图中看到虚拟手柄也会随之移动:
当 SteamVR 插件检测到手柄,它会创建出虚拟手柄。虚拟手柄被映射为 [CameraRig] 的子节点:
现在——继续在场景视图中——从结构视图中选择 Camera(eye),小心地拿起你的头盔显示器的顶部皮带,移动并微微旋转,同时观察场景视图:
摄像机和头盔显示器是连接在一起的,它会准确地捕获头盔的移动。
现在将头盔显示器戴到头上,拿起手柄,在房间里四处走动感受一下。
如果你想和物体进行交互,那么你会大失所望——什么也不会发生。要添加运动跟踪之外的功能,需要编写一点脚本。
拿起一只手柄,仔细观察。每个控制器上有这些按钮:
Touchpad 既是可以做模拟摇杆也可以当做按钮。当移动或旋转手柄时,手柄会有速度和旋转速度感应,当和物体交互时这会非常有用。
让我们来编写一些代码!在 Scripts 文件夹中创建一个新的 C# 脚本,取名为 ViveControllerInputTest 然后用任意代码编辑器打开它。
删除 Start() 方法,在 Update() 方法之上添加下列代码:
// 1
private SteamVR_TrackedObject trackedObj;
// 2
private SteamVR_Controller.Device Controller
{
get { return SteamVR_Controller.Input((int)trackedObj.index); }
}
我们在这里进行了如下操作:
头盔和手柄都是被跟踪的对象——他们在真实事件中的移动和旋转都会被 HTC Vive 跟踪到并传递到虚拟世界。
在 Update() 方法上方添加方法:
void Awake()
{
trackedObj = GetComponent();
}
当脚本加载时,trackedObj 会被赋值为 SteamVR_TrackedObject 对象,这个对象和手柄是关联的:
现在你已经能够访问手柄了,你可以读取到它的输入。在 Update() 方法中添加:
// 1
if (Controller.GetAxis() != Vector2.zero)
{
Debug.Log(gameObject.name + Controller.GetAxis());
}
// 2
if (Controller.GetHairTriggerDown())
{
Debug.Log(gameObject.name + " Trigger Press");
}
// 3
if (Controller.GetHairTriggerUp())
{
Debug.Log(gameObject.name + " Trigger Release");
}
// 4
if (Controller.GetPressDown(SteamVR_Controller.ButtonMask.Grip))
{
Debug.Log(gameObject.name + " Grip Press");
}
// 5
if (Controller.GetPressUp(SteamVR_Controller.ButtonMask.Grip))
{
Debug.Log(gameObject.name + " Grip Release");
}
上述代码包含了所有当玩家在 VR 中时你够访问到大部分方法。它将 GameObject 的名字输出到控制台,以便区分左右手柄。代码的解释如下:
来测试一下脚本。保存脚本,返回 Unity 编辑器。
在结构视图中选中两个手柄,拖动刚才创建的脚本到检视器中,为它们添加 ViveControllerInputTest 组件。
再次运行游戏,拿起两只手柄,观察控制台中的输出:
按下按钮,扳机并在 touchpad 上滑动,你会看到控制台会输出每个我们注册的动作:
这仅仅是最基本的输入。现在我们可以将虚拟世界操纵在我的手心了——差不多这个意思啦!
VR 提供了许多我们在真实世界中不可能实现的能力,比如捡起一个物体,查看它们并扔到地上,不需要你负责清理。
通过使用触发器碰撞机和编写少量脚本,HTC Vive 能够创建后顾无忧的虚拟体验。
在结构视图中选中两个手柄,为它们添加刚性体。(Add Component > Physics > Rigidbody)
勾上 Is Kinematic,反选 Use Gravity:
为两个手柄添加一个盒子碰撞体 (Add Component > Physics > Box Collider) 并勾上 Is Trigger。
默认的碰撞体有点大,我们需要重新指定大小和位置。设置中心为 (X:0, Y:-0.04, Z:0.02),大小为 (X:0.14, Y:0.07, Z:0.05)。这里需要将值精确到两位数,否则都会影响到手柄的最终效果。
运行游戏,从结构视图中选择一只手柄,并拿起真正的手柄。观察场景视图,然后将焦点置于你正在拿着的那只手柄上(按F)。将碰撞体正好放在手柄的顶端部分,这个部分是你用于抓握物体的地方。
不编写脚本,碰撞体仅仅是一个无用的方块——在 Scripts 文件夹中创建一个新脚本,取名为 ControllerGrabObject 然后打开它。、
删除 Start() 方法并在这里添加这段你已经熟悉的代码:
private SteamVR_TrackedObject trackedObj;
private SteamVR_Controller.Device Controller
{
get { return SteamVR_Controller.Input((int)trackedObj.index); }
}
void Awake()
{
trackedObj = GetComponent();
}
这段代码和你在输入测试中的代码是一样的。这里获取了手柄,然后保存到一个变量中以备后用。
在 trackedObj 下面添加变量:
// 1
private GameObject collidingObject;
// 2
private GameObject objectInHand;
这两个变量的作用分别是:
在 Awake() 方法后添加:
private void SetCollidingObject(Collider col)
{
// 1
if (collidingObject || !col.GetComponent())
{
return;
}
// 2
collidingObject = col.gameObject;
}
这个方法接受一个碰撞体作为参数,并将它的 GameObject 保存到 collidingObject 变量,以便抓住和释放这个对象。同时:
现在,添加触发器方法:
// 1
public void OnTriggerEnter(Collider other)
{
SetCollidingObject(other);
}
// 2
public void OnTriggerStay(Collider other)
{
SetCollidingObject(other);
}
// 3
public void OnTriggerExit(Collider other)
{
if (!collidingObject)
{
return;
}
collidingObject = null;
}
当触发器碰撞体进入、退出另一个碰撞体时,这些方法将被触发。
下面的代码用于抓住一个对象:
private void GrabObject()
{
// 1
objectInHand = collidingObject;
collidingObject = null;
// 2
var joint = AddFixedJoint();
joint.connectedBody = objectInHand.GetComponent();
}
// 3
private FixedJoint AddFixedJoint()
{
FixedJoint fx = gameObject.AddComponent();
fx.breakForce = 20000;
fx.breakTorque = 20000;
return fx;
}
在这里,我们:
被抓住的东西也要能够被放下。下面的代码放下一个物体:
private void ReleaseObject()
{
// 1
if (GetComponent())
{
// 2
GetComponent().connectedBody = null;
Destroy(GetComponent());
// 3
objectInHand.GetComponent().velocity = Controller.velocity;
objectInHand.GetComponent().angularVelocity = Controller.angularVelocity;
}
// 4
objectInHand = null;
}
这段代码将被抓对象的固定连接删除,并在玩家扔出去时控制它的速度和角度。这里关键的是手柄的速度。如果没有这个,扔出的东西会直直地往下掉,不管你用多大的力扔它。相信我,这绝对是错误的。
代码解释如下:
最后,在 Update() 方法中添加代码以处理手柄的输入:
// 1
if (Controller.GetHairTriggerDown())
{
if (collidingObject)
{
GrabObject();
}
}
// 2
if (Controller.GetHairTriggerUp())
{
if (objectInHand)
{
ReleaseObject();
}
}
相信你已经迫不及待地想试一把了吧?保存脚本,退出编辑器。
在结构视图中选中手柄,将新脚本拖到检视器中将它添加为一个组件。
开心的时候来了!打开你的手柄,运行游戏,戴上头盔。按下扳机,抓起几个方块或者圆球,扔出去。你可能需要适应一下。
你不得不佩服你自己——你真的很棒!但我觉得你应该让你的 VR 体验变得更好!
因为种种原因,激光笔在 VR 世界中非常有用。你可以用它们去戳破虚拟气球,做瞄准具使用或者调戏虚拟猫咪。
创建激光笔非常简单。只需要一个方块和一个脚本。在结构视图中创建一个方块 (Create > 3D Object > Cube)。
为它取名 Laser,设置它的位置为 (X:0, Y:5, Z:0),缩放为 (X:0.005, Y:0.005, Z:0) ,并去掉 Box Collider 组件。让它居中,你会看到他漂浮在其他对象之上:
激光不可能有阴影,它们只会有一种颜色,因此我们可以用一个不反光材质实现这个效果。
在 Materials 文件夹下创建一个新材质,取名为 Laser,修改它的着色器为 Unlit/Color ,设置它的 Main Color 为大红色:
通过将材质拖到场景视图的 Laser 上即可分配新材质。当然,也可以将材质拖到结构视图的 Laser 上。
最后,将 Laser 拖到 Prefabs 文件夹,然后从结构视图中删掉 Laser 对象。
现在,在 Scripts 文件夹下创建一个新脚本,名为 LaserPointer,并打开它。添加你早已熟悉的代码:
private SteamVR_TrackedObject trackedObj;
private SteamVR_Controller.Device Controller
{
get { return SteamVR_Controller.Input((int)trackedObj.index); }
}
void Awake()
{
trackedObj = GetComponent();
}
在 trackedObj 下面添加变量:
// 1
public GameObject laserPrefab;
// 2
private GameObject laser;
// 3
private Transform laserTransform;
// 4
private Vector3 hitPoint;
用这个方法显示一束激光:
private void ShowLaser(RaycastHit hit)
{
// 1
laser.SetActive(true);
// 2
laserTransform.position = Vector3.Lerp(trackedObj.transform.position, hitPoint, .5f);
// 3
laserTransform.LookAt(hitPoint);
// 4
laserTransform.localScale = new Vector3(laserTransform.localScale.x, laserTransform.localScale.y,
hit.distance);
}
这个方法使用一个 RaycastHit 作为参数,因为它会包含被击中的位置和射击的距离。
代码解释如下:
在 Update() 方法中添加下列代码,获得玩家的输入:
// 1
if (Controller.GetPress(SteamVR_Controller.ButtonMask.Touchpad))
{
RaycastHit hit;
// 2
if (Physics.Raycast(trackedObj.transform.position, transform.forward, out hit, 100))
{
hitPoint = hit.point;
ShowLaser(hit);
}
}
else // 3
{
laser.SetActive(false);
}
在空的 Start() 方法中添加代码:
// 1
laser = Instantiate(laserPrefab);
// 2
laserTransform = laser.transform;
保存脚本,返回编辑器。在结构视图中选中两个手柄,将激光的脚本拖进检视器中以添加一个组件。
现在从 Prefabs 文件夹中将 Laser 预制件拖到检视器的 Laser 栏中:
保存项目,重新运行游戏。拿起手柄,戴上头盔,按下 touchpad,激光出现了:
![](https://koenig-media.raywenderlich.com/uploads/2016/12/ShootLaser.gif)
在继续之前,右击输入测试组件,选择 Remove Component,从手柄中删除它们。
之所以要删除输入测试组件,因为会在绘制每一帧时向控制台中输出字符串。这会影响性能,在 VR 中每毫秒都会受影响。为了方便测试我们可以这样做,但在真正的游戏中这是不应该的。
接下来是通过激光在房间中进行瞬移!
在 VR 中移动不像驱使玩家前进那么简单,这样做会极易引起玩家眩晕。更可行的办法是使用瞬移。
从玩家的视觉感知来说,宁可接收位置的突然改变,而不是渐进式的改变。在 VR 设备中轻微的改变都会让你的速度感和平衡感彻底失控,还不如直接让你来到一个新的地方。
要显示你最终位于什么地方,你你可以使用 Prefabs 文件夹中的大头钉或标记。
标记是一个简单的、不反光的圆环:
要使用标记,你需要修改 LaserPointer 脚本,打开这个脚本,在类声明中添加变量:
// 1
public Transform cameraRigTransform;
// 2
public GameObject teleportReticlePrefab;
// 3
private GameObject reticle;
// 4
private Transform teleportReticleTransform;
// 5
public Transform headTransform;
// 6
public Vector3 teleportReticleOffset;
// 7
public LayerMask teleportMask;
// 8
private bool shouldTeleport;
每个变量的用途如下:
在 Update() 方法中,将这一句:
if (Physics.Raycast(trackedObj.transform.position, transform.forward, out hit, 100))
替换为这句,以便将 LayerMask 加入到判断中:
if (Physics.Raycast(trackedObj.transform.position, transform.forward, out hit, 100, teleportMask))
这确保激光只能点到你能够传送过去的 GameObjects 上。
仍然在 Update() 方法中,在 ShowLaser() 一句后添加:
// 1
reticle.SetActive(true);
// 2
teleportReticleTransform.position = hitPoint + teleportReticleOffset;
// 3
shouldTeleport = true;
代码解释如下:
仍然在 Update 方法,找到 laser.SetActive(false); 一句,在后面添加:
reticle.SetActive(false);
如果目标地点无效,隐藏传送标记。
添加下列方法,进行传送:
private void Teleport()
{
// 1
shouldTeleport = false;
// 2
reticle.SetActive(false);
// 3
Vector3 difference = cameraRigTransform.position - headTransform.position;
// 4
difference.y = 0;
// 5
cameraRigTransform.position = hitPoint + difference;
}
真正的传送只需要 5 行代码吗?让我们解释一下:
看到了没有,这个偏移起到了一个关键的作用,让我们精确地定位摄像机的位置并将玩家放到他们想去的地方。
在 Update() 的检查 touchpad 按键的 if else 语句之外添加代码:
if (Controller.GetPressUp(SteamVR_Controller.ButtonMask.Touchpad) && shouldTeleport)
{
Teleport();
}
如果玩家松开 touchpad,同时传送位置有效的话,对玩家进行传送。
最后,在 Start() 方法中添加代码:
// 1
reticle = Instantiate(teleportReticlePrefab);
// 2
teleportReticleTransform = reticle.transform;
保存脚本,返回 Unity。
在结构视图中选中两个手柄,会发现多了几个新字段:
![](https://koenig-media.raywenderlich.com/uploads/2016/12/NewFields-1.png)
将 [CameraRig] 拖到 Camera Rig Transform 栏,将 TeleportReticle 从 Prefabs 文件夹拖到 Teleport Reticle Transform 栏,将 Camera (head) 拖到 Head Transform 栏。
将 Teleport Reticle Offset 设为 (X:0, Y:0.05, Z:0) ,Teleport Mask 设为 CanTeleport。CanTeleport 不是默认层— 它是专门为这个教程创建的。这个层里面只有 Floor 和 Table 对象。
现在运行游戏,用激光照射在地板上进行瞬移。
这个示例已经完成,准备尽情地游戏吧!
你可以在这里下载完成后的项目。在本教程中,你学会了:
这个项目只是一个开始——开始在你自己的项目中使用它!我很想看到你最终完成的作品。
如果你喜欢这个教程,并向学习更多内容,你可以阅读我们的这本书:Unity Games by Tutorials,那里会有更多关于虚拟现实游戏的内容,包括对 Oculus Rift 的支持。
要理解这本书到底说了些什么,最简单的法子莫过于观看这个视频:
https://youtu.be/kgU-8Lzqy2E
谢谢观赏,希望你喜欢这篇教程,就像我很享受写它时所带来的乐趣一样。
如果有任何建议、问题或者你想战士对示例项目所进行改进,请在下面留言。