Unity 射线检测优化:使用 Job System 实现高性能射线批处理

✨ 前言

在 Unity 中,射线检测(Raycast)是游戏开发中不可或缺的一环,被广泛用于:

  • 玩家射击命中判断

  • AI 感知(视野/地形探测)

  • 环境交互

  • 鼠标点击选中

  • 地形贴合

然而,传统的 Physics.Raycast() 是一个同步阻塞的主线程操作,当射线数量变多时(>几十条/帧),会严重影响性能,造成帧率抖动甚至卡顿。


🚫 问题:传统射线检测在高并发场景下的瓶颈

csharp

for (int i = 0; i < 100; i++) { Physics.Raycast(...); // 每次调用都阻塞主线程 }

❗ 结果:

  • 每条射线都在主线程逐条执行

  • 每次 Physics.Raycast 都从 C# 跳到 C++(跨语言调用)

  • 没有并行,无法利用多核

  • 100~1000 条射线 → 主线程爆炸 → 严重掉帧


✅ 解决方案:使用 Unity 的 RaycastCommand + Job System 实现“射线批处理”

Unity 提供了一个专为此类场景设计的结构体:

csharp

RaycastCommand

它允许我们将多个射线打包,一次性发给 Unity Job System,并行在多线程中执行


🧠 原理概览

text

多个请求者(脚本) ↓ RaycastBatchManager(收集请求) ↓ NativeArray<RaycastCommand>(批处理) ↓ Job System ScheduleBatch 并行调度 ↓ NativeArray<RaycastHit>(结果) ↓ 主线程派发回调


⚡ 性能对比(实测结果)

射线数量 Physics.Raycast() 主线程 RaycastCommand 并行 Job
10 约 0.2 ms 约 0.1 ms
100 约 2~5 ms(掉帧) 约 0.3~1 ms ✅
500 10+ ms(明显卡顿) 约 2~3 ms ✅✅

(注:数据视硬件和场景复杂度不同略有浮动)


🧱 我们构建的解决方案:RaycastBatchManager

为了解耦结构、集中管理,我们封装了一个 Raycast 批处理调度器,具备:

  • 每帧收集射线请求

  • 使用 RaycastCommand.ScheduleBatch() 并行处理

  • 结果自动回调给请求者

  • 可配置每帧最大处理量(限流)

  • 可视化调试:Debug.DrawRay

🧩 使用方式

csharp

RaycastBatchManager.Instance.RequestRaycast( transform.position, transform.forward, 100f, LayerMask.GetMask("Enemy"), hit => { if (hit.collider != null) Debug.Log("Hit " + hit.collider.name); });


🛠 技术优势总结

优点 说明
✅ 主线程轻负载 所有检测在 Job 中完成,避免卡顿
✅ 支持成百上千射线并发 极高扩展性,适用于大规模 AI 感知、技能判定等
✅ 回调解耦 业务层无需关心处理流程,只管发出请求 + 收到命中
✅ LayerMask 过滤 支持每条射线独立设置过滤层级
✅ 限流/节流保护 每帧可设置最大射线数,防止爆量请求拖慢整帧
✅ Debug 可视化 每条射线可视化调试,便于开发定位问题


💡 场景适用建议

使用场景 是否推荐使用射线批处理
枪械射击系统(子弹多发) ✅✅✅
AI 感知系统(视野、地形) ✅✅✅
技能判定(扇形射线、范围碰撞) ✅✅
鼠标点击(单条) ❌(单发用 Physics.Raycast 就行)
ECS 项目(全用 Unity.Physics) ❌(使用 CollisionWorld.CastRay


✅ 总结一句话

RaycastCommand + Job System 是 Unity 非 ECS 项目中进行大量射线检测的最优解。

若你仍然在主线程用 Physics.Raycast() 做大量检测,请立即切换,性能收益巨大、架构更合理、使用难度极低

 

【核心代码】

using System; using System.Collections.Generic; using System.Diagnostics; using Unity.Collections; using Unity.Jobs; using UnityEngine; using Debug = UnityEngine.Debug;  /// <summary> /// 射线批处理调度器:收集射线请求,批量执行,主线程回调 /// </summary> public class RaycastBatchManager : MonoBehaviour {     public static RaycastBatchManager Instance { get; private set; }      [Header("性能设置")]     [Tooltip("每帧最多处理多少条射线(限流防卡顿)")]     public int maxRaysPerFrame = 100;      /// <summary>     /// 射线请求结构     /// </summary>     private struct RaycastRequest     {         public Vector3 origin;         public Vector3 direction;         public float distance;         public int layerMask;         public Action<RaycastHit> callback;     }      private readonly List<RaycastRequest> requestQueue = new();      private NativeArray<RaycastCommand> commands;     private NativeArray<RaycastHit> results;      void Awake()     {         if (Instance != null && Instance != this)         {             Destroy(this);             return;         }         Instance = this;     }      /// <summary>     /// 由外部调用:发起一条射线请求(延迟到本帧 LateUpdate 执行)     /// </summary>     public void RequestRaycast(Vector3 origin, Vector3 direction, float distance, int layerMask, Action<RaycastHit> callback)     {         requestQueue.Add(new RaycastRequest         {             origin = origin,             direction = direction,             distance = distance,             layerMask = layerMask,             callback = callback         });     }      void LateUpdate()     {         int totalRequests = requestQueue.Count;         if (totalRequests == 0) return;          int count = Mathf.Min(maxRaysPerFrame, totalRequests);         Debug.Log("当前批处理射线数量" + count);                  //性能监测         Stopwatch stopwatch = Stopwatch.StartNew();                  // 分配 NativeArray(临时 Job 用)         commands = new NativeArray<RaycastCommand>(count, Allocator.TempJob);         results = new NativeArray<RaycastHit>(count, Allocator.TempJob);          for (int i = 0; i < count; i++)         {             var req = requestQueue[i];             commands[i] = new RaycastCommand(req.origin, req.direction, req.distance, req.layerMask);              // 👇 可视化(调试用)             //Debug.DrawRay(req.origin, req.direction * req.distance, Color.green, 0.1f);         }          // 并行调度         JobHandle handle = RaycastCommand.ScheduleBatch(commands, results, 32);         handle.Complete(); // 阻塞直到 Job 完成(确保结果可用)          // 回调结果         for (int i = 0; i < count; i++)         {             requestQueue[i].callback?.Invoke(results[i]);         }          // 分帧处理,保留未处理的请求         requestQueue.RemoveRange(0, count);          // 安全释放 NativeArray         if (commands.IsCreated) commands.Dispose();         if (results.IsCreated) results.Dispose();                  // 性能统计输出         stopwatch.Stop();         UnityEngine.Debug.Log($"[RaycastBatchManager] {count} 射线耗时: {stopwatch.ElapsedMilliseconds} ms");     } }

 

【测试用例】

            RaycastBatchManager.Instance.RequestRaycast(                 transform.position,                 transform.forward,                 100f,                 LayerMask.GetMask("Default"),                 hit =>                 {                     if (hit.collider != null)                         Debug.Log("命中: " + hit.collider.name);                     else                         Debug.Log("未命中");                 });

 

发表评论

评论已关闭。

相关文章