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

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

2017-08-22 17:45 281 查看
原文:Advanced VR Mechanics With Unity and the HTC Vive – Part 2

作者:Eric Van de Kerckhove

译者:kmyhy

介绍

第一部分教程中,我们学习李如何创建交互系统以及用它来抓取、握持和扔出东西。

在第二部分中,你将学习:

制作一副功能完备的弓和箭

创建一个虚拟背包

本教程针对高级读者,它会跳过许多细节,比如添加组件、创建新 GameObjecdt、脚本等。我们假定你知道如何完成这些工作。如果不,请阅读这里的 Unity 入门教程

开始

下载开始项目,解压缩,用 Unity 打开解压缩后的文件夹。在项目窗口中的文件夹大致如下所示:



Materials: 包含所有场景中用到的材质。

Models: 包含所有模型。

Prefabs: 包含所有在上一教程中创建的预制件。

Scenes: 游戏场景及一些灯光数据。

Scripts: 所有脚本。

Sounds: 包含射箭时弓箭所发出的声音。

SteamVR: SteamVR 创建及相关脚本,预制件和示例。

Textures: 为了简单起见,几乎本教程中的模型所共享的纹理图片都放在这里。

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

弓的制作

目前场景中还没有弓。

新建一个 GameObject,命名为 Bow。

将 Bow 的 position 设为 (X:-0.1, Y:4.5, Z:-1) ,rotation 设为 (X:0, Y:270, Z:80)。

将 Bow 模型从 Models 文件夹拖到结构视图的 Bow 对象,变成它的子对象。

将它改名为 BowMesh,设置 position 、rotation 和 scale 分别为 (X:0, Y:0, Z:0)、 (X:-90, Y:0, Z:-180) 和 (X:0.7, Y:0.7, Z:0.7) 。

看起来像这个样子:



在继续之前,我需要演示一下这根弓弦要怎么用。

选中 BowMesh,找到它的 Skinned Mesh Renderer。展开 BlendShapes 字段,显示 Bend 即 blendshape 值。这就是重点。



注意观察弓。在检视器的 Bend 处拖动鼠标,将 Bend 值从 0 - 100 之间来回拖动。



将 Bend 恢复为 0。

从 BowMesh 上删除 Animator 组件,所有的动画都将通过 blendshape 来进行。



从 Prefabs 文件夹拖一个 RealArrow 实例到 Bow 上。

将它命名为 BowArrow ,修改 Transform 组件,让它的位置相对于 Bow。

这支箭不会被作为正常的箭来使用,因此删除它和预制件的连接——从顶部菜单中选择 GameObject\Break Prefab Instance 菜单。



展开 BowArrow,删除它的 Trail 子对象。这个粒子系统只是用于一般的箭的。

从 BowArrow 上删除 Rigidbody,第二个 Box Collider 以及 RWVR_Snap To Controller 组件。

只留下一个 Transform 和一个 Box Collider 组件。

这支 Box Collider 的 center 为 (X:0, Y:0, Z:-0.28) ,设置 size 为 (X:0.1, Y:0.1, Z:0.2)。这将是玩家可以抓住和松开的部位。



再次选择 Bow,为它添加一个刚性体和一个盒子碰撞体。这将允许它在未使用的时候拥有一个可见的真实形体。

将盒子碰撞体的 center 设置为 (X:0, Y:0, Z:-0.15) ,size设置为 (X:0.1, Y:1.45, Z:0.45) 。

为它添加一个 RWVR_Snap To Controller 组件。勾选 Hide Controller Model,将 Snap Position Offset 设为 (X:0, Y:0.08, Z:0) , Snap Rotation Offset 设为 (X:90, Y:0, Z:0)。



运行场景,试试看,能不能把弓拿起来?



然后应该设置控制器的 tag,以便后面的脚本可以正常工作。

展开 [CameraRig],同时选中两个 controller,将它们的 tag 设置为 Controller。



在下一节,我们将编写脚本让弓能正常工作。

箭的制作

我们制作的弓包含李 3 个主要部件:



弓上的箭

一个正常的射出去的箭

这些部件的每一个都需要编写脚本,这样弓才能完成射箭的动作。

首先,那支正常的箭需要一个能够射中物体并能随后捡起的脚本。

在 Scrits 目录下新建 C# 脚本,命名为 RealArrow。注意这个脚本不放在 RWVR 文件夹下,因为它不属于交互系统。

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

添加下列变量:

public BoxCollider pickupCollider; // 1
private Rigidbody rb; // 2
private bool launched; // 3
private bool stuckInWall; // 4


代码很简单:

箭有两个碰撞体:一个在发射时用于检测碰撞,一个用于物理交互并在射出箭后将它捡起来。这个变量引用了后者。

引用箭的刚性体。

当箭射出后,这个变量标记为 true。

当箭射中某个固体对象时,这个变量标记为 true。

添加一个 Awake() 方法:

private void Awake()
{
rb = GetComponent<Rigidbody>();
}


这个方法将箭的刚性体组件缓存起来。

然后是这个方法:

private void FixedUpdate()
{
if (launched && !stuckInWall && rb.velocity != Vector3.zero) // 1
{
rb.rotation = Quaternion.LookRotation(rb.velocity); // 2
}
}


这个方法确保箭始终保持方向为箭尖所指方向。这会产生某些好玩的效果,比如将箭射向天空,当它落到地上时,箭头会刺入土壤中。这会让某些东西变得更稳定,防止箭刺入的位置不太恰当。

这个方法分成两步:

如果箭已射出,没有刺入墙中,同时速度不为 0…

获取速度向量所指的方向。

然后是 FixedUpdate():

public void SetAllowPickup(bool allow) // 1
{
pickupCollider.enabled = allow;
}

public void Launch() // 2
{
launched = true;
SetAllowPickup(false);
}


分别解释如下:

一个助手方法,开启/禁用 pickupCollider。

当箭从弓上射出调用,将 lanched 标志设置为 true,并且不允许箭能够被拾起。

然后是这个方法,确保箭射中一个固态物体后不再移动:

private void GetStuck(Collider other) // 1
{
launched = false;
rb.isKinematic = true; // 2
stuckInWall = true; // 3
SetAllowPickup(true); // 4
transform.SetParent(other.transform); // 5
}


代码解释如下:

参数是一个碰撞体。也就是箭身上的碰撞体。

开启箭的动力学特性,以便它不受物理引擎影响。

将 stuckInWall 设置为 true。

一旦箭停止移动,就可以允许它被拾起了。

将箭附着在所射中的对象上,这样哪怕那个物体是移动着的,箭也会牢牢地粘在它身上。

最后一段脚本是在 OnTriggerEnter() 方法中,当箭击中某个物体时调用这个方法:

private void OnTriggerEnter(Collider other)
{
if (other.CompareTag("Controller") || other.GetComponent<Bow>()) // 1
{
return;
}

if (launched && !stuckInWall) // 2
{
GetStuck(other);
}
}


会报一个错给你,说 Bow 不存在。先忽略这个错误:我们后面会创建 Bow 这个脚本。

代码解释如下:

如果箭和控制器(手柄)或者弓发生碰撞,不要调用 GetStuck 方法(也就是不会发生”射入“事件)。这避免了某些异常的情况,否则箭在一射出之后立马就“粘”在弓上。

如果箭已射出,并且还没有出现“刺入”的情况,则将它“粘”在发生碰撞的物体上。

保存脚本,在 Scripts 文件夹新建另一个 C# 脚本 Bow。然后在编辑器中打开它。

编写 Bow

删除 Start() 方法,在类声明之前添加:

[ExecuteInEditMode]


这将允许这个脚本执行它的方法,就算是你正在编辑器中编辑的时候。你等会就会知道这是一个非常好用的技巧。

在 Update() 方法上面添加变量:

public Transform attachedArrow; // 1
public SkinnedMeshRenderer BowSkinnedMesh; // 2

public float blendMultiplier = 255f; // 3
public GameObject realArrowPrefab; // 4

public float maxShootSpeed = 50; // 5

public AudioClip fireSound; // 6


这些变量分别用于:

一个对 BowArrow 的引用,它会作为弓的子对象。

引用了弓的蒙皮网格。这将在改变弓的弯曲度的时候用到。

箭和弓的距离乘以 blendMultiplier 就会得到这个弯曲度最终的 Bend 值。

引用了 RealArrow 预制件,当弓弦被拉起然后松开后,会生成一个 RealArrow 并射出。

当弓满弦后箭射出时获得的速度。

箭射出时播放的声音。

在变量声明后面加一个字段:

bool IsArmed()
{
return attachedArrow.gameObject.activeSelf;
}


如果箭可用时,返回 true。这是对 attachedArrow.gameObject.activeSelf 的一种缩写。

在 Update() 方法中添加:

float distance = Vector3.Distance(transform.position, attachedArrow.position); // 1
BowSkinnedMesh.SetBlendShapeWeight(0, Mathf.Max(0, distance * blendMultiplier)); // 2


解释如下:

计算弓和箭之间的距离。

设置弓的弯曲度为前面计算出的距离乘以 blendMultiplier。

然后,在 Update() 后添加:

private void Arm() // 1
{
attachedArrow.gameObject.SetActive(true);
}

private void Disarm()
{
BowSkinnedMesh.SetBlendShapeWeight(0, 0); // 2
attachedArrow.position = transform.position; // 3
attachedArrow.gameObject.SetActive(false); // 4
}


这两个方法用于将箭放到弓上和从弓上移除。

将箭上弦,弓上的箭设置为可用,使它可见。

重置弓的 bend 值,这会让弦重新恢复成直线。

重置弓上的箭的位置。

将箭隐藏,通过将它设置为不可用。

在 Disarm() 后面添加 OnTriggerEnter() :

private void OnTriggerEnter(Collider other) // 1
{
if (
!IsArmed()
&& other.CompareTag("InteractionObject")
&& other.GetComponent<RealArrow>()
&& !other.GetComponent<RWVR_InteractionObject>().IsFree() // 2
) {
Destroy(other.gameObject); // 3
Arm(); // 4
}
}


当手柄碰到弓并按下扳机时调用这个方法。

方法参数是一个碰撞体。也就是碰到弓的扳机。

这个 if 判断很长,当弓处于未上弦,并且和一个 RealArrow 发生碰撞时。有几个判断是为了确保它只会和玩家手中的箭发生交互。

销毁 RealArrow。

将箭安装到弓上。

这段代码允许玩家在第一次装上的箭被射出后再次上弦。

最后是射箭的方法。在 OnTriggerEnter() 下方添加:

public void ShootArrow()
{
GameObject arrow = Instantiate(realArrowPrefab, transform.position, transform.rotation); // 1
float distance = Vector3.Distance(transform.position, attachedArrow.position); // 2

arrow.GetComponent<Rigidbody>().velocity = arrow.transform.forward * distance * maxShootSpeed; // 3
AudioSource.PlayClipAtPoint(fireSound, transform.position); // 4
GetComponent<RWVR_InteractionObject>().currentController.Vibrate(3500); // 5
arrow.GetComponent<RealArrow>().Launch(); // 6

Disarm(); // 7
}


代码有点多,但并不复杂:

用 RealArrow 预制件生成一支新的箭。设置它的 position 和 rotation 和弓相等。

计算弓与箭之间的距离,保存到 distance 变量。

基于 distance 给 RealArrow 施加一个向前的加速度。弓弦向后拉动的动作越大,箭所获得的加速度就越大。

播放“射箭”的声音。

让手柄振动,模拟真实的体验。

调用 RealArrow 的 Launch() 方法。

将箭从弓上移除。

然后到检视器中修改弓的设置!

保存脚本,回到编辑器。

在结构视图中选中 Bow,然后添加一个 Bow 组件。



展开 Bow,显示其子节点,将 BowArrow 拖到 Attached Arrow 字段。

然后将 BowMesh 拖到 Bow Skinned Mesh 字段,设置 Blend Multiplier 为 353。



从 Prefabs 文件夹拖一个 RealArrow 预制件到 Real Arrow Prefab 字段,将 Sounds 文件夹下的 FireBow 声音文件拖到 Fire Sound 字段。

做完后的 Bow 组件看起来是这个样子:



还记得蒙皮网格是怎样影响 bow 模型的吗?在场景视图中,拖动 BowArrow 的 local Z-axis 看一下满弦后效果:



感觉不错吧?

现在需要设置 RealArrow 让它按照我们的意图去运作。

在结构视图中,选择 RealArrow,为它添加一个 Real Arrow 组件。

将 Box Collider 下的 Is Trigger 禁用,然后将它拖进 Pickup Collider 字段。



点击检视器顶部的 Apply 按钮,将修改应用到所有 RealArrow 预制件。

最后一个需要改的地方是安在弓上的“特别”箭支。

安在弓上的箭

安在弓上的箭弧被玩家向后拉、然后释放,才能射出去。

在 Scripts \ RWVR 文件夹下新建 C# 脚本 RWVR_ArrowInBow,删除它的 Start() 和 Update() 方法。

让这个类继承 RWVR_InteractionObject :

public class RWVR_ArrowInBow : RWVR_InteractionObject


增加几个变量声明:

public float minimumPosition; // 1
public float maximumPosition; // 2

private Transform attachedBow; // 3
private const float arrowCorrection = 0.3f; // 4


它们的作用分别是:

z 轴的最小值。

z 轴的最大值。这个变量和上个变量一起,用于限制箭支的位置,使它无法被拉得太远也不能推进到弓里面。

引用了箭所在的弓的 Bow 对象。

用于矫正箭相对于弓的位置。

然后添加这个方法:

public override void Awake()
{
base.Awake();
attachedBow = transform.parent;
}


这里调用了基类的 Awake() 方法,将 transform 缓存,然后将弓保存到 attachedBow 变量。

这个方法在用户按下扳机时调用:

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

Vector3 arrowInBowSpace = attachedBow.InverseTransformPoint(controller.transform.position); // 3
cachedTransform.localPosition = new Vector3(0, 0, arrowInBowSpace.z + arrowCorrection); // 4
}


代码解释如下:

覆盖 OnTriggerIsBeingPressed() 方法,用正在和箭交互的手柄作为参数传入。

调用基类方法。这其实没有什么作用,只不过是为了保持前后写法一致而已。

调用 InverseTransformPoint() 方法,获取箭相对于弓和手柄的最新位置。这使得箭能够被正确地后拉,无论手柄是不是和弓的 z 轴对得很齐。

将箭移动到新位置,并在这个位置的 z 轴上添加 arrowCorrection 以进行矫正。

然后是这个方法:

public override void OnTriggerWasReleased(RWVR_InteractionController controller) // 1
{
attachedBow.GetComponent<Bow>().ShootArrow(); // 2
currentController.Vibrate(3500); // 3
base.OnTriggerWasReleased(controller); // 4
}


这个方法在箭被射出去之后调用。

覆盖 OnTriggerWasRelease() 方法,用正在和箭交互的控制器作为参数。

射出箭支。

震动手柄。

调用父类方法以释放 currentController。

然后是这个方法:

void LateUpdate()
{
// Limit position
float zPos = cachedTransform.localPosition.z; // 1
zPos = Mathf.Clamp(zPos, minimumPosition, maximumPosition); // 2
cachedTransform.localPosition = new Vector3(0, 0, zPos); // 3

//Limit rotation
cachedTransform.localRotation = Quaternion.Euler(Vector3.zero); // 4

if (currentController)
{
currentController.Vibrate(System.Convert.ToUInt16(500 * -zPos)); // 5
}
}


这个方法在每帧的最后调用。用这个方法对箭的位置和角度、手柄的震动进行限制,以便模拟向后拉箭的动作。

将箭的 z 坐标保存在 zPos。

将 zPos 限制在允许的最大值最小值区间。

将 zPos 应用到箭的位置上。

将箭的角度限制为 Vector3.zero。

震动手柄。箭被拉得越往后,震动的强度越大。

保存脚本回到编辑器。

在结构视图中,展开 Bow,选择 BowArrow 子节点。在它上面添加一个 RWVR_Arrow In Bow 组件,设置 Minimum Position 为 -0.4。



保存场景,拿起你的头盔和手柄准备试玩游戏!

用一支手柄抓住弓,然后用另一只手柄向后拉箭。

放开手柄将箭放出,从桌子上拿起一支箭装到弓弦上。



最后一个工作是背包(对于本例而言,也叫箭囊),这样你就可以从中抓起新的箭支装到弓弦上。

这要创建一个新的脚本了。

创建虚拟背包

为了知道玩家的手柄上是否抓得有东西,你需要一个控制器管理器,用于引用两只手柄。

在 Script/RWVR 文件夹下新建 C# 脚本 RWVR_ControllerManager。用代码编辑器打开。

删除 Start() 和 Update() ,添加变量:

public static RWVR_ControllerManager Instance; // 1

public RWVR_InteractionController leftController; // 2
public RWVR_InteractionController rightController; // 3


每个变量的作用分别为下:

一个公有的、静态的对本脚本的引用,这样你可以从任意脚本中调用到它。

引用了左手柄。

引用了右手柄。

添加方法:

private void Awake()
{
Instance = this;
}


将这个脚本的一个引用保存到 Instance 变量。

然后是这个方法:

public bool AnyControllerIsInteractingWith<T>() // 1
{
if (leftController.InteractionObject && leftController.InteractionObject.GetComponent<T>() != null) // 2
{
return true;
}

if (rightController.InteractionObject && rightController.InteractionObject.GetComponent<T>() != null) // 3
{
return true;
}

return false; // 4
}


这个助手方法用于判断是否某只手柄中正在抓着一个组件:

这是一个泛型方法,接收任意类型。

如果左手柄正在和某个对象交互,并且它抓住的对象的组件类型就是泛型参数的类型,返回true。

如果右手柄正在和某个对象交互,并且它抓住的对象的组件类型就是泛型参数的类型,返回 true。

否则,返回 false。

保存脚本,返回编辑器。

最后一个脚本是和背包对应的脚本。

在 Scripts\RWVR 目录下新建 C# 脚本 RWVR_SpecialObjectSpawner。

打开脚本,将这一句:

public class RWVR_SpecialObjectSpawner : MonoBehaviour


替换成:

public class RWVR_SpecialObjectSpawner : RWVR_InteractionObject


让我们的背包从 RWVR_InteractionObject 继承。

删除 Start() 和 Update() 方法,添加变量:

public GameObject arrowPrefab; // 1
public List<GameObject> randomPrefabs = new List<GameObject>(); // 2


它们将用于从背包中生出 GameObject。

一个对 RealArrow 预制件的引用。

一个 GameObjectd 数组,用于保存能够从背包中取出的东西。

添加这个方法:

private void SpawnObjectInHand(GameObject prefab, RWVR_InteractionController controller) // 1
{
GameObject spawnedObject = Instantiate(prefab, controller.snapColliderOrigin.position, controller.transform.rotation); // 2
controller.SwitchInteractionObjectTo(spawnedObject.GetComponent<RWVR_InteractionObject>()); // 3
OnTriggerWasReleased(controller); // 4
}


这个方法将一个对象附着在玩家的手柄上,就像玩家从背后掏出某件东西一样。

有两个参数,prefab 是将生成的 GameObject,controller 是用哪个手柄来抓住这个 GameObject。

在手柄相同的位置和方向,创建出一个新的 GameObject,然后保存到 spawnedObject 变量。

将手柄的当前 InteractionObject 换成刚刚创建的对象。

放下背包,将焦点集中在刚刚创建的对象上。

下面一个方法则决定当玩家在背包上按下扳机时,能够掏出的东西有哪些。

添加方法:

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

if (RWVR_ControllerManager.Instance.AnyControllerIsInteractingWith<Bow>()) // 3
{
SpawnObjectInHand(arrowPrefab, controller);
}
else // 4
{
SpawnObjectInHand(randomPrefabs[UnityEngine.Random.Range(0, randomPrefabs.Count)], controller);
}
}


代码解释如下:

覆盖父类的 OnTriggerWasPressed() 方法。

调用父类的 OnTriggerWasPressed() 方法。

如果任何一支手柄正在握着弓,生成一支箭。

否则,从 randomPrefabs 列表中随机生成一个 GameObject。

保存脚本,返回编辑器。

在结构视图中新建一个 Cube,命名为 BackPark,将它拖到 [CameraRig]\ Camera (head) 放到玩家头盔下面。



将它的 position 和 scale 分别设为 (X:0, Y:-0.25, Z:-0.45) 和 (X:0.6, Y:0.5, Z:0.5) 。

背包现在被放在了玩家脑袋的右后下方。

将 Box Collider 的 Is Trigger 设为 true。这个对象不需要和任何物体进行碰撞检测。

将 Cast Shadows 设为 Off,关闭 Mesh Renderer 的 Receive Shadows。



现在添加一个 RWVR_Special Object Spawner 组件,从 Prefabs 文件夹拖一个 RealArrow 到 Arrow Prefab 字段。

最终,从同一个文件夹拖一个 Book 和一个 Die 预制件到 Radom Prefabs list。



然后,添加一个新的空白 GameObjecdt,命名为 ControllerManager,然后在它上面添加一个 RWVR_Controller Manager 组件。

展开 [CameraRig] ,拖 Controller (left) 到 Left Controller 字段,拖 Controller (right) 到 Right Controller 字段。

保存场景,试一下这个背包。尝试抓一下你背上的背包,看看你能掏出什么东西来!



本教程就到此结束了!一副功能完好的弓箭及一个易于扩展的交互系统就完成了。

结尾

最终完成的项目在此处下载

在本教程中,你学习了如何为你的 HTC Vive 游戏创建和添加如下功能:

对交互系统进行扩展。

制作一副可用的弓箭。

创建一个虚拟背包。

如果你想学习更过使用 Unity 制作猎人游戏的内容,请阅读我们的《Unity 游戏教程》。

在这本书中,你会从零开始制作 4 款游戏:

一款双摇杆射击游戏

一款第一人称设计游戏

一款塔防游戏(支持 VR)

一款 2D 平台游戏

通过这本书,你将学会如何制作自己的 Windows、macOS、iOS 平台游戏!

这本书完全针对 Unity 初学者,以及准备将自己的 Unity 技能提升到专业水准的人。这本书假设你有一定的编程经验(任何语言)。

如果你有任何看法和建议,请在下面留言!
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息