在VR項目中,我們需要在用戶”凝視“某個物體時將其激活。在VRSamples中,我們構建了一個簡單的,可拓展的輕度系統,讓用戶跟場景中的物體進行交互。其中包含了三個主要的腳本文件:VREyeRaycaster,VRInput和VRInteractiveItem,下面將對這三個重要的類進行簡要的介紹。相關的源代碼也做了注釋。
VREyeRaycaster
該腳本需要和Main Camera關聯。在每次調用Update()時,該腳本都會使用Physics.Raycast來投射一條射線,以確認該射線是否命中任何collider(碰撞體)。使用該腳本還可以排除特定的Unity - Manual: Layers-在某些場景中,我們可能為了性能考慮,把所有的可交互對象移到一個單獨的層。
如果某個碰撞體被射線命中,那么該腳本將嘗試在GameObject上找到一個VRInteractiveItem組件。
C#腳本:
VRInteractiveItem interactible = hit.collider.GetComponent<VRInteractiveItem>();
//attempt to get the VRInteractiveItem on the hit object
復制代碼
從這里我們就可以判斷用戶究竟在”凝視“哪個物體,或是停止”凝視“某個物體。如果用戶開始或停止”凝視“某個物體,那么我們就可以進行一些處理,比如調用一個方法。
VRInput
VRInput是個簡單的類,可以判斷用戶在GearVR上(或是使用DK2時在PC上)所進行的一些簡單操作,比如滑動、觸碰、或雙觸。
我們可以直接在VRInput上訂閱事件:
C#腳本:
public event Action<SwipeDirection> OnSwipe;
// Called every frame passing in the swipe, including if there is no swipe.
public event Action OnClick;
// Called when Fire1 is released and it's not a double click.
public event Action OnDown;
// Called when Fire1 is pressed.
public event Action OnUp;
// Called when Fire1 is released.
public event Action OnDoubleClick;
// Called when a double click is detected.
public event Action OnCancel;
// Called when Cancel is pressed.
復制代碼
VRInteractiveItem
我們可以把該組件添加到任何希望在VR場景中進行交互的GameObject上,在該物體上需要綁定一個碰撞體。
對此我們可以選擇訂閱六種不同的事件:
C#腳本:
public event Action OnOver;
// Called when the gaze moves over this object
public event Action OnOut;
// Called when the gaze leaves this object
public event Action OnClick;
// Called when click input is detected whilst the gaze is over this object.
public event Action OnDoubleClick;
// Called when double click input is detected whilst the gaze is over this object.
public event Action OnUp;
// Called when Fire1 is released whilst the gaze is over this object.
public event Action OnDown;
// Called when Fire1 is pressed whilst the gaze is over this object.
復制代碼
此外還有一個布爾值用于判斷用戶是否”凝視“在當前物體上?
C#腳本:
public bool IsOver
{
get{ return m_IsOver; } // Is the gaze currently over this object?
}
復制代碼
我們也可以創建自己的腳本對以上事件作出響應。下面是一個簡單的示例:
C#腳本:
using UnityEngine;
using VRStandardAssets.Utils;
namespace VRStandardAssets.Examples
{
// This script is a simple example of how an interactive item can
// be used to change things on gameobjects by handling events.
public class ExampleInteractiveItem : MonoBehaviour
{
[SerializeField] private Material m_NormalMaterial;
[SerializeField] private Material m_OverMaterial;
[SerializeField] private Material m_ClickedMaterial;
[SerializeField] private Material m_DoubleClickedMaterial;
[SerializeField] private VRInteractiveItem m_InteractiveItem;
[SerializeField] private Renderer m_Renderer;
private void Awake ()
{
m_Renderer.material = m_NormalMaterial;
}
復制代碼
如果想看到實際的示例,不妨看看VRSampleScens/Scens/Examples/的InteractiveItem場景。
SelectionRadial和SelectionSlider
我們同時利用了radial選擇條(SelectionRadial),以及選擇滑動條(SelectionSlider),這樣用戶就可以按住Fire1來確認某個交互:
當按住輸入鍵時,選擇條會進行填充,并在填充完整后分發OnSelectionComplete或OnBarFilled事件。關于此部分的代碼,可以在SelectionRadial.cs和SelectionSlider.cs中找到,并進行了詳細的注釋。
在VR的世界里,從用戶交互的角度看,用戶需要時刻知道自己在做什么,而且可以掌控一切。通過這種“held input”的確認輸入方式,可以確保用戶不會出現誤操作。
VR Sample項目中的交互示例
現在讓我們來一起看看VR Sample項目中的部分交互示例。我們將提到每個場景中所使用的交互方式,以及具體實現的方式。
Menu 場景中的交互
每個menu場景都包含了幾個組件,其中我們需要重點關注的是MenuButton,VRInteractiveItem和Mesh Collider。
MenuButton組件訂閱了VRInteractiveItem組件上的OnOver和OnOut事件,這樣當十字準星移到menu上時,selection radial會出現。當用戶的實現離開菜單選項時,selection radial會消失。而當selection radial可見,且用戶按住Fire1鍵時,則radial會自動填充:
該類還訂閱了OnSelectionRadial的OnSelectionComplete事件,這樣當radial被填充滿的時候,會調用HandleSelectionComplete。該方法的作用是讓攝像機淡出,并調用所選的關卡。
C#腳本:
private void OnEnable ()
{
m_InteractiveItem.OnOver += HandleOver;
m_InteractiveItem.OnOut += HandleOut;
m_SelectionRadial.OnSelectionComplete += HandleSelectionComplete;
}
private void OnDisable ()
{
m_InteractiveItem.OnOver -= HandleOver;
m_InteractiveItem.OnOut -= HandleOut;
m_SelectionRadial.OnSelectionComplete -= HandleSelectionComplete;
}
private void HandleOver()
{
// When the user looks at the rendering of the scene, show the radial.
m_SelectionRadial.Show();
m_GazeOver = true;
}
private void HandleOut()
{
// When the user looks away from the rendering of the scene, hide the radial.
m_SelectionRadial.Hide();
m_GazeOver = false;
}
private void HandleSelectionComplete()
{
// If the user is looking at the rendering of the scene when the radial's selection finishes, activate the button.
if(m_GazeOver)
StartCoroutine (ActivateButton());
}
private IEnumerator ActivateButton()
{
// If the camera is already fading, ignore.
if (m_CameraFade.IsFading)
yield break;
// If anything is subscribed to the OnButtonSelected event, call it.
if (OnButtonSelected != null)
OnButtonSelected(this);
// Wait for the camera to fade out.
yield return StartCoroutine(m_CameraFade.BeginFadeOut(true));
// Load the level.
SceneManager.LoadScene(m_SceneToLoad, LoadSceneMode.Single);
}
復制代碼
讓我們來看看Selection Radial的部分示例,注意截圖中間的粉色元素:
Reticle only
當用戶“凝視”菜單選項時,空白的Selection Radial可見。
Selection Radial 填充(當用戶“凝視”菜單選項,且按下fire1輸入鍵)
在整個示例項目中,我們嘗試用同樣的風格,也就是使用bar和radial以固定的速度進行填充。在此建議大家在開發自己的VR項目時注意到這一點,因為交互設計中的連貫性對用戶很重要,特別是對于VR這種新媒介。
Maze場景中的交互
Maze(迷宮)游戲中提供了一個桌面式的交互示例,其中我們可以指引游戲角色到出口,并避免觸發炮塔。
在選擇角色的目的地時,會出現一個目的地標記,手游買賣同時還會顯示一個角色的路徑。玩家可以通過在觸摸板上使用swipe,按下方向鍵,或是使用游戲操縱桿上的左鍵來旋轉視圖。
在MazeFloor游戲對象上關聯了MeshCollider和VRInteractiveItem,從而允許在VR場景中進行交互:
MazeCourse 游戲對象是一個parent對象,其中包含了MazeFloor和MazeWalls GameObjects,這兩個對象依次包含了迷宮布局中的幾何信息。
MazeCourse關聯了一個MazeTargetSetting腳本,其中包含了對MazeFloor對象上VRInteractiveItem組件的引用。
MazeTargetSetting訂閱了VRInteractiveItem上的OnDoubleClick事件,隨后會分發OnTargetSet事件。該事件將把十字準星的Transform作為參數:
C#腳本:
public event Action<Transform> OnTargetSet;
// This is triggered when a destination is set.
private void OnEnable()
{
m_InteractiveItem.OnDoubleClick += HandleDoubleClick;
}
private void OnDisable()
{
m_InteractiveItem.OnDoubleClick -= HandleDoubleClick;
}
private void HandleDoubleClick()
{
// If target setting is active and there are subscribers to OnTargetSet, call it.
if (m_Active && OnTargetSet != null)
OnTargetSet (m_Reticle.ReticleTransform);
}
復制代碼
MazeCharacter游戲對象上的Player組件和MazeDestinationMarketGUI游戲對象上的DestinationMarker組件都會訂閱該事件,并作出相應的響應。
游戲角色可以使用Nav Mesh systems在迷宮中進行路徑判斷。Player組件使用HandleSetTarget函數來判斷Nav Mesh Agent到十字準星間的方向,并更新Agent的軌跡-游戲角色路徑的視覺渲染。
C#腳本:
private void HandleSetTarget(Transform target)
{
// If the game isn't over set the destination of the AI controlling the character and the trail showing its path.
if (m_IsGameOver)
return;
m_AiCharacter.SetTarget(target.position);
m_AgentTrail.SetDestination();
}
復制代碼
DestinationMarker可以將標記移動到Reticle的Transform位置:
C#腳本:
private void HandleTargetSet(Transform target)
{
// When the target is set show the marker.
Show();
// Set the marker's position to the target position.
transform.position = target.position;
// Play the audio.
m_MarkerMoveAudio.Play();
// Play the animation on whichever layer it is on, with no time offset.
m_Animator.Play(m_HashMazeNavMarkerAnimState, -1, 0.0f);
}
復制代碼
在下圖中可以看到reticle,目的地標記,玩家和軌跡。
迷宮中的切換開關也是在VR中和物體進行交互的示例,其中用到了Collider,以及VRInteractiveItem,和SelectionSlider三個類。
正如上圖中顯示的,和其它交互對象一起,SelectionSlider腳本會監聽由VRInteractiveItem和VRInput所分發的事件。
C#腳本:
private void OnEnable ()
{
m_VRInput.OnDown += HandleDown;
m_VRInput.OnUp += HandleUp;
m_InteractiveItem.OnOver += HandleOver;
m_InteractiveItem.OnOut += HandleOut;
}
復制代碼
Flyer場景中的交互
Flyer場景是一個計時”無盡飛行”游戲,在其中玩家可以通過四處看來引導飛船的方向,并使用Fire1輸入鍵進行射擊,通過擊中隕石或是引導飛船穿越空中的門來得分,跟Pilotwings或Starfox這兩款游戲有點類似。
在交互方面,Flyer使用了更簡單的方式,也就是讓FlyerLaserController訂閱VRInput的OnDown事件,從而發射激光。
C#腳本:
private void OnEnable()
{
m_VRInput.OnDown += HandleDown;
}
private void HandleDown()
{
// If the game isn't running return.
if (!m_GameController.IsGameRunning)
return;
// Fire laser from each position.
SpawnLaser(m_LaserSpawnPosLeft);
SpawnLaser(m_LaserSpawnPosRight);
}
復制代碼
Shooter180和Shooter360場景中的交互(Target Gallery/ Target Arena)。
在VR Samples包含了兩個射擊游戲,其中一個是回廊射擊游戲,玩家在180度視角的走廊中對潛在目標射擊。另外還有一個競技場射擊游戲,玩家被類似X戰警場景的潛在目標包圍。
這兩款游戲中的每個目標對象都有一個Collider,VRInteractiveItem和ShootingTarget。


ShootingTarget組件訂閱了VRInteractiveItem的OnDown事件,以判斷目標是否被擊中。該方法適用于瞬間命中(比如激光槍這種)的設定,如果要展示子彈時間,我們就需要考慮其它解決方案了。
現在我們應該對基本的VR交互組件有了大概的印象,包括任何在VR Samples項目中具體使用這些組件。現在讓我們來看看VR Samples項目中如何使用gaze(凝視)和reticles(十字星)。
GAZE(凝視)
在VR應用中判斷用戶正在看什么很重要,可能是用于判斷用戶和游戲對象的交互,或是觸發一個動畫,也可能是向目標發射子彈。我們將VR中“看”這個動作定義為gaze(凝視),而在后續的教程中我們將頻繁使用這個詞。
考慮到目前大多數HMD頭戴設備還不支持眼部追蹤,因此我們只能估計用戶的gaze(凝視)。透鏡的扭曲意味著用戶正看著正前方,有一個簡單的解決方案。正如在概覽中提到的,我們只需要從攝像機的中心發射一條射線,然后找到這條射線所碰撞的物體即可。當然,這就意味著所有要被碰撞(或是需要通過“凝視”進行交互)的對象都必須關聯一個Collider組件。
Reticle(十字準星)
十字準星用于輔助標記用戶視野的中心。十字準星的樣式可能是簡單的點,也可能是一個十字準線,具體形式取決于項目需求。
在傳統的3D游戲中,十字準星被設置為空間中的固定點,比如通常是屏幕的中央。但是在VR中使用十字準星變得非常復雜:當用戶在VR環境中四處觀望時,雙眼將匯集在靠近攝像機的物體上。如果十字準星處在一個固定的位置,那么用戶會看到兩個準星:我們在現實世界里面可以輕易模仿這種效果。把某個手指放在眼睛前面,然后聚焦到近處和遠處的物體上。當我們聚焦在這個手指上時,就會看到兩個背景,反之亦然。這就是傳說中的Diplopia 現象。
為了避免用戶在查看周圍環境和注視不同距離的物體時看到兩個準星,我們需要將準星放到3D空間的同一個點,也就是用戶所關注對象的表面。
將準星放在空間的這個點意味著從遠處看準星將非常小,當靠近時會變大。為了讓準星的大小不隨距離發生變化,我們需要根據它到攝像機的距離對其進行縮放。
為了說明這一點,我們從Examples/Reticle場景中找了一些例子,展示了處于不同距離和比例的準星。
準星放置在靠近攝像機的物體上:
準星放置在稍遠的物體上:
準星放置在遠處:
根據所處的位置和自身比例,用戶在任何距離上看到的準星大小都是相同的。
如果沒有擊中任何對象,那么我們只需把準星放到一個預設的距離上。在室外環境中,可能會放在攝像機的Far clip plane前面,在室內場景中可能會近得多。
將十字準星渲染到其它游戲對象的表面
如果十字準星恰好和某個對象的位置相同,那么準星可能會嵌入到臨近的對象中。
為了解決這個問題,我們需要確保將準星渲染到場景中所有對象的前面。在VR Samples中,我們提供了一個shader,基于Unity現有的名為UIOverlay.shader的”UI/Unlit/Text” shader。在選擇某個材質的shader時,可以在”UI/Overlay”中找到。
這個shader對UI 元素和文本都適用,會在場景中其它物體的前面繪制。
將準星和場景中的游戲對象對齊
我們希望準星的旋轉方向和它所命中的對象的法線相匹配。通過RaycastHit.normal就可以實現這一點,以下是具體的實現代碼:
C#腳本:
public void SetPosition (RaycastHit hit)
{
m_ReticleTransform.position = hit.point;
m_ReticleTransform.localScale = m_OriginalScale * hit.distance;
// If the reticle should use the normal of what has been hit...
if (m_UseNormal)
// ... set it's rotation based on it's forward vector facing along the normal.
m_ReticleTransform.rotation = Quaternion.FromToRotation (Vector3.forward, hit.normal);
else
// However if it isn't using the normal then it's local rotation should be as it was originally.
m_ReticleTransform.localRotation = m_OriginalRotation;
}
復制代碼
我們可以在Maze場景中看到這個action。
下圖展示了準星如何匹配墻壁的法線:
下圖展示了準星如何匹配地板的法線:
我們還提供了一個示例的Reticle腳本。該腳本可以跟VREyeRaycaster一起適用,從而將準星放置到場景的正確位置,并且可以選擇跟所命中的對象法線帖齊。
以上內容都可以在VRSampleScens/Scens/Examples/中看到。
在VR項目中頭部的旋轉和位置
在頭戴設備中跟蹤頭部的旋轉和位置可以用沉浸式的體驗來感受周圍環境,但同時也可以讓對象根據這些數值所相應的相應。
為了獲取這些數值我們需要用到VR.InputTracking類,并指定我們要訪問的VRNode。為了獲取頭部的旋轉,我們會希望用到VRNode.Head,而不是兩只眼睛。想了解更多的信息,可以參考Getting Started with VR Development一文中的Camera Nodes。
使用頭部旋轉作為輸入方式的可能應用是精細旋轉菜單或是其它對象。在VRSampleScenes/Examples/Rotation場景中可以看到這一點的示例。
下面是ExampleRotation的腳本:
C#腳本:
// Store the Euler rotation of the gameobject.
var eulerRotation = transform.rotation.eulerAngles;
// Set the rotation to be the same as the user's in the y axis.
eulerRotation.x = 0;
eulerRotation.z = 0;
eulerRotation.y = InputTracking.GetLocalRotation(VRNode.Head).eulerAngles.y;
復制代碼
下面的圖展示了游戲對象如何根據用戶所注視的位置來進行旋轉:
在Flyer游戲場景中,我們將看到太空飛船基于頭部的旋轉來調整自身位置,具體參考FlyerMovementController:
C#腳本:
Quaternion headRotation = InputTracking.GetLocalRotation (VRNode.Head);
m_TargetMarker.position = m_Camera.position + (headRotation * Vector3.forward) * m_DistanceFromCamera;
復制代碼
在VR游戲中使用觸摸板和鍵盤進行交互
Gear VR在頭戴設備的側邊配備了一個觸摸板。Unity把這個觸摸板當做鼠標來使用,所以我們可以使用以下方法:
Unity - Scripting API: Input.mousePosition
Unity - Scripting API: Input.GetMouseButtonDown
Unity - Scripting API: Input.GetMouseButtonUp
在使用Gear VR時,開發者可能會希望從觸摸板中獲取swipe數據。我們提供了一個名為VRInput的示例腳本,可以處理swipe,觸碰和雙觸。此外它還支持方向鍵和鍵盤上的左Ctrl鍵(在Unity中的默認輸入術語是Fire1),或者是鼠標上的左鍵,以此來處罰swipe和觸摸。
在Unity Editor中,我們可能會希望使用DK2來測試Gear VR的內容。因為目前暫時無法直接從Unity直接關聯到Gear VR進行測試。考慮到Gear VR的觸摸板作用跟鼠標類似,我們可以考慮使用鼠標來模擬輸入。當用戶佩戴HMD設備時操控鍵盤會更容易,因此VRInput同時也會講方向鍵操作處理成swipe,將Left-Ctrl(Fire1)處理成觸碰。
在使用游戲手柄時,左側的stick可以用作swipe,其中的某個按鍵可以用作觸碰。
關于如何處理swipe,可以參考VRSampleScenes/Scenes/Examples/Touchpad
以下是ExampleTouchpad腳本,其中根據swipe的方向將AddTorque方法作用于一個剛體,從而讓對象旋轉。
C#腳本:
using UnityEngine;
using VRStandardAssets.Utils;
namespace VRStandardAssets.Examples
{
// This script shows a simple example of how
// swipe controls can be handled.
public class ExampleTouchpad : MonoBehaviour
{
[SerializeField] private float m_Torque = 10f;
[SerializeField] private VRInput m_VRInput;
[SerializeField] private Rigidbody m_Rigidbody;
private void OnEnable()
{
m_VRInput.OnSwipe += HandleSwipe;
}
private void OnDisable()
{
m_VRInput.OnSwipe -= HandleSwipe;
}
//Handle the swipe events by applying AddTorque to the Ridigbody
private void HandleSwipe(VRInput.SwipeDirection swipeDirection)
{
switch (swipeDirection)
{
case VRInput.SwipeDirection.NONE:
break;
case VRInput.SwipeDirection.UP:
m_Rigidbody.AddTorque(Vector3.right * m_Torque);
break;
case VRInput.SwipeDirection.DOWN:
m_Rigidbody.AddTorque(-Vector3.right * m_Torque);
break;
case VRInput.SwipeDirection.LEFT:
m_Rigidbody.AddTorque(Vector3.up * m_Torque);
break;
case VRInput.SwipeDirection.RIGHT:
m_Rigidbody.AddTorque(-Vector3.up * m_Torque);
break;
}
}
}
}
復制代碼
VR Samples項目中的VRInput示例
正如上面所提到的,我們所有的示例游戲都使用VRInput來處理觸摸屏和鍵盤的輸入。Maze游戲中的攝像機也會對swipe作出響應:
Maze
在這個場景中,CameraOrbit對swipe進行監聽,從而允許對視點進行調整:
C#腳本:
private void OnEnable ()
{
m_VrInput.OnSwipe += HandleSwipe;
}
private void HandleSwipe(VRInput.SwipeDirection swipeDirection)
{
// If the game isn't playing or the camera is fading, return and don't handle the swipe.
if (!m_MazeGameController.Playing)
return;
if (m_CameraFade.IsFading)
return;
// Otherwise start rotating the camera with either a positive or negative increment.
switch (swipeDirection)
{
case VRInput.SwipeDirection.LEFT:
StartCoroutine(RotateCamera(m_RotationIncrement));
break;
case VRInput.SwipeDirection.RIGHT:
StartCoroutine(RotateCamera(-m_RotationIncrement));
break;
}
}