ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

DOTS Physics Raycast实战:ECS架构下的批量射线检测与性能优化

DOTS Physics Raycast实战:ECS架构下的批量射线检测与性能优化 DOTS 里还能不能写 Raycast能但不是你熟悉的Physics.Raycast。如果你正在跟码猴的 DOTS 完整版系列学习到第 020 节主题就进入了 DOTS Physics 中的射线检测。这一节的核心不是“怎么发射一条射线”而是“在 ECS 架构下射线检测应该怎么写、怎么跑、怎么批量并行”。这篇文章把这一节内容整理成一份可以直接落地的技术笔记从工程搭建、包安装、最小 System 编写、单条射线测试、批量射线 Job到性能观察和常见问题排查全部走一遍。适合正在学 DOTS/ECS、想在战斗系统里做命中判定、或者在群体 AI 里做大量索敌和避障判断的 Unity 开发者。先给结论DOTS Physics Raycast 和传统Physics.Raycast最大的区别不是 API 换了个名字而是它把“射线查询”从主线程的物理系统里拆了出来放进 Job 和 Burst 里并行执行。数量越大收益越明显。100 条射线可能看不出差距1000 条、10000 条主线程帧时间的区别就出来了。门槛也不是显卡而是你对 ECS 基本概念的熟悉程度Entity、Component、System、Query、SubScene、Job这些绕不过去。下面按顺序展开。1. 核心能力速览能力项说明项目类型Unity DOTS/ECS 物理系统下的射线检测技术说明所属技术栈DOTSEntities、Job System、Burst、Unity Physics主要功能在 ECS 世界中执行单条和多条射线检测获取命中实体、命中点、法线、距离引擎要求建议使用 Unity 2022 LTS 或更高版本以官方 DOTS 支持矩阵为准启动方式Unity 编辑器运行通过 Package Manager 安装相关包API 能力RaycastInput、CollisionWorld.CastRay、CollisionFilter等物理查询接口批量任务支持IJobParallelFor配合Burst可并行执行大量射线查询硬件要求普通开发 PC 即可建议 16GB 内存加 SSD不需要独立显卡学习成本需要先掌握 ECS 基础概念比传统 MonoBehaviour 方式多一层抽象适合场景群体 AI、索敌、寻路、地形探测、战斗命中、传感器模拟这张表里最值得关注的是“批量任务”和“API 能力”两行。DOTS Physics Raycast 的收益不是来自单条射线变快而是来自“大量射线可以被拆到多个线程并行”这件事。射线检测本身是只读查询多个 Job 同时读取同一个 CollisionWorld 是没有数据竞争问题的这正好是 DOTS 的擅长区域。2. 适用场景与使用边界2.1 适合什么场景需要“大量实体同时做空间查询”的项目是 DOTS Physics Raycast 最典型的应用场景。举几个具体例子RTS/TDS 游戏中一帧要有几十上百个单位判断弹道是否命中目标。群体 AI 里每个单位都要发射若干方向的射线来做玩家视野检测、障碍物拐角判断。无人机编队或自动寻路系统需要频繁检测前方是否有障碍物并决定避障方向。物理传感器模拟比如机器人车体下方检测地面高度、墙距。这些场景的共同点是射线数量多、查询逻辑重复、每条射线之间没有依赖关系。DOTS 的做法是把它们的检测逻辑统一放进一个IJobParallelFor或IJobEntity里配合 Burst 编译在主线程之外并行执行。你只需要提供一组RaycastInputJob 会按批次调度到工作线程上。2.2 不适合什么场景如果你的项目只是偶尔在 MonoBehaviour 里发射一两发射线比如点击物体拾取那 DOTS Physics Raycast 只会给你增加维护成本。项目的实体必须已经在 ECS 环境里跑SubScene 或者运行时创建好 Entity你才拿得到 CollisionWorld。对于一个小规模、以 GameObject 为主的普通 RPG 项目传统Physics.Raycast在可读性和上手速度上都更合理。另外Unity Physics 和 PhysX 是两套物理实现。如果你已经依赖 PhysX 的关节、弹簧、自定义物理材质这些高级物理行为切到 Unity Physics 后要重新验证一遍效果。不要因为一个射线检测的功能点就把整个物理层强行迁移到 DOTS。2.3 使用边界与合规提醒Unity Physics 包是跟随 DOTS 迭代的不同版本之间 API 有过多次调整。比如物理世界访问方式在 0.x 时代和 1.0 时代就完全不同。升级包版本前先看官方文档和 Upgrade Guide做好工程备份。涉及到参考官方示例或第三方 DOTS 插件时要注意许可证边界。来自商店、论坛、视频教程里的物理示例代码可以学习但不要直接把版权不明确的资源打包装进自己的上架项目。这部分属于最基本的合规要求长期做项目的团队应该当成默认约束。3. 环境准备与前置条件3.1 引擎与开发环境建议使用 Unity 2022 LTS 及以上版本创建 3D 工程。DOTS 相关包通常对 Long Term Support 版本支持更好LTS 版本本身也更适合学习和做项目沉淀。IDE 建议使用 Visual Studio 2022 或 Rider。DOTS 的代码里泛型多、隐式类型转换多编译报错信息经常很长。一个好用的 IDE 能通过自动补全减少很多低级错误。不需要独立显卡DOTS 的物理计算主要走 CPU普通办公显卡就能跑但内存和 SSD 建议别省钱至少 16GB 内存。3.2 需要安装的包通过在 Unity 的 Package Manager 里搜索并安装以下包包名作用EntitiesECS 核心框架Unity.PhysicsDOTS 物理系统包含 Collider、PhysicsBody、物理世界Burst高性能 Native 代码编译MathematicsDOTS 使用的数学库float3、quaternion 等类型CollectionsNative 容器NativeArray、NativeList 等安装 Unity.Physics 时Unity 会自动把 Entities、Burst、Collections、Mathematics 等依赖包一起拉下来不需要手动逐个装。但如果你用的是自定义 manifest 配置需要自己保证版本号一致。也可以直接编辑项目的Packages/manifest.json在dependencies里加入{ dependencies: { com.unity.entities: 1.0.x, com.unity.physics: 1.0.x, com.unity.burst: 1.8.x, com.unity.collections: 2.1.x, com.unity.mathematics: 1.2.x } }上面的版本号只是示例实际填写时要按照 Unity 官方当前支持的版本号来写。用 Package Manager 安装的话版本会由编辑器自动选择更省心。3.3 ECS 前置知识与低配环境下的学习路径理解 DOTS Physics Raycast 之前你需要确认自己已经知道这几个概念World 和 EntityECS 里的实体不是 GameObject而是一个可以在 World 中存活的 ID。Component数据组件只存数据不存逻辑。System处理逻辑的入口每个 System 在特定时机更新。EntityQuery 和 SystemAPI.Query用来筛选符合条件的实体集合。SubScene在编辑器里把普通 GameObject 转成 ECS Entity 的运行场景。Job 与 Burst把计算从主线程解放出来并编译为高效原生代码。如果你对 Entity、System 这些概念还不太熟建议先跑一个最简的 ECS System打印一下场景里的实体数量确认一条完整的 ECS 数据链路能跑通再来看 Physics Raycast。直接跳到物理会很容易被报错淹没。3.4 工程目录建议我建议从一开始就按功能划分目录不要把所有脚本堆在Assets/Scripts下Assets/ ├── ECS/ │ ├── Systems/ # RaycastTestSystem、RaycastBatchSystem │ ├── Components/ # 自定义的 IComponentData │ ├── Jobs/ # IJobParallelFor 实现 │ └── Authoring/ # MonoBehaviour 转 Component 的 Authoring 脚本 ├── Scenes/ │ └── RaycastTest.unity └── Configs/ └── CollisionLayer.asset这个结构不复杂但能让你在批量写测试代码时少乱找文件。4. 安装部署与启动方式4.1 创建工程并安装 Unity.Physics 包用 Unity Hub 创建一个新的 3D 工程打开后进Window Package Manager搜索Unity Physics点击 Install。安装完成后在 Project 窗口的 Packages 目录下应该能看到com.unity.physics。这一步做完你已经拥有了跑 ECS 物理的最小条件。4.2 在 SubScene 中搭建最小物理世界DOTS 物理射线检测的命中对象不是普通场景里的 MeshCollider而是 ECS 实体身上的PhysicsCollider。把物理对象放进 ECS 有两种方式在 SubScene 里挂载 Physics Body 和 Physics Shape 组件。在运行时用EntityManager动态创建带PhysicsCollider的实体。学习阶段推荐第一种方式因为可以在编辑器里手动摆放地形、箱体、角色可视化调试比较直观。先在 SubScene 里放一个 Plane给 Plane 添加Physics Body和Physics Shape两个 Authoring 组件再在场景上放几个 Box 作为命中目标。进入 Play 模式后这些 GameObject 会变成 ECS Entity物理世界会自动收集它们的碰撞体。4.3 编写最小 System 并运行写一个最简单的 RaycastTestSystem在OnUpdate里构造一条从相机位置向前的射线然后查询 CollisionWorld。下面是一份基于 Unity Physics 1.0 思路的示例代码只演示 API 结构using Unity.Burst; using Unity.Entities; using Unity.Mathematics; using Unity.Physics; using Unity.Physics.Systems; public partial struct RaycastTestSystem : ISystem { [BurstCompile] public void OnCreate(ref SystemState state) { } [BurstCompile] public void OnUpdate(ref SystemState state) { // 1.0 版本主要通过 PhysicsWorldSingleton 获取只读物理世界 var collisionWorld SystemAPI.GetSingletonPhysicsWorldSingleton().CollisionWorld; var input new RaycastInput { Start new float3(0f, 10f, 0f), End new float3(0f, -10f, 0f), Filter new CollisionFilter { BelongsTo ~0u, CollidesWith ~0u, GroupIndex 0 } }; if (collisionWorld.CastRay(input, out Unity.Physics.RaycastHit hit)) { UnityEngine.Debug.Log($Hit entity: {hit.Entity}, position: {hit.Position}); } } }如果编辑器提示PhysicsWorldSingleton不存在说明包版本还在 0.x 阶段需要按老版本GetSingletonPhysicsWorld()的方式访问。API 差异就是因为 Unity Physics 版本更新导致的。4.4 启动观察进入 Play Mode 后打开 Game 视图同时打开Window Entities Hierarchy或 ECS Debugger。你应该能看到 SubScene 中定义的 Plane 和 Box 变成了带PhysicsCollider的实体此时射线查询才会真正命中。如果 Entities 窗口里没有这些实体说明 SubScene 还没有 correctly baked射线大概率打不中任何东西。5. 功能测试与效果验证5.1 测试单条射线命中地面测试目的确认 ECS 物理世界正常构建CastRay能返回正确的命中数据。操作步骤在场景中创建 SubScene添加一个 Plane挂载 Physics Body 和 Physics Shape。挂载上面的 RaycastTestSystem。进入 Play Mode观察 Console 输出。预期结果Console 里打印一条 Hit Loghit.Position接近地面坐标。如果没有任何输出先检查 Plane 有没有成功转换成 Entity再检查射线 Start 和 End 的坐标是否合理。Start 在 10 米高度End 在 -10 米射线穿过地面理论上能命中。5.2 测试射线无命中时的稳定性测试目的确认空场景或射线完全错过物体时不会抛异常。操作步骤在 SubScene 里只保留一个地面修改射线坐标让它从(1000, 1000, 1000)发射到(2000, 1000, 1000)明显不碰任何物体。运行 System观察 Console。CastRay会返回 false代码不应该崩溃。预期结果无异常输出不会出现空引用错误。如果代码在无命中时报错多半是在hit未初始化时访问了hit.Entity注意把逻辑放在if内部。5.3 测试大量射线的批量查询这个测试直接关系到 DOTS 的实用价值。目标是让一条 Job 同时处理几百上千条射线而不是一帧调用几百次CastRay。先定义一个自定义组件用来存放射线起点和方向public struct RaycastQueryData : IComponentData { public float3 Origin; public float3 Direction; public float Distance; }然后写一个IJobEntity遍历所有RaycastQueryData每条射线调用一次 CastRayusing Unity.Burst; using Unity.Entities; using Unity.Mathematics; using Unity.Physics; using Unity.Physics.Systems; [BurstCompile] public partial struct RaycastBatchJob : IJobEntity { [ReadOnly] public CollisionWorld CollisionWorld; [BurstCompile] public void Execute(ref RaycastQueryData query, ref RaycastResult result) { var input new RaycastInput { Start query.Origin, End query.Origin query.Direction * query.Distance, Filter new CollisionFilter { BelongsTo ~0u, CollidesWith ~0u, GroupIndex 0 } }; result.Hit CollisionWorld.CastRay(input, out var hit); result.Position hit.Position; result.Normal hit.Normal; result.HitEntity hit.Entity; } }这个写法在概念上是可用的一个 Job遍历所有射线查询组件并行执行物理查询。实际项目中你需要把CollisionWorld从PhysicsWorldSingleton里取出来再调用job.ScheduleParallel()或job.Schedule()并根据单个实体的数量决定并行度。关键点在于不要在Execute里做任何分配不要在 Job 里使用Debug.Log否则 Burst 编译会报错。批量测试的预期结果是成功命中一部分射线命中目标和不命中目标的比例取决于你的场景摆放。真正值得关注的数据是耗时。你可以在同一帧记录调度前后时间对比 100 条射线和 1000 条射线占用的 Job 时间。通常射线数量的增加不会导致线性帧率下降因为 Job 会在多个线程上分摊。5.4 测试碰撞过滤DOTS 射线检测可以指定只和特定 Collider 相交。关键字段是CollisionFilter里的BelongsTo、CollidesWith、GroupIndex。射线输入和 Collider 上的 Filter 会一起参与判定。如果 Collider 的CollidesWith不包含射线输入的BelongsTo即使射线从物体中间穿过也不会命中。测试步骤给两个 Box 设置不同的CollidesWith掩码。在射线的Filter.CollidesWith里只包含其中一个掩码。运行 System观察命中结果。预期结果只命中被允许的 Box。如果两个 Box 都被命中检查 Collider 和射线输入两边的 Filter 配置。碰撞过滤是最容易踩坑的点特别是位运算配置比较隐蔽排查时要同时看射线侧和物体侧。5.5 动态目标追踪测试测试目的确认射线能命中带动态 Physics Body 的移动实体。操作步骤在场景中放置一个 Box添加Physics Body类型设为Dynamic再写一个简单的JobEntity每帧给 Box 一个速度或直接修改LocalTransform。保持射线测试 System 开启观察移动过程中射线是否持续命中。预期结果动态 Box 移动时射线命中信息持续更新。如果动态物体没被命中优先检查它是否真的有PhysicsCollider组件。我们在实际开发里遇到最多的场景是物体是动态的但 Collider 组件漏挂了射线当然打不中。6. 接口 API 与批量任务6.1 核心数据类型说明类型作用RaycastInput定义射线起点、终点和过滤规则Unity.Physics.RaycastHit射线命中结果包含 Entity、Position、Normal、Fraction 等CollisionFilter控制射线能命中哪些物体的过滤条件CollisionWorld物理世界的碰撞体集合只读查询入口PhysicsWorldSingleton1.0 版本中访问 CollisionWorld 的单例组件CollisionWorld.CastRay是最常用的查询接口。它接收一个RaycastInput返回 bool 表示是否命中out 参数是命中细节。碰撞体越大、碰撞类型越多查询耗时越长。6.2 单条射线查询 API 示例var ray new RaycastInput { Start startPos, End endPos, Filter new CollisionFilter { BelongsTo 1u 0, CollidesWith 1u 0, GroupIndex 0 } }; if (collisionWorld.CastRay(ray, out var hit)) { // hit.Entity 是命中的 ECS 实体 // hit.Position 是世界坐标下的命中点 // hit.Normal 是命中点的法线 // hit.Fraction 是起点到命中点占总射线的比例 }6.3 批量射线并行 Job 的设计要点批量任务非常适合用IJobParallelFor处理射线数组using Unity.Burst; using Unity.Collections; using Unity.Jobs; using Unity.Physics; [BurstCompile] public struct RaycastParallelJob : IJobParallelFor { [ReadOnly] public CollisionWorld CollisionWorld; [ReadOnly] public NativeArrayRaycastInput RayInputs; [WriteOnly] public NativeArrayRaycastResultData Results; public void Execute(int index) { var input RayInputs[index]; if (CollisionWorld.CastRay(input, out var hit)) { Results[index] new RaycastResultData { Hit true, Position hit.Position, Normal hit.Normal, Entity hit.Entity }; } else { Results[index] default; } } }使用IJobParallelFor时要注意CollisionWorld必须标成[ReadOnly]。物理查询本来就是只读的不要尝试在 Job 里修改 CollisionWorld。结果数组要用[WriteOnly]避免回写开销。6.4 批量任务调度与队列设计批量任务不是简单写一个 Job 就行。工程化的做法是用NativeArrayRaycastInput保存所有射线输入Allocator 用TempJob或Persistent。用JobHandle管理 Job 依赖避免顶层 System 和物理构建 System 的执行顺序冲突。每帧复用 NativeArray或者放在 System 里用DisposeOnUpdate在帧尾释放避免 GC 压力。遇到批量任务卡住时先检查 JobHandle 是否没有被 Complete接着检查数组容量和索引是否越界。失败的批量任务还有一个常见原因直接把 C# 的ListRaycastInput传给 Job。Burst 不能编译托管容器必须改成NativeArray或NativeList。7. 资源占用与性能观察7.1 自己动手做性能对比很多人第一次跑 DOTS Physics Raycast都会觉得自己项目里射线数量并不多怎么没变快。这里有一个比较关键的测试思路先建立基线再做对比。在 Profiler 里单独记录三组数据单条射线检测在主线程上的耗时。100 条射线用IJobEntity调度后的耗时。1000 条射线用IJobParallelFor调度后的耗时。记录时要排除物理世界构造成本因为每帧碰撞世界会由物理系统自动构建射线查询之前需要等待世界构建完成。对比下来你会发现射线数量越少DOTS 的优势越小甚至由于BuildPhysicsWorld每帧都要更新物理世界总耗时反而可能高于传统的简单射线。这是正常的DOTS Physics 的适用点是大规模、高频率查询而不是单次查询的极致速度。7.2 观察资源占用的指标Profiler CPU Usage观察主线程时间和 Job 线程时间。ECS Debugger查看每个 System 的耗时确认 Raycast Job 有没有按预期并行执行。Memory Profiler检查原生容器有没有泄漏特别是NativeArray分配后有没有释放。Unity Physics 物理世界的构造成本受碰撞体数量影响。你的场景里放几千个 Box哪怕不发射任何射线物理世界构建本身也有开销。射线查询只是物理世界的读取方不是唯一消耗来源。7.3 如何降低查询开销优化 DOTS Physics Raycast优先级从高到低调整碰撞过滤尽早过滤掉不参与查询的物体。用CollisionFilter的BelongsTo和CollidesWith让射线少遍历一批 Collider。减少 CollisionWorld 中的无效碰撞体无关物体不挂 PhysicsCollider。静态物体优先Static 类型的 Collider 会被物理世界做静态数据结构优化命中查询速度会比动态物体列表更稳定。减少射线数量不必每个 Agent 每帧都发 8 条射线先做剔除再在需要时发起查询。控制射线长度射线起点到终点越长物理世界需要排查的范围越广。避免每帧分配把射线输入和结果数组放进 System 的字段用Allocator.Persistent管理帧尾释放或复用。8. 常见问题与排查方法问题现象可能原因排查方式解决方案射线永远打不中物体SubScene 没转换成功或物体没有 PhysicsCollider打开 Entities Hierarchy确认物体存在于 ECS 世界给物体添加 Physics Body/Physics Shape 并重新进入 Play Mode射线打中了但命中坐标不对Start/End 使用了 LocalSpace 而不是 WorldSpace打印射线起点和命中点坐标对比场景世界坐标用LocalTransform.Position等世界空间数值构造射线碰撞过滤生效却被穿透Collider 和射线两侧 Filter 不匹配在场景和 Job 里分别打印 Filter 值统一 BelongsTo/CollidesWith 掩码建立分层表批量 Job 编译报错Job 里使用了托管类型或 Debug.Log查看 Burst 报错截图删除 Debug.Log改用 NativeArray 收集数据帧后统一输出PhysicsWorldSingleton找不到包版本较老或命名空间不对查看包版本和官方 API改用GetSingletonPhysicsWorld().CollisionWorld或升级包每帧 GC 飙升每帧都 new NativeArray打开 Memory Profiler 观察分配使用 Persistent Allocator 复用数组帧尾释放批量任务卡死JobHandle 没有 Complete 或互相依赖检查 System 执行顺序在读取结果前jobHandle.Complete()多层 Job 用依赖链动态物体追踪不到动态物体没有 PhysicsCollider检查实体组件列表给动态物体补上 PhysicsCollider9. 最佳实践与使用建议9.1 先跑最小用例再上批量第一次接触 DOTS Physics Raycast不要一上来就写 IJobParallelFor。先在 MonoBehaviour 的OnDrawGizmos或 System 里用CollisionWorld.CastRay跑通一条简单射线确认物理世界构建、坐标转换、Collider 挂载这几个环节没问题再往批量方向扩展。最小可运行示例能隔离问题单条不工作批量必然不工作。9.2 建立统一碰撞分层表CollisionFilter 的位掩码很容易错。团队内建议维护一张碰撞分层表比如Layer掩码用途Layer01u 0地面、静态地形Layer11u 1玩家实体Layer21u 2AI 敌人射线查询只关心静态地形时CollidesWith就设成1u 0要多层都命中用多个位的按位或。不要在整个工程里随手写~0u那样等于关闭过滤物理查询会遍历所有可命中物体性能会下降。9.3 目录与资源管理把 Authoring 脚本、System、Job、IComponentData 分目录存放测试用 SubScene 单独建一个场景。模型、材质、碰撞体配置不要和测试代码混在一起。项目一变大目录结构混乱会直接拖慢调试效率。9.4 Job 和 NativeArray 的内存安全底层的点使用Allocator.Persistent或TempJob分配的 NativeArray必须在 Job 完成后释放。可以用EntityCommandBuffer在帧尾做释放或者把这个释放逻辑放在 System 的OnDestroy和OnUpdate里。漏一次释放就是一次内存泄漏服务跑几天内存就会涨上去。9.5 版本升级策略Unity DOTS 迭代比较快。升级 Entities 或 Unity.Physics 前先备份工程阅读官方 Upgrade Guide最好在分支上先跑一遍最小示例。不要直接把项目里的包拖到最新 Preview 版本除非你愿意接受一批新报错。9.6 第三方代码与素材合规学习阶段参考官方Physics示例和视频教程都行但要把最终代码沉淀成自己的实现。来自第三方商店插件、论坛示例的脚本和资源用到商业化项目前一定先确认许可证。涉及物理碰撞、场景资源等可复用素材不要直接搬运到商用项目里。10. 总结与下一步DOTS Physics Raycast 最值得尝试的地方是它把“大量射线查询”重新设计成了可并行的 Job 任务。单条射线没有太大区别但在群体 AI、战斗索敌、地形检测这类高频查询场景里收益是实打实的。最先需要验证的功能永远是那条最基础的CollisionWorld.CastRay。在这个上面多花半小时比直接写几百条批量查询更稳。最容易踩的坑有三个包版本升级导致的 API 变化、CollisionFilter 位掩码配置不对、SubScene 中的物体没有成功变成带 PhysicsCollider 的实体。这三个坑几乎覆盖了 DOTS 新手学习 Physics Raycast 的大部分报错。下一步可以继续深入的方向是Unity Physics 的碰撞事件、Trigger 触发、ColliderCast形状扫描以及用 DOTS 做避障和寻路时的分组过滤策略。建议先收藏这篇笔记等真正在项目里开始写 ECS 射线时再翻出来对照排查。
返回列表