VRTK使用心得(七):将The Lab中的LongBow移植到
近日在我们的“VR共同成长群”内有朋友要求将Interaction System中的LongBow模块转制为VRTK版本,本文将对此过程实现的主要步骤进行演示。鉴于要求是将LongBow的交互完全移植到VRTK,所以我们从原Interaction System模块出发进行修改。在VRTK自带的第23个实例中亦有相关射箭功能的演示,但是交互方式和机制会有些细微区别,有兴趣的读者可根据实例场景进行相关的修改。本文旨在了解射箭交互的实现原理,比较Interaction System与VRTK两个交互系统的区别,达到更加深入理解其运作机制的目的,实际开发中建议不要重复造轮子。
修改思路:
通过对于模块的观察和分析,LongBow基于ItemPackage机制——关于ItemPackage的实现机制,可以参见SDK自带的文档或我的视频教程第11课时——参与交互的物体分别为LongBow和ArrowHand。通过之前将PC端的Survival Shooter移植到VR端的经验可知,我们移植的大部分工作是修改其交互,又,Interaction System中引入了一个Player这个封装好的预制体,等同于CameraRig,其中两个Hand类便是我们重点修改的对象。
模块中涉及到三个比较重要的脚本,分别是LongBow.cs,ArrowHand.cs,和Arrow.cs,通过观察三个脚本的变量声明,我们看到Arrow.cs的变量多是Unity的基本变量声明,很少涉及到Player的引用,即很少有涉及到VR交互的变量,所以这个脚本我们基本上不会做修改。
Arrow.cs
反观ArrowHand.cs与LongBow.cs,如下图,其中大量引用了Player的Hand对象,同时Hand对象又有otherHand这个变量的引用,所以我们要进行代码修改的对象只有这两个脚本。
ArrowHand.cs
LongBow.cs
为了不影响Interaction System中的脚本,我们新建两个脚本VRTKLongBow.cs和VRTKArrowHand.cs,对应原LongBow.cs和ArrowHand.cs,分别挂载到LongBow和ArrowHand预制体上,命名空间与脚本内容保持不变,只修改类名,对于新脚本在检视面板上的引用,保持照抄。
原系统中LongBow的实现过程:
原系统使用ItemPackage机制实现配套道具的抓取。场景中摆放的弓箭为预览,包含其中的SphereCollider上挂载了ItemPackageSpawner脚本,与ItemPackage配合实现两个手柄的物体抓取。真正实现拉弓射箭交互的是挂载了脚本的LongBow和ArrowHand两个Prefab,可在相应的文件夹下找到。当手柄悬停并按下Trigger键进行抓取时,根据ItemPackage的配置,找到并生成(基于Instantiate)需要抓取的物体于指定的手柄,同时在指定的位置生成轮廓,当LongBow放回原位置时,销毁生成的两个对象。
预制体LongBow自带一个动画效果,播放弓被拉伸的过程,挂载的脚本会实时判断箭与弓的位置关系来判断是否给到瞄准的姿态,当抓取箭的手柄松开Trigger键的时候,给生成的箭一个方向上的力,实现箭的射出,同时播放弓回弹的动画。
image.png
image.png
VRTK替换过程:
我们先来看一下LongBow模块用到的自有组件及可以使用VRTK组件代替的地方,如下图:
LongBow预制体中:
- Interactble 组件是原系统设置物体为可交互必须的脚本,用来标记此物体为可交互,移植前可直接去掉,转而挂载VRTK_InteractableObject组件。
- LongBow 组件是实现交互的核心,也是我们需要改动代码的组件,使用新建的VRTK_LongBow组件代替。
- Item Package Reference 属于实现ItemPackage机制的组成部分,可直接去掉。
- Destroy On Detached From Hand 组件在原系统中用于物体在被释放以后进行销毁,这里我们可以在VRTK中物体被Ungrab事件中进行处理,并且移植以后我们只是将其放下,而不对其进行销毁处理。
- Hide On Hand Focus Lost 组件在原系统中用于物体在失去焦点以后隐藏,这里我们也直接去掉。
- Animator 组件是关于弓及弓弦张合的过程动画,所以重点来了:拉弓的过程,就是通过计算一个距离,去动态改变这段动画播放进度的过程。
- Linear Animator与Linear Mapping 是InteractionSystem中比较重要的机制,用来通过数值的映射而改变动画的播放进度,此模块中通过手柄与瞄准点的距离作为数据映射,改变弓弦的张合程度,移植以后的脚本中,我们可以直接通过计算出来的数值来控制动画的播放,代码如下:
//public LinearMapping bowDrawLinearMapping;
public Animator longBowAni;
注释掉LinearMapping的声明,新建一个动画的引用。
longBowAni = GetComponent<Animator>();
longBowAni.speed = 0;
在Awake函数中引用Animator组件,并将其速度置0。
//this.bowDrawLinearMapping.value = drawTension;
longBowAni.Play(0, 0, drawTension);
在代码中将所有原bowDrawLinearMapping的引用,皆替换为如上代码,drawTension为一个0到1的浮点数据类型,即控制动画播放的百分比。
下面再来看一下ArrowHand预制体挂载的组件。
ArrowHand预制体中:
- ArrowHand 组件实现对于Arrow预制体的控制,包括生成、射击等,这里我们使用新建的VRTKArrowHand脚本替换。
- Item Package Reference 组件同上,去掉。
- Destroy On Detached From Hand与Hide On Hand Focus Lost 组件同上,去掉。
- Allow Teleport Whild Attached To Hand 组件用来实现当抓取时可同时实现瞬移。在VRTK中,将物体设置为可交互的VRTK_Interactable Object组件中有"Stay Grabbed On Teleport"属性即可实现此功能,见下图,故去掉。
新建一个场景,按照之前文章中的VRTK配置过程进行VRTK的配置。
1. 设置物体为VRTK可交互
选中场景中的LongBow实例,为其添加可感应区域,添加一个位置及大小合适的SphereCollider,或使用原场景中Pickup携带的SphereCollider。然后选择Window->VRTK->SetUp Interactable Object,如下图:
image.png
同样的过程,配置ArrowHand预制体。需要注意的是,为ArrowHand添加感应区域时,添加的Collider不宜过大,这样容易在箭被发射的瞬间与其发生碰撞,从而导致不正常的出射轨迹。可以将预制体拖入场景进行配置,完毕以后点击属性面板中的Apply,然后将其删除,因为ArrowHand通过Instantiate prefab生成。
为了达到与原系统一样的交互过程,实现抓取的键为Trigger,故修改各Controller上挂载的VRTK_Interact Grab组件的Grab Button为"Trigger Press"。
设置抓取键
同时要实现抓取以后手柄不可见,在LongBow实例与ArrowHand预制体上挂载VRTK_InteractControllerAppearance脚本,勾选Hide Controller On Grab属性,即抓取时隐藏手柄,如下图。
VRTK_Interact Controller Appearance
2. 替换原系统Hand的交互
由于脚本中大量引用了Interaction System中Player预制体的Hand,所以要使用VRTK替换掉原系统中关于hand的引用,我们将原系统中的hand.otherHand拆分,分别命名为hand,otherHand,并在LongBow被抓取是实现左右手的判断和指定,代码如下:
//private Hand hand;
//抓取当前物体的控制器
private GameObject hand;
//另一只手
private GameObject otherHand;
private VRTKArrowHand arrowHand;
3. 替换ItemPackage机制:
ItemPackage的机制,本质上是根据其提供的配置列表,告诉交互系统,当触发该机制的时候,当前手柄抓取指定的物体,同时另一只手抓取指定的物体(如果指定)。作为替换,我们将LongBow预制体放到场景当中,替代预览用的弓,不再自动生成,而是直接抓取,至于同时抓取生成箭的功能,我们使用VRTK提供的抓取事件实现, 在Awake函数中注册抓取及释放事件处理函数:
GetComponent<VRTK_InteractableObject>().InteractableObjectGrabbed += VRTKLongBow_InteractableObjectGrabbed;
GetComponent<VRTK_InteractableObject>().InteractableObjectUngrabbed += VRTKLongBow_InteractableObjectUngrabbed;
在抓取事件中,生成ArrowHand预制体的实例,代码如下:
private void VRTKLongBow_InteractableObjectGrabbed(object sender, InteractableObjectEventArgs e)
{
bowCollider.enabled = false;
isGrab = true;
///当抓取LongBow时生成ArrowHand
GameObject arrowHandClone = Instantiate(arrowHandPrefab);
handType = VRTK_DeviceFinder.GetControllerHand(e.interactingObject);
///分别指定相应控制器,即左右手
if (handType == SDK_BaseController.ControllerHand.Left)
{
hand = VRTK_DeviceFinder.GetControllerLeftHand();
otherHand = VRTK_DeviceFinder.GetControllerRightHand();
}
else if (handType == SDK_BaseController.ControllerHand.Right)
{
hand = VRTK_DeviceFinder.GetControllerRightHand();
otherHand = VRTK_DeviceFinder.GetControllerLeftHand();
}
//另一只手抓取ArrowHand
otherHand.GetComponent<VRTK_InteractTouch>().ForceTouch(arrowHandClone);
otherHand.GetComponent<VRTK_InteractGrab>().AttemptGrab();
}
4.手柄震动的替换。 VRTK中使用VRTK_SDK_Bridge.HapticPulse()方法替换进行震动的调用,代码如下
ushort hapticStrength = (ushort)Util.RemapNumber(nockDistanceTravelled, 0, maxPull, bowPullPulseStrengthLow, bowPullPulseStrengthHigh);
///手柄震动
VRTK_SDK_Bridge.HapticPulse(VRTK_ControllerReference.GetControllerReference(hand), hapticStrength);
VRTK_SDK_Bridge.HapticPulse(VRTK_ControllerReference.GetControllerReference(otherHand), hapticStrength);
//hand.controller.TriggerHapticPulse(hapticStrength);
//hand.otherHand.controller.TriggerHapticPulse(hapticStrength);
5. 修改hand发送的update事件。 虽然我们在代码中修改了hand的相关引用,但是代码中的动态刷新瞄准、旋转功能即核心功能还是依赖hand从外部SendMessage来实现,可见Hand.cs中的相关代码:
void Update()
{
UpdateNoSteamVRFallback();
GameObject attached = currentAttachedObject;
if ( attached )
{
attached.SendMessage( "HandAttachedUpdate", this, SendMessageOptions.DontRequireReceiver );
}
if ( hoveringInteractable )
{
hoveringInteractable.SendMessage( "HandHoverUpdate", this, SendMessageOptions.DontRequireReceiver );
}
}
所以我们将VRTKLongBow和VRTKArrowHand中的HandAttachedUpdate改为Update函数。但此时如果运行程序,Update会自动运行函数内容,由于此时控制器尚未指定,会出现运行异常,所以需要置一个标志位isGrab,来控制逻辑的运行,即只有在物体被抓取以后才执行Update下的程序逻辑。
说明:
本文主要叙述移植过程思路及主要步骤,代码修改的过程也仅是引用片段,因为此过程是一个逐渐调错的过程,即根据调试过程中的报错来对需要修改的地方进行修改,属于良性调错,其过程琐碎且丑陋,故省略。至于射箭击中靶子以及箭与火把的交互,不涉及到VR范畴,亦无可修改之处,故省略,读者可自行完善。需要说明的是,箭击中靶子且吸附其上的原理是判断有无指定的Physics Material,如有则认为是射中了靶子,继而吸附在上面。在此过程中一些必要且明显的逻辑修改,亦不再赘述。整个过程详尽的实现可参考文末所附的代码,欢迎提出修改意见:
VRTKLongBow.cs:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using VRTK;
namespace Valve.VR.InteractionSystem
{
public class VRTKLongBow : MonoBehaviour
{
public enum Handedness { Left, Right };
public Handedness currentHandGuess = Handedness.Left;
private float timeOfPossibleHandSwitch = 0f;
private float timeBeforeConfirmingHandSwitch = 1.5f;
private bool possibleHandSwitch = false;
public Transform pivotTransform;
public Transform handleTransform;
//private Hand hand;
//抓取当前物体的控制器
private GameObject hand;
//另一只手
private GameObject otherHand;
private VRTKArrowHand arrowHand;
public Transform nockTransform;
public Transform nockRestTransform;
public bool autoSpawnArrowHand = true;
public ItemPackage arrowHandItemPackage;
public GameObject arrowHandPrefab;
public bool nocked;
public bool pulled;
private const float minPull = 0.05f;
private const float maxPull = 0.5f;
private float nockDistanceTravelled = 0f;
private float hapticDistanceThreshold = 0.01f;
private float lastTickDistance;
private const float bowPullPulseStrengthLow = 100;
private const float bowPullPulseStrengthHigh = 500;
private Vector3 bowLeftVector;
public float arrowMinVelocity = 3f;
public float arrowMaxVelocity = 30f;
private float arrowVelocity = 30f;
private float minStrainTickTime = 0.1f;
private float maxStrainTickTime = 0.5f;
private float nextStrainTick = 0;
private bool lerpBackToZeroRotation;
private float lerpStartTime;
private float lerpDuration = 0.15f;
private Quaternion lerpStartRotation;
private float nockLerpStartTime;
private Quaternion nockLerpStartRotation;
public float drawOffset = 0.06f;
//public LinearMapping bowDrawLinearMapping;
public Animator longBowAni;
private bool deferNewPoses = false;
private Vector3 lateUpdatePos;
private Quaternion lateUpdateRot;
public SoundBowClick drawSound;
private float drawTension;
public SoundPlayOneshot arrowSlideSound;
public SoundPlayOneshot releaseSound;
public SoundPlayOneshot nockSound;
SteamVR_Events.Action newPosesAppliedAction;
SDK_BaseController.ControllerHand handType;
private bool isGrab = false;
/// <summary>
/// 防止与弓发生碰撞
/// </summary>
private SphereCollider bowCollider;
//-------------------------------------------------
//被VRTK抓取事件处理函数替代
//private void OnAttachedToHand(Hand attachedHand)
//{
// hand = attachedHand;
//}
//-------------------------------------------------
void Awake()
{
longBowAni = GetComponent<Animator>();
longBowAni.speed = 0;
bowCollider = GetComponent<SphereCollider>();
newPosesAppliedAction = SteamVR_Events.NewPosesAppliedAction(OnNewPosesApplied);
GetComponent<VRTK_InteractableObject>().InteractableObjectGrabbed += VRTKLongBow_InteractableObjectGrabbed;
GetComponent<VRTK_InteractableObject>().InteractableObjectUngrabbed += VRTKLongBow_InteractableObjectUngrabbed;
}
private void VRTKLongBow_InteractableObjectUngrabbed(object sender, InteractableObjectEventArgs e)
{
isGrab = false;
bowCollider.enabled = true;
if (handType == SDK_BaseController.ControllerHand.Left)
{
VRTK_DeviceFinder.GetControllerRightHand().GetComponent<VRTK_InteractGrab>().ForceRelease();
}
else
{
VRTK_DeviceFinder.GetControllerLeftHand().GetComponent<VRTK_InteractGrab>().ForceRelease();
}
}
/// <summary>
/// 弓被抓取事件处理函数
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void VRTKLongBow_InteractableObjectGrabbed(object sender, InteractableObjectEventArgs e)
{
bowCollider.enabled = false;
isGrab = true;
///当抓取LongBow时生成ArrowHand
GameObject arrowHandClone = Instantiate(arrowHandPrefab);
handType = VRTK_DeviceFinder.GetControllerHand(e.interactingObject);
///分别指定相应控制器,即左右手
if (handType == SDK_BaseController.ControllerHand.Left)
{
hand = VRTK_DeviceFinder.GetControllerLeftHand();
otherHand = VRTK_DeviceFinder.GetControllerRightHand();
}
else if (handType == SDK_BaseController.ControllerHand.Right)
{
hand = VRTK_DeviceFinder.GetControllerRightHand();
otherHand = VRTK_DeviceFinder.GetControllerLeftHand();
}
//另一只手抓取ArrowHand
otherHand.GetComponent<VRTK_InteractTouch>().ForceTouch(arrowHandClone);
otherHand.GetComponent<VRTK_InteractGrab>().AttemptGrab();
}
//-------------------------------------------------
void OnEnable()
{
newPosesAppliedAction.enabled = true;
}
//-------------------------------------------------
void OnDisable()
{
newPosesAppliedAction.enabled = false;
}
//-------------------------------------------------
void LateUpdate()
{
if (deferNewPoses)
{
lateUpdatePos = transform.position;
lateUpdateRot = transform.rotation;
}
}
//-------------------------------------------------
private void OnNewPosesApplied()
{
if (deferNewPoses)
{
// Set longbow object back to previous pose position to avoid jitter
transform.position = lateUpdatePos;
transform.rotation = lateUpdateRot;
deferNewPoses = false;
}
}
//-------------------------------------------------
//private void HandAttachedUpdate(Hand hand)
private void Update()
{
if (!isGrab)
return;
// Reset transform since we cheated it right after getting poses on previous frame
transform.localPosition = Vector3.zero;
transform.localRotation = Quaternion.identity;
// Update handedness guess
//EvaluateHandedness();
if (nocked)
{
deferNewPoses = true;
Vector3 nockToarrowHand = (arrowHand.arrowNockTransform.parent.position - nockRestTransform.position); // Vector from bow nock transform to arrowhand nock transform - used to align bow when drawing
// Align bow
// Time lerp value used for ramping into drawn bow orientation
float lerp = Util.RemapNumberClamped(Time.time, nockLerpStartTime, (nockLerpStartTime + lerpDuration), 0f, 1f);
float pullLerp = Util.RemapNumberClamped(nockToarrowHand.magnitude, minPull, maxPull, 0f, 1f); // Normalized current state of bow draw 0 - 1
//Vector3 arrowNockTransformToHeadset = ((Player.instance.hmdTransform.position + (Vector3.down * 0.05f)) - arrowHand.arrowNockTransform.parent.position).normalized;
Vector3 arrowNockTransformToHeadset = ((VRTK_SDK_Bridge.GetHeadset().position + (Vector3.down * 0.05f)) - arrowHand.arrowNockTransform.parent.position).normalized;
Vector3 arrowHandPosition = (arrowHand.arrowNockTransform.parent.position + ((arrowNockTransformToHeadset * drawOffset) * pullLerp)); // Use this line to lerp arrowHand nock position
//Vector3 arrowHandPosition = arrowHand.arrowNockTransform.position; // Use this line if we don't want to lerp arrowHand nock position
Vector3 pivotToString = (arrowHandPosition - pivotTransform.position).normalized;
Vector3 pivotToLowerHandle = (handleTransform.position - pivotTransform.position).normalized;
bowLeftVector = -Vector3.Cross(pivotToLowerHandle, pivotToString);
pivotTransform.rotation = Quaternion.Lerp(nockLerpStartRotation, Quaternion.LookRotation(pivotToString, bowLeftVector), lerp);
// Move nock position
if (Vector3.Dot(nockToarrowHand, -nockTransform.forward) > 0)
{
float distanceToarrowHand = nockToarrowHand.magnitude * lerp;
nockTransform.localPosition = new Vector3(0f, 0f, Mathf.Clamp(-distanceToarrowHand, -maxPull, 0f));
nockDistanceTravelled = -nockTransform.localPosition.z;
arrowVelocity = Util.RemapNumber(nockDistanceTravelled, minPull, maxPull, arrowMinVelocity, arrowMaxVelocity);
drawTension = Util.RemapNumberClamped(nockDistanceTravelled, 0, maxPull, 0f, 1f);
//this.bowDrawLinearMapping.value = drawTension; // Send drawTension value to LinearMapping script, which drives the bow draw animation
longBowAni.Play(0, 0, drawTension);
if (nockDistanceTravelled > minPull)
{
pulled = true;
}
else
{
pulled = false;
}
if ((nockDistanceTravelled > (lastTickDistance + hapticDistanceThreshold)) || nockDistanceTravelled < (lastTickDistance - hapticDistanceThreshold))
{
ushort hapticStrength = (ushort)Util.RemapNumber(nockDistanceTravelled, 0, maxPull, bowPullPulseStrengthLow, bowPullPulseStrengthHigh);
///手柄震动
VRTK_SDK_Bridge.HapticPulse(VRTK_ControllerReference.GetControllerReference(hand), hapticStrength);
VRTK_SDK_Bridge.HapticPulse(VRTK_ControllerReference.GetControllerReference(otherHand), hapticStrength);
//hand.controller.TriggerHapticPulse(hapticStrength);
//hand.otherHand.controller.TriggerHapticPulse(hapticStrength);
drawSound.PlayBowTensionClicks(drawTension);
lastTickDistance = nockDistanceTravelled;
}
if (nockDistanceTravelled >= maxPull)
{
if (Time.time > nextStrainTick)
{
//hand.controller.TriggerHapticPulse(400);
//hand.otherHand.controller.TriggerHapticPulse(400);
VRTK_SDK_Bridge.HapticPulse(VRTK_ControllerReference.GetControllerReference(hand), 400);
VRTK_SDK_Bridge.HapticPulse(VRTK_ControllerReference.GetControllerReference(otherHand), 400);
drawSound.PlayBowTensionClicks(drawTension);
nextStrainTick = Time.time + Random.Range(minStrainTickTime, maxStrainTickTime);
}
}
}
else
{
nockTransform.localPosition = new Vector3(0f, 0f, 0f);
//this.bowDrawLinearMapping.value = 0f;
longBowAni.Play(0, 0, 0);
}
}
else
{
if (lerpBackToZeroRotation)
{
float lerp = Util.RemapNumber(Time.time, lerpStartTime, lerpStartTime + lerpDuration, 0, 1);
pivotTransform.localRotation = Quaternion.Lerp(lerpStartRotation, Quaternion.identity, lerp);
if (lerp >= 1)
{
lerpBackToZeroRotation = false;
}
}
}
}
//-------------------------------------------------
public void ArrowReleased()
{
nocked = false;
//hand.HoverUnlock(GetComponent<Interactable>());
//hand.otherHand.HoverUnlock(arrowHand.GetComponent<Interactable>());
if (releaseSound != null)
{
releaseSound.Play();
}
this.StartCoroutine(this.ResetDrawAnim());
}
//-------------------------------------------------
private IEnumerator ResetDrawAnim()
{
float startTime = Time.time;
float startLerp = drawTension;
while (Time.time < (startTime + 0.02f))
{
float lerp = Util.RemapNumberClamped(Time.time, startTime, startTime + 0.02f, startLerp, 0f);
//this.bowDrawLinearMapping.value = lerp;
longBowAni.Play(0, 0, lerp);
yield return null;
}
//this.bowDrawLinearMapping.value = 0;
longBowAni.Play(0, 0, 0);
yield break;
}
//-------------------------------------------------
public float GetArrowVelocity()
{
return arrowVelocity;
}
//-------------------------------------------------
public void StartRotationLerp()
{
lerpStartTime = Time.time;
lerpBackToZeroRotation = true;
lerpStartRotation = pivotTransform.localRotation;
Util.ResetTransform(nockTransform);
}
//-------------------------------------------------
public void StartNock(VRTKArrowHand currentArrowHand)
{
arrowHand = currentArrowHand;
//hand.HoverLock(GetComponent<Interactable>());
nocked = true;
nockLerpStartTime = Time.time;
nockLerpStartRotation = pivotTransform.rotation;
// Sound of arrow sliding on nock as it's being pulled back
arrowSlideSound.Play();
// Decide which hand we're drawing with and lerp to the correct side
DoHandednessCheck();
}
//-------------------------------------------------
//private void EvaluateHandedness()
//{
// Hand.HandType handType = hand.GuessCurrentHandType();
// if (handType == Hand.HandType.Left)// Bow hand is further left than arrow hand.
// {
// // We were considering a switch, but the current controller orientation matches our currently assigned handedness, so no longer consider a switch
// if (possibleHandSwitch && currentHandGuess == Handedness.Left)
// {
// possibleHandSwitch = false;
// }
// // If we previously thought the bow was right-handed, and were not already considering switching, start considering a switch
// if (!possibleHandSwitch && currentHandGuess == Handedness.Right)
// {
// possibleHandSwitch = true;
// timeOfPossibleHandSwitch = Time.time;
// }
// // If we are considering a handedness switch, and it's been this way long enough, switch
// if (possibleHandSwitch && Time.time > (timeOfPossibleHandSwitch + timeBeforeConfirmingHandSwitch))
// {
// currentHandGuess = Handedness.Left;
// possibleHandSwitch = false;
// }
// }
// else // Bow hand is further right than arrow hand
// {
// // We were considering a switch, but the current controller orientation matches our currently assigned handedness, so no longer consider a switch
// if (possibleHandSwitch && currentHandGuess == Handedness.Right)
// {
// possibleHandSwitch = false;
// }
// // If we previously thought the bow was right-handed, and were not already considering switching, start considering a switch
// if (!possibleHandSwitch && currentHandGuess == Handedness.Left)
// {
// possibleHandSwitch = true;
// timeOfPossibleHandSwitch = Time.time;
// }
// // If we are considering a handedness switch, and it's been this way long enough, switch
// if (possibleHandSwitch && Time.time > (timeOfPossibleHandSwitch + timeBeforeConfirmingHandSwitch))
// {
// currentHandGuess = Handedness.Right;
// possibleHandSwitch = false;
// }
// }
//}
//-------------------------------------------------
private void DoHandednessCheck()
{
// Based on our current best guess about hand, switch bow orientation and arrow lerp direction
if (currentHandGuess == Handedness.Left)
{
pivotTransform.localScale = new Vector3(1f, 1f, 1f);
}
else
{
pivotTransform.localScale = new Vector3(1f, -1f, 1f);
}
}
//-------------------------------------------------
public void ArrowInPosition()
{
DoHandednessCheck();
if (nockSound != null)
{
nockSound.Play();
}
}
//-------------------------------------------------
public void ReleaseNock()
{
// ArrowHand tells us to do this when we release the buttons when bow is nocked but not drawn far enough
nocked = false;
//hand.HoverUnlock(GetComponent<Interactable>());
this.StartCoroutine(this.ResetDrawAnim());
}
//改写Shutdown内容
private void ShutDown()
{
//if (hand != null && hand.otherHand.currentAttachedObject != null)
//{
// if (hand.otherHand.currentAttachedObject.GetComponent<ItemPackageReference>() != null)
// {
// if (hand.otherHand.currentAttachedObject.GetComponent<ItemPackageReference>().itemPackage == arrowHandItemPackage)
// {
// hand.otherHand.DetachObject(hand.otherHand.currentAttachedObject);
// }
// }
//}
}
//-------------------------------------------------
private void OnHandFocusLost(Hand hand)
{
gameObject.SetActive(false);
}
//-------------------------------------------------
private void OnHandFocusAcquired(Hand hand)
{
gameObject.SetActive(true);
//调用抓取事件处理函数,不再使用
///OnAttachedToHand(hand);
}
//不再使用,不再销毁
private void OnDetachedFromHand(Hand hand)
{
Destroy(gameObject);
}
//-------------------------------------------------
void OnDestroy()
{
ShutDown();
}
}
}
VRTKArrowHand.cs:
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using VRTK;
namespace Valve.VR.InteractionSystem
{
public class VRTKArrowHand : MonoBehaviour
{
//private Hand hand;
private GameObject hand;
private GameObject otherHand;
private VRTKLongBow bow;
private GameObject currentArrow;
public GameObject arrowPrefab;
public Transform arrowNockTransform;
public float nockDistance = 0.1f;
public float lerpCompleteDistance = 0.08f;
public float rotationLerpThreshold = 0.15f;
public float positionLerpThreshold = 0.15f;
private bool allowArrowSpawn = true;
private bool nocked;
private bool inNockRange = false;
private bool arrowLerpComplete = false;
public SoundPlayOneshot arrowSpawnSound;
//private AllowTeleportWhileAttachedToHand allowTeleport = null;
public int maxArrowCount = 10;
private List<GameObject> arrowList;
private bool triggerPressed = false;
/// <summary>
/// 物体是否被抓取
/// </summary>
private bool isGrab = false;
SDK_BaseController.ControllerHand handType;
//-------------------------------------------------
void Awake()
{
//allowTeleport = GetComponent<AllowTeleportWhileAttachedToHand>();
//allowTeleport.teleportAllowed = true;
//allowTeleport.overrideHoverLock = false;
arrowList = new List<GameObject>();
GetComponent<VRTK_InteractableObject>().InteractableObjectGrabbed += VRTKArrowHand_InteractableObjectGrabbed;
GetComponent<VRTK_InteractableObject>().InteractableObjectUngrabbed += VRTKArrowHand_InteractableObjectUngrabbed;
}
/// <summary>
/// ArrowHand被释放处理函数
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void VRTKArrowHand_InteractableObjectUngrabbed(object sender, InteractableObjectEventArgs e)
{
isGrab = false;
hand = e.interactingObject;
hand.GetComponent<VRTK_ControllerEvents>().TriggerReleased -= VRTKArrowHand_TriggerReleased;
hand.GetComponent<VRTK_ControllerEvents>().TriggerPressed -= VRTKArrowHand_TriggerPressed;
//Destroy(gameObject);
}
/// <summary>
/// ArrowHand被抓取处理函数,替代OnAttachedToHand函数
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void VRTKArrowHand_InteractableObjectGrabbed(object sender, InteractableObjectEventArgs e)
{
isGrab = true;
hand = e.interactingObject;
handType = VRTK_DeviceFinder.GetControllerHand(e.interactingObject);
///分别指定相应控制器,即左右手
if (handType == SDK_BaseController.ControllerHand.Left)
{
hand = VRTK_DeviceFinder.GetControllerLeftHand();
otherHand = VRTK_DeviceFinder.GetControllerRightHand();
}
else if (handType == SDK_BaseController.ControllerHand.Right)
{
hand = VRTK_DeviceFinder.GetControllerRightHand();
otherHand = VRTK_DeviceFinder.GetControllerLeftHand();
}
hand.GetComponent<VRTK_ControllerEvents>().TriggerReleased += VRTKArrowHand_TriggerReleased;
hand.GetComponent<VRTK_ControllerEvents>().TriggerPressed += VRTKArrowHand_TriggerPressed;
FindBow();
}
/// <summary>
/// Trigger键按下
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void VRTKArrowHand_TriggerPressed(object sender, ControllerInteractionEventArgs e)
{
triggerPressed = true;
}
/// <summary>
/// Trigger键松开
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void VRTKArrowHand_TriggerReleased(object sender, ControllerInteractionEventArgs e)
{
triggerPressed = false;
}
//等同于onGrab
//private void OnAttachedToHand(Hand attachedHand)
//{
// hand = attachedHand;
// FindBow();
//}
/// <summary>
/// 生成Arrow
/// </summary>
/// <returns></returns>
private GameObject InstantiateArrow()
{
GameObject arrow = Instantiate(arrowPrefab, arrowNockTransform.position, arrowNockTransform.rotation) as GameObject;
arrow.name = "Bow Arrow";
arrow.transform.parent = arrowNockTransform;
Util.ResetTransform(arrow.transform);
arrowList.Add(arrow);
while (arrowList.Count > maxArrowCount)
{
GameObject oldArrow = arrowList[0];
arrowList.RemoveAt(0);
if (oldArrow)
{
Destroy(oldArrow);
}
}
return arrow;
}
//注释掉HandAttachedUpdate函数,函数内容保持不变,函数名改为Update
//private void HandAttachedUpdate(Hand hand)
private void Update()
{
if (!isGrab)
return;
if (bow == null)
{
FindBow();
}
if (bow == null)
{
return;
}
if (allowArrowSpawn && (currentArrow == null)) // If we're allowed to have an active arrow in hand but don't yet, spawn one
{
currentArrow = InstantiateArrow();
arrowSpawnSound.Play();
}
float distanceToNockPosition = Vector3.Distance(transform.parent.position, bow.nockTransform.position);
// If there's an arrow spawned in the hand and it's not nocked yet
if (!nocked)
{
// If we're close enough to nock position that we want to start arrow rotation lerp, do so
if (distanceToNockPosition < rotationLerpThreshold)
{
float lerp = Util.RemapNumber(distanceToNockPosition, rotationLerpThreshold, lerpCompleteDistance, 0, 1);
arrowNockTransform.rotation = Quaternion.Lerp(arrowNockTransform.parent.rotation, bow.nockRestTransform.rotation, lerp);
}
else // Not close enough for rotation lerp, reset rotation
{
arrowNockTransform.localRotation = Quaternion.identity;
}
// If we're close enough to the nock position that we want to start arrow position lerp, do so
if (distanceToNockPosition < positionLerpThreshold)
{
float posLerp = Util.RemapNumber(distanceToNockPosition, positionLerpThreshold, lerpCompleteDistance, 0, 1);
posLerp = Mathf.Clamp(posLerp, 0f, 1f);
arrowNockTransform.position = Vector3.Lerp(arrowNockTransform.parent.position, bow.nockRestTransform.position, posLerp);
}
else // Not close enough for position lerp, reset position
{
arrowNockTransform.position = arrowNockTransform.parent.position;
}
// Give a haptic tick when lerp is visually complete
if (distanceToNockPosition < lerpCompleteDistance)
{
if (!arrowLerpComplete)
{
arrowLerpComplete = true;
//hand.controller.TriggerHapticPulse(500);
VRTK_SDK_Bridge.HapticPulse(VRTK_ControllerReference.GetControllerReference(hand), 500);
}
}
else
{
if (arrowLerpComplete)
{
arrowLerpComplete = false;
}
}
// Allow nocking the arrow when controller is close enough
if (distanceToNockPosition < nockDistance)
{
if (!inNockRange)
{
inNockRange = true;
bow.ArrowInPosition();
}
}
else
{
if (inNockRange)
{
inNockRange = false;
}
}
// If arrow is close enough to the nock position and we're pressing the trigger, and we're not nocked yet, Nock
//拉弓瞄准
if ((distanceToNockPosition < nockDistance) && triggerPressed && !nocked)
{
if (currentArrow == null)
{
currentArrow = InstantiateArrow();
}
nocked = true;
bow.StartNock(this);
//hand.HoverLock(GetComponent<Interactable>());
//allowTeleport.teleportAllowed = false;
currentArrow.transform.parent = bow.nockTransform;
Util.ResetTransform(currentArrow.transform);
Util.ResetTransform(arrowNockTransform);
}
}
// If arrow is nocked, and we release the trigger
if (nocked && !triggerPressed)
{
if (bow.pulled) // If bow is pulled back far enough, fire arrow, otherwise reset arrow in arrowhand
{
FireArrow();
}
else
{
arrowNockTransform.rotation = currentArrow.transform.rotation;
currentArrow.transform.parent = arrowNockTransform;
Util.ResetTransform(currentArrow.transform);
nocked = false;
bow.ReleaseNock();
//hand.HoverUnlock(GetComponent<Interactable>());
//allowTeleport.teleportAllowed = true;
}
bow.StartRotationLerp(); // Arrow is releasing from the bow, tell the bow to lerp back to controller rotation
}
}
//改写为grab unity事件
private void OnDetachedFromHand(Hand hand)
{
Destroy(gameObject);
}
//-------------------------------------------------
private void FireArrow()
{
currentArrow.transform.parent = null;
Arrow arrow = currentArrow.GetComponent<Arrow>();
arrow.shaftRB.isKinematic = false;
arrow.shaftRB.useGravity = true;
arrow.shaftRB.transform.GetComponent<BoxCollider>().enabled = true;
arrow.arrowHeadRB.isKinematic = false;
arrow.arrowHeadRB.useGravity = true;
arrow.arrowHeadRB.transform.GetComponent<BoxCollider>().enabled = true;
arrow.arrowHeadRB.AddForce(currentArrow.transform.forward * bow.GetArrowVelocity(), ForceMode.VelocityChange);
arrow.arrowHeadRB.AddTorque(currentArrow.transform.forward * 10);
nocked = false;
currentArrow.GetComponent<Arrow>().ArrowReleased(bow.GetArrowVelocity());
bow.ArrowReleased();
allowArrowSpawn = false;
Invoke("EnableArrowSpawn", 0.5f);
StartCoroutine(ArrowReleaseHaptics());
currentArrow = null;
//allowTeleport.teleportAllowed = true;
}
//-------------------------------------------------
private void EnableArrowSpawn()
{
allowArrowSpawn = true;
}
//-------------------------------------------------
private IEnumerator ArrowReleaseHaptics()
{
yield return new WaitForSeconds(0.05f);
//hand.otherHand.controller.TriggerHapticPulse(1500);
VRTK_SDK_Bridge.HapticPulse(VRTK_ControllerReference.GetControllerReference(otherHand), 1500);
yield return new WaitForSeconds(0.05f);
//hand.otherHand.controller.TriggerHapticPulse(800);
VRTK_SDK_Bridge.HapticPulse(VRTK_ControllerReference.GetControllerReference(otherHand), 800);
yield return new WaitForSeconds(0.05f);
//hand.otherHand.controller.TriggerHapticPulse(500);
VRTK_SDK_Bridge.HapticPulse(VRTK_ControllerReference.GetControllerReference(otherHand), 500);
yield return new WaitForSeconds(0.05f);
//hand.otherHand.controller.TriggerHapticPulse(300);
VRTK_SDK_Bridge.HapticPulse(VRTK_ControllerReference.GetControllerReference(otherHand), 300);
}
//-------------------------------------------------
private void OnHandFocusLost(Hand hand)
{
gameObject.SetActive(false);
}
//-------------------------------------------------
private void OnHandFocusAcquired(Hand hand)
{
gameObject.SetActive(true);
}
//-------------------------------------------------
private void FindBow()
{
SDK_BaseController.ControllerHand handType = VRTK_DeviceFinder.GetControllerHand(hand);
GameObject bowGo;
bowGo = otherHand.GetComponent<VRTK_InteractGrab>().GetGrabbedObject();
bow = bowGo.GetComponent<VRTKLongBow>();
//bow = hand.otherHand.GetComponentInChildren<VRTKLongBow>();
}
}
}
本文是我的视频教程《HTC VIVE交互开发实例教程》的节选文字版,更多VRTK实例教程可参见蛮牛教育首页