很多开发者在从传统 MonoBehaviour 切换到 DOTS 之后,第一个卡住的地方往往不是 ECS 本身的语法,而是物理系统——以前一行Physics.Raycast就能完成的射线检测,在 ECS 世界里居然找不到对应的 API,网上资料又零散不成体系。本文围绕 DOTS Physics 中的 Raycast 做一次完整拆解,从核心概念、环境准备、参数含义,到可直接运行的射线检测系统、批量射线方案和常见踩坑排查,尽量做到新手能看懂原理,老手能直接抄代码落地。
需要提前说明的是,DOTS 的包版本和 API 变化速度比较快,本文示例以 Entities 1.0 系列和 Unity Physics 1.0 系列的 API 风格为准。如果你的项目版本不同,个别命名和系统组名可能需要微调,但整体思路是通用的。
1. DOTS Physics 与 Raycast 到底是什么
1.1 从 DOTS 说起
DOTS 是 Unity 面向数据的整套技术栈缩写,核心包含三部分:ECS、Job System、Burst Compiler。它的核心思想是把游戏对象拆成 Entity(实体)、Component(组件数据)、System(处理逻辑),让数据连续排列在内存中,配合多线程和 Burst 编译获得高性能。
在这个体系里,“物体”不再是一个GameObject,而是一个Entity。“物体的位置”不再是一个Transform组件,而是LocalTransform这样的纯数据组件。“物理碰撞体”也是一个组件,叫PhysicsCollider。
很多初学者最大的困惑是:我明明把PhysicsShape和PhysicsBody挂在了 GameObject 上,为什么代码里访问不到?这是因为 DOTS 的物理系统工作在 ECS World 里,GameObject 只是“编辑器下的编辑壳”,真正参与运算的是转换(baking)之后的 Entity 和 Component。理解这一点,后面很多问题都会迎刃而解。
1.2 Unity Physics 在 DOTS 生态中的角色
Unity Physics(Package 名称com.unity.physics)是 DOTS 生态里的官方物理系统实现。它负责:
- 给 Entity 添加刚体属性(静态、动态、运动学);
- 给 Entity 添加碰撞体(盒体、球体、胶囊、凸包、网格等);
- 维护一个可查询的物理世界结构,包括
CollisionWorld(碰撞世界,用于射线、距离、碰撞查询)和DynamicsWorld(动力学世界,用于模拟求解); - 提供碰撞事件、触发器事件、距离查询、射线检测等能力。
传统 Unity 物理引擎的碰撞数据是引擎内部维护的黑盒,你只能通过 API 查询;而 Unity Physics 的碰撞世界是一个可以显式访问、可以放进 Job、可以并行处理的“数据集合”。这也是 DOTS 物理性能优势的来源之一。
1.3 传统 Raycast 与 DOTS Raycast 的区别
传统写法:
if (Physics.Raycast(origin, direction, out RaycastHit hit, maxDistance)) { Debug.Log(hit.collider.name); }DOTS 射线检测的思考方式完全不同:
| 对比项 | 传统 Physics.Raycast | DOTS Physics Raycast |
|---|---|---|
| 查询对象 | Unity 场景里的 GameObject Collider | ECS 世界里的 PhysicsCollider 组件 |
| 世界来源 | 引擎内部自动维护 | PhysicsWorldSingleton中的CollisionWorld |
| 命中结果 | RaycastHit(带collider/transform) | Unity.Physics.RaycastHit(带Entity/ColliderKey) |
| 过滤方式 | LayerMask | CollisionFilter(BelongsTo / CollidesWith) |
| 多线程 | 一般在主线程调用 | 可放入 Job / Burst 并行执行 |
| Is Trigger 处理 | trigger 默认不阻挡或可配置 | 碰撞体需要区分 Collider 与 Trigger,射线只对普通碰撞体生效 |
简单说,DOTS 的 Raycast 不是“调一个全局函数”,而是“向一个物理世界数据结构发起查询”。你需要先拿到物理世界的引用,再构造一个RaycastInput去查询,最后从结果里读取命中的 Entity。
2. 环境准备与版本说明
2.1 Unity 版本与包依赖
本文示例推荐使用较新的 LTS 版本 Unity(例如 Unity 2022.3 或更高版本),并且在 Package Manager 中安装以下两个包:
com.unity.entities(Entities 1.0 系列)com.unity.physics(Unity Physics 1.0 系列)
安装方式有两种:
- 打开 Package Manager 窗口,左上角选择 “Unity Registry”,搜索
Entities和Physics安装; - 直接编辑项目的
Packages/manifest.json,添加依赖后让 Unity 自动解析。
参考片段如下:
{ "dependencies": { "com.unity.entities": "1.0.16", "com.unity.physics": "1.0.16" } }这里需要注意:版本号一定要根据你的 Unity 版本和包管理器里实际能解析到的版本去填,不要盲目照抄。因为 DOTS 的 API 还在持续演进,不同小版本的类名、系统组名、方法签名偶尔会有差异。一个稳妥的做法是:先通过 Package Manager 安装,再查看包版本对应的 changelog。
2.2 示例项目整体结构
为了后续代码演示清晰,我建议按下面的目录结构组织脚本:
Assets/ ├── Scripts/ │ ├── RaycastDebugData.cs │ ├── SceneInitSystem.cs │ ├── SingleRaycastSystem.cs │ └── RaycastDebugSystem.cs └── Scenes/ └── DotsRaycastScene.unity在 Scene 中,你需要:
- 创建一个空物体挂一个初始化的 SubScene(或者直接使用纯代码创建实体);
- 在场景中放置若干受物理影响的物体;
- 进入 Play Mode 后观察射线命中结果与调试线。
3. DOTS Physics Raycast 核心原理拆解
3.1 碰撞体与物理体
在 DOTS Physics 中,一个能被射线命中的 Entity 必须至少满足两个条件:
- 有
PhysicsCollider组件(保存碰撞体的几何形状数据); - 有
LocalTransform(或LocalToWorld)组件(决定碰撞体在世界中的位置和姿态)。
如果是纯代码创建实体,通常还需要PhysicsBody组件来标记刚体类型。PhysicsBody.MotionType有三个值:
MotionType.Static:静态刚体,不参与模拟,适合地面、墙壁;MotionType.Dynamic:动态刚体,受重力、外力影响,适合小球、箱子;MotionType.Kinematic:运动学刚体,由代码驱动,不受力但能推倒其他物体。
一个常见的误区是:只有动态刚体才能被射线命中。实际上,只要有PhysicsCollider,静态、动态、运动学刚体都可以被射线命中。射线检测并不关心刚体是否参与模拟,它只关心碰撞世界里的碰撞体是否存在。
PhysicsCollider.Value是一个BlobAssetReference<Collider>,这是 DOTS 中一种把碰撞几何数据以二进制形式保存在非托管内存中的结构。运行时直接调用BoxCollider.Create()会产生一块 BlobAsset 内存,使用完毕后需要手动Dispose(),否则会内存泄漏。这也是为什么工程上更推荐用 Baker / SubScene 烘焙,而不是在运行时反复手动创建 Collider。
3.2 PhysicsWorld 与 PhysicsWorldSingleton
在 DOTS Physics 中,物理系统会定期把所有带PhysicsCollider的 Entity 构建成一个物理世界。这个世界的本体是PhysicsWorld,它包含:
CollisionWorld:用于射线检测、距离检测、碰撞查询;DynamicsWorld:用于动力学模拟、速度、质量等数据。
但PhysicsWorld本身太大且可能被每帧重建,不能直接作为唯一组件到处传递。于是 Unity Physics 提供了一个轻量级封装:PhysicsWorldSingleton。它是一个特殊的单例组件,用来在 System 里安全地访问当前的物理世界。
获取方式非常固定:
PhysicsWorldSingleton physicsWorld = SystemAPI.GetSingleton<PhysicsWorldSingleton>();拿到之后,直接调用它的CastRay方法即可:
physicsWorld.CastRay(input, out Unity.Physics.RaycastHit hit);你也可以通过physicsWorld.PhysicsWorld.CollisionWorld访问底层碰撞世界,比如查询当前世界里的刚体数量:
if (physicsWorld.PhysicsWorld.CollisionWorld.NumBodies == 0) return;这个空判断很重要,因为物理世界可能在第一帧还未构建完成,或者场景里根本没有碰撞体。
3.3 RaycastInput 参数解读
RaycastInput是射线检测的输入结构,核心字段有三个:
public struct RaycastInput { public float3 Start; // 射线起点,世界坐标 public float3 End; // 射线终点,世界坐标 public CollisionFilter Filter; // 碰撞过滤 }这里最容易踩坑的是End字段。它表示的是“终点世界坐标”,不是“方向向量”,也不是“方向乘以长度后的偏移量”省略写法。我们经常看到有人这样写:
// 错误示例:End 被写成了方向向量 var input = new RaycastInput { Start = start, End = direction, // 这里 direction 如果是 (1,0,0),射线区间会非常短 Filter = CollisionFilter.Default };正确写法应该是:
float3 start = new float3(0f, 1f, 0f); float3 end = start + new float3(10f, 0f, 0f);如果已知方向和长度,可以这样计算:
float3 direction = math.normalize(new float3(1f, 0f, 0f)); float maxDistance = 10f; float3 start = new float3(0f, 1f, 0f); float3 end = start + direction * maxDistance;3.4 CollisionFilter 的过滤逻辑
CollisionFilter是 DOTS Physics 里替代 LayerMask 的过滤机制,它有三个字段:
public struct CollisionFilter { public uint BelongsTo; // 自己属于哪个层 public uint CollidesWith; // 能和哪些层碰撞 public int GroupIndex; // 分组索引,0 表示按层判断 }BelongsTo和CollidesWith是 32 位的 bitmask。通常用位移运算符定义:
uint layerCharacter = 1 << 0; uint layerEnvironment = 1 << 1;如果一个实体的Collider上 Filter 设置为:
new CollisionFilter { BelongsTo = layerCharacter, CollidesWith = layerEnvironment }表示这个实体属于角色层,只和环境层碰撞。
射线查询中的Filter.CollidesWith如果是 0,那么射线什么都打不到。一个常见问题就是 Filter 不匹配导致射线永远 Miss。如果不关心分层,直接用CollisionFilter.Default,它相当于BelongsTo = ~0u、CollidesWith = ~0u,即所有层都参与。
注意RaycastInput.Filter与Collider.Filter是“两边同时满足”的关系:射线要命中一个碰撞体,必须射线的CollidesWith包含碰撞体的BelongsTo位,同时碰撞体的CollidesWith也要包含射线的BelongsTo位。
4. 完整实战:DOTS 射线检测系统
下面进入正题,我们完整走一遍从搭建场景、创建实体,到射线检测、结果调试的流程。
4.1 用 SubScene 搭建测试关卡
DOTS 场景的推荐做法是使用 SubScene。你可以先创建一个普通的 Scene,然后右键 Hierarchy 选择创建 SubScene,在 SubScene 里摆放游戏物体。
给一个 GameObject 添加物理属性时:
- 添加
Physics Body组件,把Motion Type设置为Static(地面、墙); - 添加
Physics Shape组件,把Shape Type设置为Box,并调整Size; - 把物体拖入 SubScene,Unity 会在烘焙时自动把它转换成带
PhysicsCollider和PhysicsBody的 Entity。
这样搭建出来的关卡数据会在进入 Play Mode 后自动参与 DOTS 物理世界构建,无需写任何代码。对于地面、障碍物这种静态物体,强烈建议用 SubScene 而不是纯代码创建,因为它会让 Unity Physics 自动处理 Collider BlobAsset 的生成与释放。
4.2 纯代码创建障碍物实体
有些场景无法使用 SubScene,比如运行时动态生成地形或物体。这种情况下可以用ISystem在初始化阶段创建实体。下面是一段可以放在Assets/Scripts/SceneInitSystem.cs的代码,用来创建一块地面和一个墙壁方块:
using Unity.Entities; using Unity.Mathematics; using Unity.Physics; [UpdateInGroup(typeof(InitializationSystemGroup))] public partial struct SceneInitSystem : ISystem { public void OnCreate(ref SystemState state) { var em = state.EntityManager; // 创建地面 var groundEntity = em.CreateEntity(); em.AddComponentData(groundEntity, new PhysicsBody { MotionType = MotionType.Static }); em.AddComponentData(groundEntity, new PhysicsCollider { Value = BoxCollider.Create(new BoxGeometry { Size = new float3(20f, 1f, 20f), Center = new float3(0f, -0.5f, 0f), Orientation = quaternion.identity }) }); em.AddComponentData(groundEntity, new LocalTransform { Position = float3.zero, Rotation = quaternion.identity, Scale = 1f }); // 创建静态墙壁方块 var wallEntity = em.CreateEntity(); em.AddComponentData(wallEntity, new PhysicsBody { MotionType = MotionType.Static }); em.AddComponentData(wallEntity, new PhysicsCollider { Value = BoxCollider.Create(new BoxGeometry { Size = new float3(1f, 1f, 1f), Center = float3.zero, Orientation = quaternion.identity }) }); em.AddComponentData(wallEntity, new LocalTransform { Position = new float3(3f, 0.5f, 0f), Rotation = quaternion.identity, Scale = 1f }); // 这个系统只需要执行一次 state.Enabled = false; } public void OnUpdate(ref SystemState state) { } }代码里有几个细节值得解释:
AddComponentData会以组件默认值创建组件,但我们在括号里直接传入了带初始值的结构体,所以创建出来就是配置好的状态;BoxCollider.Create返回的是BlobAssetReference<Collider>,运行时创建后如果不释放会有内存泄漏,示例为了方便演示暂时“默认”为整个游戏生命周期存活;- 创建 Entity 后必须给
LocalTransform,否则物理系统无法知道碰撞体在世界中的位置。
4.3 单条射线检测系统实现
接下来是本文的核心:射线检测系统。为了让调试效果直观,我故意不在这段代码上开启 Burst,因为UnityEngine.Debug.DrawLine是托管 API,无法在 Burst 编译的方法里调用。代码放在Assets/Scripts/SingleRaycastSystem.cs:
using Unity.Entities; using Unity.Mathematics; using Unity.Physics; using Unity.Physics.Systems; using UnityEngine; [UpdateInGroup(typeof(FixedStepSimulationSystemGroup))] [UpdateAfter(typeof(PhysicsSystemGroup))] public partial struct SingleRaycastSystem : ISystem { public void OnCreate(ref SystemState state) { state.RequireForUpdate<PhysicsWorldSingleton>(); } public void OnUpdate(ref SystemState state) { PhysicsWorldSingleton physicsWorld = SystemAPI.GetSingleton<PhysicsWorldSingleton>(); if (physicsWorld.PhysicsWorld.CollisionWorld.NumBodies == 0) return; float3 start = new float3(0f, 1f, 0f); float3 end = new float3(8f, 1f, 0f); RaycastInput input = new RaycastInput { Start = start, End = end, Filter = CollisionFilter.Default }; if (physicsWorld.CastRay(input, out Unity.Physics.RaycastHit hit)) { Debug.DrawLine(start, hit.Position, Color.green, 0.1f); Debug.Log($"命中实体:{hit.Entity},命中点:{hit.Position}"); } else { Debug.DrawLine(start, end, Color.red, 0.1f); } } }这段代码的执行流程是:
- OnCreate 中通过
RequireForUpdate<PhysicsWorldSingleton>()保证物理世界单例存在时系统才更新; - 每次更新先拿到
PhysicsWorldSingleton; - 判断碰撞世界里是否有刚体,避免世界还没构建好就查询;
- 构造
RaycastInput,从(0,1,0)向(8,1,0)发射射线; - 调用
CastRay,如果命中就画一条绿线并打印结果,否则画红线。
把这条射线放在y = 1的高度,是为了确保它正好能打在y范围是[0,1]的静态方块上,同时不会打中地面。如果射线高度设成 0,就会命中地面而不是墙壁,这也是调试时很容易混淆的地方。
4.4 Burst 友好写法:结果回写单例组件
上面的写法简单直观,但直接在 System 里调用Debug.Log会破坏 Burst 编译,不适合生产环境。正确的做法是把射线查询和可视化分离:射线查询走 Burst,结果写入一个普通组件;可视化由一个非 Burst 系统读取组件后画线。
先定义一个调试数据组件,放在Assets/Scripts/RaycastDebugData.cs:
using Unity.Entities; using Unity.Mathematics; public struct RaycastDebugData : IComponentData { public bool IsHit; // 是否命中 public float3 Start; // 射线起点 public float3 End; // 射线终点 public float3 HitPosition; // 命中点 }然后在系统 OnCreate 中创建一个带这个组件的单例实体,方便其他系统GetSingleton:
var debugEntity = state.EntityManager.CreateEntity(); state.EntityManager.AddComponentData(debugEntity, new RaycastDebugData { IsHit = false, Start = float3.zero, End = float3.zero, HitPosition = float3.zero });修改后的射线查询系统可以这样写:
using Unity.Burst; using Unity.Entities; using Unity.Mathematics; using Unity.Physics; using Unity.Physics.Systems; [UpdateInGroup(typeof(FixedStepSimulationSystemGroup))] [UpdateAfter(typeof(PhysicsSystemGroup))] public partial struct BurstRaycastSystem : ISystem { [BurstCompile] public void OnCreate(ref SystemState state) { state.RequireForUpdate<PhysicsWorldSingleton>(); state.RequireForUpdate<RaycastDebugData>(); } [BurstCompile] public void OnUpdate(ref SystemState state) { var physicsWorld = SystemAPI.GetSingleton<PhysicsWorldSingleton>(); var physicsWorldData = physicsWorld.PhysicsWorld; if (physicsWorldData.CollisionWorld.NumBodies == 0) return; float3 start = new float3(0f, 1f, 0f); float3 end = new float3(8f, 1f, 0f); var input = new RaycastInput { Start = start, End = end, Filter = CollisionFilter.Default }; bool isHit = physicsWorld.CastRay(input, out Unity.Physics.RaycastHit hit); SystemAPI.SetSingleton(new RaycastDebugData { IsHit = isHit, Start = start, End = end, HitPosition = isHit ? hit.Position : end }); } }可视化的 Debug 系统如下,它放在Assets/Scripts/RaycastDebugSystem.cs:
using Unity.Entities; using Unity.Physics.Systems; using UnityEngine; [UpdateInGroup(typeof(FixedStepSimulationSystemGroup))] [UpdateAfter(typeof(BurstRaycastSystem))] public partial struct RaycastDebugSystem : ISystem { public void OnUpdate(ref SystemState state) { if (!SystemAPI.HasSingleton<RaycastDebugData>()) return; var data = SystemAPI.GetSingleton<RaycastDebugData>(); if (data.IsHit) Debug.DrawLine(data.Start, data.HitPosition, Color.green, 0.1f); else Debug.DrawLine(data.Start, data.End, Color.red, 0.1f); } }这种“查询系统只负责计算,调试系统只负责表现”的架构,在 DOTS 项目里非常推荐。它保证了核心物理查询仍然可以被 Burst 编译,同时把可视化、日志这类托管操作隔离出去。
4.5 批量射线与并行 Raycast
很多实际需求不止一条射线,例如子弹弹幕、雷达扫描、群群体索敌。DOTS 的射线查询天然适合并行化。一种常见的做法是把“每条射线的请求”做成组件,然后通过IJobEntity批量处理。
先定义请求组件和结果组件:
using Unity.Entities; using Unity.Mathematics; public struct RaycastRequest : IComponentData { public float3 Direction; public float MaxDistance; } public struct RaycastResultData : IComponentData { public bool Hit; public float3 HitPosition; public Entity HitEntity; }然后在实体上同时挂上LocalTransform、RaycastRequest、RaycastResultData,用下面的 Job 处理:
using Unity.Burst; using Unity.Entities; using Unity.Mathematics; using Unity.Physics; [BurstCompile] public partial struct RaycastRequestJob : IJobEntity { [ReadOnly] public PhysicsWorldSingleton PhysicsWorld; public void Execute(in LocalTransform localTransform, in RaycastRequest request, ref RaycastResultData result) { float3 end = localTransform.Position + request.Direction * request.MaxDistance; var input = new RaycastInput { Start = localTransform.Position, End = end, Filter = CollisionFilter.Default }; if (PhysicsWorld.CastRay(input, out Unity.Physics.RaycastHit hit)) { result.Hit = true; result.HitPosition = hit.Position; result.HitEntity = hit.Entity; } else { result.Hit = false; result.HitEntity = Entity.Null; } } }调度这个 Job 的 System 写法如下:
using Unity.Burst; using Unity.Entities; using Unity.Physics; [UpdateInGroup(typeof(FixedStepSimulationSystemGroup))] [UpdateAfter(typeof(PhysicsSystemGroup))] public partial struct RaycastRequestSystem : ISystem { [BurstCompile] public void OnCreate(ref SystemState state) { state.RequireForUpdate<RaycastRequest>(); state.RequireForUpdate<PhysicsWorldSingleton>(); } [BurstCompile] public void OnUpdate(ref SystemState state) { state.Dependency = new RaycastRequestJob { PhysicsWorld = SystemAPI.GetSingleton<PhysicsWorldSingleton>() }.Schedule(state.Dependency); } }这里的关键点在于PhysicsWorldSingleton是只读传递给 Job 的,因此多个 Entity 的射线查询可以在不同线程上并行执行,互不干扰。比起在主线程里循环发几百条射线,这种方式在大量查询场景下效率要高得多。
如果不需要把请求建模成组件,也可以直接用NativeArray<RaycastCommand>一次性构造一批射线命令,然后调用RaycastCommand.Schedule调度。不同 Unity Physics 版本里RaycastCommand.Schedule的重载略有不同,使用前建议先查看当前包版本的 API 文档,避免签名对不上。
5. 常见问题与排查思路
DOTS Physics Raycast 虽然性能好,但踩坑也不少。下面把我在实战中经常遇到的问题整理成表,方便快速定位。
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| 射线永远打不到任何东西 | 实体没有PhysicsCollider,或碰撞体位置不对 | 检查目标实体是否带PhysicsCollider,确认LocalTransform位置 |
GetSingleton<PhysicsWorldSingleton>()报错 | 没有安装 Unity Physics 包,或物理系统未初始化 | 安装com.unity.physics,在 OnCreate 中加RequireForUpdate |
| 命中的实体是地面,不是目标墙壁 | 射线高度不合适,先碰到了其他碰撞体 | 调整射线起点/终点高度,或用 Filter 过滤不需要的层 |
| 射线 miss,但物体明明有 Collider | RaycastInput.End被写成了方向向量 | 确认End是世界坐标终点,不是 direction |
| 命中位置和物体显示位置不一致 | 碰撞体由LocalTransform定位,与可视化 Mesh 位置不同步 | 检查实体的LocalTransform和烘焙组件是否正确 |
| Burst 编译报错:“Managed method call” | 在[BurstCompile]方法里调用了Debug.Log、Debug.DrawLine | 把这些托管调用拆到独立非 Burst 系统 |
| 运行时创建 Collider 后内存持续增长 | BoxCollider.Create()/SphereCollider.Create()创建的 BlobAsset 未释放 | 生产环境使用 SubScene/Baker,或统一用 BlobAssetStore 管理生命周期 |
除了表格里的问题,还有一个容易忽略的现象:如果射线打在了 Trigger 碰撞体上,很多版本默认是不返回命中的。因为在 Unity Physics 的设计里,Trigger 主要用于“进入/离开”事件,而不是作为射线阻挡物。如果你希望射线忽略墙壁、只检测某个特殊标记,请优先使用CollisionFilter,而不是在结果里做二次判断。
6. 最佳实践与工程建议
在实际项目里使用 DOTS Physics Raycast,建议从一开始就养成良好的工程习惯,避免后面大规模重构。
第一,射线检测系统要明确更新顺序。物理世界是在PhysicsSystemGroup中构建的,所以射线查询系统最好放在FixedStepSimulationSystemGroup中,并用[UpdateAfter(typeof(PhysicsSystemGroup))]保证物理世界已经构建完成。如果你的项目里用到了多个物理查询系统,要给它们之间定义清晰的先后依赖关系,不要让两个系统同帧互相覆盖数据。
第二,射线查询和调试表现分离。所有物理查询逻辑尽量保持 Burst 编译,把Debug.Log、Debug.DrawLine、Gizmos 等托管 API 隔离到专门的调试系统里。这样做的收益在于:发布时可以直接剔除调试系统,查询逻辑本身不需要修改。结果数据写入单例组件或 Buffer,再由调试系统读取。
第三,CollisionFilter 使用显式定义,不要全部套 Default。虽然CollisionFilter.Default开发时很方便,但项目中期之后,射线需要区分玩家、敌人、环境、Trigger、特效等不同层。建议项目里有一个静态类统一管理 Layer Bit:
public static class PhysicsLayers { public const uint Player = 1 << 0; public const uint Enemy = 1 << 1; public const uint Environment = 1 << 2; public const uint Trigger = 1 << 3; }查询时使用带名字的常量,而不是魔法数字。这能避免 Filter 错配带来的诡异 Miss。
第四,谨慎处理运行时创建的 Collider BlobAsset。上文已经提到,BoxCollider.Create这类 API 会在非托管内存中分配数据。如果运行时频繁创建实体(比如生成大量障碍物、子弹命中后动态改变碰撞体),一定要统一管理 BlobAsset 的释放,否则 Unity Profiler 里可以看到非托管内存持续上涨。
第五,优先使用 SubScene / Baker 声明关卡数据。对于一个固定关卡,摆在地图上的静态墙壁、地面完全可以通过 SubScene 烘焙自动生成,不需要在运行时创建。这不仅让物理世界构建更快,也让 Collider 数据生命周期由 Unity 自动管理,从根源上规避内存泄漏问题。
第六,大规模射线查询优先走 Job。如果是几十条射线,普通循环也能接受;如果是几百条甚至更多,一定要让查询并行执行。IJobEntity搭配PhysicsWorldSingleton,或者RaycastCommand批量调度,都比主线程循环性能好很多。
7. 总结
这篇文章围绕 DOTS Physics Raycast 做了一次从概念到落地的完整梳理。我们知道了 DOTS 物理的核心不是“调用一个全局函数”,而是向由PhysicsCollider构建出的CollisionWorld发起查询;学会了通过PhysicsWorldSingleton获取物理世界,构造带Start、End、Filter的RaycastInput,并从Unity.Physics.RaycastHit中提取命中的 Entity 和位置;也通过完整示例掌握了单条射线、Burst 友好写法、以及基于IJobEntity的并行批量射线方案。
下一步值得继续研究的方向包括:接触事件与触发器事件、DistanceQuery(距离查询)、CCD 碰撞检测、以及物理查询与 Havok Physics 的对照使用。如果你是在一个刚切换 DOTS 的项目里做功能,请优先记住三件事:检查 Collider 是否存在、确认 Filter 是否匹配、不要把调试代码写进 Burst 方法里。把这三件事做好,射线检测这块基本就不会再出幺蛾子。
如果这篇文章对你有帮助,可以收藏备用。后续再遇到 DOTS 物理相关的疑难杂症,也欢迎在评论区一起交流。