Unity 3D中的射線與碰撞檢測
在我們的遊戲開發過程中,有一個很重要的工作就是進行碰撞檢測。例如在射擊遊戲中子彈是否擊中敵人,在RPG遊戲中是否撿到裝備等等。在進行碰撞檢測時,我們最常用的工具就是射線,Unity 3D的物理引擎也為我們提供了射線類以及相關的函式介面。本文將對射線的使用進行一個總結。
射線是在三維世界中從一個點沿一個方向發射的一條無限長的線。在射線的軌跡上,一旦與添加了碰撞器的模型發生碰撞,將停止發射。我們可以利用射線實現子彈擊中目標的檢測,滑鼠點選拾取物體等功能。
射線的建立和顯示
Ray射線類和RaycastHit射線投射碰撞資訊類是兩個最常用的射線工具類。
建立一條射線Ray需要指明射線的起點(origin)和射線的方向(direction)。這兩個引數也是Ray的成員變數。注意,射線的方向在設定時如果未單位化,Unity 3D會自動進行單位歸一化處理。射線Ray的建構函式為 :
public Ray(Vector3 origin, Vector3 direction);
RaycastHit類用於儲存發射射線後產生的碰撞資訊。常用的成員變數如下:collider與射線發生碰撞的碰撞器
distance 從射線起點到射線與碰撞器的交點的距離
normal 射線射入平面的法向量
point 射線與碰撞器交點的座標(Vector3物件)
Physics.Raycast靜態函式用於在場景中發射一條可以和碰撞器碰撞的射線,相關的API如下:
**1)public static bool Raycast(Vector3 origin, Vector3 direction, float distance=Mathf.Infinity, intlayerMask=DefaultRaycastLayers) ;**
**引數說明:**
origin 射線起點世界座標
direction 射線方向向量
distance 射線長度(起點到終點的距離),預設設定為無限長
layerMask 顯示層掩碼(只選擇層次為layerMask指定層次的碰撞器進行碰撞,其他層次的碰撞器忽略)
**返回值說明:**
當射線與碰撞器發生碰撞時返回值為true,未穿過任何碰撞器時返回為false。
**2)public static boolRaycast(Vector3 origin, Vector3 direction, RaycastHit hitInfo, float distance =Mathf.Infinity, int layerMask = DefaultRaycastLayers);**
這個過載函式定義了一個碰撞資訊類**RaycastHit**,在使用時通過out關鍵字傳入一個空的碰撞資訊物件。當射線與碰撞器發生碰撞時,該物件將被賦值,可以獲得碰撞資訊包括transform、rigidbody、point 等。如果未發生碰撞,該物件為空。
**3)public static boolRaycast(Ray ray, float distance = Mathf.Infinity, int layerMask =DefaultRaycastLayers);**
這個過載函式使用已有的一條射線Ray來作為引數。
**4)public static boolRaycast(Ray ray, RaycastHit hitInfo, float distance = Mathf.Infinity, intlayerMask = DefaultRaycastLayers);**
這個過載函式使用已有的射線Ray來作為引數並獲取碰撞資訊RaycastHit。
在除錯時如果想顯示一條射線,可以使用Debug.DrawLine來實現。
**public static void DrawLine(Vector3start, Vector3 end, Color color);**
只有當發生碰撞時,在Scene檢視中才能看到畫出的射線。
下面這個例子建立了一個從主攝像機向y軸負向發射一條射線檢測下方是否有平面存在。在場景中攝像機下方建立一個Plane遊戲物件,並將下面的指令碼RayDemo01.cs掛載到攝像機上。
using UnityEngine;
using System.Collections;
public class RayDemo01 : MonoBehaviour {
void Update () {
// 以攝像機所在位置為起點,建立一條向下發射的射線
Ray ray = new Ray(transform.position, -transform.up);
RaycastHit hit;
if(Physics.Raycast(ray, out hit, Mathf.Infinity))
{
// 如果射線與平面碰撞,列印碰撞物體資訊
Debug.Log("碰撞物件: " + hit.collider.name);
// 在場景檢視中繪製射線
Debug.DrawLine(ray.origin, hit.point, Color.red);
}
}
}
執行程式後,如圖1所示,在場景檢視中可以看見攝像機發出的射線。當檢測到下方的平面時,會在控制檯中列印輸出檢測結果,如圖2所示。
圖1 在場景中顯示的射線.jpg
圖2 在控制檯中列印的碰撞檢測資訊.jpg
定向發射射線的實現
當我們要使用滑鼠拾取物體或判斷子彈是否擊中物體時,我們往往是沿著特定的方向發射射線,這個方向可能是朝向螢幕上的一個點,或者是世界座標系中的一個向量方向,沿世界座標系中的向量方向發射射線我們已經在上面演示過如何實現。針對向螢幕上的某一點發射射線,Unity 3D為我們提供了兩個API函式以供使用,分別是ScreenPointToRay和ViewportPointToRay。
public Ray ScreenPointToRay(Vector3 position);
引數說明:position是螢幕上的一個參考點座標。
返回值說明:返回射向position參考點的射線。當發射的射線未碰撞到物體時,碰撞點hit.point的值為(0,0,0)。
ScreenPointToRay方法從攝像機的近視口nearClip向螢幕上的一點position發射射線。Position用實際畫素值表示射線到螢幕上的位置。當參考點position的x分量或y分量從0增長到最大值時,射線將從螢幕的一邊移動到另一邊。由於position在螢幕上,因此z分量始終為0。
下面我們用一段程式示例說明如何利用ScreenPointToRay來發射一條指向螢幕上的某點來進行定向檢測碰撞體。在場景中建立一個Cube位於攝像機的正前方,將下面的指令碼RayDemo02.cs掛載到攝像機上。
using UnityEngine;
using System.Collections;
public class RayDemo02 : MonoBehaviour {
Ray ray;
RaycastHit hit;
// 建立射線到螢幕上的參考點,畫素座標
Vector3 position = new Vector3(Screen.width/2.0f, Screen.height/2.0f, 0.0f);
void Update () {
// 射線沿著螢幕x軸從左向右迴圈掃描
position.x = position.x >= Screen.width ? 0.0f : position.x + 1.0f;
// 生成射線
ray = Camera.main.ScreenPointToRay(position);
if(Physics.Raycast(ray, out hit, 100.0f))
{
// 如果與物體發生碰撞,在Scene檢視中繪製射線
Debug.DrawLine(ray.origin, hit.point, Color.green);
// 列印射線檢測到的物體的名稱
Debug.Log("射線檢測到的物體名稱: " + hit.transform.name);
}
}
}
在這段程式碼中,首先聲明瞭一個變數position,用於記錄射線到螢幕上的實際交點的畫素座標,然後在Update方法中更改position的x分量值,使得射線從螢幕左方向右方不斷迴圈掃描,接著呼叫方法ScreenPointToRay生成射線ray,最後繪製射線和列印射線探測到的物體的名稱。執行程式後,如圖3所示,在Scene檢視中可以看到我們繪製的射線正在場景中掃描,圖4是在控制檯下列印輸出射線探測到的物體名稱。
圖3 使用ScreenPointToRay方法發射射線.jpg
圖4 控制檯中輸出的碰撞檢測資訊.jpg
public Ray ViewportPointToRay(Vector3 position);
引數說明:position為螢幕上的一個參考點座標(座標已單位化處理)。
返回值說明:返回射向position參考點的射線。當發射的射線未碰撞到物體時,碰撞點hit.point的值為(0,0,0)。
ViewportPointToRay方法從攝像機的近視口nearClip向螢幕上的一點position發射射線。Position用單位化比例值的方式表示射線到螢幕上的位置。當參考點position的x分量或y分量從0增長到1時,射線將從螢幕的一邊移動到另一邊。由於position在螢幕上,因此z分量始終為0。
下面我們用一段程式示例說明如何利用ViewportPointToRay來發射一條指向螢幕上的某點來進行定向檢測碰撞體。在場景中建立一個Cube位於攝像機的正前方,將下面的指令碼RayDemo03.cs掛載到攝像機上。
using UnityEngine;
using System.Collections;
public class RayDemo03 : MonoBehaviour {
Ray ray;
RaycastHit hit;
// 建立射線到螢幕上的參考點,單位化座標
Vector3 position = new Vector3(0.5f, 0.5f, 0.0f);
void Update () {
// 射線沿著螢幕x軸從左向右迴圈掃描
position.x = position.x >= 1.0f ? 0.0f : position.x + 0.002f;
// 生成射線
ray = Camera.main.ViewportPointToRay(position);
if(Physics.Raycast(ray, out hit, 100.0f))
{
// 如果與物體發生碰撞,在Scene檢視中繪製射線
Debug.DrawLine(ray.origin, hit.point, Color.green);
// 列印射線檢測到的物體的名稱
Debug.Log("射線檢測到的物體名稱: " + hit.transform.name);
}
}
}
在這段程式碼中,首先聲明瞭一個變數position,用於記錄射線到螢幕上的實際交點的畫素座標,然後在Update方法中更改position的x分量值,使得射線從螢幕左方向右方不斷迴圈掃描,接著呼叫方法ViewportPointToRay生成射線ray,最後繪製射線和列印射線探測到的物體的名稱。執行程式後,如圖5所示,在Scene檢視中可以看到我們繪製的射線正在場景中掃描,圖6是在控制檯下列印輸出射線探測到的物體名稱。
圖5 使用ViewportPointToRay方法發射射線.jpg
圖6 控制檯中輸出的碰撞檢測資訊.jpg
利用二次發射射線的方式檢測內部物體
有的時候我們要檢測的物體在其他物體的內部,並且這兩個物體都具有碰撞器,用射線檢測返回的是第一個物體的資訊。在這種情況下,我們需要使用二次射線發射的做法,即以第一次射線碰撞的外層物體的碰撞點作為第二次射線發射的起點,沿原來方向發射射線,判斷是否與內部物體發生碰撞。
下面我們用一段程式碼示例來說明如何用二次發射射線來檢測位於物體內部的目標。在場景中建立兩個Cube,位於攝像機的正前方。在其中一個Cube的位置上建立一個Sphere,並設定它的大小為Cube的一半,這樣Sphere就位於Cube的內部。將下面的指令碼RayDemo04.cs掛載到攝像機上。
using UnityEngine;
using System.Collections;
public class RayDemo04 : MonoBehaviour {
GameObject wrapper; // 外層物體
GameObject target; // 內層物體
string info = ""; // 碰撞檢測資訊
void Update () {
if(Input.GetMouseButton (0))
{
// 當滑鼠左鍵按下時,向滑鼠所在的螢幕位置發射一條射線
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
RaycastHit hitInfo;
if(Physics.Raycast(ray, out hitInfo))
{
// 當射線與物體發生碰撞時,在場景檢視中繪製射線
Debug.DrawLine(ray.origin, hitInfo.point, Color.red);
// 獲得第一次碰撞的外層物體物件
wrapper = hitInfo.collider.gameObject;
// 以第一次的碰撞點為起點,沿原來的方向二次發射射線
Ray ray2= new Ray(hitInfo.point, ray.direction);
RaycastHit hitInfo2;
if(Physics.Raycast(ray2, out hitInfo2))
{
// 當射線與內層物體碰撞時,在場景中繪製射線
Debug.DrawLine(ray2.origin, ray2.direction, Color.green);
// 獲得內層物體物件
target = hitInfo2.collider.gameObject;
// 將外層物體的網格隱藏
wrapper.GetComponent<MeshRenderer().enabled = false;
// 設定碰撞資訊
info = "檢測到物體: " + target.name + "座標: " + target.transform.position;
}
else
{
// 如果二次發射的射線沒有與內層物體碰撞
// 顯示外層物體的網格
wrapper.GetComponent<MeshRenderer>().enabled = true;
// 設定碰撞資訊
info = "檢測到物體: " + wrapper.name + "座標: " + wrapper.transform.position;
}
}
}
}
void OnGUI(){
// 在螢幕上列印輸出射線檢測的資訊
GUILayout.Label(info);
}
}
在上面這段程式碼中我們使用左移位操作符<<來設定碰撞層的掩碼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層時進行的射線碰撞檢測.jpg
圖10 顯示所有層時進行的射線碰撞檢測.jpg
當然還有很多的關於射線使用的API不能一一贅述,這篇只是做一個簡單的梳理,更多的API例如SphereCast、LineCast的具體用法可以查閱官方文件。
文/Orpheus(簡書作者)
原文連結:http://www.jianshu.com/p/d6d3d7bf5151
著作權歸作者所有,轉載請聯絡作者獲得授權,並標註“簡書作者”。