目录
前言
碰撞检测可以帮助我们实现诸如抵达某个地点自动触发剧情、判断子弹是否击中玩家等功能,但我如果想要实现如当鼠标悬浮某个人物上,自动弹出该人物信息,要如何判断呢?这时使用碰撞检测,从摄像机生成一个透明碰撞体朝着人物移动,等碰撞到了人物再弹出该人物信息?会不会太繁琐了。或许你又会想,若我直接生成一个足够长的透明碰撞体呢,是不是在创建的那一刻就可以触发该人物的弹出信息逻辑?没错这样的确可以,而这就是射线!不过是把无限长的透明碰撞体变为了无限长的一条线,仅此而已。
一、射线的创建方法
常用的直线射线类型用类型Ray表示,Ray包含了 起点origin 跟 方向direction的定义,起点和方向都用Vector3类型表示,前者是一个坐标,后者是一个表示方向的向量。
Vector3 origin = transform.position; Vector3 direction = Vector3.down; Ray ray = new Ray(origin, direction);
二、射线检测
在游戏中发射一条射线,最常用的是Physics.Raycast()和Physics.RaycastAll(),前者只能检测一个碰撞体而被返回,后者可以一起检测多个碰撞体。如果使用 Physics.RaycastAll() 进行多次射线投射,将返回一个 RaycastHit[] 数组,其中包含所有射线与场景中对象的交互信息。这在需要获取所有碰撞点信息的情况下很有用。
1、Raycast()
从某个初始点开始,沿着特定的方向发射一条不可见且无限长的射线,通过此射线检测是否有任何模型添加了Collider碰撞器组件。一旦检测到碰撞,停止射线继续发射。
Raycast()不使用射线Ray
public static bool Raycast (Vector3 origin, Vector3 direction, out RaycastHit hitInfo, float maxDistance, int layerMask, QueryTriggerInteraction queryTriggerInteraction);
参数:
- origin:射线在世界坐标系中的起点。
- direction:射线的方向。
- hitInfo:如果返回 true,则 hitInfo 将包含有关最近的碰撞体的命中位置的更多信息。(另请参阅:RaycastHit)。
- maxDistance:射线应检查碰撞的最大距离。(可选,不写默认无限长)
- layerMask:层遮罩,用于在投射射线时有选择地忽略碰撞体。(可选,不写默认检测所有层)
- queryTriggerInteraction:指定该查询是否应该命中触发器。可以通过指定 queryTriggerInteraction 来控制是让触发碰撞体生成命中效果,还是使用全局 Physics.queriesHitTriggers 设置。
返回
bool 当光线与任何碰撞体相交时,返回 true,否则返回 false
Raycast()使用射线Ray
public static bool Raycast (Ray ray, out RaycastHit hitInfo, float maxDistance= Mathf.Infinity, int layerMask= DefaultRaycastLayers, QueryTriggerInteraction queryTriggerInteraction= QueryTriggerInteraction.UseGlobal);
参数:
- ray 光线的起点和方向。
- hitInfo:如果返回 true,则 hitInfo 将包含有关最近的碰撞体的命中位置的更多信息。(另请参阅:RaycastHit)。
- maxDistance 射线应检查碰撞的最大距离。(可选,不写默认无限长)
- layerMask 层遮罩,用于在投射射线时有选择地忽略碰撞体。(可选,不写默认检测所有层)
- queryTriggerInteraction 指定该查询是否应该命中触发器。
返回
bool 当光线与任何碰撞体相交时,返回 true,否则返回 false
2、RaycastAll()
使用射线Ray
public static RaycastHit[] RaycastAll (Ray ray, float maxDistance= Mathf.Infinity, int layerMask= DefaultRaycastLayers, QueryTriggerInteraction queryTriggerInteraction= QueryTriggerInteraction.UseGlobal);
参数:
- ray 光线的起点和方向。
- maxDistance 从射线起点开始,允许射线命中的最大距离。(可选,不写默认无限长)
- layerMask 层遮罩,用于在投射射线时有选择地忽略碰撞体。(可选,不写默认检测所有层)
- queryTriggerInteraction 指定该查询是否应该命中触发器。
返回
RaycastHit[] RaycastHit 对象的数组。注意,这些结果的顺序未定义。
描述
向场景中投射射线并返回所有命中对象。注意,这些结果的顺序未定义。
RaycastAll() 不使用射线Ray
public static RaycastHit[] RaycastAll (Vector3 origin, Vector3 direction, float maxDistance= Mathf.Infinity, int layerMask= DefaultRaycastLayers, QueryTriggerInteraction queryTriggerInteraction= QueryTriggerInteraction.UseGlobal);
参数
- origin 射线在世界坐标系中的起点。
- direction 射线的方向。
- maxDistance 从射线起点开始,允许射线命中的最大距离。(可选,不写默认无限长)
- layermask 层遮罩,用于在投射射线时有选择地忽略碰撞体。(可选,不写默认检测所有层)
- queryTriggerInteraction 指定该查询是否应该命中触发器。
3、射线的碰撞信息
我们再重点讲讲第三个参数RaycastHit,若没有这个参数,我们的返回值仅仅只是“是否碰到了物体”,而无法确定碰撞点在哪,也不知道碰撞体是哪一个。射线检测其实有着非常丰富的碰撞信息,如可以获得其碰撞坐标,信息,以及法线。这些丰富的信息都被保存在RaycastHit结构体中。
综合用法演示如下:
//声明变量,用于保存信息 RaycastHit hitInfo; //发射射线,起点是当前物体位置,方向是世界前方 if(Physics.Raycast(transform.position,Vector3.forward,out hitInfo)) { //如果碰到物体,运行此处,返回的是bOOl值 //获取碰撞点坐标 Vector3 point = hitInfo.point; //获取对方碰撞体组件 Collider coll = hitInfo.collider; //获取对方的Transgorm组件 Transform trans = hitInfo.transform; //获取对方的物体名称 string name = hitInfo.collider.name; //获取碰撞点的法线向量 Vector3 normal = hitInfo.normal; }
注意,2D游戏跟3D游戏获取碰撞体的方式有些不一样,具体对比如下
//声明变量,将碰撞信息直接赋值给变量 RaycastHit2D hitInfo=Physics2D.Raycast(transform.position,Vector3.forward); //发射射线,起点是当前物体位置,方向是世界前方 if(hitInfo.collider != null) { //如果检测的碰撞体不为空,运行此处。 //获取碰撞点坐标 Vector3 point = hitInfo.point; //获取对方碰撞体组件 Collider coll = hitInfo.collider; //获取对方的Transgorm组件 Transform trans = hitInfo.transform; //获取对方的物体名称 string name = hitInfo.collider.name; //获取碰撞点的法线向量 Vector3 normal = hitInfo.normal; }
三、示例
//第一个简单小栗子 void Update() { Ray ray = new Ray(transform.position, transform.forward); //声明一个Ray结构体,用于存储该射线的发射点,方向 RaycastHit hitInfo; //声明一个RaycastHit结构体,存储碰撞信息 if (Physics.Raycast(ray, out hitInfo)) { Debug.Log(hitInfo.collider.gameObject.name); //这里使用了RaycastHit结构体中的collider属性 //因为hitInfo是一个结构体类型,其collider属性用于存储射线检测到的碰撞器。 //通过collider.gameObject.name,来获取该碰撞器的游戏对象的名字。 } }
//第二个简单小栗子 void Update() { RaycastHit hitInfo; if (Physics.Raycast(transform.position, transform.forward, out hitInfo)) { Debug.Log(hitInfo.collider.gameObject.name); } }
示例:在场景中从鼠标发射一条射线 , 用于点击物体 获取物体的名字 位置等等 ;
Ray ray= Camera.main,ScreenPointToRay(Input.mousePosition); RaycastHit; if (Physics.Raycast(ray,out hit)) { Debug.Log(" 当前鼠标点击物体的名字是————"+ hit.collider.name); }
四、具体使用场景
当使用 RaycastHit 类进行射线投射时,有许多不同的使用场景,包括检测碰撞、拾取物体、瞄准检测、物体高亮等。在以下每个场景示例中,我将向您展示如何使用 RaycastHit,并加入一些粒子效果(“lizi”,即粒子效果在中文中的写法)来增强可视化效果。
- 检测碰撞并发射粒子:
在这个场景中,我们将使用射线投射来检测是否与一个可交互的对象发生碰撞,然后在碰撞点发射一些粒子效果。
using UnityEngine; public class RaycastCollisionExample : MonoBehaviour { public ParticleSystem collisionParticles; // 粒子效果 void Update() { Ray ray = new Ray(transform.position, transform.forward); RaycastHit hitInfo; if (Physics.Raycast(ray, out hitInfo)) { // 碰撞点发射粒子效果 if (collisionParticles != null) { collisionParticles.transform.position = hitInfo.point; collisionParticles.Play(); } Debug.Log("碰撞对象:" + hitInfo.collider.gameObject.name); } } }
在这个示例中,我们在碰撞点发射了一个粒子效果,从而在用户与可交互对象发生碰撞时产生视觉效果。
- 物体拾取与交互:
在这个场景中,我们将使用射线投射来检测是否与一个可拾取的物体发生碰撞,然后可以将物体拾取并交互。
using UnityEngine; public class ObjectPickupExample : MonoBehaviour { private Transform pickedObject = null; // 当前拾取的物体 void Update() { Ray ray = new Ray(transform.position, transform.forward); RaycastHit hitInfo; if (Physics.Raycast(ray, out hitInfo)) { // 拾取物体 if (Input.GetKeyDown(KeyCode.E)) { if (hitInfo.collider.CompareTag("Pickable")) { pickedObject = hitInfo.collider.transform; Debug.Log("拾取了:" + pickedObject.name); } } // 交互 if (Input.GetKeyDown(KeyCode.F)) { if (pickedObject != null) { Debug.Log("与 " + pickedObject.name + " 进行了交互。"); } } } } }
在这个示例中,我们使用 Input.GetKeyDown(KeyCode.E) 来拾取与射线相交的可拾取物体,并使用 Input.GetKeyDown(KeyCode.F) 来与拾取的物体进行交互。
- 物体高亮效果:
在这个场景中,我们将使用射线投射来检测是否与一个物体发生碰撞,然后在碰撞对象上显示一个高亮的粒子效果。
using UnityEngine; public class ObjectHighlightExample : MonoBehaviour { public ParticleSystem highlightParticles; // 高亮粒子效果 private Transform lastHighlightedObject = null; // 上一个高亮的物体 void Update() { Ray ray = new Ray(transform.position, transform.forward); RaycastHit hitInfo; if (Physics.Raycast(ray, out hitInfo)) { // 高亮物体 if (hitInfo.collider.CompareTag("Highlightable")) { if (lastHighlightedObject != hitInfo.collider.transform) { if (lastHighlightedObject != null) { highlightParticles.Stop(); } lastHighlightedObject = hitInfo.collider.transform; highlightParticles.transform.position = lastHighlightedObject.position; highlightParticles.Play(); } } else if (lastHighlightedObject != null) { highlightParticles.Stop(); lastHighlightedObject = null; } } } }
在这个示例中,我们使用高亮粒子效果来显示玩家当前所指的物体。粒子效果在物体上播放和停止,从而实现高亮效果。
- 通过鼠标点击的位置获取与射线碰撞的物体
private void Update() { if (Input.GetMouseButtonDown(0)) { /*判断鼠标是否点击在UI上*/ if (EventSystem.current.IsPointerOverGameObject() == false) { /*通过鼠标位置获取射线*/ Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition); /*用于存储射线投射操作的结果*/ RaycastHit hit; /* Physics.Raycast参数: * 射线在世界坐标系中的起点、 * 射线的方向和存储射线投射操作的结果、 * 射线应检查碰撞的最大距离、 * 层遮罩,用于在投射射线时有选择地忽略碰撞体*/ bool isCpllider = Physics.Raycast(ray, out hit, 1000, LayerMask.GetMask("MapCobe")); if (isCpllider) { /*得到点击的MapCude*/ GameObject mapCude = hit.collider.gameObject; Debug.Log("成功"); } } } }
注意:Collider组件中Is Trigger选项的开关并不影响射线检测! 对了还有一个参数,写在Raycast末尾,QueryTriggerInteraction(指定该射线是否应该命中触发器),上面我说过Is Trigger选项的开关不影响射线检测,但是前提是QueryTriggerInteraction该参数设置为检测触发器了,你也可以将该参数设置为仅对碰撞器进行检测,这个参数可以全局设置。对于射线投射起点位于碰撞体内的情况,Raycast 不会检测到碰撞体。在所有这些示例中,都使用了 FixedUpdate 而不是 Update。请参阅事件函数的执行顺序,以了解 Update 与 FixedUpdate 的区别,以及它们与物理查询的关系。
射线的调试方法
利用Debug调试
在游戏开发中,射线的调试方法非常重要,可以将看不见的射线以可视化的形式表现出来,方便我们查看数据是否正确。这个方法是使用Debug.DrawLine()函数和Debug.DrawRay()
1、Debug.DrawLine()
public static void DrawLine (Vector3 start, Vector3 end, Color color= Color.white, float duration= 0.0f, bool depthTest= true);
参数:
- start 应作为该直线起始点的世界空间中的点。
- end 应作为该直线结束点的世界空间中的点。
- color 该直线的颜色。
- duration 该直线的可见长度应为多长。
- depthTest 该直线是否应被靠近此摄像机的对象遮挡?
描述
在指定的起始点与结束点之间绘制一条直线。
当游戏正在运行并且启用辅助图标绘图时,将在 Editor 的游戏视图中绘制直线。当直线在游戏视图中可见时,还会在场景中绘制该直线。让游戏运行并显示直线。切换到场景视图,该直线将可见。
duration 是在第一次显示该直线后该直线可见的时间长短(单位为秒)。如果持续时间为零,则该直线仅显示一帧。
注意:这仅用于调试播放模式。应改用 Gizmos.Drawline 或 Handles.DrawLine 来绘制 Editor 辅助图标。
Debug.DrawLine 更多详情
2、Debug.DrawRay
public static void DrawRay (Vector3 start, Vector3 dir, Color color= Color.white, float duration= 0.0f, bool depthTest= true);
参数:
- start 应作为该射线起始点的世界空间中的点。
- dir 该射线的方向和长度。
- color 绘制的直线的颜色。
- duration 该直线的可见时长(单位为秒)。
- depthTest 该直线是否应被靠近此摄像机的其他对象遮挡?
描述
在世界坐标中绘制一条从 start 到 start + dir 的直线。
duration 参数确定在绘制该直线所在的帧之后该直线可见时长。如果持续时间为 0(默认值),则该直线被渲染 1 帧。
如果将 depthTest 设置为 true,则该直线将被此场景中更靠近摄像机的其他对象遮挡。将在该 Editor 的场景视图中绘制该直线。如果在游戏视图中启用了辅助图标绘图,则在该视图中也将绘制该直线。
效果图可以看上面,都有涉及使用到。需要说明的是在默认的情况下,该辅助线仅在编辑器的场景窗口可见,如果要在game窗口看到,则需要单机Game窗口右上角的Gizmos开关。
利用Gizmos
调用OnDrawGizmos()函数或OnDrawGizmosSelected,它们同样都可以用来绘制辅助图标。区别在于
函数OnDrawGizmos()在程序一运行就执行,之后每帧都在执行,
函数OnDrawGizmosSelected()在鼠标点击到脚本挂载的物体的身上的时候运行,不管有多少父类对象,它都会执行。
这里推荐使用OnDrawGizmosSelected()
范例如下
private void OnDrawGizmosSelected() { Gizmos.DrawLine(Vector3 from, Vector3 to); Gizmos.DrawRay((Vector3 from, Vector3 direction); }
可在scence窗口查看辅助线,同样想在game窗口可以看到,则需要单机Game窗口右上角的Gizmos开关。