unity Physics.RaycastNonAlloc

发布于:2025-07-19 ⋅ 阅读:(16) ⋅ 点赞:(0)

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 时,数组大小的选择至关重要:

  1. 过小的数组:会导致遗漏目标,可能影响游戏逻辑
  2. 过大的数组:浪费内存,但确保完整性
  3. 最佳实践:根据场景复杂度合理估算,加上安全余量
  4. 监控机制:在开发阶段监控实际使用情况,调整数组大小

记住:宁可数组稍大一些,也不要因为大小不足而遗漏重要的碰撞检测


网站公告

今日签到

点亮在社区的每一天
去签到