Unity 3D中的射線(上)
圖6 控制檯中輸出的碰撞檢測資訊
利用二次發射射線的方式檢測內部物體
有的時候我們要檢測的物體在其他物體的內部,並且這兩個物體都具有碰撞器,用射線檢測返回的是第一個物體的資訊。在這種情況下,我們需要使用二次射線發射的做法,即以第一次射線碰撞的外層物體的碰撞點作為第二次射線發射的起點,沿原來方向發射射線,判斷是否與內部物體發生碰撞。
下面我們用一段程式碼示例來說明如何用二次發射射線來檢測位於物體內部的目標。在場景中建立兩個Cube,位於攝像機的正前方。在其中一個Cube的位置上建立一個Sphere,並設定它的大小為Cube的一半,這樣Sphere就位於Cube的內部。將下面的指令碼RayDemo04.cs掛載到攝像機上。
在這段指令碼中,當我們發射的射線第一次與外層物體碰撞時,會記錄當前的碰撞點位置,並以該位置為起點,繼續沿原來方向發射射線,若存在內層物體,則會在第二次碰撞中獲得該物體的資訊。執行程式後,如圖7所示,在場景檢視中用紅線和綠線繪製了兩次發射的射線軌跡。在圖8中,當我們點選右邊的Cube時,射線碰撞的是它內部的Sphere,同時將Cube隱藏,螢幕上輸出的也是Sphere的位置資訊。<span style="font-family:Times New Roman;"><span style="white-space:pre"> </span>using UnityEngine; <span style="white-space:pre"> </span>using System.Collections; <span style="white-space:pre"> </span>public class RayDemo04 : MonoBehaviour { <span style="white-space:pre"> </span>GameObject wrapper; // 外層物體 <span style="white-space:pre"> </span>GameObject target; // 內層物體 <span style="white-space:pre"> </span>string info = ""; // 碰撞檢測資訊 <span style="white-space:pre"> </span>void Update () { <span style="white-space:pre"> </span>if(Input.GetMouseButton (0)) <span style="white-space:pre"> </span>{ <span style="white-space:pre"> </span>// 當滑鼠左鍵按下時,向滑鼠所在的螢幕位置發射一條射線 <span style="white-space:pre"> </span>Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition); <span style="white-space:pre"> </span>RaycastHit hitInfo; <span style="white-space:pre"> </span>if(Physics.Raycast(ray, out hitInfo)) <span style="white-space:pre"> </span>{ <span style="white-space:pre"> </span>// 當射線與物體發生碰撞時,在場景檢視中繪製射線 <span style="white-space:pre"> </span>Debug.DrawLine(ray.origin, hitInfo.point, Color.red); <span style="white-space:pre"> </span>// 獲得第一次碰撞的外層物體物件 <span style="white-space:pre"> </span>wrapper = hitInfo.collider.gameObject; <span style="white-space:pre"> </span>// 以第一次的碰撞點為起點,沿原來的方向二次發射射線 <span style="white-space:pre"> </span>Ray ray2= new Ray(hitInfo.point, ray.direction); <span style="white-space:pre"> </span>RaycastHit hitInfo2; <span style="white-space:pre"> </span>if(Physics.Raycast(ray2, out hitInfo2)) <span style="white-space:pre"> </span>{ <span style="white-space:pre"> </span>// 當射線與內層物體碰撞時,在場景中繪製射線 <span style="white-space:pre"> </span>Debug.DrawLine(ray2.origin, ray2.direction, Color.green); <span style="white-space:pre"> </span>// 獲得內層物體物件 <span style="white-space:pre"> </span>target = hitInfo2.collider.gameObject; <span style="white-space:pre"> </span>// 將外層物體的網格隱藏 <span style="white-space:pre"> </span>wrapper.GetComponent<MeshRenderer>().enabled = false; <span style="white-space:pre"> </span>// 設定碰撞資訊 <span style="white-space:pre"> </span>info = "檢測到物體: " + target.name + "座標: " + target.transform.position; <span style="white-space:pre"> </span>} <span style="white-space:pre"> </span>else <span style="white-space:pre"> </span>{ <span style="white-space:pre"> </span>// 如果二次發射的射線沒有與內層物體碰撞 <span style="white-space:pre"> </span>// 顯示外層物體的網格 <span style="white-space:pre"> </span>wrapper.GetComponent<MeshRenderer>().enabled = true; <span style="white-space:pre"> </span>// 設定碰撞資訊 <span style="white-space:pre"> </span>info = "檢測到物體: " + wrapper.name + "座標: " + wrapper.transform.position; <span style="white-space:pre"> </span>} <span style="white-space:pre"> </span>} <span style="white-space:pre"> </span>} <span style="white-space:pre"> </span>} <span style="white-space:pre"> </span>void OnGUI(){ <span style="white-space:pre"> </span>// 在螢幕上列印輸出射線檢測的資訊 <span style="white-space:pre"> </span>GUILayout.Label(info); <span style="white-space:pre"> </span>} <span style="white-space:pre"> </span>}</span><span style="font-family:Microsoft YaHei;"> </span>
圖7 在場景檢視中繪製的兩次發射的射線
圖8 二次發射射線檢測物體內部的目標
發射射線返回所有碰撞體資訊
使用二次射線發射的方式,我們的處理目標往往是內層物體,如果我們想要同時處理射線方向上的所有碰撞物體,就要使用RaycastAll這個API函式。
public static RaycastHit[] RaycastAll(Ray ray, float distance =Mathf.Infinity, int layerMask = DefaultRaycastLayers);
public static RaycastHit[] RaycastAll(Vector3 origin, Vector3direction, float distance = Mathf.Infinity, int layermask =DefaultRaycastLayers);
返回的是一個成員型別RaycastHit的陣列,記錄了在射線方向上碰撞的所有物體資訊。
同樣,我們用一段程式示例來說明一下如何使用這個API。在場景中建立一個Cube,一個Sphere和一個Capsule,他們的中心位於同一點,大小比例為3:2:1,形成巢狀的關係。將下列指令碼RayDemo05.cs掛載到攝像機上。<span style="font-family:Times New Roman;"><span style="white-space:pre"> </span>using UnityEngine;
<span style="white-space:pre"> </span>using System.Collections;
<span style="white-space:pre"> </span>public class RayDemo05 : MonoBehaviour {
<span style="white-space:pre"> </span>// 各個按鈕上的文字標籤
<span style="white-space:pre"> </span>string cubeLayerStr = "僅顯示Cube層";
<span style="white-space:pre"> </span>string sphereLayerStr = "僅顯示Sphere層";
<span style="white-space:pre"> </span>string capsuleLayerStr = "僅顯示Capsule層";
<span style="white-space:pre"> </span>string allLayerStr = "顯示所有層";
<span style="white-space:pre"> </span>// 設定碰撞發生層次的掩碼
<span style="white-space:pre"> </span>int layerMask = ~(1<<0); // 開啟所有層
<span style="white-space:pre"> </span>// 記錄碰撞資訊的陣列
<span style="white-space:pre"> </span>RaycastHit[] hitInfos;
<span style="white-space:pre"> </span>// 碰撞資訊顯示
<span style="white-space:pre"> </span>string info;
<span style="white-space:pre"> </span>void Update () {
<span style="white-space:pre"> </span>if(Input.GetMouseButton(0)){
<span style="white-space:pre"> </span>// 當滑鼠左鍵按下時,向滑鼠所在的螢幕位置發射一條射線
<span style="white-space:pre"> </span>Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
<span style="white-space:pre"> </span>// 使用RaycastAll發射射線
<span style="white-space:pre"> </span>// 在layerMask設定的碰撞層上返回所有碰撞的物體資訊
<span style="white-space:pre"> </span>hitInfos = Physics.RaycastAll(ray, Mathf.Infinity, layerMask);
<span style="white-space:pre"> </span>// 在場景檢視中繪製射線資訊
<span style="white-space:pre"> </span>Debug.DrawLine(ray.origin, ray.direction);
<span style="white-space:pre"> </span>// 遍歷射線碰撞的所有物體,將他們的材質的透明度修改為半透明
<span style="white-space:pre"> </span>foreach(RaycastHit hit in hitInfos){
<span style="white-space:pre"> </span>Debug.Log(hit.collider.name);
<span style="white-space:pre"> </span>hit.collider.gameObject.transform.renderer.material.color = new Color(1.0f, 1.0f, 1.0f, 0.5f);
<span style="white-space:pre"> </span>info = "檢測到碰撞物體: " + hit.collider.name;
<span style="white-space:pre"> </span>}
<span style="white-space:pre"> </span>}
<span style="white-space:pre"> </span>else{
<span style="white-space:pre"> </span>// 鬆開滑鼠左鍵時,設定所有物體材質的透明度為透明
<span style="white-space:pre"> </span>GameObject.Find("Cube").transform.renderer.material.color = new Color(1.0f, 1.0f, 1.0f, 0.0f);
<span style="white-space:pre"> </span>GameObject.Find("Sphere").transform.renderer.material.color = new Color(1.0f, 1.0f, 1.0f, 0.0f);
<span style="white-space:pre"> </span>GameObject.Find("Capsule").transform.renderer.material.color = new Color(1.0f, 1.0f, 1.0f, 0.0f);
<span style="white-space:pre"> </span>}
<span style="white-space:pre"> </span>}
<span style="white-space:pre"> </span>void OnGUI(){
<span style="white-space:pre"> </span>// 在OnGUI設定按鈕切換時更改碰撞層掩碼
<span style="white-space:pre"> </span>if(GUILayout.Button(cubeLayerStr))
<span style="white-space:pre"> </span>{
<span style="white-space:pre"> </span>layerMask = (1 << LayerMask.NameToLayer("Cube"));
<span style="white-space:pre"> </span>}
<span style="white-space:pre"> </span>if(GUILayout.Button(sphereLayerStr))
<span style="white-space:pre"> </span>{
<span style="white-space:pre"> </span>layerMask = (1 << LayerMask.NameToLayer("Sphere"));
<span style="white-space:pre"> </span>}
<span style="white-space:pre"> </span>if(GUILayout.Button(capsuleLayerStr))
<span style="white-space:pre"> </span>{
<span style="white-space:pre"> </span>layerMask = (1 << LayerMask.NameToLayer("Capsule"));
<span style="white-space:pre"> </span>}
<span style="white-space:pre"> </span>if(GUILayout.Button(allLayerStr))
<span style="white-space:pre"> </span>{
<span style="white-space:pre"> </span>layerMask = (1 << LayerMask.NameToLayer("Cube")) | (1 << LayerMask.NameToLayer("Sphere")) | (1 << LayerMask.NameToLayer("Capsule"));
<span style="white-space:pre"> </span>}
<span style="white-space:pre"> </span>GUILayout.Label(info); // 實時顯示碰撞資訊
<span style="white-space:pre"> </span>}
<span style="white-space:pre"> </span>}</span><span style="font-family:Microsoft YaHei;">
</span>
在上面這段程式碼中我們使用左移位操作符<<來設定碰撞層的掩碼layerMask。Unity 3D中共有32個層,對應使用一個32位整數的各個位來表示每個層級,當這個位為1時表示使用這個層,為0時表示不使用這個層。LayerMask.NameToLayer這個API是返回我們使用自定義命名所定義的層的層索引,注意從0開始。當我們使用左移位操作設定層次掩碼時,對應的自定義層級是n我們就將1左移n位,這樣射線就只在layerMask指定的層次上進行碰撞檢測。可供使用的自定義的層級從第8層開始,我們將8~10層分別命名為Capsule、Sphere和Cube,並將Capsule、Shpere和Cube三個物體的layer分別設定為對應的層次。一開始我們將所有物體設定為透明不可見。當按下滑鼠左鍵發射射線時,返回射線方向上所有碰撞的物體資訊,將獲取到的物體物件,全部設定為半透明可見。點選按鈕可以切換檢測碰撞的層次。
執行程式碼,如圖9、圖10所示,當切換不同的按鈕控制射線在不同的層次上檢測碰撞,顯示的物體也便不同。圖9 僅顯示Cube層時進行的射線碰撞檢測
圖10 顯示所有層時進行的射線碰撞檢測
當然還有很多的關於射線使用的API不能一一贅述,這篇只是做一個簡單的梳理,更多的API例如SphereCast、LineCast的具體用法可以查閱官方文件,在Unity 3D中的射線(下)一文中將會利用射線檢測做一個滑鼠抓取物體的例項。