LOADING

加载过慢请开启缓存 浏览器默认开启

射线和射线检测

2024/1/21 学习 Unity

碰撞检测可以帮助我们实现诸如抵达某个地点自动触发剧情、判断子弹是否击中玩家等功能,但我如果想要实现如当鼠标悬浮某个人物上,自动弹出该人物信息,要如何判断呢?这时使用碰撞检测会太繁琐了。射线检测可以很好地解决这个问题。

一、使用并且创建射线

射线检测:从初始某点开始,发射一条「不可见」的射线,用来检测是否有模型添加了Collider组件,一旦检测到就停止发射。

之前使用的刚体或者碰撞器都是组件,代码来处理碰撞逻辑,而射线是纯代码处理的,需要我们自行调用API来实现。

Collider组件中的Is Trigger选项不影响射线是否检测,但是前提是QueryTriggerInteraction该参数设置为检测触发器了,你也可以将该参数设置为仅对碰撞器进行检测,这个参数可以全局设置,这个参数写在Raycast末尾。

Physics.Raycast一共有16个重载方法,可以自行组合

Physics.Raycast(origin(v3),direction(v3),hitInfo(RaycastHit),distance(float),LaterMask(int));

即射线的发射点、方向、碰撞信息(结构体)、距离(可写,不写默认无限长)、碰撞层(可写,不写默认检测所有层)。

void Update()
{
	//声明Ray结构体,存储射线的发射点和方向
	Ray ray = new Ray(transform.position,transform.forward);
	//声明RaycastHit结构体,用来存储碰撞信息
	RaycastHIt hitInfo;
	if(Physics.Raycast(ray,out hitInfo))
	{
		Debug.Log(hitInfo.collider.gameObject.name);
	//用了RaycastHit结构体中的collider属性
	//因为hitInfo是一个结构体类型,其collider属性用来村出纳射线检测到的碰撞器
	//通过(hitInfo.collider.gameObject.name来获取该碰撞器的游戏对象的名字
	}
}
void Update()
{
    RaycastHit hitInfo;
    if (Physics.Raycast(transform.position, transform.forward, out hitInfo))
    {
        Debug.Log(hitInfo.collider.gameObject.name);
    }
}
//本例和上一个基本差不多,唯一差别在于没有声明射线,而是直接把射线的起始点和终点作为参数赋予Raycast了。第一种比较好,声明好射线之后,如果后续还要用,可以直接拿来用了。

关于RaycastHit结构体相关的参数

首先RaycastHit是一个struct,其内部可以存放如下变量:

point 世界坐标系下射线碰到碰撞器的碰撞点

normal 射线碰到的表面的法线

barycentricCoordinate 碰到的三角形的重心坐标

distance 光线从原点到碰撞点的距离

triangleIndex 碰到的三角形的索引

textureCoord 在碰撞点的UV纹理坐标

textureCoord2 在碰撞点的第二个UV纹理坐标

lightmapCoord 碰撞点的光照图UV坐标

collider 碰到的碰撞器

rigbody 没有碰到则为null

transform

二、可视化射线

Unity中通过使用Debug.DrawLine()和Debug.DrawRay()都可以让射线现出原形。
需要特别注意的是,这里画出的线其实跟射线毫无关联的,因为就算没有射线,这里也能画出线来。两点一线,只要确定两个点就行了。所以这里的线只是辅助开发者而已。

注意在Scene窗口中如果不显示射线的话,是因为Gizmos开关没有打开,这个开关在Scene窗口的右上角。

Debug.DrawLine(transform.position, hitInfo.point,Color.yellow);
//Debug.DrawRay(transform.position, transform.position + transform.forward * 10, Color.yellow);

这两个的不同在于。DrawLine是真正的两点确定一条线,而DrawRay是从初始点出发画一条射线,所以需要初始点,加上一个具有具体长度和方向的向量,这样才能得到一个射线。

起始点我们可以使用transfrom.position,终点呢,我们就用上面声明的hitInfo结构体中的point,该属性存储了射线碰撞到碰撞器的碰撞点,也是一个V3类型向量

三、Unity的层级

Unity中,层有着很大的用途,比如控制摄像机仅渲染指定层来节省资源,控制光源仅照亮哪些层等,当然层也可以控制射线仅检测某些碰撞器而忽略其他的碰撞器。

四、应用实例

1.被指到的坏人会变红,而好人不会

被指到的更换材质即可

//用于存储需要检测的层、需要更换的材质
public LayerMask mask;
public Material enemyMaterial;
void Update()
{
    Ray ray = new Ray(transform.position, transform.forward);
    RaycastHit hitInfo;
    if (Physics.Raycast(ray, out hitInfo, Mathf.Infinity, mask))    //这里的Mathf.Infinity代表射线无限长,省略不写也可以
    {	
    	//通过hitInfo存贮的碰撞信息来获取实际对象的Renderer组件,然后更换其材质。
    	hitInfo.collider.gameObject.GetComponent<Renderer>().material = enemyMaterial;
    	Debug.DrawLine(transform.position, hitInfo.point, Color.yellow);

    }
    else
    {
        //当射线没有检测到与物体碰撞则画一条蓝色线条,检测到了则画黄色线条,仅起到辅助作用。
        Debug.DrawRay(transform.position, transform.forward * 10, Color.blue);
    }
}

2.让射线穿透检测

如果有一个坏人被前面的挡住了,导致最后没有变红,这里我们可以用一行代码解决这个问题,坏人被挡住的原因是因为在他前面还有一个坏人,那么!把前面那个坏人变成好人,让射线忽略他,不就能检测到后面的坏人了吗?所以原理就是,把已经检测到的物体换一个层即可。

下面这句话放到if判断中,当判断发生碰撞时,就把当前碰撞物体的层改掉,这样射线就会忽略,所以就实现了这个穿透功能

hitInfo.collider.gameObject.layer = 10;
// 这里的数字10代表第十层,LayerMask使用数字来表示层的。

3.让射线检测多个

可以通过RaycastHit结构体获得检测到的碰撞体,但是似乎每次只能返回一个,我们如果想要所有已经检测到的碰撞体的合集该怎么办呢?我们可以声明一个RaycastHit结构体数组,在if判断中,将每次检测到的值存入数组中。事实上,Unity也想到了Raycast的弊端,为此它提供了RaycastAll(),这个函数所需要的RaycastHit结构体就是一个结构体数组。

private void Update()
{
    Ray ray = new Ray(transform.position, transform.forward);
    RaycastHit[] hitInfos;    //声明了一个RaycastHit结构体数组
    hitInfos = Physics.RaycastAll(ray); //注意!Raycast是返回bool值,而这里则是返回一个数组
    Debug.DrawRay(transform.position, transform.forward * 100);
    //这里遍历输出检测到的游戏对象名字
    for (int i = 0; i < hitInfos.Length; i++)
    {     
    	Debug.Log(hitInfos[i].collider.gameObject.name);
    }
}

4.鼠标悬浮显示场景物体名称

原理很简单,从摄像机发射一条射线,然后获取碰撞物体的对象的名字即可。

需要用到:Camera.ScreenPointToRay(position);
这个函数的作用是发射并返回一条射线,射线从相机的近裁剪面出发,穿过屏幕的XY(这里使用的当前鼠标的屏幕坐标XY)。

using UnityEngine;
using UnityEngine.UI;
public class MouseShow : MonoBehaviour
{
    private LayerMask mask;
    private Text show;
    void Start()
    {
        //通过Start来给文本组件和需要检测的层初始化
        mask = LayerMask.GetMask("we");
        show = GameObject.Find("show").GetComponent<Text>();
    }
    void Update()
    {
        Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
        RaycastHit hitInfo;
    if (Physics.Raycast(ray, out hitInfo, Mathf.Infinity, mask))
    {
        show.text = hitInfo.collider.gameObject.name;
        //把获取到的碰撞组件的对象的名字显示到UI组件上
        Debug.DrawLine(Camera.main.transform.position, hitInfo.point);
        //用于演示的线,不用管
    }
    else
    {
        show.text = "当前无物体信息";
    }
}

5.鼠标点击移动(LOL那种右键移动)

当你鼠标点击时,从摄像机发射一条射线,穿过鼠标的屏幕坐标XY位置,然后抵达世界空间,当检测到设置的层时(也就是地面),将当前碰撞点保存,调用Move函数,然后移动当前物体到这个位置。

private bool isNextMove = false;
private LayerMask mask;
private Vector3 point;
void Start()
{
    mask = LayerMask.GetMask("plane");
}
void Update()
{
    Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
    RaycastHit hitInfo;
    if (Input.GetMouseButtonDown(0))
    //当鼠标点击时,才触发射线检测
    {
        if (Physics.Raycast(ray, out hitInfo, mask))
        //当检测到地面
        {
            isNextMove = true;
            point = hitInfo.point;
            //将isNextMove设为true,然后保存当前撞击点位置
        }
    }
    if(isNextMove == true)
    //当isNextMove为真,则不停调用Move
    {
        Move(point);
    }
}
void Move(Vector3 pos)
{
    //使用Vector3的插值函数来移动位置
    transform.position = Vector3.MoveTowards(transform.position, pos, Time.deltaTime * 3.0f);
    if (transform.position == pos)
    //当目标抵达位置的时候,将isNextMove置为false,等待下一次移动指令
        isNextMove = false;
}

这个例子还是有不足之处和Bug的,比如没有写转向,导致完全是平移,还有就是如果地面处于倾斜状态,当前物体依旧是90度垂直,看起来很不和谐。

注意!!!因为某个GO的position的坐标是其中心,如果你的GO具有一定体积且你的GO也是被检测的目标之一,那么请在使用Raycast的时候,将起点换算到GO的体积之外,否则将一直返回true。

参考资料

详解Unity中的射线与射线检测_unity 射线检测-CSDN博客