【Unity入门】详解Unity中的射线与射线检测

目录

前言

碰撞检测可以帮助我们实现诸如抵达某个地点自动触发剧情、判断子弹是否击中玩家等功能,但我如果想要实现如当鼠标悬浮某个人物上,自动弹出该人物信息,要如何判断呢?这时使用碰撞检测,从摄像机生成一个透明碰撞体朝着人物移动,等碰撞到了人物再弹出该人物信息?会不会太繁琐了。或许你又会想,若我直接生成一个足够长的透明碰撞体呢,是不是在创建的那一刻就可以触发该人物的弹出信息逻辑?没错这样的确可以,而这就是射线!不过是把无限长的透明碰撞体变为了无限长的一条线,仅此而已。

一、射线的创建方法

常用的直线射线类型用类型Ray表示,Ray包含了 起点origin 跟 方向direction的定义,起点和方向都用Vector3类型表示,前者是一个坐标,后者是一个表示方向的向量。

csharp 复制代码
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

csharp 复制代码
public static bool Raycast (Vector3 origin, Vector3 direction, out RaycastHit hitInfo, float maxDistance, int layerMask, QueryTriggerInteraction queryTriggerInteraction);

参数:

  1. origin:射线在世界坐标系中的起点。
  2. direction:射线的方向。
  3. hitInfo:如果返回 true,则 hitInfo 将包含有关最近的碰撞体的命中位置的更多信息。(另请参阅:RaycastHit)。
  4. maxDistance:射线应检查碰撞的最大距离。(可选,不写默认无限长)
  5. layerMask:层遮罩,用于在投射射线时有选择地忽略碰撞体。(可选,不写默认检测所有层)
  6. queryTriggerInteraction:指定该查询是否应该命中触发器。可以通过指定 queryTriggerInteraction 来控制是让触发碰撞体生成命中效果,还是使用全局 Physics.queriesHitTriggers 设置。

返回

bool 当光线与任何碰撞体相交时,返回 true,否则返回 false

Raycast()使用射线Ray

csharp 复制代码
public static bool Raycast (Ray ray, out RaycastHit hitInfo, float maxDistance= Mathf.Infinity, int layerMask= DefaultRaycastLayers, QueryTriggerInteraction queryTriggerInteraction= QueryTriggerInteraction.UseGlobal);

参数:

  1. ray 光线的起点和方向。
  2. hitInfo:如果返回 true,则 hitInfo 将包含有关最近的碰撞体的命中位置的更多信息。(另请参阅:RaycastHit)。
  3. maxDistance 射线应检查碰撞的最大距离。(可选,不写默认无限长)
  4. layerMask 层遮罩,用于在投射射线时有选择地忽略碰撞体。(可选,不写默认检测所有层)
  5. queryTriggerInteraction 指定该查询是否应该命中触发器。

返回

bool 当光线与任何碰撞体相交时,返回 true,否则返回 false

Physics.Raycast 更多详情

2、RaycastAll()

使用射线Ray

csharp 复制代码
public static RaycastHit[] RaycastAll (Ray ray, float maxDistance= Mathf.Infinity, int layerMask= DefaultRaycastLayers, QueryTriggerInteraction queryTriggerInteraction= QueryTriggerInteraction.UseGlobal);

参数:

  1. ray 光线的起点和方向。
  2. maxDistance 从射线起点开始,允许射线命中的最大距离。(可选,不写默认无限长)
  3. layerMask 层遮罩,用于在投射射线时有选择地忽略碰撞体。(可选,不写默认检测所有层)
  4. queryTriggerInteraction 指定该查询是否应该命中触发器。

返回

RaycastHit[] RaycastHit 对象的数组。注意,这些结果的顺序未定义。

描述

向场景中投射射线并返回所有命中对象。注意,这些结果的顺序未定义。

RaycastAll() 不使用射线Ray

csharp 复制代码
public static RaycastHit[] RaycastAll (Vector3 origin, Vector3 direction, float maxDistance= Mathf.Infinity, int layerMask= DefaultRaycastLayers, QueryTriggerInteraction queryTriggerInteraction= QueryTriggerInteraction.UseGlobal);

参数

  1. origin 射线在世界坐标系中的起点。
  2. direction 射线的方向。
  3. maxDistance 从射线起点开始,允许射线命中的最大距离。(可选,不写默认无限长)
  4. layermask 层遮罩,用于在投射射线时有选择地忽略碰撞体。(可选,不写默认检测所有层)
  5. queryTriggerInteraction 指定该查询是否应该命中触发器。

Physics.RaycastAll 更多详情

3、射线的碰撞信息

我们再重点讲讲第三个参数RaycastHit,若没有这个参数,我们的返回值仅仅只是"是否碰到了物体",而无法确定碰撞点在哪,也不知道碰撞体是哪一个。射线检测其实有着非常丰富的碰撞信息,如可以获得其碰撞坐标,信息,以及法线。这些丰富的信息都被保存在RaycastHit结构体中。

综合用法演示如下:

csharp 复制代码
//声明变量,用于保存信息
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游戏获取碰撞体的方式有些不一样,具体对比如下

csharp 复制代码
//声明变量,将碰撞信息直接赋值给变量
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;
}

三、示例

csharp 复制代码
//第一个简单小栗子
    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,来获取该碰撞器的游戏对象的名字。
        }
    }
csharp 复制代码
//第二个简单小栗子
    void Update()
    {
        RaycastHit hitInfo;
        if (Physics.Raycast(transform.position, transform.forward, out hitInfo))
        {
            Debug.Log(hitInfo.collider.gameObject.name);
        }
    }

四、具体使用场景

当使用 RaycastHit 类进行射线投射时,有许多不同的使用场景,包括检测碰撞、拾取物体、瞄准检测、物体高亮等。在以下每个场景示例中,我将向您展示如何使用 RaycastHit,并加入一些粒子效果("lizi",即粒子效果在中文中的写法)来增强可视化效果。

  1. 检测碰撞并发射粒子:
    在这个场景中,我们将使用射线投射来检测是否与一个可交互的对象发生碰撞,然后在碰撞点发射一些粒子效果。
csharp 复制代码
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);
        }
    }
}

在这个示例中,我们在碰撞点发射了一个粒子效果,从而在用户与可交互对象发生碰撞时产生视觉效果。

  1. 物体拾取与交互:
    在这个场景中,我们将使用射线投射来检测是否与一个可拾取的物体发生碰撞,然后可以将物体拾取并交互。
csharp 复制代码
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) 来与拾取的物体进行交互。

  1. 物体高亮效果:
    在这个场景中,我们将使用射线投射来检测是否与一个物体发生碰撞,然后在碰撞对象上显示一个高亮的粒子效果。
csharp 复制代码
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;
            }
        }
    }
}

在这个示例中,我们使用高亮粒子效果来显示玩家当前所指的物体。粒子效果在物体上播放和停止,从而实现高亮效果。

  1. 通过鼠标点击的位置获取与射线碰撞的物体
csharp 复制代码
    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()

csharp 复制代码
public static void DrawLine (Vector3 start, Vector3 end, Color color= Color.white, float duration= 0.0f, bool depthTest= true);

参数:

  1. start 应作为该直线起始点的世界空间中的点。
  2. end 应作为该直线结束点的世界空间中的点。
  3. color 该直线的颜色。
  4. duration 该直线的可见长度应为多长。
  5. depthTest 该直线是否应被靠近此摄像机的对象遮挡?

描述

在指定的起始点与结束点之间绘制一条直线。

当游戏正在运行并且启用辅助图标绘图时,将在 Editor 的游戏视图中绘制直线。当直线在游戏视图中可见时,还会在场景中绘制该直线。让游戏运行并显示直线。切换到场景视图,该直线将可见。

duration 是在第一次显示该直线后该直线可见的时间长短(单位为秒)。如果持续时间为零,则该直线仅显示一帧。

注意 :这仅用于调试播放模式。应改用 Gizmos.Drawline 或 Handles.DrawLine 来绘制 Editor 辅助图标。
Debug.DrawLine 更多详情

2、Debug.DrawRay

csharp 复制代码
public static void DrawRay (Vector3 start, Vector3 dir, Color color= Color.white, float duration= 0.0f, bool depthTest= true);

参数:

  1. start 应作为该射线起始点的世界空间中的点。
  2. dir 该射线的方向和长度。
  3. color 绘制的直线的颜色。
  4. duration 该直线的可见时长(单位为秒)。
  5. depthTest 该直线是否应被靠近此摄像机的其他对象遮挡?

描述

在世界坐标中绘制一条从 start 到 start + dir 的直线。

duration 参数确定在绘制该直线所在的帧之后该直线可见时长。如果持续时间为 0(默认值),则该直线被渲染 1 帧。

如果将 depthTest 设置为 true,则该直线将被此场景中更靠近摄像机的其他对象遮挡。将在该 Editor 的场景视图中绘制该直线。如果在游戏视图中启用了辅助图标绘图,则在该视图中也将绘制该直线。

Debug.DrawRay 更多详情

效果图可以看上面,都有涉及使用到。需要说明的是在默认的情况下,该辅助线仅在编辑器的场景窗口可见,如果要在game窗口看到,则需要单机Game窗口右上角的Gizmos开关。

利用Gizmos

调用OnDrawGizmos()函数或OnDrawGizmosSelected,它们同样都可以用来绘制辅助图标。区别在于

函数OnDrawGizmos()在程序一运行就执行,之后每帧都在执行,

函数OnDrawGizmosSelected()在鼠标点击到脚本挂载的物体的身上的时候运行,不管有多少父类对象,它都会执行。

这里推荐使用OnDrawGizmosSelected()

范例如下

csharp 复制代码
  private void OnDrawGizmosSelected()
    {
      
       Gizmos.DrawLine(Vector3 from, Vector3 to);
       Gizmos.DrawRay((Vector3 from, Vector3 direction);
    }

可在scence窗口查看辅助线,同样想在game窗口可以看到,则需要单机Game窗口右上角的Gizmos开关。

相关推荐
charon87782 小时前
UE ARPG | 虚幻引擎战斗系统
游戏引擎
小春熙子3 小时前
Unity图形学之Shader结构
unity·游戏引擎·技术美术
Sitarrrr6 小时前
【Unity】ScriptableObject的应用和3D物体跟随鼠标移动:鼠标放置物体在场景中
3d·unity
极梦网络无忧6 小时前
Unity中IK动画与布偶死亡动画切换的实现
unity·游戏引擎·lucene
逐·風13 小时前
unity关于自定义渲染、内存管理、性能调优、复杂物理模拟、并行计算以及插件开发
前端·unity·c#
_oP_i15 小时前
Unity Addressables 系统处理 WebGL 打包本地资源的一种高效方式
unity·游戏引擎·webgl
代码盗圣19 小时前
GODOT 4 不用scons编译cpp扩展的方法
游戏引擎·godot
Leoysq1 天前
【UGUI】实现点击注册按钮跳转游戏场景
游戏·unity·游戏引擎·ugui
PandaQue1 天前
《潜行者2切尔诺贝利之心》游戏引擎介绍
游戏引擎
_oP_i1 天前
unity中 骨骼、纹理和材质关系
unity·游戏引擎·材质