前言
什么是射线检测?
射线检测(Raycasting)是游戏开发中一种常用的碰撞检测技术,核心原理是发射一条虚拟的"射线"(有起点和方向的无限长直线),检测这条射线是否与场景中的碰撞体(Collider)相交,并获取相交的详细信息。它比传统的碰撞体检测更灵活、高效,尤其适合处理"点到面"“线到体” 的交互场景。
一、怎么创建射线?
Ray ray = new Ray(transform.position, Vector3.down);
transform.position
表示射线的起点(origin
),即当前挂载该脚本的游戏对象在世界空间中的位置。
Vector3.down
表示射线的方向(direction
),等价于 new Vector3(0, -1, 0)
,即沿世界坐标系的 Y 轴负方向(向下)延伸。
最后通过 new Ray(origin,direction)
创建射线对象,这条射线可用于后续的射线检测(如 Physics.Raycast
),判断射线是否与场景中的碰撞体相交,常用于实现鼠标交互、碰撞检测等功能(比如检测射线是否击中某个物体)。
二、基本原理
射线的定义:射线由两部分组成
起点(Origin
):射线开始的位置(通常是某个物体的位置,如摄像机、角色、鼠标在世界空间的对应点)。
方向(Direction
):射线延伸的方向(单位向量,如向前、向下、鼠标指向的方向等)。
检测逻辑:
射线沿方向无限延伸(或按设定的最大距离延伸),当它与场景中带有 Collider 组件 的物体相交时,会返回 “击中” 状态,并记录相交的详细信息(如击中的点、法线、所在物体等)。
注意:射线只能与带有 Collider 的物体发生检测(触发器 Trigger 需特殊设置是否检测)。
其在unity文档中的定义为:
向场景中的所有碰撞体投射一条射线,该射线起点为 origin,朝向 direction,长度为 maxDistance。
可以选择写入一个 LayerMask,以过滤掉不想生成与其碰撞的碰撞体。
也可以通过指定 queryTriggerInteraction
来控制是让触发碰撞体生成命中效果,还是使用全局
Physics.queriesHitTriggers
设置。
三、射线检测的典型应用场景
1. 玩家交互:连接 2D 输入与 3D 世界
玩家通过鼠标、触屏等 2D 输入设备与 3D 场景交互时,射线检测是"桥梁",负责将屏幕坐标映射到 3D 空间中的物体。
(1)鼠标拾取 / 选中物体
场景:RPG 游戏中点击 NPC 对话、策略游戏中选中单位、解谜游戏中点击道具。
实现思路:从摄像机向鼠标位置发射射线,检测射线击中的物体,触发交互逻辑。
示例代码:
void Update() {
// 鼠标左键点击时检测
if (Input.GetMouseButtonDown(0))
{
// 从主摄像机发射射线到鼠标位置(屏幕坐标转世界射线)
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
// 检测射线(最大距离100米,忽略UI层)
if (Physics.Raycast(ray, out RaycastHit hit, 100f, ~LayerMask.GetMask("UI")))
{
// 若击中NPC,触发对话
if (hit.transform.CompareTag("NPC"))
{
hit.transform.GetComponent<NPCDialogue>().StartDialogue();
}
// 若击中道具,拾取物品
else if (hit.transform.CompareTag("Item"))
{
Destroy(hit.transform.gameObject);
Inventory.Instance.AddItem(hit.transform.name);
}
}
}
}
(2)鼠标悬浮提示
场景:MOBA
游戏中鼠标悬停在英雄上显示属性、策略游戏中悬停在建筑上显示信息。
实现思路:每帧发射射线检测鼠标位置,若持续击中某物体,显示其信息面板。
示例代码:
private GameObject currentHoverObject; // 当前悬浮的物体
void Update()
{
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
if (Physics.Raycast(ray, out RaycastHit hit, 200f))
{
// 若悬浮在新物体上,更新当前物体并显示信息
if (hit.transform != currentHoverObject)
{
// 隐藏上一个物体的信息
if (currentHoverObject != null)
{
currentHoverObject.GetComponent<HoverInfo>().HidePanel();
}
// 显示当前物体的信息
currentHoverObject = hit.transform.gameObject;
currentHoverObject.GetComponent<HoverInfo>().ShowPanel();
}
}
else
{
// 未击中物体,隐藏信息
if (currentHoverObject != null)
{
currentHoverObject.GetComponent<HoverInfo>().HidePanel();
currentHoverObject = null;
}
}
}
2. 游戏机制:实现核心玩法逻辑
射线检测可用于判断 “是否命中”“是否可达”"是否可见"等核心规则。
这是很多玩法的基础,地面检测与角色移动是一个很经典的问题。
场景:第三人称游戏中角色贴地移动(避免悬浮)、跳跃后判断是否落地、车辆行驶时检测地面高度。
实现思路:从角色脚下发射向下的射线,检测与地面的距离,动态调整角色位置或状态。
示例图:
示例代码:
void isOnGroundCheck()
{
isOnGround = Physics2D.Raycast(groundCheck.position, Vector2.down,groundCheckDistance, groundLayer);
}
private void OnDrawGizmos()
{
//程序编写中可视化
Gizmos.DrawLine(transform.position, new Vector3(transform.position.x, transform.position.y - groundCheckDistance));
//可以理解为根据提供的两个点,连成一条直线
}
3. AI 行为:实现智能决策
射线检测是 AI “感知环境” 的核心手段,帮助 AI判断视野、目标可见性、攻击范围等。
视线检测(AI 视野)与攻击范围检测
场景:潜行游戏中敌人是否看到玩家、AI 守卫是否发现入侵者,AI近战攻击时判断是否在攻击距离内、远程 AI 判断是否可射击目标。
实现思路:从 AI 眼睛向目标(玩家)发射射线,若射线未被障碍物阻挡,则判定
“看到目标”,向目标发射射线,若距离小于攻击范围且无遮挡,则触发攻击。
示例图:
扇形区域为怪物视野
被障碍物遮住,怪物和玩家之间不会产生射线
无障碍物遮挡,且在怪物视线之内,发出射线
怪物视野(可操控视野角度范围)示例代码:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class enemy : MonoBehaviour
{
[Header("视野设置")]
[Tooltip("AI的视野距离")]
public float viewDistance = 10f;
[Tooltip("AI的视野角度(度)")]
[Range(0, 180)]
public float viewAngle = 60f;
[Tooltip("玩家图层")]
public LayerMask playerLayer;
[Tooltip("障碍物图层")]
public LayerMask obstacleLayer;
[Header("引用")]
[Tooltip("AI的眼睛位置(射线起点)")]
public Transform eyePosition;
[Tooltip("玩家Transform组件")]
public Transform player;
[Header("调试")]
[Tooltip("是否显示视野Gizmos")]
public bool showGizmos = true;
// 玩家是否在视野内
public bool IsPlayerInSight { get; private set; }
private void Update()
{
CheckForPlayer();
}
/// 检测玩家是否在视野范围内且可见
private void CheckForPlayer()
{
if (player == null)
{
IsPlayerInSight = false;
return;
}
// 计算AI到玩家的方向和距离
Vector2 directionToPlayer = (player.position - eyePosition.position).normalized;
float distanceToPlayer = Vector2.Distance(eyePosition.position, player.position);
Debug.Log(distanceToPlayer);
// 1. 检查距离:玩家是否在视野范围内
if (distanceToPlayer > viewDistance)
{
IsPlayerInSight = false;
return;
}
Debug.Log(IsPlayerInSight);
// 2. 检查角度:玩家是否在视野角度内
float angleToPlayer = Vector2.Angle(transform.right, directionToPlayer);
if (angleToPlayer > viewAngle / 2)
{
IsPlayerInSight = false;
return;
}
// 3. 检查遮挡:射线是否能击中玩家(无障碍物)
if (Physics2D.Raycast(eyePosition.position, directionToPlayer, distanceToPlayer, obstacleLayer))
{
// 射线被障碍物阻挡
IsPlayerInSight = false;
return;
}
// 4. 检查是否真的击中了玩家
RaycastHit2D hit = Physics2D.Raycast(eyePosition.position, directionToPlayer, distanceToPlayer, playerLayer);
Debug.Log(hit.collider);
if (hit.collider != null)
{
IsPlayerInSight = true;
OnPlayerDetected();
}
else
{
IsPlayerInSight = false;
}
}
private void OnPlayerDetected()
{
// 可以在这里添加检测到玩家后的逻辑
// 例如:切换AI状态、播放警报音效、追击玩家等
Debug.Log("发现玩家!");
}
/// 绘制视野范围的Gizmos
private void OnDrawGizmos()
{
if (!showGizmos || eyePosition == null) return;
// 绘制视野距离
Gizmos.color = Color.yellow;
Gizmos.DrawWireSphere(eyePosition.position, viewDistance);
// 绘制视野角度边界
Vector2 leftBoundary = Quaternion.Euler(0, 0, viewAngle / 2) * transform.right * viewDistance;
Vector2 rightBoundary = Quaternion.Euler(0, 0, -viewAngle / 2) * transform.right * viewDistance;
Gizmos.DrawLine(eyePosition.position, eyePosition.position + (Vector3)leftBoundary);
Gizmos.DrawLine(eyePosition.position, eyePosition.position + (Vector3)rightBoundary);
// 如果发现玩家,绘制指向玩家的射线
if (IsPlayerInSight && player != null)
{
Gizmos.color = Color.red;
Gizmos.DrawLine(eyePosition.position, player.position);
}
}
}
ine(eyePosition.position, eyePosition.position + (Vector3)rightBoundary);
// 如果发现玩家,绘制指向玩家的射线
if (IsPlayerInSight && player != null)
{
Gizmos.color = Color.red;
Gizmos.DrawLine(eyePosition.position, player.position);
}
}
}