Unity–射线检测–RayCast
1.射线检测的含义
射线检测,根据名称而言,使用一条射线来检测是击中了某个物体/多个物体
射线检测的包含两个部分: 射线和检测
2.射线检测可以用在哪些地方
- 射击游戏:
- 玩家的瞄准和射击:检测玩家视线是否与敌人或其他目标相交。
- 子弹轨迹和效果:模拟子弹的飞行路径和击中效果。
- 交互和UI:
- 鼠标点击检测:检测玩家的鼠标点击是否与游戏对象或UI元素相交。
- 触摸屏交互:在移动设备上检测玩家的触摸是否与特定的游戏元素相交。
- 角色控制器和AI:
- 视野检测:NPC或敌人在一定范围内检测玩家或其他角色。
- 碰撞避免:AI角色在移动时使用射线检测来避免碰撞。
- 虚拟现实(VR)和增强现实(AR):
- 眼睛或手部追踪:在VR中检测玩家的视线或手部位置。
- 对象交互:在AR中检测玩家是否与虚拟对象相交。
3.Unity中的射线Ray
在日常生活中的场景射线是很多地方都可以见到的, 比如手电筒,ppt激光翻页笔,庆余年中的镭射眼
射线由一个起点,一个方向和一个距离构成, 即: origin
, direction
和 distance
在物理上射线的距离是无限远的, 因此物理上的射线只有一个起点和一个方向. 在游戏中,射线的最大距离也是被系统限制的, 一般我们是自定义距离,例如1000.0米. 以下是关于射线Ray的说明.
在Unity中,射线Ray是一个结构体,结构体积的基本成员包括origin
, direction
和GetPoint
. 也就是起点,方向和沿射线一定距离的点的位置. 以下是Unity中有关Ray的代码
using System;namespace UnityEngine
{public struct Ray : IFormattable{ public Ray(Vector3 origin, Vector3 direction);public Vector3 origin { get; set; } // 起点(向量)public Vector3 direction { get; set; }// 方向(向量)public Vector3 GetPoint(float distance);// 沿着射线一定距离的点(向量)public override string ToString();public string ToString(string format);public string ToString(string format, IFormatProvider formatProvider);}
}
3.1 构建射线
根据上面的代码,可以看出,可以直接Ray的构造函数来构建一条射线,例如:
Ray ray = new Ray(Vector3.zero, Vector3.forward); // 射线的起点 + 射线的方向
根据上面的代码,我们可以看出,射线的起点是Unity中世界坐标的原点(0,0,0), 射线的方向是世界坐标的向前方向.
3.2 如何显示射线
现实中我们的上激光笔,手电筒是可以看见的,而Unity中的射线是看不见,因此,如果要将射线显示出来,我们可以使用的方法有
- 使用
Debug.DrawRay()
进行显示射线 - 使用
LineRenderer
组件将射线绘制出来
4.Unity中的射线检测
只是将射线构建出来或者显示出来并没有什么意义, 这就相当于手上拿着一个工具,不用工具来干活一个道理.
如何使用射线来进行物体检测.
仔细思考一下: 日常中我们的激光笔或者手电筒发出激光后可以在墙上显示出来光点或者照亮某个地方.这说明墙面是可以被交互的的,换句话说就是射线碰撞到了物体/检测到了物体. 在Unity中使用以下的API来判断是否检测到了物体.
4.1Raycat 函数
// 基础版API
public static bool Raycast(Ray ray);public static bool Raycast(Vector3 origin, Vector3 direction, float maxDistance, int layerMask);
public static bool Raycast(Ray ray, out RaycastHit hitInfo);
public static bool Raycast(Ray ray, out RaycastHit hitInfo, float maxDistance);// 常用版API
public static bool Raycast(Ray ray, out RaycastHit hitInfo, float maxDistance, int layerMask);public static bool Raycast(Ray ray, float maxDistance);
要使用射线检测, 需要使用Unity中的Physics
库, 里面包含都是和物理相关的静态函数
看到上面的这么的重载函数,我们往往不知道使用哪一个. 其实, 只需要基础基本的即可, 参数多的其实就是根据需求来使用不同的重载参数.
首先看基础版本的API public static bool Raycast(Ray ray);
根据返回值我们可以获取了解到的是射线是否击中了一个物体,击中了就返回true,没有击中就返回false.
4.2HitInfo 结构体
在另外一个API中射线检测public static bool Raycast(Ray ray, out RaycastHit hitInfo);
中多了一个参数hitInfo
这个hitInfo参数就是射线击中的物体的信息结构体. 该结构体比较大,指的包含的信息比较多,这和Unreal Engine中的射线检测击中物体的结果(FHitResult
)是类似的. 从宏观上来讲或者从现实上来讲, 射线击中的物体, 我们可以获取物体本身的信息和击中点的信息
- 物体的信息, 通过transfor可以获得所有信息
- 击中点的信息
获取物体的信息很好理解: 比如物体的名称,物体的transfrom组件,该物体的Tag…
获取击中点的信息即射线击中物体的一个点(命中点)的信息:比如该点的坐标,法线(normal),法平面,该点的顶点颜色…
以下是RaycastHit
结构体的信息, 其中常用的已添加注释. 没有添加的注释的也有很多是常用的,比如lightmapCoord
是光照贴图坐标, 用于渲染的.
namespace UnityEngine
{public struct RaycastHit{public Collider collider { get; } // 碰撞器public int colliderInstanceID { get; }public Vector3 point { get; set; } // 击中的点(命中点)public Vector3 normal { get; set; } // 命中点的法线public Vector3 barycentricCoordinate { get; set; } // 重心坐标public float distance { get; set; } // 命中点距离射线起点的距离public int triangleIndex { get; }public Vector2 textureCoord { get; }public Vector2 textureCoord2 { get; }public Transform transform { get; } // Transform组件public Rigidbody rigidbody { get; } // 刚体组件public ArticulationBody articulationBody { get; }public Vector2 lightmapCoord { get; }public Vector2 textureCoord1 { get; }}
}
也就是说HitInfo
保存了我们击中物体的信息, 我们可以通过该信息来做更多的事情
4.3 layerMask
在Unity中,layerMask
是一个用于控制物理碰撞、光线投射、射线检测等操作的对象层选择机制。通过设置 layerMask
,你可以指定哪些层应该被包括或排除在这些操作中。
继续回到常用的射线检测API public static bool Raycast(Ray ray, out RaycastHit hitInfo, float maxDistance, int layerMask);
中.
在上面的API中,maxDistance这个不用多说, 就是射线的距离,系统也有一个设定最大值为Mathf.Infinity
. 该APi中还有一个参数 layerMask, 也就是层级遮罩.
在Unity中区分游戏对象, 我们可以通过添加标签的方式将游戏对象区分开来, 即添加Tag. 但是Tag比较麻烦, 需要我们手动输入标签名,手敲还容易敲错. 同时, 由于是字符串, 在Unity底层计算的时候速度要慢一点. 在在Unity中即有层的概念.
在Unity中的layerMask中包含32层, 其中部分层级是系统已经使用了的,比如Player
层, UI
层. 还有很多层我们没有被系统使用, 我们可以添加层, 然后给游戏对象添加童工层的方式来分类.
如何添加层? 需要在任何一个物体的Insepector面板上点击Layer,然后点击AddLayer即可, 然后将需要需要修改层级的物体,手动指定层即可.
手动添加了Layer后要如何使用.
在public static bool Raycast(Ray ray, out RaycastHit hitInfo, float maxDistance, int layerMask);
中的layereMask
我们了解到它使一个int
类型的整数, 但是, 我们不能直接填写数字,填写规则使用移位操作,
如何填写layerMask:
-
获取Layer Mask值:
- 每个层都有一个对应的整数值,从0开始。例如,默认层(Default)的值为0,UI层通常为5。
- 要为特定层创建layerMask,可以使用
1 << LayerMask.NameToLayer("LayerName")
。这将返回一个整数值,表示该层的layerMask。
-
组合多个层:
- 如果你想要组合多个层,可以使用位或操作符
|
。例如,layerMask = LayerMask.GetMask("Layer1", "Layer2")
会创建一个layerMask,包括 “Layer1” 和 “Layer2”。
- 如果你想要组合多个层,可以使用位或操作符
-
排除层:
- 要排除一个层,可以先创建一个包含所有层的mask,然后使用位异或操作符
^
来排除特定层。例如,layerMask = ~LayerMask.GetMask("ExcludeLayer")
。
- 要排除一个层,可以先创建一个包含所有层的mask,然后使用位异或操作符
-
检查层:
- 要检查一个对象是否在指定的layerMask中,可以使用
layerMask.value & (1 << gameObject.layer)
。如果结果不为0,则表示对象在layerMask中。
// 示例代码 // 创建一个包含Layer1和Layer2的layerMask int layerMask = LayerMask.GetMask("Layer1", "Layer2");// 排除Layer3 layerMask = ~LayerMask.GetMask("Layer3");// 使用layerMask进行射线检测 RaycastHit hit; if (Physics.Raycast(ray, out hit, maxDistance, layerMask)) {// 处理射线击中的对象 }
- 要检查一个对象是否在指定的layerMask中,可以使用
5.射线检测代码
以下是使用不含有hitInfo和含有hitInfo参数的代码
using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class RayCast : MonoBehaviour
{/// 构建一条射线 : 射线产生的起始点 + 射线的方向.private Ray ray1 = new Ray(Vector3.zero, Vector3.forward);// 射线距离private float rayDistance = 100.0f;// 击中的判定结果bool hitResult = false;// 射线击中的物体private RaycastHit hitInfo;void Start(){// 不含有hitInfo的函数bool result1 = Physics.Raycast(Vector3.zero + new Vector3(0,0,10), Vector3.forward, 1000.0f, 1 << LayerMask.NameToLayer("Default"), QueryTriggerInteraction.UseGlobal);if (result1){Debug.Log("射线击中物体");}// 含有hitInfo的函数hitResult = Physics.Raycast(ray1, out hitInfo, rayDistance, 1 << LayerMask.NameToLayer("Default"), QueryTriggerInteraction.UseGlobal);if (hitResult == true){print(hitInfo.collider.name);print(hitInfo.transform);print(hitInfo.point);}}
}
6.射线警报器
使用射线检测实现一个旋转的激光笔, 遇到物体射线长度就减少, 需要绘制出射线
需要用到的技能: 射线检测 + 旋转 + LineRender
以下是代码
using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class RotateRay : MonoBehaviour
{// LinerRender组件private LineRenderer lineRenderer = null;// 构建一条射线 : 射线产生的起始点 + 射线的方向.private Ray ray = new Ray(Vector3.zero, Vector3.forward);// 射线距离private float rayDistance = 1000.0f;// 击中的判定结果bool hitResult = false;// 射线击中的物体private RaycastHit hitInfo;void Start(){// 添加线条绘制组件lineRenderer = this.gameObject.AddComponent<LineRenderer>();InitLineRenderer(lineRenderer);// 设置射线的起点和方向ray.origin = this.transform.position;ray.direction = this.transform.forward;}// Update is called once per framevoid Update(){// 重新设置射线的位置ray.origin = this.transform.position;ray.direction = this.transform.forward;// 旋转游戏对象 -- 每秒旋转60°Quaternion quaternion = Quaternion.AngleAxis(60f * Time.deltaTime, this.transform.up);this.transform.rotation *= quaternion; // 判断击中的物体hitResult = Physics.Raycast(ray, out hitInfo, rayDistance, 1 << LayerMask.NameToLayer("Default"), QueryTriggerInteraction.UseGlobal);if (hitResult == true){print(hitInfo.collider.name);}// 显示并更新射线UpdateLineRendererByRay(lineRenderer, ray, hitResult,hitInfo, rayDistance);}/// <summary>/// 初始化线条渲染组件/// </summary>void InitLineRenderer(LineRenderer lineRenderer){// lineRenderer = this.gameObject.AddComponent<LineRenderer>();lineRenderer.positionCount = 2;lineRenderer.startWidth = 0.2f;lineRenderer.endWidth = 0.2f;lineRenderer.startColor = Color.red;lineRenderer.endColor = Color.green;}/// <summary>/// 击中物体的时候修改射线的长度/// </summary>/// <param name="lineRenderer">lineRenderer组件</param>/// <param name="ray">射线</param>/// <param name="hitResult">是否命中物体</param>/// <param name="hitInfo">命中物体信息</param>/// <param name="rayDistance">射线距离</param>void UpdateLineRendererByRay(LineRenderer lineRenderer,Ray ray, bool hitResult, RaycastHit hitInfo, float rayDistance){if (lineRenderer == null || lineRenderer.positionCount < 2){Debug.Log("LineRender组件不可以使用");return;}// 修改起点位置lineRenderer.SetPosition(0, ray.origin);// 修改终点位置if (hitResult == true){lineRenderer.SetPosition(1, hitInfo.point);}else {lineRenderer.SetPosition(1, ray.GetPoint(rayDistance));}}
}
优化后的代码
using UnityEngine;public class RotateRay : MonoBehaviour
{private LineRenderer lineRenderer;private Ray ray;private float rayDistance = 1000.0f;private RaycastHit hitInfo;void Start(){lineRenderer = this.gameObject.AddComponent<LineRenderer>();InitLineRenderer(lineRenderer);ray = new Ray(Vector3.zero, Vector3.forward);}void Update(){UpdateRayPosition();RotateObject();PerformRaycast();UpdateLineRenderer();}void InitLineRenderer(LineRenderer lineRenderer){lineRenderer.positionCount = 2;lineRenderer.startWidth = 0.2f;lineRenderer.endWidth = 0.2f;lineRenderer.startColor = Color.red;lineRenderer.endColor = Color.green;}void UpdateRayPosition(){ray.origin = this.transform.position;ray.direction = this.transform.forward;}void RotateObject(){Quaternion rotation = Quaternion.AngleAxis(60f * Time.deltaTime, this.transform.up);this.transform.rotation *= rotation;}void PerformRaycast(){int layerMask = 1 << LayerMask.NameToLayer("Default");hitInfo = new RaycastHit(); // 初始化hitInfo,避免未击中时的错误Physics.Raycast(ray, out hitInfo, rayDistance, layerMask, QueryTriggerInteraction.UseGlobal);}void UpdateLineRenderer(){if (lineRenderer == null || lineRenderer.positionCount < 2){Debug.LogError("LineRenderer component is not available or not properly initialized.");return;}lineRenderer.SetPosition(0, ray.origin);lineRenderer.SetPosition(1, hitInfo.collider != null ? hitInfo.point : ray.GetPoint(rayDistance));}
}
效果图如下
7.鼠标点击生成一个特效
using UnityEngine;public class CameraRay: MonoBehaviour
{// 射线距离private float rayDistance = 1000.0f;// 特效Prefab--外部可以自定义特效public GameObject effectPrefab;void Update(){if (Input.GetMouseButtonDown(0)){// 从摄像机发出一条射线Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);RaycastHit hitInfo;// 如果射线击中了指定层级的物体if (Physics.Raycast(ray, out hitInfo, rayDistance, LayerMask.GetMask("Wall"))){// 生成特效 --格局法线来计算特效位置GameObject effectObject = Instantiate(effectPrefab, hitInfo.point, Quaternion.LookRotation(hitInfo.normal));// 销毁特效,参数为延迟时间Destroy(effectObject, EffectPrefab.GetComponent<ParticleSystem>().main.duration);}}}
}