- 正文開始 -
圖來自:Geeky Gadgets
前言
本文使用兩個工具為Unity2018.4.26和SteamVR2.6.1,SteamVR2.6.1相比之前的版本有了很大的改變,其中在交互上有了很大的提升,SteamVR2.6.1上給出的案例中提供了拋射物體、線性驅動、環形驅動以及複雜的射箭操作等。儘管給出了諸多的交互案例,但是在實際開發中依然會有新的交互情況出現,在SteamVR2.6.1中沒有詳細的使用說明下,本文首先大概介紹其各種交互案例,然後詳細的介紹其交互的核心組件如Interactable和Hand等,最後結合我使用的案例實現如何動態添加各種交互。
/ 簡單交互Simple Interactable /
如圖1所示,為實現的簡單交互,手觸碰到物體會有使物體呈現黃色輪廓框,然後按下扳機鍵既可以移動物體,但是在這裡手的模型隱藏掉了。該交互方式只需要添加核心交互組件Interactable到父物體上,子物體中有帶碰撞體的就可以實現。
圖1
實現邏輯:
第一步:首先,Hand在激活的時候重複不斷的調用UpdateHovering方法,該方法即處理手懸浮帶Interactable組件的物體:
protected virtual void OnEnable(){inputFocusAction.enabled = true;float hoverUpdateBegin = ((otherHand != null) && (otherHand.GetInstanceID() < GetInstanceID())) ? (0.5f * hoverUpdateInterval) : (0.0f);InvokeRepeating("UpdateHovering", hoverUpdateBegin, hoverUpdateInterval);InvokeRepeating("UpdateDebugText", hoverUpdateBegin, hoverUpdateInterval);}第二步:在UpdateHovering方法中判斷哪個Interactable物體時和手最近的,判斷的方法為CheckHoveringForTransform,在這個方法中會遍歷子物體中的所有Collider,然後獲取該Collider的父物體上的Interactable組件,如果不為Null,則比較與手的距離,找到最近的那一個,並將其Interactable的實例化對象賦值給Hand中的hoveringInteractable;
第三步:在Hand中hoveringInteractable為Interactable類型的屬性,當該屬性被賦值時會進行廣播消息處理,所有繼承了MonoBehaviour的腳本中定義了 OnHandHoverBegin和OnHandHoverEnd方法的都將被執行。
public Interactable hoveringInteractable{get { return _hoveringInteractable; }set{if (_hoveringInteractable != value){if (_hoveringInteractable != null){if (spewDebugText)HandDebugLog("HoverEnd " + _hoveringInteractable.gameObject);_hoveringInteractable.SendMessage("OnHandHoverEnd", this, SendMessageOptions.DontRequireReceiver);if (_hoveringInteractable != null){this.BroadcastMessage("OnParentHandHoverEnd", _hoveringInteractable, SendMessageOptions.DontRequireReceiver); }}_hoveringInteractable = value;if (_hoveringInteractable != null){if (spewDebugText)HandDebugLog("HoverBegin " + _hoveringInteractable.gameObject);_hoveringInteractable.SendMessage("OnHandHoverBegin", this, SendMessageOptions.DontRequireReceiver);if (_hoveringInteractable != null){this.BroadcastMessage("OnParentHandHoverBegin", _hoveringInteractable, SendMessageOptions.DontRequireReceiver); }}}}}最後:在該案例的腳本InteractableExample中實現OnHandHoverBegin和OnHandHoverEnd方法。
/ 拋射物體Throwable /
如圖2所示為手抓取物體然後進行拋射的過程,當手抓取物體時會變換為手剛好握住物體的姿態且手指都為靜態的,當手釋放掉物體的時候又恢復到原來的狀態,並且物體拋出去之後具有一定的速度。
圖2
實現邏輯:
第一步:同樣需要添加核心交互組件Interactable,並且需要添加SteamVR_Skeleton_Poser、Rigidbody以及Throwable(或子類);
第二步:編輯SteamVR_Skeleton_Poser中所需要的手部姿勢。
1)如圖3所示,點擊Create創建一個新的姿勢,所有的姿勢都是SteamVR_Skeleton_Pose的ScriptableObject,保存後為.asset為後綴的文件,可以通過Resources.Load方法直接加載或者直接拖到面板上使用。
圖3
2) 如圖4所示,勾選Show Right Preview 即對手勢進行編輯,此時可以看到在物體的附近有一個手的模型,如果想在模板的基礎上編輯可以選擇Reference Pose:選擇之後手即可變成模板的樣子。該手部編輯模型會作為子物體出現。
圖4
但是僅僅時在編輯模式下出現,作為編輯使用,編輯完之後需要取消 Show Right Preview的勾選方可正常顯示和使用。
直接調整手的位置和關鍵,使其達到符合要求的握住物體的樣子即可,然後勾選Show Left Preview此時下面的Copy Left pose to Right Hand和Copy Right pose to Right Hand會被激活。注意:因為剛剛編輯的時Right的,因此點右邊下面的Copy Left pose to Right Hand按鈕,將右邊的鏡像處理得到 左邊的數據並覆蓋當前左邊的,點擊之後即可看到兩隻手都以同樣的姿勢握住物體。這裡一定要點對,不然前面的工作會被覆蓋而需要重新做。最後,點擊Save Pose 即可。
第三步:Throwable編輯,在Throwable腳本中同樣實現了OnHandHoverBegin和OnHandHoverEnd方法,處理握住物體的邏輯。並且還實現了HandHoverUpdate方法,在該方法中首先判斷當前手的按鍵類型,只要不是抓取的按鍵觸發就握住物體,該方法在Hand的Update中被廣播,另外,還有HandAttachedUpdate方法。
protected virtual void HandHoverUpdate( Hand hand ){GrabTypes startingGrabType = hand.GetGrabStarting();if (startingGrabType != GrabTypes.None){ hand.AttachObject( gameObject, startingGrabType, attachmentFlags, attachmentOffset );hand.HideGrabHint();}}protected virtual void Update(){UpdateNoSteamVRFallback();GameObject attachedObject = currentAttachedObject;if (attachedObject != null){attachedObject.SendMessage("HandAttachedUpdate", this, SendMessageOptions.DontRequireReceiver);}if (hoveringInteractable){hoveringInteractable.SendMessage("HandHoverUpdate", this, SendMessageOptions.DontRequireReceiver);}}在Throwable中實現HandAttachedUpdate方法,代碼如下:該方法每幀都執行,判斷手的按鍵已經釋放掉了該物體的時執行手放棄物體的操作 hand.DetachObject(gameObject, restoreOriginalParent);,然後Hand的DetachObject方法裡調用廣播函數廣播OnDetachedFromHand,最終在Throwable腳本中的實現的OnDetachedFromHand方法裡處理了最後被扔出去的邏輯。
protected virtual void HandAttachedUpdate(Hand hand){if (hand.IsGrabEnding(this.gameObject)){hand.DetachObject(gameObject, restoreOriginalParent);}if (onHeldUpdate != null)onHeldUpdate.Invoke(hand);}public void DetachObject(GameObject objectToDetach, bool restoreOriginalParent = true { ...
if (attachedObjects[index].attachedObject != null){if (attachedObjects[index].interactable == null ||(attachedObjects[index].interactable != null &&attachedObjects[index].interactable.isDestroying == false))attachedObjects[index].attachedObject.SetActive(true);attachedObjects[index].attachedObject.SendMessage("OnDetachedFromHand",this, SendMessageOptions.DontRequireReceiver);}...}/ 線性驅動LinearDrive /
如圖5所示為線性驅動的效果示意圖,手捂住操作的物體保持姿勢不動,移動手柄,手捂住的物體跟隨運動,但是只保持在橫向的線性位置移動,手握住的物體不會超過該線性區域。
圖5
第一步 :核心組件Interactable當然比不可少,然後實現HandHoverUpdate和HandAttachedUpdate以及OnDetachedFromHand方法,編輯握住物體所需的手勢;
第二步:獲取手部捂住物體之後手移動的參數,計算方法為:獲取手現在的位置和線性起點的位置組成的向量A和終點到起點的向量B,得到向量A和B的點積,然後將這個值作為線性插值的變化因子
protected virtual void HandAttachedUpdate(Hand hand){UpdateLinearMapping(hand.transform);if (hand.IsGrabEnding(this.gameObject)){hand.DetachObject(gameObject);}}protected void UpdateLinearMapping( Transform updateTransform ) { prevMapping = linearMapping.value; linearMapping.value = Mathf.Clamp01( initialMappingOffset + CalculateLinearMapping( updateTransform ) ); mappingChangeSamples[sampleCount % mappingChangeSamples.Length] = ( 1.0f / Time.deltaTime ) * ( linearMapping.value - prevMapping ); sampleCount++;if ( repositionGameObject ) { transform.position = Vector3.Lerp( startPosition.position, endPosition.position, linearMapping.value ); } }protected float CalculateLinearMapping( Transform updateTransform ) { Vector3 direction = endPosition.position - startPosition.position;float length = direction.magnitude; direction.Normalize(); Vector3 displacement = updateTransform.position - startPosition.position;return Vector3.Dot( displacement, direction ) / length; }/ 環形驅動CircularDrive /
如圖6所示,其處理邏輯和線性驅動類似,只是在計算物體旋轉上有所差別
圖6
/ 懸浮按鈕Hover Button /
如圖7所示,手懸浮在按鈕上,然後向下壓物體可以實現物體按下效果。實現的邏輯和前面的簡單交互類似。
圖7
/ 射箭 /
如圖8所示為雙手射箭的操作,這個交互應該是SteamVR2.6.1這個版本中最複雜的一部分。同樣需要添加Interactable組件。重點是ItemPackageSpawner組件,該組件實現了手用弓箭的所有邏輯。
圖8
1)ItemPackageSpawner實現了HandHoverUpdate方法,並且在面板上勾選了requireGrabActionToTake,因此在手觸碰到弓並且按下抓取的扳機鍵的時候調用SpawnAndAttachObject生成一些列後續操作所需要的包並且這隻手抓住弓。
private void HandHoverUpdate( Hand hand ) {...if ( requireGrabActionToTake ) {GrabTypes startingGrab = hand.GetGrabStarting();if (startingGrab != GrabTypes.None) { SpawnAndAttachObject( hand, GrabTypes.Scripted); } } }SpawnAndAttachObject方法裡先根據ItemPackageType類型來清空手上的東西,然後重新生成一個itemPackage裡面的itemPrefab,然後讓手抓住它,這個時候生成的物體為Longbow,是帶握住手勢的弓,如圖9所示。如果itemPackage的otherHandItemPrefab不為空的話也實例化該物體,並且用另外一隻手抓住它。這裡的otherHandItemPrefab為握住箭的手勢ArrowHand,如圖10所示,在ArrowHand中初始化只保留了一個握住箭的手勢,箭的生成要在其內部 HandAttachedUpdate方法裡實現。
圖9
圖10
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; }private void HandAttachedUpdate( Hand hand ){if ( allowArrowSpawn && ( currentArrow == null ) ) { currentArrow = InstantiateArrow(); arrowSpawnSound.Play(); }}2)、放箭的過程在ArrowHand的HandAttachedUpdate方法中實現,當弓被拉握住箭的手柄按鍵釋放的時候,即射出箭。
private void HandAttachedUpdate( Hand hand ) {...if ( nocked && hand.IsGrabbingWithType(nockedWithType) == false ) {if ( bow.pulled ) { FireArrow(); }else { arrowNockTransform.rotation = currentArrow.transform.rotation; currentArrow.transform.parent = arrowNockTransform; Util.ResetTransform( currentArrow.transform ); nocked = false;nockedWithType = GrabTypes.None; bow.ReleaseNock(); hand.HoverUnlock( GetComponent<Interactable>() ); allowTeleport.teleportAllowed = true; } bow.StartRotationLerp(); }}/ 遠程控制 /
如圖11a和11b所示為操作虛擬手柄遠程控制物體的案例,這兩個案例非常類似,只是處理的過程非常繁瑣,這裡不再詳細展開了,唯一沒有讓我完全搞清楚的地方是,控制虛擬手柄的手部動作是如何做到動態的。跟前面手指靜態的不同,
圖11a
這裡的手姿勢控制未找到SteamVR_Skeleton_Poser的使用。
圖10b
總結
交互的核心組件為Interactable,凡是涉及到用手進行交互都需要添加該組件,後面會講射線與物體交互也會用到該組件;
手握住物體的姿勢為SteamVR_Skeleton_Pose,是一個ScriptableObject的資源類,可以在編輯器中進行編輯並且保存為後綴.asset文件,該文件可以實現動態加載;
需要在獲取手和物體的處理邏輯上一定要實現Hand中廣播的方法;
遠程操作的手握住虛擬手柄的姿勢可以動手指,目前還不知道怎麼編輯或設置。