前言

在 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("未命中");
});

Unity 射线检测优化:使用 Job System 实现高性能射线批处理的更多相关文章

  1. Unity——射线检测(鼠标点击开关门效果)

    Unity射线检测--实现简单的开关门效果 简要:通过鼠标点击来发射一条射线,来获得射线所碰到的物体名称,再通过改变门的Rotation值来实现开关门的效果. 一.代码实现 1.1 简易的场景搭建 注 ...

  2. Unity射线检测的用法总结

    RayCast 射线检测 本文提供全流程,中文翻译. Chinar 坚持将简单的生活方式,带给世人!(拥有更好的阅读体验 -- 高分辨率用户请根据需求调整网页缩放比例) Chinar -- 心分享.心 ...

  3. unity射线检测

    unity中射线检测时非常实用也经常实用的一种手段.下面讲解一下射线检测问题. 1)Ray 根据射线端点和射线的方向定义一条射线 Ray ray= new Ray(transform.position ...

  4. unity射线碰撞检测+LayerMask的使用

    射线在unity中是个很方便的东西,对对象查找.多用于碰撞检测(如:子弹飞行是否击中目标).角色移动等提供了很大的帮助,在此做个总结与大家分享下 ,若有不足欢迎吐槽 好了,话补多说啦,直接进入主题: ...

  5. Unity3D_(API)射线检测Raycast()

    Unity射线检测官方文档: 传送门 一.检测前方是否有游戏物体(射线无限长度) 二.检测前方是否有游戏物体(射线长度为1m) 三.检测前方游戏物体碰撞信息(射线无限长度): 四.指定检测碰撞Tag层 ...

  6. Unity - Raycast 射线检测

    本文简要分析了Unity中射线检测的基本原理及用法,包括: Ray 射线 RaycastHit 光线投射碰撞信息 Raycast 光线投射 SphereCast 球体投射 OverlapSphere ...

  7. Unity的学习笔记(射线检测)

    首先,射线检测的API是这样的,网上找了一下,这个图片看得很清楚: 接下来是自己使用这个进行测试 using System.Collections; using System.Collections. ...

  8. 日常小节----unity小坑记(射线检测固定层级)

    unity中射线检测需设定所需层级时,必须加上距离!!! //一条从主相机到屏幕点击点的射线 Ray ray = Camera.Main.ScreenPointToRay(Input.mousePos ...

  9. [Unity]射线的简单应用和对UGUI的检测

    最近做的小游戏,需要通过触屏来控制移动,主要做法就是在Update中检测Input.TouchCount,但是问题是会盖住UGUI的Button事件,第一时间想到射线检测 常用射线 Unity有射线类 ...

  10. pico g2 触摸板手柄射线检测---for unity

    1.pico g2手柄射线检测UI,需要在canvas添加Graphic Raycaster脚本和Pvr_Ui Canvas脚本. 2.删除掉原有的maincamera,将Pvr_unitySDK下h ...

随机推荐

  1. 仿EXCEL插件,智表ZCELL产品V1.9 版本发布,增加导入、导出EXCEL功能

    详细请移步 智表(ZCELL)官网www.zcell.net 更新说明  这次更新主要应用户要求,主要增加了导入.导出EXCEL文件功能,并增加了获取单元格公式.显示值等功能,欢迎大家体验使用. 本次 ...

  2. golang random string

    package main import ( "math/rand" "strings" "testing" "time" ...

  3. .NET外挂系列:3. 了解 harmony 中灵活的纯手工注入方式

    一:背景 1. 讲故事 上一篇我们讲到了 注解特性,harmony 在内部提供了 20个 HarmonyPatch 重载方法尽可能的让大家满足业务开发,那时候我也说了,特性虽然简单粗暴,但只能解决 9 ...

  4. iOS快捷指令——记录今天、今年已过进度的工具

    起因是看到了 大佬博客 里面一个计时的小工具,于是也想搞一个来提醒自己珍惜时间. 经过一段时间对快捷指令的摸索,最终选择了如下的方式完成: 快捷指令的链接在这里给出: https://www.iclo ...

  5. WindowsPE文件格式入门05.PE加载器LoadPE

    https://bpsend.net/thread-316-1-1.html LoadPE - pe 加载器 壳的前身 如果想访问一个程序运行起来的内存,一种方法就是跨进程读写内存,但是跨进程读写内存 ...

  6. acwing 智商药

    题目链接:5046. 智商药 - AcWing题库 首先考虑dfs 不用想肯定超时 过了10/17个测试点 代码 1 #include<bits/stdc++.h> 2 3 using n ...

  7. Lombok 类库使用详解

    Lombok 是一个 Java 库,通过注解自动生成常用的样板代码(如 getter/setter.构造函数.日志声明等),显著减少代码量,同时提高代码整洁度. 一.配置方法 (1)IDE:需安装 L ...

  8. 从基础到高级,带你结合案例深入学习curl命令

    目录 简介 发送get请求 显示通信过程-v 模仿浏览器 -A 发送 Cookie -b 获取cookie -c 伪造来源页面 -e 构造GET请求查询字符串 -G 添加HTTP请求头 -H 显示头信 ...

  9. Spring Boot 整合Redisson操作Redis基础篇

    <Spring Boot 整合Redisson配置篇> <Spring Boot 整合Redisson操作Redis基础篇> <Redisson批量操作类RBuckets ...

  10. jupyter的使用 -- 快捷键

    jupyter的使用 1.快捷键的使用 插入cell:a,b 删除cell:x 执行cell:shift+enter 切换cell的模式:m,y cell执行后,在cell的左侧双击就可以回到cell ...