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
时,数组大小的选择至关重要:
- 过小的数组:会导致遗漏目标,可能影响游戏逻辑
- 过大的数组:浪费内存,但确保完整性
- 最佳实践:根据场景复杂度合理估算,加上安全余量
- 监控机制:在开发阶段监控实际使用情况,调整数组大小
记住:宁可数组稍大一些,也不要因为大小不足而遗漏重要的碰撞检测。