2000字范文,分享全网优秀范文,学习好帮手!
2000字范文 > 用 Unity 和 HTC Vive 实现高级 VR 机制(2)

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

时间:2019-10-04 05:52:59

相关推荐

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

原文: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; // 1private Rigidbody rb; // 2private bool launched; // 3private 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; // 2stuckInWall = true; // 3SetAllowPickup(true); // 4transform.SetParent(other.transform); // 5}

代码解释如下:

参数是一个碰撞体。也就是箭身上的碰撞体。开启箭的动力学特性,以便它不受物理引擎影响。将 stuckInWall 设置为 true。一旦箭停止移动,就可以允许它被拾起了。将箭附着在所射中的对象上,这样哪怕那个物体是移动着的,箭也会牢牢地粘在它身上。

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

private void OnTriggerEnter(Collider other){if (pareTag("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; // 1public SkinnedMeshRenderer BowSkinnedMesh; // 2public float blendMultiplier = 255f; // 3public GameObject realArrowPrefab; // 4public float maxShootSpeed = 50; // 5public 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); // 1BowSkinnedMesh.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); // 2attachedArrow.position = transform.position; // 3attachedArrow.gameObject.SetActive(false); // 4}

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

将箭上弦,弓上的箭设置为可用,使它可见。重置弓的 bend 值,这会让弦重新恢复成直线。重置弓上的箭的位置。将箭隐藏,通过将它设置为不可用。

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

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

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

方法参数是一个碰撞体。也就是碰到弓的扳机。这个 if 判断很长,当弓处于未上弦,并且和一个 RealArrow 发生碰撞时。有几个判断是为了确保它只会和玩家手中的箭发生交互。销毁 RealArrow。将箭安装到弓上。

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

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

public void ShootArrow(){GameObject arrow = Instantiate(realArrowPrefab, transform.position, transform.rotation); // 1float distance = Vector3.Distance(transform.position, attachedArrow.position); // 2arrow.GetComponent<Rigidbody>().velocity = arrow.transform.forward * distance * maxShootSpeed; // 3AudioSource.PlayClipAtPoint(fireSound, transform.position); // 4GetComponent<RWVR_InteractionObject>().currentController.Vibrate(3500); // 5arrow.GetComponent<RealArrow>().Launch(); // 6Disarm(); // 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; // 1public float maximumPosition; // 2private Transform attachedBow; // 3private 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); // 2Vector3 arrowInBowSpace = attachedBow.InverseTransformPoint(controller.transform.position); // 3cachedTransform.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(); // 2currentController.Vibrate(3500); // 3base.OnTriggerWasReleased(controller); // 4}

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

覆盖 OnTriggerWasRelease() 方法,用正在和箭交互的控制器作为参数。射出箭支。震动手柄。调用父类方法以释放 currentController。

然后是这个方法:

void LateUpdate(){// Limit positionfloat zPos = cachedTransform.localPosition.z; // 1zPos = Mathf.Clamp(zPos, minimumPosition, maximumPosition); // 2cachedTransform.localPosition = new Vector3(0, 0, zPos); // 3//Limit rotationcachedTransform.localRotation = Quaternion.Euler(Vector3.zero); // 4if (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; // 1public RWVR_InteractionController leftController; // 2public 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; // 1public 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); // 2controller.SwitchInteractionObjectTo(spawnedObject.GetComponent<RWVR_InteractionObject>()); // 3OnTriggerWasReleased(controller); // 4}

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

有两个参数,prefab 是将生成的 GameObject,controller 是用哪个手柄来抓住这个 GameObject。在手柄相同的位置和方向,创建出一个新的 GameObject,然后保存到 spawnedObject 变量。将手柄的当前 InteractionObject 换成刚刚创建的对象。放下背包,将焦点集中在刚刚创建的对象上。

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

添加方法:

public override void OnTriggerWasPressed(RWVR_InteractionController controller) // 1{base.OnTriggerWasPressed(controller); // 2if (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 技能提升到专业水准的人。这本书假设你有一定的编程经验(任何语言)。

如果你有任何看法和建议,请在下面留言!

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。