原文:Advanced VR Mechanics With Unity and the HTC Vive – Part 2
作者:Eric Van de Kerckhove
译者:kmyhy
在第一部分教程中,我们学习李如何创建交互系统以及用它来抓取、握持和扔出东西。
在第二部分中,你将学习:
本教程针对高级读者,它会跳过许多细节,比如添加组件、创建新 GameObjecdt、脚本等。我们假定你知道如何完成这些工作。如果不,请阅读这里的 Unity 入门教程。
下载开始项目,解压缩,用 Unity 打开解压缩后的文件夹。在项目窗口中的文件夹大致如下所示:
打开 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
代码很简单:
添加一个 Awake() 方法:
private void Awake()
{
rb = GetComponent();
}
这个方法将箭的刚性体组件缓存起来。
然后是这个方法:
private void FixedUpdate()
{
if (launched && !stuckInWall && rb.velocity != Vector3.zero) // 1
{
rb.rotation = Quaternion.LookRotation(rb.velocity); // 2
}
}
这个方法确保箭始终保持方向为箭尖所指方向。这会产生某些好玩的效果,比如将箭射向天空,当它落到地上时,箭头会刺入土壤中。这会让某些东西变得更稳定,防止箭刺入的位置不太恰当。
这个方法分成两步:
然后是 FixedUpdate():
public void SetAllowPickup(bool allow) // 1
{
pickupCollider.enabled = allow;
}
public void Launch() // 2
{
launched = true;
SetAllowPickup(false);
}
分别解释如下:
然后是这个方法,确保箭射中一个固态物体后不再移动:
private void GetStuck(Collider other) // 1
{
launched = false;
rb.isKinematic = true; // 2
stuckInWall = true; // 3
SetAllowPickup(true); // 4
transform.SetParent(other.transform); // 5
}
代码解释如下:
最后一段脚本是在 OnTriggerEnter() 方法中,当箭击中某个物体时调用这个方法:
private void OnTriggerEnter(Collider other)
{
if (other.CompareTag("Controller") || other.GetComponent()) // 1
{
return;
}
if (launched && !stuckInWall) // 2
{
GetStuck(other);
}
}
会报一个错给你,说 Bow 不存在。先忽略这个错误:我们后面会创建 Bow 这个脚本。
代码解释如下:
保存脚本,在 Scripts 文件夹新建另一个 C# 脚本 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
这些变量分别用于:
在变量声明后面加一个字段:
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
解释如下:
然后,在 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
}
这两个方法用于将箭放到弓上和从弓上移除。
在 Disarm() 后面添加 OnTriggerEnter() :
private void OnTriggerEnter(Collider other) // 1
{
if (
!IsArmed()
&& other.CompareTag("InteractionObject")
&& other.GetComponent()
&& !other.GetComponent().IsFree() // 2
) {
Destroy(other.gameObject); // 3
Arm(); // 4
}
}
当手柄碰到弓并按下扳机时调用这个方法。
这段代码允许玩家在第一次装上的箭被射出后再次上弦。
最后是射箭的方法。在 OnTriggerEnter() 下方添加:
public void ShootArrow()
{
GameObject arrow = Instantiate(realArrowPrefab, transform.position, transform.rotation); // 1
float distance = Vector3.Distance(transform.position, attachedArrow.position); // 2
arrow.GetComponent().velocity = arrow.transform.forward * distance * maxShootSpeed; // 3
AudioSource.PlayClipAtPoint(fireSound, transform.position); // 4
GetComponent().currentController.Vibrate(3500); // 5
arrow.GetComponent().Launch(); // 6
Disarm(); // 7
}
代码有点多,但并不复杂:
然后到检视器中修改弓的设置!
保存脚本,回到编辑器。
在结构视图中选中 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
它们的作用分别是:
然后添加这个方法:
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
}
代码解释如下:
然后是这个方法:
public override void OnTriggerWasReleased(RWVR_InteractionController controller) // 1
{
attachedBow.GetComponent().ShootArrow(); // 2
currentController.Vibrate(3500); // 3
base.OnTriggerWasReleased(controller); // 4
}
这个方法在箭被射出去之后调用。
然后是这个方法:
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
}
}
这个方法在每帧的最后调用。用这个方法对箭的位置和角度、手柄的震动进行限制,以便模拟向后拉箭的动作。
保存脚本回到编辑器。
在结构视图中,展开 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() // 1
{
if (leftController.InteractionObject && leftController.InteractionObject.GetComponent() != null) // 2
{
return true;
}
if (rightController.InteractionObject && rightController.InteractionObject.GetComponent() != null) // 3
{
return true;
}
return false; // 4
}
这个助手方法用于判断是否某只手柄中正在抓着一个组件:
保存脚本,返回编辑器。
最后一个脚本是和背包对应的脚本。
在 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 randomPrefabs = new List(); // 2
它们将用于从背包中生出 GameObject。
添加这个方法:
private void SpawnObjectInHand(GameObject prefab, RWVR_InteractionController controller) // 1
{
GameObject spawnedObject = Instantiate(prefab, controller.snapColliderOrigin.position, controller.transform.rotation); // 2
controller.SwitchInteractionObjectTo(spawnedObject.GetComponent()); // 3
OnTriggerWasReleased(controller); // 4
}
这个方法将一个对象附着在玩家的手柄上,就像玩家从背后掏出某件东西一样。
下面一个方法则决定当玩家在背包上按下扳机时,能够掏出的东西有哪些。
添加方法:
public override void OnTriggerWasPressed(RWVR_InteractionController controller) // 1
{
base.OnTriggerWasPressed(controller); // 2
if (RWVR_ControllerManager.Instance.AnyControllerIsInteractingWith()) // 3
{
SpawnObjectInHand(arrowPrefab, controller);
}
else // 4
{
SpawnObjectInHand(randomPrefabs[UnityEngine.Random.Range(0, randomPrefabs.Count)], controller);
}
}
代码解释如下:
保存脚本,返回编辑器。
在结构视图中新建一个 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 款游戏:
通过这本书,你将学会如何制作自己的 Windows、macOS、iOS 平台游戏!
这本书完全针对 Unity 初学者,以及准备将自己的 Unity 技能提升到专业水准的人。这本书假设你有一定的编程经验(任何语言)。
如果你有任何看法和建议,请在下面留言!