近日在我們的“VR共同成長(zhǎng)群”內(nèi)有朋友要求將Interaction System中的LongBow模塊轉(zhuǎn)制為VRTK版本泥栖,本文將對(duì)此過(guò)程實(shí)現(xiàn)的主要步驟進(jìn)行演示佃声。鑒于要求是將LongBow的交互完全移植到VRTK寨典,所以我們從原Interaction System模塊出發(fā)進(jìn)行修改颓帝。在VRTK自帶的第23個(gè)實(shí)例中亦有相關(guān)射箭功能的演示锻煌,但是交互方式和機(jī)制會(huì)有些細(xì)微區(qū)別翩隧,有興趣的讀者可根據(jù)實(shí)例場(chǎng)景進(jìn)行相關(guān)的修改樊展。本文旨在了解射箭交互的實(shí)現(xiàn)原理,比較Interaction System與VRTK兩個(gè)交互系統(tǒng)的區(qū)別,達(dá)到更加深入理解其運(yùn)作機(jī)制的目的专缠,實(shí)際開(kāi)發(fā)中建議不要重復(fù)造輪子雷酪。
修改思路:
通過(guò)對(duì)于模塊的觀察和分析,LongBow基于ItemPackage機(jī)制——關(guān)于ItemPackage的實(shí)現(xiàn)機(jī)制涝婉,可以參見(jiàn)SDK自帶的文檔或我的視頻教程第11課時(shí)——參與交互的物體分別為L(zhǎng)ongBow和ArrowHand哥力。通過(guò)之前將PC端的Survival Shooter移植到VR端的經(jīng)驗(yàn)可知,我們移植的大部分工作是修改其交互墩弯,又吩跋,Interaction System中引入了一個(gè)Player這個(gè)封裝好的預(yù)制體,等同于CameraRig渔工,其中兩個(gè)Hand類便是我們重點(diǎn)修改的對(duì)象锌钮。
模塊中涉及到三個(gè)比較重要的腳本,分別是LongBow.cs,ArrowHand.cs,和Arrow.cs引矩,通過(guò)觀察三個(gè)腳本的變量聲明梁丘,我們看到Arrow.cs的變量多是Unity的基本變量聲明,很少涉及到Player的引用旺韭,即很少有涉及到VR交互的變量氛谜,所以這個(gè)腳本我們基本上不會(huì)做修改。
反觀ArrowHand.cs與LongBow.cs茂翔,如下圖混蔼,其中大量引用了Player的Hand對(duì)象履腋,同時(shí)Hand對(duì)象又有otherHand這個(gè)變量的引用珊燎,所以我們要進(jìn)行代碼修改的對(duì)象只有這兩個(gè)腳本。
為了不影響Interaction System中的腳本遵湖,我們新建兩個(gè)腳本VRTKLongBow.cs和VRTKArrowHand.cs悔政,對(duì)應(yīng)原LongBow.cs和ArrowHand.cs,分別掛載到LongBow和ArrowHand預(yù)制體上延旧,命名空間與腳本內(nèi)容保持不變谋国,只修改類名,對(duì)于新腳本在檢視面板上的引用迁沫,保持照抄芦瘾。
原系統(tǒng)中LongBow的實(shí)現(xiàn)過(guò)程:
原系統(tǒng)使用ItemPackage機(jī)制實(shí)現(xiàn)配套道具的抓取。場(chǎng)景中擺放的弓箭為預(yù)覽集畅,包含其中的SphereCollider上掛載了ItemPackageSpawner腳本近弟,與ItemPackage配合實(shí)現(xiàn)兩個(gè)手柄的物體抓取。真正實(shí)現(xiàn)拉弓射箭交互的是掛載了腳本的LongBow和ArrowHand兩個(gè)Prefab挺智,可在相應(yīng)的文件夾下找到祷愉。當(dāng)手柄懸停并按下Trigger鍵進(jìn)行抓取時(shí),根據(jù)ItemPackage的配置,找到并生成(基于Instantiate)需要抓取的物體于指定的手柄二鳄,同時(shí)在指定的位置生成輪廓赴涵,當(dāng)LongBow放回原位置時(shí),銷毀生成的兩個(gè)對(duì)象订讼。
預(yù)制體LongBow自帶一個(gè)動(dòng)畫(huà)效果髓窜,播放弓被拉伸的過(guò)程,掛載的腳本會(huì)實(shí)時(shí)判斷箭與弓的位置關(guān)系來(lái)判斷是否給到瞄準(zhǔn)的姿態(tài)躯嫉,當(dāng)抓取箭的手柄松開(kāi)Trigger鍵的時(shí)候纱烘,給生成的箭一個(gè)方向上的力,實(shí)現(xiàn)箭的射出祈餐,同時(shí)播放弓回彈的動(dòng)畫(huà)擂啥。
VRTK替換過(guò)程:
我們先來(lái)看一下LongBow模塊用到的自有組件及可以使用VRTK組件代替的地方,如下圖:
LongBow預(yù)制體中:
- Interactble 組件是原系統(tǒng)設(shè)置物體為可交互必須的腳本帆阳,用來(lái)標(biāo)記此物體為可交互哺壶,移植前可直接去掉,轉(zhuǎn)而掛載VRTK_InteractableObject組件蜒谤。
- LongBow 組件是實(shí)現(xiàn)交互的核心山宾,也是我們需要改動(dòng)代碼的組件,使用新建的VRTK_LongBow組件代替鳍徽。
- Item Package Reference 屬于實(shí)現(xiàn)ItemPackage機(jī)制的組成部分资锰,可直接去掉。
- Destroy On Detached From Hand 組件在原系統(tǒng)中用于物體在被釋放以后進(jìn)行銷毀阶祭,這里我們可以在VRTK中物體被Ungrab事件中進(jìn)行處理绷杜,并且移植以后我們只是將其放下,而不對(duì)其進(jìn)行銷毀處理濒募。
- Hide On Hand Focus Lost 組件在原系統(tǒng)中用于物體在失去焦點(diǎn)以后隱藏鞭盟,這里我們也直接去掉。
- Animator 組件是關(guān)于弓及弓弦張合的過(guò)程動(dòng)畫(huà)瑰剃,所以重點(diǎn)來(lái)了:拉弓的過(guò)程齿诉,就是通過(guò)計(jì)算一個(gè)距離,去動(dòng)態(tài)改變這段動(dòng)畫(huà)播放進(jìn)度的過(guò)程晌姚。
- Linear Animator與Linear Mapping 是InteractionSystem中比較重要的機(jī)制粤剧,用來(lái)通過(guò)數(shù)值的映射而改變動(dòng)畫(huà)的播放進(jìn)度,此模塊中通過(guò)手柄與瞄準(zhǔn)點(diǎn)的距離作為數(shù)據(jù)映射挥唠,改變弓弦的張合程度抵恋,移植以后的腳本中,我們可以直接通過(guò)計(jì)算出來(lái)的數(shù)值來(lái)控制動(dòng)畫(huà)的播放猛遍,代碼如下:
//public LinearMapping bowDrawLinearMapping;
public Animator longBowAni;
注釋掉LinearMapping的聲明馋记,新建一個(gè)動(dòng)畫(huà)的引用号坡。
longBowAni = GetComponent<Animator>();
longBowAni.speed = 0;
在Awake函數(shù)中引用Animator組件列肢,并將其速度置0选泻。
//this.bowDrawLinearMapping.value = drawTension;
longBowAni.Play(0, 0, drawTension);
在代碼中將所有原bowDrawLinearMapping的引用岭佳,皆替換為如上代碼重虑,drawTension為一個(gè)0到1的浮點(diǎn)數(shù)據(jù)類型糙臼,即控制動(dòng)畫(huà)播放的百分比群叶。
下面再來(lái)看一下ArrowHand預(yù)制體掛載的組件蒋譬。
ArrowHand預(yù)制體中:
- ArrowHand 組件實(shí)現(xiàn)對(duì)于Arrow預(yù)制體的控制属愤,包括生成号胚、射擊等籽慢,這里我們使用新建的VRTKArrowHand腳本替換。
- Item Package Reference 組件同上猫胁,去掉箱亿。
- Destroy On Detached From Hand與Hide On Hand Focus Lost 組件同上,去掉弃秆。
- Allow Teleport Whild Attached To Hand 組件用來(lái)實(shí)現(xiàn)當(dāng)抓取時(shí)可同時(shí)實(shí)現(xiàn)瞬移届惋。在VRTK中,將物體設(shè)置為可交互的VRTK_Interactable Object組件中有"Stay Grabbed On Teleport"屬性即可實(shí)現(xiàn)此功能菠赚,見(jiàn)下圖脑豹,故去掉。
新建一個(gè)場(chǎng)景衡查,按照之前文章中的VRTK配置過(guò)程進(jìn)行VRTK的配置瘩欺。
1. 設(shè)置物體為VRTK可交互
選中場(chǎng)景中的LongBow實(shí)例,為其添加可感應(yīng)區(qū)域拌牲,添加一個(gè)位置及大小合適的SphereCollider俱饿,或使用原場(chǎng)景中Pickup攜帶的SphereCollider。然后選擇Window->VRTK->SetUp Interactable Object们拙,如下圖:
同樣的過(guò)程稍途,配置ArrowHand預(yù)制體阁吝。需要注意的是砚婆,為ArrowHand添加感應(yīng)區(qū)域時(shí),添加的Collider不宜過(guò)大突勇,這樣容易在箭被發(fā)射的瞬間與其發(fā)生碰撞装盯,從而導(dǎo)致不正常的出射軌跡〖撞觯可以將預(yù)制體拖入場(chǎng)景進(jìn)行配置埂奈,完畢以后點(diǎn)擊屬性面板中的Apply,然后將其刪除定躏,因?yàn)锳rrowHand通過(guò)Instantiate prefab生成账磺。
為了達(dá)到與原系統(tǒng)一樣的交互過(guò)程芹敌,實(shí)現(xiàn)抓取的鍵為T(mén)rigger,故修改各Controller上掛載的VRTK_Interact Grab組件的Grab Button為"Trigger Press"垮抗。
同時(shí)要實(shí)現(xiàn)抓取以后手柄不可見(jiàn)氏捞,在LongBow實(shí)例與ArrowHand預(yù)制體上掛載VRTK_InteractControllerAppearance腳本,勾選Hide Controller On Grab屬性冒版,即抓取時(shí)隱藏手柄液茎,如下圖。
2. 替換原系統(tǒng)Hand的交互
由于腳本中大量引用了Interaction System中Player預(yù)制體的Hand辞嗡,所以要使用VRTK替換掉原系統(tǒng)中關(guān)于hand的引用捆等,我們將原系統(tǒng)中的hand.otherHand拆分,分別命名為hand续室,otherHand栋烤,并在LongBow被抓取是實(shí)現(xiàn)左右手的判斷和指定,代碼如下:
//private Hand hand;
//抓取當(dāng)前物體的控制器
private GameObject hand;
//另一只手
private GameObject otherHand;
private VRTKArrowHand arrowHand;
3. 替換ItemPackage機(jī)制:
ItemPackage的機(jī)制挺狰,本質(zhì)上是根據(jù)其提供的配置列表班缎,告訴交互系統(tǒng),當(dāng)觸發(fā)該機(jī)制的時(shí)候她渴,當(dāng)前手柄抓取指定的物體达址,同時(shí)另一只手抓取指定的物體(如果指定)。作為替換趁耗,我們將LongBow預(yù)制體放到場(chǎng)景當(dāng)中沉唠,替代預(yù)覽用的弓,不再自動(dòng)生成苛败,而是直接抓取满葛,至于同時(shí)抓取生成箭的功能,我們使用VRTK提供的抓取事件實(shí)現(xiàn)罢屈, 在Awake函數(shù)中注冊(cè)抓取及釋放事件處理函數(shù):
GetComponent<VRTK_InteractableObject>().InteractableObjectGrabbed += VRTKLongBow_InteractableObjectGrabbed;
GetComponent<VRTK_InteractableObject>().InteractableObjectUngrabbed += VRTKLongBow_InteractableObjectUngrabbed;
在抓取事件中嘀韧,生成ArrowHand預(yù)制體的實(shí)例,代碼如下:
private void VRTKLongBow_InteractableObjectGrabbed(object sender, InteractableObjectEventArgs e)
{
bowCollider.enabled = false;
isGrab = true;
///當(dāng)抓取LongBow時(shí)生成ArrowHand
GameObject arrowHandClone = Instantiate(arrowHandPrefab);
handType = VRTK_DeviceFinder.GetControllerHand(e.interactingObject);
///分別指定相應(yīng)控制器缠捌,即左右手
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.手柄震動(dòng)的替換锄贷。 VRTK中使用VRTK_SDK_Bridge.HapticPulse()方法替換進(jìn)行震動(dòng)的調(diào)用,代碼如下
ushort hapticStrength = (ushort)Util.RemapNumber(nockDistanceTravelled, 0, maxPull, bowPullPulseStrengthLow, bowPullPulseStrengthHigh);
///手柄震動(dòng)
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發(fā)送的update事件曼月。 雖然我們?cè)诖a中修改了hand的相關(guān)引用谊却,但是代碼中的動(dòng)態(tài)刷新瞄準(zhǔn)、旋轉(zhuǎn)功能即核心功能還是依賴hand從外部SendMessage來(lái)實(shí)現(xiàn)哑芹,可見(jiàn)Hand.cs中的相關(guān)代碼:
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函數(shù)炎辨。但此時(shí)如果運(yùn)行程序,Update會(huì)自動(dòng)運(yùn)行函數(shù)內(nèi)容聪姿,由于此時(shí)控制器尚未指定碴萧,會(huì)出現(xiàn)運(yùn)行異常乙嘀,所以需要置一個(gè)標(biāo)志位isGrab,來(lái)控制邏輯的運(yùn)行破喻,即只有在物體被抓取以后才執(zhí)行Update下的程序邏輯乒躺。
說(shuō)明:
本文主要敘述移植過(guò)程思路及主要步驟,代碼修改的過(guò)程也僅是引用片段低缩,因?yàn)榇诉^(guò)程是一個(gè)逐漸調(diào)錯(cuò)的過(guò)程嘉冒,即根據(jù)調(diào)試過(guò)程中的報(bào)錯(cuò)來(lái)對(duì)需要修改的地方進(jìn)行修改,屬于良性調(diào)錯(cuò)咆繁,其過(guò)程瑣碎且丑陋讳推,故省略。至于射箭擊中靶子以及箭與火把的交互玩般,不涉及到VR范疇银觅,亦無(wú)可修改之處,故省略坏为,讀者可自行完善究驴。需要說(shuō)明的是,箭擊中靶子且吸附其上的原理是判斷有無(wú)指定的Physics Material匀伏,如有則認(rèn)為是射中了靶子洒忧,繼而吸附在上面。在此過(guò)程中一些必要且明顯的邏輯修改够颠,亦不再贅述熙侍。整個(gè)過(guò)程詳盡的實(shí)現(xiàn)可參考文末所附的代碼,歡迎提出修改意見(jiàn):
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;
//抓取當(dāng)前物體的控制器
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>
/// 防止與弓發(fā)生碰撞
/// </summary>
private SphereCollider bowCollider;
//-------------------------------------------------
//被VRTK抓取事件處理函數(shù)替代
//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>
/// 弓被抓取事件處理函數(shù)
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void VRTKLongBow_InteractableObjectGrabbed(object sender, InteractableObjectEventArgs e)
{
bowCollider.enabled = false;
isGrab = true;
///當(dāng)抓取LongBow時(shí)生成ArrowHand
GameObject arrowHandClone = Instantiate(arrowHandPrefab);
handType = VRTK_DeviceFinder.GetControllerHand(e.interactingObject);
///分別指定相應(yīng)控制器履磨,即左右手
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);
///手柄震動(dòng)
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());
}
//改寫(xiě)Shutdown內(nèi)容
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);
//調(diào)用抓取事件處理函數(shù)蛉抓,不再使用
///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被釋放處理函數(shù)
/// </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被抓取處理函數(shù)剃诅,替代OnAttachedToHand函數(shù)
/// </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);
///分別指定相應(yīng)控制器巷送,即左右手
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鍵松開(kāi)
/// </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函數(shù),函數(shù)內(nèi)容保持不變矛辕,函數(shù)名改為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
//拉弓瞄準(zhǔn)
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
}
}
//改寫(xiě)為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交互開(kāi)發(fā)實(shí)例教程》的節(jié)選文字版笑跛,更多VRTK實(shí)例教程可參見(jiàn)蠻牛教育首頁(yè)