一般來說手機類型的VR遊戲模式,如果不連接上手把控制器,除了少部分手機支援Cardboard旁邊的磁力板機外,最基本的操控方式就是以視覺中心來當作觸發條件,因為我們唯一能控制的就是頭的旋轉(陀螺儀水平儀),本篇就試著用射線來實現,焦點計時觸發的功能。
(上圖側邊的鐵環就是磁力板機)
1.首先準備一個簡易場景,把攝影機置中並稍微離開地面(將Y設為2)
,這點稍微有點重要性,攝影機的位置在世界中心便於GoogleVR控制(主要原因是啟動後預設將攝影機放置世界中心當起點),避免射線起始發射的位置有偏差最好讓起始點重合。
2.接下來建立我們的3D UI Canvas重新命名為FocusCanvas並調整RenderMode為WorldSpace
隨後調整RectTransform的原始座標Pos的XY都先將他歸零Z些微增加放置攝影機前確保可以看見,並調整UI的寬高250確保清晰
3.接下來用PS(PhotoShop)準備我們的UI素材,一個計時進度用的圓圈,製作時用128或256正方白色即可,用白色可以方便日後染色改變。
4.再加入Image和Text的UI到Canvas下
並且做全版撐開的設定讓大小的控制交由外層的Canvas
5.調整Image和Text的內容如下:
將做好的圖片轉成Sprite格式放入SourceImage、調整顏色、Type改成Filled。
文字部分可以先清空,調整適中的大小、Alignment對齊置中、變更顏色。為了文字不被遮蔽順便將TOP屬性-300至於圓圈上方(位置可以自行調整),看起來會像這樣:
6.由於是3D的UI,希望她能跟著攝影機移動直接將它塞入MainCamera下並且使用標準單位1的CUBE對照下UI的大小尺寸比例,然後調整Canvas的縮放(目的是為了算出物件和UI之間的比例關係)
大約是0.01的XY縮放比例,Z可以保持不變。
7.接下來撰寫程式RayGun.cs掛載於主攝影機上
using UnityEngine; using System.Collections; using UnityEngine.UI; public class RayGun : MonoBehaviour { public GameObject focusCanvas; public Image focus; public Text secText; float focusTimer = 0f; Camera camera; RaycastHit hit; void Start () { camera = this.gameObject.GetComponent<Camera> (); focus.fillAmount = focusTimer; } void Update(){ } }
首先宣告放Canvas的GameObject以及底下的Image、Text的public欄位,還有計時用的float、要打出射線的Camera本體以及被擊中的物體,並且在Start初始化攝影機本身並且讓UI Image的fillAmount初始化為0,=focusTimer計時用的浮點數。
8.接下來將Update改為FixedUpdate在裡面增加射線的程式碼
using UnityEngine; using System.Collections; using UnityEngine.UI; public class RayGun : MonoBehaviour { public GameObject focusCanvas; public Image focus; public Text secText; float focusTimer = 0f; Camera camera; RaycastHit hit; void Start () { camera = this.gameObject.GetComponent<Camera> (); focus.fillAmount = focusTimer; } void FixedUpdate(){ Ray ray = new Ray(this.transform.position, camera.ScreenToWorldPoint( new Vector3(Screen.width/2 , Screen.height/2 , 200))); Debug.DrawLine (this.transform.position, camera.ScreenToWorldPoint( new Vector3(Screen.width/2 , Screen.height/2 , 20)),Color.red); } }
首先Ray的條件是透過兩個點畫出一條線的概念執行,所以言下之意我們需要兩個座標。一個是由攝影機的位置發出,那目標點呢?這部分比較複雜些,應該視攝影機前方一段距離的一個延伸,也就是她是一個不斷在改變的座標位置,所以我們透過攝影機的ScreenToWorldPoint取得螢幕正中央的XY座標然後把Z指定一個長度(這裡使用200),取得螢幕正前方遠處的一個虛擬的座標位置把設線產生,透過Debug.DrawLine我們可以在畫面上看到這條畫出的射線。
9.由於這條射線可以可以產生碰撞偵測的,透過Physics.Raycast物理的射線投放導出碰撞的物體
using UnityEngine; using System.Collections; using UnityEngine.UI; public class RayGun : MonoBehaviour { public GameObject focusCanvas; public Image focus; public Text secText; float focusTimer = 0f; Camera camera; RaycastHit hit; void Start () { camera = this.gameObject.GetComponent<Camera> (); focus.fillAmount = focusTimer; } void FixedUpdate(){ Ray ray = new Ray(this.transform.position, camera.ScreenToWorldPoint( new Vector3(Screen.width/2 , Screen.height/2 , 200))); Debug.DrawLine (this.transform.position, camera.ScreenToWorldPoint( new Vector3(Screen.width/2 , Screen.height/2 , 20)),Color.red); if (hit.collider.gameObject.tag == "MONSTER") { Debug.Log (hit.collider.name); } } }
可以放置一個帶有MONSTER標籤的Cube測試看看可否印出他的名子
10.接下來就可以寫一個FocusDo方法來執行想做的事情
void FocusDo(float O,Collider G){ if(G != null){ focusCanvas.transform.position = G.transform.position; if(O >= 1f){ Destroy (G.gameObject); } } }
執行時檢查是否有打擊到物體,有的話將UI的位置移動到該物體上,並透過引數O檢查我們的計時UI跑滿了沒有(大於等於1)就銷毀該物件。
11.FocusDo方法的引數O自然需要另個帶回傳值的方法計數器來輔助
float FocusTimer(float f){ if (f != 0f) { focusTimer += f * Time.deltaTime; secText.text = (focusTimer/f).ToString("f2"); } else { focusTimer = 0f; secText.text = ""; } focus.fillAmount = focusTimer; return focusTimer; }
FocusTimer的引數f用來決定計時器跑得快慢,數字越大增加的越快,而且可以使用文字輔助知道這次要多少時間,假設沒有打到物體時帶入0就可以淨空計時器和文字。不斷刷新環形UI百分比(focus.fillAmount = focusTimer),並回傳focusTimer讓FocusDo知道何時可以摧毀該物體。
12.最後補完打擊到MONSTER的方法條件可以得到如下效果:
完整程式:
using UnityEngine; using System.Collections; using UnityEngine.UI; public class RayGun : MonoBehaviour { public GameObject focusCanvas; public Image focus; public Text secText; float focusTimer = 0f; Camera camera; RaycastHit hit; void Start () { camera = this.gameObject.GetComponent<Camera> (); focus.fillAmount = focusTimer; } void FixedUpdate(){ Ray ray = new Ray(this.transform.position, camera.ScreenToWorldPoint( new Vector3(Screen.width/2 , Screen.height/2 , 200))); Debug.DrawLine (this.transform.position, camera.ScreenToWorldPoint( new Vector3(Screen.width/2 , Screen.height/2 , 20)),Color.red); if (Physics.Raycast (ray, out hit)) { if (hit.collider.gameObject.tag == "MONSTER") { FocusDo (FocusTimer(2f),hit.collider); } else { FocusDo (FocusTimer(0f),null); } } } float FocusTimer(float f){ if (f != 0f) { focusTimer += f * Time.deltaTime; secText.text = (focusTimer/f).ToString("f2"); } else { focusTimer = 0f; secText.text = ""; } focus.fillAmount = focusTimer; return focusTimer; } void FocusDo(float O,Collider G){ if(G != null){ focusCanvas.transform.position = G.transform.position; if(O >= 1f){ Destroy (G.gameObject); } } } }
您好,不好意思,想請問一下,使用了這個方法製作視線焦點觸發,但應用在Vive頭盔時,輸出成exe後射線的位置都會不準的話要怎麼調整,因為是使用射線去做觸發事件,所以射線不準的話就會觸發不到。另外在unity裡看原本射線位置都會是在(0,0),但只要戴上頭盔就會偏移,所以另外寫了調整射線位置的程式碼,調整到正確位置之後在unity裡預覽是可以觸發的,但也是只要輸出成exe,射線就會不準,不知道是否有方法可以解決。 希望能得到您的回應,謝謝~