Unity 射线检测优化:使用 Job System 实现高性能射线批处理
前言
在 Unity 中,射线检测(Raycast)是游戏开发中不可或缺的一环,被广泛用于:
玩家射击命中判断
AI 感知(视野/地形探测)
环境交互
鼠标点击选中
地形贴合
然而,传统的 Physics.Raycast() 是一个同步阻塞的主线程操作,当射线数量变多时(>几十条/帧),会严重影响性能,造成帧率抖动甚至卡顿。
问题:传统射线检测在高并发场景下的瓶颈
for (int i = 0; i < 100; i++) { Physics.Raycast(...); // 每次调用都阻塞主线程 } 结果:
每条射线都在主线程逐条执行
每次
Physics.Raycast都从 C# 跳到 C++(跨语言调用)没有并行,无法利用多核
100~1000 条射线 → 主线程爆炸 → 严重掉帧
解决方案:使用 Unity 的 RaycastCommand + Job System 实现“射线批处理”
Unity 提供了一个专为此类场景设计的结构体:
RaycastCommand 它允许我们将多个射线打包,一次性发给 Unity Job System,并行在多线程中执行。
原理概览
多个请求者(脚本) ↓ 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
使用方式
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 实现高性能射线批处理的更多相关文章
- Unity——射线检测(鼠标点击开关门效果)
Unity射线检测--实现简单的开关门效果 简要:通过鼠标点击来发射一条射线,来获得射线所碰到的物体名称,再通过改变门的Rotation值来实现开关门的效果. 一.代码实现 1.1 简易的场景搭建 注 ...
- Unity射线检测的用法总结
RayCast 射线检测 本文提供全流程,中文翻译. Chinar 坚持将简单的生活方式,带给世人!(拥有更好的阅读体验 -- 高分辨率用户请根据需求调整网页缩放比例) Chinar -- 心分享.心 ...
- unity射线检测
unity中射线检测时非常实用也经常实用的一种手段.下面讲解一下射线检测问题. 1)Ray 根据射线端点和射线的方向定义一条射线 Ray ray= new Ray(transform.position ...
- unity射线碰撞检测+LayerMask的使用
射线在unity中是个很方便的东西,对对象查找.多用于碰撞检测(如:子弹飞行是否击中目标).角色移动等提供了很大的帮助,在此做个总结与大家分享下 ,若有不足欢迎吐槽 好了,话补多说啦,直接进入主题: ...
- Unity3D_(API)射线检测Raycast()
Unity射线检测官方文档: 传送门 一.检测前方是否有游戏物体(射线无限长度) 二.检测前方是否有游戏物体(射线长度为1m) 三.检测前方游戏物体碰撞信息(射线无限长度): 四.指定检测碰撞Tag层 ...
- Unity - Raycast 射线检测
本文简要分析了Unity中射线检测的基本原理及用法,包括: Ray 射线 RaycastHit 光线投射碰撞信息 Raycast 光线投射 SphereCast 球体投射 OverlapSphere ...
- Unity的学习笔记(射线检测)
首先,射线检测的API是这样的,网上找了一下,这个图片看得很清楚: 接下来是自己使用这个进行测试 using System.Collections; using System.Collections. ...
- 日常小节----unity小坑记(射线检测固定层级)
unity中射线检测需设定所需层级时,必须加上距离!!! //一条从主相机到屏幕点击点的射线 Ray ray = Camera.Main.ScreenPointToRay(Input.mousePos ...
- [Unity]射线的简单应用和对UGUI的检测
最近做的小游戏,需要通过触屏来控制移动,主要做法就是在Update中检测Input.TouchCount,但是问题是会盖住UGUI的Button事件,第一时间想到射线检测 常用射线 Unity有射线类 ...
- pico g2 触摸板手柄射线检测---for unity
1.pico g2手柄射线检测UI,需要在canvas添加Graphic Raycaster脚本和Pvr_Ui Canvas脚本. 2.删除掉原有的maincamera,将Pvr_unitySDK下h ...
随机推荐
- 仿EXCEL插件,智表ZCELL产品V1.9 版本发布,增加导入、导出EXCEL功能
详细请移步 智表(ZCELL)官网www.zcell.net 更新说明 这次更新主要应用户要求,主要增加了导入.导出EXCEL文件功能,并增加了获取单元格公式.显示值等功能,欢迎大家体验使用. 本次 ...
- golang random string
package main import ( "math/rand" "strings" "testing" "time" ...
- .NET外挂系列:3. 了解 harmony 中灵活的纯手工注入方式
一:背景 1. 讲故事 上一篇我们讲到了 注解特性,harmony 在内部提供了 20个 HarmonyPatch 重载方法尽可能的让大家满足业务开发,那时候我也说了,特性虽然简单粗暴,但只能解决 9 ...
- iOS快捷指令——记录今天、今年已过进度的工具
起因是看到了 大佬博客 里面一个计时的小工具,于是也想搞一个来提醒自己珍惜时间. 经过一段时间对快捷指令的摸索,最终选择了如下的方式完成: 快捷指令的链接在这里给出: https://www.iclo ...
- WindowsPE文件格式入门05.PE加载器LoadPE
https://bpsend.net/thread-316-1-1.html LoadPE - pe 加载器 壳的前身 如果想访问一个程序运行起来的内存,一种方法就是跨进程读写内存,但是跨进程读写内存 ...
- acwing 智商药
题目链接:5046. 智商药 - AcWing题库 首先考虑dfs 不用想肯定超时 过了10/17个测试点 代码 1 #include<bits/stdc++.h> 2 3 using n ...
- Lombok 类库使用详解
Lombok 是一个 Java 库,通过注解自动生成常用的样板代码(如 getter/setter.构造函数.日志声明等),显著减少代码量,同时提高代码整洁度. 一.配置方法 (1)IDE:需安装 L ...
- 从基础到高级,带你结合案例深入学习curl命令
目录 简介 发送get请求 显示通信过程-v 模仿浏览器 -A 发送 Cookie -b 获取cookie -c 伪造来源页面 -e 构造GET请求查询字符串 -G 添加HTTP请求头 -H 显示头信 ...
- Spring Boot 整合Redisson操作Redis基础篇
<Spring Boot 整合Redisson配置篇> <Spring Boot 整合Redisson操作Redis基础篇> <Redisson批量操作类RBuckets ...
- jupyter的使用 -- 快捷键
jupyter的使用 1.快捷键的使用 插入cell:a,b 删除cell:x 执行cell:shift+enter 切换cell的模式:m,y cell执行后,在cell的左侧双击就可以回到cell ...