尧图精选

Unity射线检测:Physics.Raycast与LayerMask实战

🕒 发布时间:2026/9/18 19:53:46 📁 来源:尧图网络
1. 先说清楚射线检测到底在解决什么问题刚接触 Unity 的时候很多人对射线的第一印象是鼠标点一下打中一个物体这确实是最常见的入门场景但如果只把射线理解成鼠标拾取就把它想窄了。射线与射线检测本质上是在三维空间里做一次从某点沿某方向看会先撞到谁的几何查询。它不产生力、不推动刚体只回答一个问题这条线路上有什么第一个命中的是谁命中在哪个位置、哪个面、哪个碰撞体上。这个能力看起来简单但它撑起了很多玩法的骨架。射击游戏里的子弹判定、RTS 里的框选与地面寻路点击、第三人称角色的摄像机防穿墙、AI 的视野可见性判断、载具的悬挂贴地检测、放置类游戏的落点吸附、甚至 UI 上的点击穿透判断背后都是同一套射线逻辑。区别只在于射线从哪儿发、往哪儿打、只关心哪些层、命中之后做什么。我见过不少项目的射线代码写得很随意结果是射击偶尔打空、角色卡进墙里、点击隔着一层 UI 还把场景物体选中了。这些问题大多不是 Unity 的锅而是没搞清楚射线的几个关键参数和物理查询的边界条件。比如direction不归一化、maxDistance形同虚设、LayerMask传错位、触发器被莫名其妙地命中这些都是高频翻车点。所以这篇内容我会按为什么这么设计—API 具体怎么用—实操怎么落地—性能怎么收—坑怎么避的顺序讲。适合刚学 Unity 物理系统的新手也适合写了两年但一直靠Physics.Raycast一把梭、没系统梳理过的开发者。你不需要很深的图形学背景只要会用 C#、知道预制体和碰撞体就够了。我会把每个参数为什么这么设讲清楚让你以后不是抄代码而是能自己判断该怎么写。2. 射线检测的整体设计与 API 格局拆解2.1 射线在物理引擎里是怎样被处理的Unity 的 3D 物理底层是 PhysX射线查询在引擎内部分成两步走。第一步是粗筛Broadphase物理世界维护了一套加速结构本质上是按空间划分的包围盒层级射线先和这些包围盒粗略求交把大量明显不相干的碰撞体直接踢掉。第二步是精筛Narrowphase对少数候选碰撞体做射线与具体形状的精确求交算出交点、法线、距离和三角面索引。最后引擎把所有命中按距离排序返回最近的那个这就是Physics.Raycast返回单个RaycastHit的由来。理解这个流程有两个直接收益。第一你会明白为什么给射线加LayerMask能显著提速——它是在粗筛之前就把层过滤掉了候选集直接缩小。第二你会明白为什么从物体内部往外打射线打不到自己——PhysX 默认不检测背面Physics.queriesHitBackfaces默认关闭射线只和碰撞体的正面相交。这个特性有时候是好事避免自遮挡有时候是坑模型法线反了射线就从它身上穿过去排错时要想到这一层。顺便说一个新手常问的问题射线检测要不要碰撞体要。射线命中的依据是碰撞体Collider不是渲染网格MeshRenderer。你给一个模型加了 MeshRenderer 但不加 Collider射线照样从它身上穿过去。反过来一个空物体加了 BoxCollider即使看不见也能被射线打中。这一点在做拾取时特别关键很多人调半天发现打不中就是因为忘了挂碰撞体或者碰撞体被缩放成了很扁的一层。2.2 为什么是射线而不是触发器或距离判断新手最容易犯的思维定式是要判断有没有东西就放个触发器或者算距离。这两种办法在很多场合是够用的但射线有它不可替代的地方。触发器Trigger是持续性的区域检测它的逻辑是进入/停留在某个体积内就触发。如果你要判断玩家是不是站在这个平台范围内用触发器很自然。但如果你要判断的是玩家正前方 5 米内有没有墙触发器就得做成一个 5 米长的锥体或者方盒既占物理资源又不够精确还会因为物体进出体积边界产生抖动。距离判断Vector3.Distance更简单但它是无方向的粗略估算。两个物体球心距离是 3 米不代表它们之间没有遮挡——中间可能隔着一堵墙。而且距离判断面对非球形物体时会误导你一个长条形的墙球心离你很远但边缘可能已经贴到你脸上了。射线的价值就在于它有起点、有方向、有精确的交点。它回答的是沿这条路径我首先会撞到什么这个语义在视线、弹道、落点、贴地这些场景里是天然契合的。你可以把它想成拿一根无限细的探针往某个方向戳戳到东西就告诉你戳到了什么、戳在哪儿。这个探针模型比圈一块区域和量个距离都更接近很多玩法的真实需求。2.3 射线家族的完整成员对照Unity 给射线查询提供了一整套变体很多人只用过Physics.Raycast其实其他几个在特定场景里更合适。我把常用的整理成一张表方便你按需求选。API几何形态典型用途是否有体积Physics.Raycast无限细的线段鼠标拾取、视线、弹道无Physics.Linecast由两个端点定义的线段两点之间是否连通无Physics.SphereCast沿射线扫过的球角色移动、弹道带粗细有Physics.CapsuleCast沿射线扫过的胶囊角色胶囊体贴地有Physics.BoxCast沿射线扫过的盒方形物体的占位检测有Physics.OverlapSphere静态球体内部爆炸范围、附近敌人有Physics.RaycastAll线段的全部命中穿透查询、多重命中无Physics.RaycastNonAlloc同上但无分配高频调用的穿透查询无这里最值得单独说的是SphereCast。新手做角色移动时经常用一条细射线从角色中心往下打来判断是否落地表面上看没问题但角色走到台阶边缘、斜面接缝处时细射线会从缝隙里漏过去导致角色突然悬空。换成SphereCast并给一个和角色半径接近的球半径就等于给探针加上了厚度缝隙被覆盖住贴地判断稳定很多。这个思路和碰撞检测里用胶囊体而不是点是一样的道理——真实物体有体积检测也应该有体积。RaycastNonAlloc则是一个性能优化点。RaycastAll每次调用都会新建一个数组来装所有命中结果在高频调用比如每帧对多个敌人做视线检测时会产生大量垃圾触发 GC 卡顿。RaycastNonAlloc让你自己传一个预先分配好的数组进去引擎往里填不产生新分配。区别就是在参数里多一个RaycastHit[] results但这个改动在移动端上的收益非常明显。3. 核心参数与返回值的逐条拆解3.1 每一个参数都不是随便填的先看最基础的签名public static bool Raycast(Vector3 origin, Vector3 direction, out RaycastHit hitInfo, float maxDistance, int layerMask, QueryTriggerInteraction queryTriggerInteraction);origin是起点这个好理解。真正容易被忽略的是direction它必须是归一化向量长度为 1。很多教程里写(target.position - transform.position)直接扔进去向量长度不是 1这时候maxDistance的语义就变了——引擎内部会把方向乘以maxDistance来确定终点方向长度不为 1 时实际检测距离会被缩放。实测下来方向长度是 3 的时候你写的maxDistance 10实际检测到了 30 米。这个 bug 隐蔽性极强因为大部分情况方向刚好接近 1肉眼看不出问题直到某次目标特别远才暴露。养成习惯direction.normalized。maxDistance是最大检测距离。不传的话默认Mathf.Infinity也就是一直打到底。这在小场景里没问题但在开放世界里一个没有距离限制的射线会穿过整个地图去撞远处的碰撞体白白浪费精筛开销。能限定距离就限定哪怕写个 100 米也比无限强。layerMask是最有讲究的参数之一。它是个 int通过位运算表示只检测哪些层。常见写法// 只检测 Ground 和 Obstacle 两层 int mask LayerMask.GetMask(Ground, Obstacle); // 只检测第 8 层 int mask 1 8; // 检测除 Player 之外的所有层 int mask ~(1 LayerMask.NameToLayer(Player));LayerMask.GetMask内部其实也是按名字查层索引再左移运行时调用有少量开销如果是在Update里频繁用建议把结果缓存在字段里不要每次都算。另外1 8里的 8 是层索引0 到 310 到 7 是 Unity 内置层Default、TransparentFX、Ignore Raycast、Water、UI 等自定义层从 8 开始。写死数字可读性差用LayerMask.NameToLayer(Enemy)更稳妥但同样建议缓存。queryTriggerInteraction控制是否命中触发器。它有三个取值UseGlobal跟随项目设置、Ignore忽略触发器、Collide命中触发器。项目设置里默认是Physics.queriesHitTriggers你可以在 Edit → Project Settings → Physics 里改。这个参数是很多诡异 bug 的源头你明明只想检测实心墙结果打了触发器或者相反你想检测某个触发区域结果射线从它身上穿过去了。显式传QueryTriggerInteraction.Ignore比依赖全局设置靠谱得多因为别人改一次项目设置就能把你的逻辑改崩。3.2 RaycastHit 里藏着多少信息命中之后拿到的RaycastHit是一个结构体字段信息量比新手以为的多字段含义实际用途point命中点的世界坐标生成弹孔、粒子特效normal命中表面的法线弹孔朝向、子弹反弹distance起点到命中点的距离伤害衰减、距离排序collider命中的碰撞体获取对象、分层处理transform碰撞体所属的 Transform拿父物体逻辑rigidbody命中的刚体可能为 null施加冲量textureCoordUV 坐标贴花定位triangleIndex命中三角面索引精细的网格操作barycentricCoordinate重心坐标插值顶点数据实际开发里用得最多的是point、normal和collider。rigidbody要注意只有碰撞体所在的物体或它的父级挂了刚体时才不为 null。如果你在打中一棵纯静态的树hit.rigidbody会是 null直接调AddForce会报空引用。稳妥写法是hit.rigidbody?.AddForce(...)或者先判断。textureCoord有个前提——碰撞体所在的网格必须开启了 Read/Write。没开的话这个值可能是零向量。如果你要做贴花还得保证网格有 UV 信息。这些细节在打包到某些平台后才会暴露尤其是用第三方模型时。3.3 结构与类、值类型与 GC 的细节RaycastHit和Ray都是结构体struct这意味着它们通常分配在栈上不会产生 GC。Physics.Raycast本身是零分配的——这一点很关键因为它允许你在Update里每帧调用而不用太担心性能当然调用次数太多还是会有 CPU 开销。真正会分配的是Physics.RaycastAll返回新数组和Physics.RaycastAll(..., RaycastHit[] results)之外的那些返回数组的方法。做高频查询时用非分配版本把数组当成员字段复用private RaycastHit[] hitsBuffer new RaycastHit[8]; void Update() { int count Physics.RaycastNonAlloc(origin, dir, hitsBuffer, 100f, mask); for (int i 0; i count; i) { // 处理 hitsBuffer[i] } }这里有个玄机RaycastNonAlloc返回的数组不保证按距离排序。如果你需要最近的命中要么自己遍历找最小 distance要么改用Physics.Raycast它保证返回最近的。这个差异网上很多答案都没提吃过亏的人才知道穿透查询时顺序是乱的。注意RaycastNonAlloc的数组大小如果不够超出的命中会被静默丢弃不会报错也不会提示。缓冲区开太小会导致偶尔漏检某些物体排查起来非常痛苦。缓冲区大小要根据场景里可能同时命中的最大数量来定宁可开大一点。3.4 分层与过滤把候选集压到最小分层过滤是射线检测里最实用的优化手段也是最容易配错的地方。思路很简单把场景对象按功能分到不同层让射线只关心它该关心的层。举个例子一个射击玩法里射线该检测什么它应该检测敌人、场景遮挡物但不该检测玩家自己、不该检测地面上的装饰物、不该检测触发器区域。那么建三个层Enemy、Obstacle、Player。射击射线检测Enemy | Obstacle把Player和装饰物层排除掉。这样做的收益不只是性能更重要的是逻辑正确性——射线打在枪口附近自己的手臂碰撞体上是很多 FPS 新手项目里子弹半路消失的经典原因。还有一个常被忽视的层Ignore Raycast索引 2。Unity 内置了这个层专门给那些不想被射线打中的物体用。临时把物体挪到这个层射线就绕开它了。有人用它做临时禁用拾取比如动画播放期间不想让角色被点中就切到Ignore Raycast。层和 Tag 的区别也要说清楚。Tag 是字符串标记用于逻辑识别Layer 是位掩码索引用于物理过滤。射线检测只看 Layer不看 Tag。有人问我设了 Tag 为什么射线还打中它——因为 Tag 跟过滤无关。想让射线忽略它要么换层要么在命中后用 Tag 二次判断再 continue。4. 从鼠标拾取到 AI 视线完整实操落地4.1 屏幕坐标转世界射线鼠标点击拾取是所有射线玩法的基础。核心就一步把鼠标的屏幕坐标转成一条世界空间的射线。void Update() { if (Input.GetMouseButtonDown(0)) { Ray ray Camera.main.ScreenPointToRay(Input.mousePosition); if (Physics.Raycast(ray, out RaycastHit hit, 100f, pickableMask, QueryTriggerInteraction.Ignore)) { Debug.Log($点中了 {hit.collider.name}位置 {hit.point}); // 比如高亮一下 var highlight hit.collider.GetComponentHighlightable(); highlight?.Toggle(); } } }ScreenPointToRay背后的原理是以摄像机为原点把屏幕上的一点投影到近裁剪面上再从原点穿过这个点射出。正交摄像机Orthographic也支持射线方向统一平行于摄像机朝向。要注意的是Camera.main每次访问会做一次查找从 Unity 2020 起有缓存如果每帧调用最好自己缓存一个Camera字段。这里第一个坑就是射线被 UI 挡住。你的鼠标点在了一个全屏 UI 面板上射线照样打到背后的场景物体。解决办法是先判断EventSystem.current.IsPointerOverGameObject()返回 true 就直接 return别往下走场景射线。if (EventSystem.current ! null EventSystem.current.IsPointerOverGameObject()) return;IsPointerOverGameObject在移动端有个小坑它检测的是上一个帧的输入状态触屏刚按下那一帧可能不准稳妥做法是把PointerEventData传进去判断或者用IsPointerOverGameObject(fingerId)指定触点。4.2 地面点击移动与落点吸附RTS 或者点击移动的游戏需要把射线打在地面上拿到落点再让角色走过去。地面层单独设一个Ground层射线只打这个层public LayerMask groundMask; public bool TryGetGroundPoint(Vector3 screenPos, out Vector3 worldPoint) { Ray ray Camera.main.ScreenPointToRay(screenPos); if (Physics.Raycast(ray, out RaycastHit hit, 500f, groundMask, QueryTriggerInteraction.Ignore)) { worldPoint hit.point; return true; } worldPoint Vector3.zero; return false; }拿到hit.point之后一般还要做一步寻路可达性判断。因为射线打中的地方可能是个不可行走的悬崖边或者装饰石头上。这里可以配合NavMesh.SamplePosition把落点吸附到最近的可寻路区域两者结合使用。if (NavMesh.SamplePosition(hit.point, out NavMeshHit navHit, 2f, NavMesh.AllAreas)) { agent.SetDestination(navHit.position); }放置类游戏也是同样的套路只不过吸附更重——把落点对齐到网格中心、检查该格是否已被占用、检查是否在可建造区域内。整个流程都是围绕hit.point和hit.normal展开的。4.3 AI 视线检测与射击判定的写法差异AI 的视野判断经常被写成距离 角度但那只能判断在扇形范围内判断不了中间有没有墙挡着。完整做法是扇形筛选再加一条射线验证bool CanSeeTarget(Transform target) { Vector3 eye transform.position Vector3.up * eyeHeight; Vector3 toTarget target.position Vector3.up * targetHeight * 0.5f - eye; float dist toTarget.magnitude; if (dist viewDistance) return false; float angle Vector3.Angle(transform.forward, toTarget); if (angle viewAngle * 0.5f) return false; // 关键视线射线只检测遮挡层 if (Physics.Raycast(eye, toTarget.normalized, out RaycastHit hit, dist, obstacleMask, QueryTriggerInteraction.Ignore)) { // 打到了东西说明被挡住 return false; } return true; }注意这里的obstacleMask只包含会遮挡视线的东西比如墙、箱子。目标本身、地面、杂草都不应该在里面。如果目标层也被包含进去射线会打在目标自己身上永远返回被挡住AI 就成了瞎子。这个 bug 我见过至少三次每次都是层配错。射击判定和视线检测的思路一样但起点不同。射击应该从**枪口Muzzle**出发而不是摄像机。因为摄像机在角色头部从摄像机打出去的射线和枪口实际朝向有偏差近距离射击时偏差会更明显这就是为什么有些游戏近战射击需要两套判定。从枪口出发的射线打到敌人伤害从hit.collider往上找IDamageable接口if (Physics.Raycast(muzzle.position, muzzle.forward, out RaycastHit hit, range, shootMask)) { var damageable hit.collider.GetComponentInParentIDamageable(); damageable?.TakeDamage(damage, hit.point, hit.normal); SpawnImpactEffect(hit.point, hit.normal); }GetComponentInParent是因为伤害脚本通常挂在模型根节点上而碰撞体可能挂在子节点。4.4 用 Gizmos 把射线画出来射线最大的调试痛点是看不见。你只能靠日志猜它打到了哪儿。Debug.DrawRay和Debug.DrawLine能在 Scene 视图里画出射线但它只在当帧有效所以要么放Update里持续画要么设一个持续时间。void Update() { Vector3 origin transform.position; Vector3 dir transform.forward * range; Debug.DrawRay(origin, dir, Color.red); if (Physics.Raycast(origin, dir, out RaycastHit hit, range)) { Debug.DrawLine(origin, hit.point, Color.green); Debug.DrawRay(hit.point, hit.normal, Color.blue, 0.5f); } }更进阶的做法是自定义OnDrawGizmos把角色的视野扇形、射线起点、命中点都画出来。Gizmos.DrawWireSphere画球体投射的半径Gizmos.DrawLine画射线。这些只在编辑器里生效打包后自动去掉不影响性能。我个人的习惯是给所有涉及射线的脚本都加一段 Gizmos 绘制出问题时直接切 Scene 视图看一眼比翻日志快十倍。提示Debug.DrawRay的第二个参数是方向和长度合一的向量Debug.DrawLine的第二个参数是终点坐标。两者容易混画出来不对时先检查是不是把hit.point传给了DrawRay。5. UI 射线、2D 射线与性能优化5.1 UI 点击为什么走的是另一套射线UGUI 的点击检测和场景射线是两套系统。场景射线走的是Physics.Raycast物理层UI 射线走的是EventSystemGraphicRaycaster渲染层。UI 元素Image、Text 等没有 Collider所以物理射线打不到它们。这也是为什么点到 UI 时场景物体不该被选中需要手动判断。UGUI 内部每帧会把所有raycastTarget true的图形收集起来用鼠标位置做一次矩形/网格检测命中列表通过PointerEventData分发下去。想手动查询 UI 上的东西可以这样public static bool RaycastUI(Vector2 screenPos, out ListRaycastResult results) { var data new PointerEventData(EventSystem.current) { position screenPos }; results new ListRaycastResult(); EventSystem.current.RaycastAll(data, results); return results.Count 0; }这里有个性能常识不需要交互的 UI 图把raycastTarget关掉。比如纯装饰的背景图、分隔线、纯展示的文字勾着raycastTarget只会让每次点击都多算一遍。一个复杂的设置界面里往往有几十个raycastTarget全关掉能省下不少 UI 检测开销。这个问题在低端机上特别明显点击响应会变迟钝。5.2 扩大按钮点击范围的几种做法按钮太小不好点是移动端和车载 HUD 上的高频需求。做法有几种取舍不同。第一种是给按钮加一层透明 Image 做点击区域。把透明图的raycastTarget打开color的 alpha 设成 0尺寸放大。优点是简单直观缺点是会多一个 Draw Call透明图也是要渲染的。第二种是利用RectTransform的 padding 思路写一个自定义的IPointerClickHandler组件挂在按钮父节点上手动做矩形包含判断。命中范围完全可控不增加渲染开销但代码量多一点。第三种是Image.alphaHitTestMinimumThreshold。这个属性可以让点击只在图片不透明区域生效比如异形按钮。设置它需要图片开启 Read/Write Enabled否则会抛异常。很多人第一次用就报错原因就是忘了勾这个。它的作用和上面相反——它是缩小有效点击区而不是扩大别搞混。方案是否增加渲染开销命中形状适用场景透明 Image 扩大是矩形快速实现不在意 DC自定义 Handler 判断否任意代码控制精细控制、异形按钮alphaHitTestMinimumThreshold否图片不透明像素不规则图形按钮5.3 2D 物理与 3D 物理千万别混用Unity 里 2D 和 3D 物理是两套完全独立的系统Physics.Raycast打不到 2D 碰撞体Physics2D.Raycast打不到 3D 碰撞体。2D 的 API 在命名空间和参数上都有差异RaycastHit2D hit Physics2D.Raycast(origin2D, direction2D, distance, layerMask2D);RaycastHit2D的字段和RaycastHit不同没有rigidbody而是rigidbody2D法线是normal另外它有个特点——即使没命中返回的RaycastHit2D也不会是 null而是collider为 null 的对象。所以判断命中要写if (hit.collider ! null)而不是if (hit ! null)。这个和 3D 的Physics.Raycast返回 bool 完全不一样是 2D 项目里最常见的空引用来源。另外 2D 里还有个Physics2D.queriesStartInColliders全局设置默认是 true意味着射线从碰撞体内部出发时会检测到出发的那个碰撞体。做角色移动检测时经常需要把它关掉或者在射线起点上偏移一点。3D 里对应的是Physics.queriesHitBackfaces。5.4 射线多了之后性能该怎么收射线本身不贵但架不住多。假设一个塔防游戏里有 200 个敌人每个敌人每帧都做一次视线检测那就是每帧 200 次Physics.Raycast再加上玩家和其他逻辑很容易把物理线程吃满。优化思路大概有这么几条。降频。不是所有检测都需要每帧做。AI 的视线检测完全可以每 3 帧做一次或者用协程间隔 0.1 秒。人眼对 0.1 秒的 AI 反应延迟基本无感但 CPU 开销直接降到十分之一。分批。把 200 个敌人的射线分散到不同帧执行比如按索引取模第 0 帧处理 0、5、10……第 1 帧处理 1、6、11……这样每帧的峰值被摊平虽然平均开销不变但没有了尖峰帧率更稳。缩小候选集。先用OverlapSphere或者距离判断粗筛把明显太远的敌人排除掉只对可能看得见的少数敌人做射线。射线是精查前面加一层便宜的粗查能省掉大量无效射线。用 Job System RaycastCommand 批处理。Unity 提供了RaycastCommand.ScheduleBatch可以把大量射线扔到 Job 线程里并行执行主线程不用等。适合那种一次要打几百条射线的场景比如弹幕游戏的命中判定、大规模单位的视野检测。它的用法和普通射线不同需要分配NativeArray存结果用完Dispose。这个属于进阶话题但性能收益是数量级的。优化手段实现难度收益适用场景降低检测频率低高AI 视线、非实时判定分帧处理低中削峰大量同质单位粗筛后精查中高远近距离差异大RaycastCommand高极高数百条以上批量射线LayerMask 过滤低中所有场景6. 踩过的坑与排查速查6.1 射线打不中先按这个顺序查射线没反应是最常见的求助。按下面顺序排查基本能覆盖九成情况。第一有没有碰撞体。目标只有 MeshRenderer 没有 Collider射线一定穿过。检查是不是把碰撞体挂在了别的物体上或者碰撞体被缩放成极小。第二层对不对。layerMask是不是把目标层排除了或者1 8里的数字是不是写错了层。临时把layerMask改成~0全层试试如果这时候能打中就是层的问题。第三距离够不够。maxDistance有没有被方向向量的长度缩放或者本身设小了。临时改成Mathf.Infinity验证。第四方向对不对。打印一下direction看看是不是零向量或者方向反了。transform.forward和transform.up用混是新手常事。第五是不是从物体内部发射。PhysX 默认不检测背面如果射线起点在目标碰撞体内部往外打也打不到它。第六是不是触发了queryTriggerInteraction。你想要的物体可能是个触发器被Ignore掉了。6.2 命中列表里的奇怪东西有时候射线确实命中了但命中的是你不认识的对象。往往是这几种情况命中了角色自己的碰撞体层没排除、命中了地面地面层被包含进来了、命中了触发器区域全景层、命中了不可见的辅助碰撞体导航、触发器之类。处理办法是在命中后加一层业务判断if (Physics.Raycast(ray, out RaycastHit hit, dist, mask)) { // 二次确认命中的是不是想要的目标 var target hit.collider.GetComponentInParentSelectableUnit(); if (target null) return; // 是场景装饰忽略 target.Select(); }这种射线粗查 业务精查的两段式在复杂场景里非常必要因为物理层没法表达所有业务规则比如同阵营单位不互相阻挡。物理层负责性能过滤代码负责逻辑过滤各司其职。6.3 常见问题速查表现象可能原因快速修复射线完全打不中目标无 Collider / 层被排除加碰撞体layerMask设~0测试命中距离不对direction未归一化加.normalized命中自己的角色层未排除自身排除 Player 层或起点偏移从物体内部打不出去背面检测关闭起点外移或开queriesHitBackfacesUI 上点击穿透到场景未判断 IsPointerOverGameObject开头加判断穿透检测漏物体NonAlloc 缓冲区太小加大数组或改用 RaycastAll 排查2D 射线报空引用用hit ! null判断改成hit.collider ! null快速移动物体打不中隧道效应用 SphereCast或开连续碰撞检测6.4 我个人的几条实操心得写射线代码写了这么多年有几条经验是我每开一个新项目都会遵守的。永远显式传QueryTriggerInteraction.Ignore除非你明确要检测触发器。依赖全局设置是埋雷因为项目设置是团队共享的别人改一次你就崩一次。给每个涉及射线的脚本加 Gizmos 可视化哪怕只是简单的Debug.DrawRay。射线问题靠读代码很难发现靠眼睛看一秒钟就定位了。射线检测的层名统一管理写一个静态类把常用的LayerMask缓存起来避免到处1 8这种魔法数字。团队协作时这一点尤其重要不然换个项目你会发现层索引全对不上。别把射线当万能钥匙。视线检测、弹道、落点这些用它很合适但判断两个物体是否重叠应该用OverlapSphere判断物体是否在范围里应该用触发器。用错工具的代价往往不是性能而是逻辑上多了一堆特判和补丁。最后一个小技巧调试射线时把Debug.DrawRay的颜色区分开——红色画原始射线绿色画命中后的实际线段蓝色画命中法线。一眼就能看出射线是短了、偏了还是根本没打出去。7. 再往上走射线检测的扩展方向上面讲的是 3D 物理射线的基本盘实际项目里还有几个值得了解的分支。数学射线Ray结构体配合Plane.Raycast可以在没有任何物理碰撞体的情况下做纯几何求交比如把鼠标位置投影到一个虚拟的水平面上做无地形的点击移动。它的优点是零物理开销、零碰撞体依赖缺点是不会被任何障碍物阻挡需要自己处理。射线检测和导航结合做视线可达性判断之后再SamplePosition这套组合在 RTS 和 MOBA 里几乎是标配。射线检测和渲染结合可以用Camera.ScreenPointToRay配合深度纹理做后处理的选中描边效果这是另一条路不依赖物理碰撞体。还有一个绕不开的话题是多平台行为差异。移动端的触屏要用Input.touchCount和GetTouch射线起点用touch.position某些平台对Physics.queriesHitTriggers的默认值可能被插件改过。跨平台项目里把这些全局设置当成代码必须显式覆盖的项比指望平台默认值一致要稳得多。射线这块内容看着简单能讲的东西其实很深从 API 到物理引擎内部再到性能工程每一层都有可以打磨的地方。把基础打牢后面做任何涉及空间查询的玩法都会轻松很多。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →