Physics.RaycastNonAlloc
是 Unity 中用于 3D 物理射线检测的高性能方法,它是 Physics.Raycast
的非分配版本。
方法签名
public static int RaycastNonAlloc(Ray ray, RaycastHit[] results, float maxDistance = Mathf.Infinity, int layerMask = DefaultRaycastLayers, QueryTriggerInteraction queryTriggerInteraction = QueryTriggerInteraction.UseGlobal)public static int RaycastNonAlloc(Vector3 origin, Vector3 direction, RaycastHit[] results, float maxDistance = Mathf.Infinity, int layerMask = DefaultRaycastLayers, QueryTriggerInteraction queryTriggerInteraction = QueryTriggerInteraction.UseGlobal)
参数说明
- ray/origin+direction: 射线或起点+方向
- results: 预分配的 RaycastHit 数组
- maxDistance: 射线最大距离
- layerMask: 层级掩码
- queryTriggerInteraction: 是否检测触发器
数组大小限制的重要性
问题说明
当可能击中的目标数量超过 results
数组的大小时,超出容量的击中目标将被忽略,这可能导致重要的碰撞检测被遗漏。
演示示例
using UnityEngine;public class RaycastLimitationDemo : MonoBehaviour
{[SerializeField] private int arraySize = 3;[SerializeField] private float rayDistance = 100f;private RaycastHit[] smallArray;private RaycastHit[] largeArray;void Start(){smallArray = new RaycastHit[arraySize]; // 小数组largeArray = new RaycastHit[50]; // 大数组}void Update(){if (Input.GetKeyDown(KeyCode.Space)){CompareRaycastResults();}}void CompareRaycastResults(){Vector3 origin = transform.position;Vector3 direction = transform.forward;// 使用小数组检测int smallHitCount = Physics.RaycastNonAlloc(origin, direction, smallArray, rayDistance);// 使用大数组检测int largeHitCount = Physics.RaycastNonAlloc(origin, direction, largeArray, rayDistance);Debug.Log($"小数组(大小:{arraySize})检测到: {smallHitCount} 个目标");Debug.Log($"大数组(大小:50)检测到: {largeHitCount} 个目标");if (largeHitCount > smallHitCount){Debug.LogWarning($"⚠️ 遗漏了 {largeHitCount - smallHitCount} 个目标!");// 显示被遗漏的目标for (int i = smallHitCount; i < largeHitCount; i++){Debug.LogWarning($"遗漏目标: {largeArray[i].collider.name} (距离: {largeArray[i].distance:F2})");}}}
}
实际问题场景
1. 子弹穿透系统问题
public class BulletPenetration : MonoBehaviour
{[SerializeField] private int maxPenetrations = 3;private RaycastHit[] hits;void Start(){// ❌ 错误:数组太小,可能遗漏目标hits = new RaycastHit[maxPenetrations];}public void FireBullet(Vector3 origin, Vector3 direction, float range){int hitCount = Physics.RaycastNonAlloc(origin, direction, hits, range);Debug.Log($"检测到 {hitCount} 个目标");// 问题:如果路径上有5个目标,但数组只能容纳3个// 最后2个目标不会被检测到,即使它们在射线路径上for (int i = 0; i < hitCount && i < maxPenetrations; i++){ProcessHit(hits[i]);}}void ProcessHit(RaycastHit hit){Debug.Log($"击中: {hit.collider.name}");}
}
2. 改进的解决方案
public class ImprovedBulletPenetration : MonoBehaviour
{[SerializeField] private int maxPenetrations = 3;[SerializeField] private int maxDetectionTargets = 20; // 增大检测容量private RaycastHit[] allHits;void Start(){// ✅ 正确:使用更大的数组确保不遗漏目标allHits = new RaycastHit[maxDetectionTargets];}public void FireBullet(Vector3 origin, Vector3 direction, float range){int totalHits = Physics.RaycastNonAlloc(origin, direction, allHits, range);Debug.Log($"路径上共检测到 {totalHits} 个目标");// 根据距离排序(RaycastNonAlloc 默认已按距离排序)int processedHits = 0;for (int i = 0; i < totalHits && processedHits < maxPenetrations; i++){if (CanPenetrate(allHits[i])){ProcessHit(allHits[i]);processedHits++;}else{// 遇到无法穿透的目标,停止处理ProcessHit(allHits[i]);break;}}// 显示未处理的目标(因为穿透限制)if (totalHits > processedHits){Debug.Log($"因穿透限制,忽略了后续 {totalHits - processedHits} 个目标");}}bool CanPenetrate(RaycastHit hit){// 检查材质或标签决定是否可穿透return hit.collider.CompareTag("Penetrable");}void ProcessHit(RaycastHit hit){Debug.Log($"处理击中: {hit.collider.name} (距离: {hit.distance:F2})");}
}
3. 动态数组大小管理
public class DynamicRaycastSystem : MonoBehaviour
{private RaycastHit[] raycastBuffer;private int currentBufferSize = 10;private const int MAX_BUFFER_SIZE = 100;void Start(){raycastBuffer = new RaycastHit[currentBufferSize];}public RaycastHit[] PerformRaycast(Vector3 origin, Vector3 direction, float distance){int attempts = 0;int hitCount;do{hitCount = Physics.RaycastNonAlloc(origin, direction, raycastBuffer, distance);// 如果数组已满,说明可能还有更多目标if (hitCount == raycastBuffer.Length && currentBufferSize < MAX_BUFFER_SIZE){// 扩大数组currentBufferSize = Mathf.Min(currentBufferSize * 2, MAX_BUFFER_SIZE);raycastBuffer = new RaycastHit[currentBufferSize];Debug.LogWarning($"扩大射线检测缓冲区至 {currentBufferSize}");attempts++;}else{break;}}while (attempts < 3); // 最多尝试3次扩展// 返回实际击中的结果RaycastHit[] results = new RaycastHit[hitCount];System.Array.Copy(raycastBuffer, results, hitCount);return results;}
}
最佳实践建议
1. 合理估算数组大小
public class RaycastBestPractices : MonoBehaviour
{// 根据场景复杂度设置缓冲区大小private RaycastHit[] hits;void Start(){// 分析你的场景:// - 最密集区域可能有多少个碰撞器?// - 射线最长距离内可能遇到多少目标?// - 加上安全余量int estimatedMaxTargets = AnalyzeSceneComplexity();int safetyBuffer = estimatedMaxTargets / 2;int bufferSize = estimatedMaxTargets + safetyBuffer;hits = new RaycastHit[bufferSize];Debug.Log($"射线检测缓冲区大小: {bufferSize}");}int AnalyzeSceneComplexity(){// 简单的场景复杂度分析Collider[] allColliders = FindObjectsOfType<Collider>();// 可以根据场景大小、碰撞器密度等因素计算return Mathf.Max(20, allColliders.Length / 10);}
}
2. 性能监控
public class RaycastPerformanceMonitor : MonoBehaviour
{private RaycastHit[] hits = new RaycastHit[50];private int maxHitsRecorded = 0;void Update(){if (Input.GetMouseButton(0)){PerformMonitoredRaycast();}}void PerformMonitoredRaycast(){Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);int hitCount = Physics.RaycastNonAlloc(ray, hits, 100f);// 记录最大击中数if (hitCount > maxHitsRecorded){maxHitsRecorded = hitCount;Debug.Log($"新的最大击中数记录: {maxHitsRecorded}");}// 检查是否接近数组限制if (hitCount >= hits.Length * 0.8f){Debug.LogWarning($"射线检测接近缓冲区限制!当前: {hitCount}/{hits.Length}");}}void OnGUI(){GUI.Label(new Rect(10, 10, 300, 20), $"最大击中记录: {maxHitsRecorded}");GUI.Label(new Rect(10, 30, 300, 20), $"缓冲区大小: {hits.Length}");}
}
总结
使用 Physics.RaycastNonAlloc
时,数组大小的选择至关重要:
- 过小的数组:会导致遗漏目标,可能影响游戏逻辑
- 过大的数组:浪费内存,但确保完整性
- 最佳实践:根据场景复杂度合理估算,加上安全余量
- 监控机制:在开发阶段监控实际使用情况,调整数组大小
记住:宁可数组稍大一些,也不要因为大小不足而遗漏重要的碰撞检测。