原文:HTC Vive Tutorial for Unity
做者: Eric Van de Kerckhove
译者:kmyhyphp
HTC Vive 是一个虚拟现实头盔,由 HTC 和 Valve 公司制造。它提供一种在虚拟世界中的浸入式体验,而不是屏幕头像。编辑器
若是你是一个 Unity 开发者,在虚拟现实游戏中使用 HTC Vive 很是简单——你能够认为 HTC Vive 和 Unity 是天生一对。ide
在这篇 HTC Vive 教程中,你会学习如何在 Unity 游戏中集成 HTC Vive。包括:工具
在本文最后,你将对将来体验有一个粗略的了解。让咱们开始吧!性能
注:每一个人在戴着头戴式显示器都会对运动和旋转产生不一样的反应。若是你是第一此穿戴此类设备,当感受不适时请放松并深呼吸。大部分人很快就会适应 VR。开头几回若是你不适应请不要着急——它很快就会过去。学习
在正式开始学习以前,你必须拥有下列条件:测试
确认 HTC Vive 已经打开并链接!优化
下载开始项目。解压缩到任意目录并用 Unity 打开。在项目窗口中看一眼文件夹:ui
每一个文件夹都和对应的资源一一对应:spa
看一看场景视图,按 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<SteamVR_TrackedObject>(); }
当脚本加载时,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<SteamVR_TrackedObject>(); }
这段代码和你在输入测试中的代码是同样的。这里获取了手柄,而后保存到一个变量中以备后用。
在 trackedObj 下面添加变量:
// 1 private GameObject collidingObject; // 2 private GameObject objectInHand;
这两个变量的做用分别是:
在 Awake() 方法后添加:
private void SetCollidingObject(Collider col) { // 1 if (collidingObject || !col.GetComponent<Rigidbody>()) { 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<Rigidbody>(); } // 3 private FixedJoint AddFixedJoint() { FixedJoint fx = gameObject.AddComponent<FixedJoint>(); fx.breakForce = 20000; fx.breakTorque = 20000; return fx; }
在这里,咱们:
被抓住的东西也要可以被放下。下面的代码放下一个物体:
private void ReleaseObject() { // 1 if (GetComponent<FixedJoint>()) { // 2 GetComponent<FixedJoint>().connectedBody = null; Destroy(GetComponent<FixedJoint>()); // 3 objectInHand.GetComponent<Rigidbody>().velocity = Controller.velocity; objectInHand.GetComponent<Rigidbody>().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<SteamVR_TrackedObject>(); }
在 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 对象。
如今运行游戏,用激光照射在地板上进行瞬移。
这个示例已经完成,准备尽情地游戏吧!