Unity基础-碰撞检测
八、碰撞检测
概述
射线检测是Unity物理系统中的一种重要机制,它可以在指定点发射一个指定方向的射线,用于判断该射线与哪些碰撞器相交,并获取对应的对象信息。它主要用于解决以下问题:
- 鼠标选择场景上的物体:例如点击一个物体,获取其信息。
- FPS射击游戏:实现无弹道射击(不产生实际的子弹对象进行移动),判断子弹的命中。
- 判断一条线与物体的碰撞情况:例如激光、探照灯等。
与碰撞检测(需要刚体和碰撞器)和范围检测(只需要碰撞器,瞬时判断一个区域)不同,射线检测更侧重于对“线”与物体相交的判断。
1. 射线对象 (Ray)
在Unity中,Ray 是一个结构体,用于表示3D世界中的一条无限长的射线。它由一个起点(origin)和一个方向(direction)组成。
注意:
Ray的第二个参数direction是一个方向向量,而不是终点。它代表了射线的方向,而不是由两个点确定的线段。
// 1. 在3D世界中定义一条射线
// 假设定义一条起点为世界坐标(1,0,0),方向为世界坐标Z轴正方向的射线
Ray r = new Ray(Vector3.right, Vector3.forward);// 访问Ray的参数:
print(r.origin); // 输出射线的起点
print(r.direction); // 输出射线的方向向量
// 2. 从摄像机发射出的射线
// 得到一条从屏幕位置作为起点,摄像机视口方向为方向的射线
// 这在处理鼠标点击或触摸屏幕与3D世界交互时非常常用
Ray screenRay = Camera.main.ScreenPointToRay(Input.mousePosition);
2. 射线碰撞检测函数 (Physics.Raycast)
Physics 类提供了多种静态函数用于进行射线检测。它们有很多种重载类型,可以根据需求选择合适的。
注意:
- 射线检测也是瞬时的,只在执行代码时进行一次检测。
2.1 检测是否有碰撞 (返回 bool)
这是最原始的射线检测方式,只判断射线是否碰撞到对象,返回 true 或 false。
-
Physics.Raycast(ray, maxDistance, layerMask, queryTriggerInteraction)ray:要发射的射线对象(Ray类型)。maxDistance:射线检测的最大距离。超出这个距离将不检测。如果设置为Mathf.Infinity或不填,则检测无限远。layerMask:可选参数,要检测的层级(int)。如果不填,则检测所有层。详情参考本文件的“层级 (LayerMask)”部分。queryTriggerInteraction:可选参数,用于指定是否检测触发器(Trigger Collider)。QueryTriggerInteraction.UseGlobal:使用项目物理设置中的全局设置。QueryTriggerInteraction.Collide:检测触发器。QueryTriggerInteraction.Ignore:忽略触发器。
- 返回值:
bool,当碰撞到对象时返回true,没有碰撞到则返回false。
-
Physics.Raycast(origin, direction, maxDistance, layerMask, queryTriggerInteraction)- 此重载版本不需要预先创建
Ray对象,直接传入射线的起点和方向向量。功能与上述相同。
- 此重载版本不需要预先创建
示例代码:
// 准备一条射线
Ray myRay = new Ray(Vector3.zero, Vector3.forward);// 进行射线检测,判断是否碰撞到"Monster"层上的对象,最大检测距离1000
if (Physics.Raycast(myRay, 1000, 1 << LayerMask.NameToLayer("Monster"), QueryTriggerInteraction.UseGlobal))
{Debug.Log("射线碰撞到了Monster层上的物体!");
}// 或者直接传入起点和方向进行检测
if (Physics.Raycast(Vector3.one, Vector3.forward, 1000, 1 << LayerMask.NameToLayer("Monster"), QueryTriggerInteraction.UseGlobal))
{Debug.Log("从Vector3.one发出的射线碰撞到了Monster层上的物体!");
}
2.2 获取相交的单个物体信息 (返回 bool 并通过 out RaycastHit)
此方法在检测到碰撞的同时,会通过一个 out RaycastHit 参数返回碰撞的详细信息。RaycastHit 是一个结构体(值类型)。
Physics.Raycast(ray, out hitInfo, maxDistance, layerMask, queryTriggerInteraction)hitInfo:RaycastHit类型的输出参数。Unity会在函数内部处理后,将碰撞数据填充到这个参数中。- 其他参数与2.1节相同。
- 返回值:
bool,当碰撞到对象时返回true,并填充hitInfo;否则返回false。
示例代码:
RaycastHit hitInfo; // 声明一个RaycastHit变量用于存储碰撞信息
Ray r1 = new Ray(transform.position, Vector3.forward);// 检测射线是否碰撞到"Default"层上的物体,并获取碰撞信息
if(Physics.Raycast(r1, out hitInfo, 10, 1 << LayerMask.NameToLayer("Default")))
{Debug.Log("碰到物体:" + hitInfo.collider.gameObject.name);Debug.Log("碰撞到的世界坐标点:" + hitInfo.point);Debug.Log("碰撞点处的法线信息:" + hitInfo.normal);Debug.Log("碰撞到的物体Transform位置:" + hitInfo.transform.position);Debug.Log("射线起点到碰撞点的距离:" + hitInfo.distance);
}
else
{Debug.Log("未检测到物体。");
}
2.3 获取相交的多个物体信息 (Physics.RaycastAll)
用于获取射线路径上所有碰撞到的对象。如果没有碰撞到任何对象,则返回一个容量为0的数组。
-
Physics.RaycastAll(ray, maxDistance, layerMask, queryTriggerInteraction)- 参数与2.1节相同。
- 返回值:
RaycastHit[]数组,包含所有碰撞到的对象的RaycastHit信息。
-
Physics.RaycastNonAlloc(ray, results, maxDistance, layerMask, queryTriggerInteraction)- 非分配式版本,需要传入一个预先分配好的
RaycastHit[]数组来存储结果,推荐在频繁调用时使用,以减少GC。
- 非分配式版本,需要传入一个预先分配好的
示例代码:
Ray r2 = new Ray(transform.position, Vector3.forward);// 获取射线检测上的所有碰撞体
RaycastHit[] allHits = Physics.RaycastAll(r2, 10, 1 << LayerMask.NameToLayer("Default"));if (allHits.Length > 0)
{Debug.Log($"射线检测到了 {allHits.Length} 个物体:");foreach (RaycastHit hit in allHits){Debug.Log($" - 物体名字:{hit.collider.gameObject.name}, 碰撞点:{hit.point}");}
}
else
{Debug.Log("射线未检测到任何物体。");
}// 使用 RaycastNonAlloc 示例
RaycastHit[] nonAllocHits = new RaycastHit[10]; // 预分配数组
int numHits = Physics.RaycastNonAlloc(r2, nonAllocHits, 10, 1 << LayerMask.NameToLayer("Default"));if (numHits > 0)
{Debug.Log($"RaycastNonAlloc 检测到了 {numHits} 个物体:");for (int i = 0; i < numHits; i++){Debug.Log($" - 物体名字:{nonAllocHits[i].collider.gameObject.name}, 碰撞点:{nonAllocHits[i].point}");}
}
3. RaycastHit 中的常用成员
RaycastHit 结构体包含了关于射线碰撞的详细信息,通过这些信息,我们可以得到物体的位置、方向等,进行后续逻辑处理。
hitInfo.transform:碰撞到的对象的Transform信息。可以通过它获取到碰撞对象的旋转、缩放等。hitInfo.collider:碰撞到的对象的Collider组件信息。hitInfo.point:射线与碰撞体表面相交的世界坐标点。这是最常用的信息之一,常用于实例化特效或放置物体。hitInfo.normal:碰撞到的点处的法线信息(世界坐标系)。法线是一个垂直于碰撞表面的向量,常用于确定实例化对象的朝向,使其与表面对齐。hitInfo.distance:射线起点到碰撞点的距离。
4. 注意事项
- 参数顺序:在
Physics.Raycast的一些重载中,距离参数和层级(layerMask)参数都是int类型。请务必注意它们的顺序,距离参数通常在层级参数之前。不可只传层级而不传距离,否则可能导致参数错位或默认值问题。 out关键字:当需要获取RaycastHit信息时,务必在RaycastHit变量前加上out关键字。- 层级设置:射线检测只会与具有碰撞器且在指定
layerMask范围内的对象发生碰撞。 - 调试可视化:使用
Debug.DrawRay可以方便地在 Scene 视图中可视化你发射的射线,有助于调试碰撞问题。
5. 实际应用示例:鼠标交互实现子弹特效、弹孔生成与物体拖动
这个示例提供一个脚本 Lea43.cs,用于实现鼠标点击场景中的墙壁生成子弹特效和弹孔,以及点击并拖动场景中的立方体的功能。
习题要求:
- 子弹特效与弹孔生成:请实现鼠标点击场景上一面墙,在点击的位置创建子弹特效和弹孔。
- 物体拖动与选中:场景上有一个平面和一个立方体。当鼠标点击选中立方体时,长按鼠标左键可以拖动立方体在平面上移动。点击鼠标右键取消选中。
核心思想:
- 射线检测:将鼠标屏幕坐标转换为世界空间射线,用于检测与场景中物体的碰撞。
- 层级(LayerMask)过滤:精确控制射线只检测特定层级(如
Wall、Player、Ground)的物体,避免不必要的检测和逻辑混淆。 - 物体实例化:在检测到碰撞点后,实例化预设体(如子弹特效、弹孔)。
- 对象拖动逻辑:通过记录当前选中的对象,并在鼠标按住时持续更新其位置到射线检测到的地面点。
代码实现 (Lea43.cs)
点击展开/折叠 Lea43.cs 代码using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class Lea43 : MonoBehaviour
{RaycastHit raycastHit; // 用于存储射线检测结果的信息GameObject nowObj; // 用于存储当前被鼠标选中的可拖动对象void Update(){// === 功能2: 立方体选中与拖动 ===// 鼠标左键按下(只在按下的第一帧触发):尝试选中"Player"层上的对象或触发子弹特效if(Input.GetMouseButtonDown(0)){// 首先尝试射线检测"Player"层,看是否选中了可拖动的立方体if(Physics.Raycast(Camera.main.ScreenPointToRay(Input.mousePosition), out raycastHit, 100, 1<<LayerMask.NameToLayer("Player"))){nowObj = raycastHit.collider.gameObject; // 选中射线击中的Player层对象Debug.Log("选中物体: " + nowObj.name); // 打印选中信息,用于调试}// 如果没有选中"Player"物体(即nowObj仍然为null),则尝试在"Wall"层上创建子弹特效else if (nowObj == null) {// === 功能1: 在墙上创建子弹特效和弹孔 ===// 射线检测"Wall"层,获取碰撞信息if(Physics.Raycast(Camera.main.ScreenPointToRay(Input.mousePosition), out raycastHit, 100, 1<<LayerMask.NameToLayer("Wall"))){// 实例化子弹特效(假定Resources/BulletEffect预制体已存在)GameObject bulletEffect = Instantiate(Resources.Load<GameObject>("BulletEffect"));bulletEffect.transform.position = raycastHit.point; // 设置特效位置为射线碰撞点// 使特效面朝墙的法线方向,看起来像是从墙里射出的bulletEffect.transform.rotation = Quaternion.LookRotation(raycastHit.normal);// 实例化弹孔(假定Resources/BulletHole预制体已存在)GameObject bulletHole = Instantiate(Resources.Load<GameObject>("BulletHole"));// 弹孔位置稍微沿法线方向偏移一点,避免Z-fighting(与墙面重叠闪烁)bulletHole.transform.position = raycastHit.point + raycastHit.normal * 0.01f;// 使弹孔面朝墙的法线方向bulletHole.transform.rotation = Quaternion.LookRotation(raycastHit.normal);Debug.Log("在墙上创建了子弹特效和弹孔!"); // 打印信息,用于调试}}}// 鼠标左键按住(每帧触发):如果当前有选中的对象,则拖动它在"Ground"层上移动if(Input.GetMouseButton(0) && nowObj != null){// 射线检测"Ground"层,获取碰撞信息if(Physics.Raycast(Camera.main.ScreenPointToRay(Input.mousePosition), out raycastHit, 100, 1<<LayerMask.NameToLayer("Ground"))){// 更新选中对象的位置为射线在地面上的碰撞点nowObj.transform.position = raycastHit.point;}}// 鼠标右键按下(只在按下的第一帧触发):取消选中对象if(Input.GetMouseButtonDown(1)){if (nowObj != null){Debug.Log("取消选中物体: " + nowObj.name); // 打印取消选中信息nowObj = null; // 将选中对象设为null,取消选中状态}}}
}
