本文來源于兩年前我的一篇CSDN博客写隶。CSDN博客本來就沒寫多少倔撞,現(xiàn)在也基本是到簡書上混了。所以各位大大請自覺繞過哈慕趴。
在我們的游戲開發(fā)過程中痪蝇,有一個很重要的工作就是進(jìn)行碰撞檢測鄙陡。例如在射擊游戲中子彈是否擊中敵人,在RPG游戲中是否撿到裝備等等霹俺。在進(jìn)行碰撞檢測時柔吼,我們最常用的工具就是射線,Unity 3D的物理引擎也為我們提供了射線類以及相關(guān)的函數(shù)接口丙唧。本文將對射線的使用進(jìn)行一個總結(jié)。
射線是在三維世界中從一個點(diǎn)沿一個方向發(fā)射的一條無限長的線觅玻。在射線的軌跡上想际,一旦與添加了碰撞器的模型發(fā)生碰撞,將停止發(fā)射溪厘。我們可以利用射線實(shí)現(xiàn)子彈擊中目標(biāo)的檢測胡本,鼠標(biāo)點(diǎn)擊拾取物體等功能。
射線的創(chuàng)建和顯示
Ray射線類和RaycastHit射線投射碰撞信息類是兩個最常用的射線工具類畸悬。
創(chuàng)建一條射線Ray需要指明射線的起點(diǎn)(origin)和射線的方向(direction)侧甫。這兩個參數(shù)也是Ray的成員變量。注意蹋宦,射線的方向在設(shè)置時如果未單位化披粟,Unity 3D會自動進(jìn)行單位歸一化處理。射線Ray的構(gòu)造函數(shù)為 :
public Ray(Vector3 origin, Vector3 direction);
RaycastHit類用于存儲發(fā)射射線后產(chǎn)生的碰撞信息冷冗。常用的成員變量如下:collider與射線發(fā)生碰撞的碰撞器
distance 從射線起點(diǎn)到射線與碰撞器的交點(diǎn)的距離
normal 射線射入平面的法向量
point 射線與碰撞器交點(diǎn)的坐標(biāo)(Vector3對象)
Physics.Raycast靜態(tài)函數(shù)用于在場景中發(fā)射一條可以和碰撞器碰撞的射線守屉,相關(guān)的API如下:
**1)public static bool Raycast(Vector3 origin, Vector3 direction, float distance=Mathf.Infinity, intlayerMask=DefaultRaycastLayers);**
**參數(shù)說明:**
origin 射線起點(diǎn)世界坐標(biāo)
direction 射線方向矢量
distance 射線長度(起點(diǎn)到終點(diǎn)的距離),默認(rèn)設(shè)置為無限長
layerMask 顯示層掩碼(只選擇層次為layerMask指定層次的碰撞器進(jìn)行碰撞蒿辙,其他層次的碰撞器忽略)
**返回值說明:**
當(dāng)射線與碰撞器發(fā)生碰撞時返回值為true拇泛,未穿過任何碰撞器時返回為false。
**2)public static boolRaycast(Vector3 origin, Vector3 direction, RaycastHit hitInfo, float distance =Mathf.Infinity, int layerMask = DefaultRaycastLayers);**
這個重載函數(shù)定義了一個碰撞信息類**RaycastHit**思灌,在使用時通過out關(guān)鍵字傳入一個空的碰撞信息對象俺叭。當(dāng)射線與碰撞器發(fā)生碰撞時,該對象將被賦值泰偿,可以獲得碰撞信息包括transform熄守、rigidbody、point 等甜奄。如果未發(fā)生碰撞柠横,該對象為空。
**3)public static boolRaycast(Ray ray, float distance = Mathf.Infinity, int layerMask =DefaultRaycastLayers);**
這個重載函數(shù)使用已有的一條射線Ray來作為參數(shù)课兄。
**4)public static boolRaycast(Ray ray, RaycastHit hitInfo, float distance = Mathf.Infinity, intlayerMask = DefaultRaycastLayers);**
這個重載函數(shù)使用已有的射線Ray來作為參數(shù)并獲取碰撞信息RaycastHit牍氛。
在調(diào)試時如果想顯示一條射線,可以使用Debug.DrawLine來實(shí)現(xiàn)烟阐。
**public static void DrawLine(Vector3start, Vector3 end, Color color);**
只有當(dāng)發(fā)生碰撞時搬俊,在Scene視圖中才能看到畫出的射線紊扬。
下面這個例子創(chuàng)建了一個從主攝像機(jī)向y軸負(fù)向發(fā)射一條射線檢測下方是否有平面存在。在場景中攝像機(jī)下方創(chuàng)建一個Plane游戲?qū)ο蟀蓿⑾旅娴哪_本RayDemo01.cs掛載到攝像機(jī)上餐屎。
using UnityEngine;
using System.Collections;
public class RayDemo01 : MonoBehaviour {
void Update () {
// 以攝像機(jī)所在位置為起點(diǎn),創(chuàng)建一條向下發(fā)射的射線
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);
}
}
}
運(yùn)行程序后腹缩,如圖1所示,在場景視圖中可以看見攝像機(jī)發(fā)出的射線空扎。當(dāng)檢測到下方的平面時藏鹊,會在控制臺中打印輸出檢測結(jié)果,如圖2所示转锈。
定向發(fā)射射線的實(shí)現(xiàn)
當(dāng)我們要使用鼠標(biāo)拾取物體或判斷子彈是否擊中物體時盘寡,我們往往是沿著特定的方向發(fā)射射線,這個方向可能是朝向屏幕上的一個點(diǎn)撮慨,或者是世界坐標(biāo)系中的一個矢量方向竿痰,沿世界坐標(biāo)系中的矢量方向發(fā)射射線我們已經(jīng)在上面演示過如何實(shí)現(xiàn)。針對向屏幕上的某一點(diǎn)發(fā)射射線砌溺,Unity 3D為我們提供了兩個API函數(shù)以供使用影涉,分別是ScreenPointToRay和ViewportPointToRay。
public Ray ScreenPointToRay(Vector3 position);
參數(shù)說明:position是屏幕上的一個參考點(diǎn)坐標(biāo)抚吠。
返回值說明:返回射向position參考點(diǎn)的射線常潮。當(dāng)發(fā)射的射線未碰撞到物體時,碰撞點(diǎn)hit.point的值為(0,0,0)楷力。
ScreenPointToRay方法從攝像機(jī)的近視口nearClip向屏幕上的一點(diǎn)position發(fā)射射線喊式。Position用實(shí)際像素值表示射線到屏幕上的位置。當(dāng)參考點(diǎn)position的x分量或y分量從0增長到最大值時萧朝,射線將從屏幕的一邊移動到另一邊岔留。由于position在屏幕上,因此z分量始終為0检柬。
下面我們用一段程序示例說明如何利用ScreenPointToRay來發(fā)射一條指向屏幕上的某點(diǎn)來進(jìn)行定向檢測碰撞體献联。在場景中創(chuàng)建一個Cube位于攝像機(jī)的正前方,將下面的腳本RayDemo02.cs掛載到攝像機(jī)上何址。
using UnityEngine;
using System.Collections;
public class RayDemo02 : MonoBehaviour {
Ray ray;
RaycastHit hit;
// 創(chuàng)建射線到屏幕上的參考點(diǎn)里逆,像素坐標(biāo)
Vector3 position = new Vector3(Screen.width/2.0f, Screen.height/2.0f, 0.0f);
void Update () {
// 射線沿著屏幕x軸從左向右循環(huán)掃描
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))
{
// 如果與物體發(fā)生碰撞,在Scene視圖中繪制射線
Debug.DrawLine(ray.origin, hit.point, Color.green);
// 打印射線檢測到的物體的名稱
Debug.Log("射線檢測到的物體名稱: " + hit.transform.name);
}
}
}
在這段代碼中用爪,首先聲明了一個變量position原押,用于記錄射線到屏幕上的實(shí)際交點(diǎn)的像素坐標(biāo),然后在Update方法中更改position的x分量值偎血,使得射線從屏幕左方向右方不斷循環(huán)掃描诸衔,接著調(diào)用方法ScreenPointToRay生成射線ray盯漂,最后繪制射線和打印射線探測到的物體的名稱。運(yùn)行程序后笨农,如圖3所示就缆,在Scene視圖中可以看到我們繪制的射線正在場景中掃描,圖4是在控制臺下打印輸出射線探測到的物體名稱谒亦。
public Ray ViewportPointToRay(Vector3 position);
參數(shù)說明:position為屏幕上的一個參考點(diǎn)坐標(biāo)(坐標(biāo)已單位化處理)竭宰。
返回值說明:返回射向position參考點(diǎn)的射線。當(dāng)發(fā)射的射線未碰撞到物體時诊霹,碰撞點(diǎn)hit.point的值為(0,0,0)羞延。
ViewportPointToRay方法從攝像機(jī)的近視口nearClip向屏幕上的一點(diǎn)position發(fā)射射線。Position用單位化比例值的方式表示射線到屏幕上的位置脾还。當(dāng)參考點(diǎn)position的x分量或y分量從0增長到1時,射線將從屏幕的一邊移動到另一邊入愧。由于position在屏幕上鄙漏,因此z分量始終為0。
下面我們用一段程序示例說明如何利用ViewportPointToRay來發(fā)射一條指向屏幕上的某點(diǎn)來進(jìn)行定向檢測碰撞體棺蛛。在場景中創(chuàng)建一個Cube位于攝像機(jī)的正前方怔蚌,將下面的腳本RayDemo03.cs掛載到攝像機(jī)上。
using UnityEngine;
using System.Collections;
public class RayDemo03 : MonoBehaviour {
Ray ray;
RaycastHit hit;
// 創(chuàng)建射線到屏幕上的參考點(diǎn)旁赊,單位化坐標(biāo)
Vector3 position = new Vector3(0.5f, 0.5f, 0.0f);
void Update () {
// 射線沿著屏幕x軸從左向右循環(huán)掃描
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))
{
// 如果與物體發(fā)生碰撞桦踊,在Scene視圖中繪制射線
Debug.DrawLine(ray.origin, hit.point, Color.green);
// 打印射線檢測到的物體的名稱
Debug.Log("射線檢測到的物體名稱: " + hit.transform.name);
}
}
}
在這段代碼中,首先聲明了一個變量position终畅,用于記錄射線到屏幕上的實(shí)際交點(diǎn)的像素坐標(biāo)籍胯,然后在Update方法中更改position的x分量值,使得射線從屏幕左方向右方不斷循環(huán)掃描离福,接著調(diào)用方法ViewportPointToRay生成射線ray杖狼,最后繪制射線和打印射線探測到的物體的名稱。運(yùn)行程序后妖爷,如圖5所示蝶涩,在Scene視圖中可以看到我們繪制的射線正在場景中掃描,圖6是在控制臺下打印輸出射線探測到的物體名稱絮识。
利用二次發(fā)射射線的方式檢測內(nèi)部物體
有的時候我們要檢測的物體在其他物體的內(nèi)部绿聘,并且這兩個物體都具有碰撞器,用射線檢測返回的是第一個物體的信息次舌。在這種情況下熄攘,我們需要使用二次射線發(fā)射的做法,即以第一次射線碰撞的外層物體的碰撞點(diǎn)作為第二次射線發(fā)射的起點(diǎn)垃它,沿原來方向發(fā)射射線鲜屏,判斷是否與內(nèi)部物體發(fā)生碰撞烹看。
下面我們用一段代碼示例來說明如何用二次發(fā)射射線來檢測位于物體內(nèi)部的目標(biāo)。在場景中創(chuàng)建兩個Cube洛史,位于攝像機(jī)的正前方惯殊。在其中一個Cube的位置上創(chuàng)建一個Sphere,并設(shè)置它的大小為Cube的一半也殖,這樣Sphere就位于Cube的內(nèi)部土思。將下面的腳本RayDemo04.cs掛載到攝像機(jī)上。
using UnityEngine;
using System.Collections;
public class RayDemo04 : MonoBehaviour {
GameObject wrapper; // 外層物體
GameObject target; // 內(nèi)層物體
string info = ""; // 碰撞檢測信息
void Update () {
if(Input.GetMouseButton (0))
{
// 當(dāng)鼠標(biāo)左鍵按下時忆嗜,向鼠標(biāo)所在的屏幕位置發(fā)射一條射線
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
RaycastHit hitInfo;
if(Physics.Raycast(ray, out hitInfo))
{
// 當(dāng)射線與物體發(fā)生碰撞時己儒,在場景視圖中繪制射線
Debug.DrawLine(ray.origin, hitInfo.point, Color.red);
// 獲得第一次碰撞的外層物體對象
wrapper = hitInfo.collider.gameObject;
// 以第一次的碰撞點(diǎn)為起點(diǎn),沿原來的方向二次發(fā)射射線
Ray ray2= new Ray(hitInfo.point, ray.direction);
RaycastHit hitInfo2;
if(Physics.Raycast(ray2, out hitInfo2))
{
// 當(dāng)射線與內(nèi)層物體碰撞時捆毫,在場景中繪制射線
Debug.DrawLine(ray2.origin, ray2.direction, Color.green);
// 獲得內(nèi)層物體對象
target = hitInfo2.collider.gameObject;
// 將外層物體的網(wǎng)格隱藏
wrapper.GetComponent<MeshRenderer().enabled = false;
// 設(shè)置碰撞信息
info = "檢測到物體: " + target.name + "坐標(biāo): " + target.transform.position;
}
else
{
// 如果二次發(fā)射的射線沒有與內(nèi)層物體碰撞
// 顯示外層物體的網(wǎng)格
wrapper.GetComponent<MeshRenderer>().enabled = true;
// 設(shè)置碰撞信息
info = "檢測到物體: " + wrapper.name + "坐標(biāo): " + wrapper.transform.position;
}
}
}
}
void OnGUI(){
// 在屏幕上打印輸出射線檢測的信息
GUILayout.Label(info);
}
}
在上面這段代碼中我們使用左移位操作符<<來設(shè)置碰撞層的掩碼layerMask闪湾。Unity 3D中共有32個層,對應(yīng)使用一個32位整數(shù)的各個位來表示每個層級绩卤,當(dāng)這個位為1時表示使用這個層途样,為0時表示不使用這個層。
LayerMask.NameToLayer這個API是返回我們使用自定義命名所定義的層的層索引濒憋,注意從0開始何暇。當(dāng)我們使用左移位操作設(shè)置層次掩碼時,對應(yīng)的自定義層級是n我們就將1左移n位凛驮,這樣射線就只在layerMask指定的層次上進(jìn)行碰撞檢測煞肾×骰瑁可供使用的自定義的層級從第8層開始媒熊,我們將8~10層分別命名為Capsule溯祸、Sphere和Cube,并將Capsule纠修、Shpere和Cube三個物體的layer分別設(shè)置為對應(yīng)的層次胳嘲。一開始我們將所有物體設(shè)置為透明不可見。當(dāng)按下鼠標(biāo)左鍵發(fā)射射線時扣草,返回射線方向上所有碰撞的物體信息了牛,將獲取到的物體對象,全部設(shè)置為半透明可見辰妙。點(diǎn)擊按鈕可以切換檢測碰撞的層次鹰祸。
運(yùn)行代碼,如圖9密浑、圖10所示蛙婴,當(dāng)切換不同的按鈕控制射線在不同的層次上檢測碰撞,顯示的物體也便不同尔破。
當(dāng)然還有很多的關(guān)于射線使用的API不能一一贅述街图,這篇只是做一個簡單的梳理浇衬,更多的API例如SphereCast、LineCast的具體用法可以查閱官方文檔餐济。