碰撞检测可以帮助我们实现诸如抵达某个地点自动触发剧情、判断子弹是否击中玩家等功能,但我如果想要实现如当鼠标悬浮某个人物上,自动弹出该人物信息,要如何判断呢?这时使用碰撞检测会太繁琐了。射线检测可以很好地解决这个问题。
一、使用并且创建射线
射线检测:从初始某点开始,发射一条「不可见」的射线,用来检测是否有模型添加了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。
参考资料