UE5.8 碰撞通道与 Trace 查询
从一次诡异的射偏说起
凌晨两点,测试服里传来一阵哀嚎。我们负责的新英雄「影狙」正在做最后一轮射击校验,策划小哥盯着屏幕直挠头:子弹明明从准星正中飞出,穿过一片灌木后,却在敌人脚边激起尘土。更离谱的是,同一把枪站在二楼窗台能命中,趴到草丛里却连墙面都穿不透。
我把断点下到伤害结算函数,发现问题不在伤害公式,而在射线查询本身。引擎返回的 Hit 对象要么指向了不该命中的草叶,要么在复杂几何里提前终止。这类症状几乎所有射击游戏都遇到过:命中判定与视觉表现错位、范围技能漏掉贴身目标、拾取检测在斜坡和楼梯上反复横跳。它们表面看是 bug,根子往往是对碰撞通道和 Trace 查询的理解不够完整。
虚幻引擎把物理世界的交互抽象成两层:一层负责「你是谁」,也就是碰撞对象类型 Object Type 与响应关系 Response;另一层负责「你想查什么」,也就是 Trace 查询。把这两层拧清楚,很多怪异行为都能找到解释。这篇文章从三个具体场景出发,把 UE5.8 里的碰撞通道与 Trace 查询完整走一遍。
故事场景一:狙击枪的命中判定
影狙的瞬发子弹需要一条从摄像机出发、沿准星方向的射线。射线一路上会遇到墙面、玻璃、树叶、敌人躯干、敌人武器,甚至队友。我们希望的结果是:树叶和玻璃可以穿透或忽略,墙面和敌人躯干阻挡子弹,队友不被伤害但可能触发受击特效。
这个需求本质上是在问:射线与每个物体之间该产生什么关系?在虚幻里,答案由 Object Type 和 Response 共同决定。
Collision Object Type 与 Response
每个拥有碰撞组件的 Actor 都要声明自己的 Collision Object Type。可以把类型理解为物体在世界里的身份标签,例如 WorldStatic、WorldDynamic、Pawn、PhysicsBody、Vehicle、Destructible 等。UE5.8 保留了这些内置类型,也允许项目在 Project Settings -> Collision 里新增自定义类型。
Object Type 本身只是标签,真正决定行为的是 Response。响应关系有三种:Block、Overlap、Ignore。
- Block 表示两者相遇时会发生阻挡,Trace 会返回命中点。
- Overlap 表示两者可以互相穿过,但会触发 Overlap 事件。
- Ignore 表示两者在物理查询中形同陌路。
这里有一个容易混淆的地方:Response 不是单向的,而是成对出现的通道关系。例如把敌人 Pawn 的 Object Type 设为 Pawn,再把「Visibility」通道对 Pawn 的响应设为 Block,那么 Visibility 射线就会打在敌人身上。如果把 Weapon 对 Visibility 设为 Ignore,那么枪模型就不会挡住瞄准射线。
狙击枪的例子可以抽象成一个简单的响应矩阵。下图中,纵轴是射线所在的 Trace Channel,横轴是物体的 Object Type,交叉格子里填写响应方式。
graph LR
A["Visibility 射线"] -->|Block| B[Pawn]
A -->|Ignore| C[Weapon]
A -->|Block| D[WorldStatic]
A -->|Ignore| E[Foliage]
F["Weapon 射线"] -->|Block| B
F -->|Ignore| C
F -->|Block| D
F -->|Block| E上图只画了几种典型关系。实际项目中,Foliage 对 Visibility 常设为 Ignore,这样草叶不会遮挡瞄准;对 Weapon 则设为 Block,让爆炸物和范围伤害能扫到掩体后的草丛。响应矩阵没有绝对正确答案,取决于游戏规则。关键是矩阵一旦确定,所有依赖该通道的查询都会统一服从,不会出现某个地方写死判断、另一个地方又绕过去的混乱。
响应关系存储在 UCollisionProfile 里,运行时会根据碰撞通道或对象类型查表。这个查表过程可以概括为:先拿射线通道去匹配物体的 Object Type,再读 Response。整个过程发生在物理场景查询之前,由 Chaos 物理引擎的过滤层完成。
Collision Preset 与 Profile
手动给每个 StaticMesh 或 SkeletalMesh 设置 Object Type 和逐通道响应非常繁琐,于是 Unreal 提供了 Collision Preset。Preset 是一份预置的碰撞配置,包含一个默认 Object Type 和一组对各个通道的响应。例如 Pawn、CharacterMesh、BlockAll、OverlapAll、NoCollision 都是常见 Preset。
项目可以在 DefaultEngine.ini 的 [/Script/Engine.CollisionProfile] 段新增自定义 Preset,也可以在编辑器 Project Settings -> Collision -> Preset 里可视化编辑。自定义 Preset 的名字一旦确定,就可以在蓝图或 C++ 里直接引用。
狙击枪的子弹轨迹需要同时考虑视觉和逻辑两条线。视觉射线通常用 Visibility 通道,逻辑射线用 Weapon 通道。两者可以指向同一个 Preset,但对不同通道的响应不同。例如敌人身体的 Preset 是 Pawn,Visibility 和 Weapon 都设为 Block;队友的 Preset 也是 Pawn,但 Weapon 通道对队友的响应可以被 Gameplay 层在运行时动态改成 Ignore,从而实现友军免伤。
UE5.8 的 Collision Profile 还支持运行时切换。通过 SetCollisionProfileName 可以在代码中把角色从 Pawn 切到 Ragdoll,从 BlockAllDynamic 切到 NoCollision。切换时只需要改 Profile Name,不必逐个通道调整。这对于死亡布娃娃、技能无敌帧、穿墙闪现等机制很方便。
下面这幅架构图展示了 Object Type、Response、Preset 和 Trace 查询之间的层级关系。
flowchart TD
mesh_component["Mesh Component"]
collision_profile["Collision Preset"]
object_type["Object Type"]
response_matrix["Response Matrix"]
trace_query["Trace Query"]
chaos["Chaos 物理场景"]
mesh_component --> collision_profile
collision_profile --> object_type
collision_profile --> response_matrix
trace_query --> response_matrix
trace_query --> chaos故事场景二:法师的范围技能检测
影狙项目里的法师英雄「霜环」有一个冰爆技能:以自身为中心释放球形冲击波,半径 600 厘米,命中所有敌方 Pawn 并施加减速。实现这个技能时,策划最初要求用 LineTrace,结果发现站在角色斜上方的敌人、矮墙后的目标、甚至紧贴角色的单位都可能漏检。后来改成 SphereTrace,问题才解决。
这个场景引出了 Trace 的几何形状选择。
LineTrace / SphereTrace / CapsuleTrace
Trace 查询可以理解成把某种几何体沿一条路径扫过场景,返回与场景几何相交的信息。UE5.8 里常见的 Trace 类型有三种:
- LineTrace:一条零粗细的射线,适合瞬发子弹、视线检测、激光类技能。
- SphereTrace:以球体沿路径扫过,适合爆炸范围、技能溅射、近身拾取检测。
- CapsuleTrace:以胶囊体沿路径扫过,适合角色身形的范围检测,例如冲锋技能命中判定。
三种 Trace 的数学本质相同,都是求解运动几何体与场景几何的相交。以 SphereTrace 为例,半径为 的球体沿方向向量 移动距离 ,可以等效为把场景几何向外膨胀 后,用一条 LineTrace 去查询。这个膨胀过程在数学上叫做 Minkowski 和。球体扫过的体积 与直线段不同,命中结果也更有容错性。
霜环的冰爆用 SphereTrace 时,只需要从角色位置向周围做一次半径 600 厘米的球形扫描。命中结果会返回所有与球体相交的物体,按距离排序。这比从角色向多个方向发射线稳定得多,不会因为目标位置偏上或偏下就漏掉。
行内公式里常出现的几个量包括:射线起点 、方向 、命中参数 ,命中点 。当使用 SphereTrace 时,实际查询的是膨胀后的几何,命中距离会包含半径修正。
下面是一个 C++ 示例,展示如何在角色位置做一次球形对象查询:
UWorld* World = GetWorld();
FVector Center = GetActorLocation();
float Radius = 600.0f;
TArray<TEnumAsByte<EObjectTypeQuery>> ObjectTypes;
ObjectTypes.Add(UEngineTypes::ConvertToObjectType(ECC_Pawn));
TArray<FHitResult> HitResults;
FCollisionShape Sphere = FCollisionShape::MakeSphere(Radius);
bool bHit = World->SweepMultiByObjectType(
HitResults,
Center,
Center + FVector::UpVector * 0.1f,
FQuat::Identity,
ObjectTypes,
Sphere
);这段代码把起点和终点稍微错开一点点,是因为某些物理接口对零长度扫描有优化或限制。实际使用时,如果确实是纯球形范围检测,也可以把终点设为起点加上一个极小偏移。
Single vs Multi Trace
Trace 查询还有一个重要维度:返回单个结果还是多个结果。
- Single:只返回第一个命中对象,命中即停止。适合子弹、视线遮挡等场景。
- Multi:返回路径上所有命中对象,按距离排序。适合范围伤害、穿透射击、多目标技能。
霜环的冰爆显然需要 Multi,因为一次技能要命中多个敌人。影狙的瞬发子弹如果没有穿透设定,用 Single 更省性能。需要注意的是,Multi Trace 在密集场景下开销明显更高,因为射线要继续穿过前面的物体去查找后面的目标。如果项目中大量子弹同时发射,Multi Trace 的累积消耗会成为一个瓶颈。
下面这幅流程图总结了根据场景选择 Trace 类型和返回方式的决策路径。
flowchart TD
A["需要查询场景中的物体"] --> B{是否需要范围容错}
B -->|"是"| C["选择 SphereTrace 或 CapsuleTrace"]
B -->|"否"| D["选择 LineTrace"]
C --> E{是否需要多个命中}
D --> E
E -->|"是"| F["使用 Multi 查询"]
E -->|"否"| G["使用 Single 查询"]
F --> H["遍历 HitResults 处理伤害"]
G --> I["处理首个 HitResult"]故事场景三:拾取检测在斜坡上的反复横跳
影狙的补给箱散落在地图各处,玩家靠近后按交互键拾取。测试时发现,站在斜坡上方能拾取,走到斜坡下方同样的水平距离却提示「没有可交互目标」。查看代码,发现策划用了一个固定半径的 SphereTrace,检测以玩家胸部为中心的球形范围。
问题出在查询原点的选择。角色碰撞胶囊的中心在腰部附近,斜坡上方时补给箱中心与查询中心高度接近,能进入半径;斜坡下方时补给箱中心比查询中心低很多,水平距离虽然不变,但三维距离超出了半径。更合理的做法是把查询中心下移到脚底附近,或者根据相机方向做胶囊体检测。
这个例子说明,Trace 查询的结果不只取决于通道和响应,还取决于几何形状、查询原点、终点和半径。
通道查询与对象查询
UE5.8 的 Trace 查询分为两大类:通道查询 ByChannel 和对象查询 ByObjectType。
通道查询使用 Collision Channel,例如 Visibility、Camera、Weapon。它依赖于 Response 矩阵,可以判断射线与物体之间是 Block、Overlap 还是 Ignore。通道查询适合处理「我要查某类通道能否通过」的问题,例如视线、子弹、相机碰撞。
对象查询使用 Object Type,例如 WorldStatic、Pawn、PhysicsBody。它只关心物体的身份标签,返回与该 Object Type 匹配且响应不为 Ignore 的物体。对象查询适合处理「我要找到某类物体」的问题,例如范围内所有 Pawn、所有可拾取道具。
两者的核心区别在过滤维度。通道查询同时看射线通道和物体对该通道的响应;对象查询只看物体类型,并且默认把所有响应非 Ignore 的物体都纳入结果。在 C++ 中,前者对应 SweepMultiByChannel / LineTraceSingleByChannel,后者对应 SweepMultiByObjectType / LineTraceSingleByObjectType。
拾取检测通常用对象查询。因为补给箱的 Object Type 可以设为 Pickup,查询时只关心 Pickup 类型,不关心它是被 Visibility 阻挡还是被 Camera 忽略。这样职责清晰,拾取逻辑和视觉逻辑互不干扰。
命中结果解析
无论是通道查询还是对象查询,结果都封装在 FHitResult 里。FHitResult 包含的信息非常丰富,常用字段包括:
- bBlockingHit:是否发生阻挡命中。
- bStartPenetrating:起点是否已经在几何内部。
- Time / Distance:命中点在线段上的归一化参数和实际距离。
- Location / ImpactPoint:命中点坐标,Location 在表面,ImpactPoint 在接触点。
- Normal / ImpactNormal:命中表面法线。
- Actor / Component / BoneName:命中的 Actor、组件和骨骼名。
- FaceIndex / Item:命中三角面的索引或物理体索引。
- TraceStart / TraceEnd:本次查询的起点和终点。
对于角色受击,BoneName 很关键。影狙的爆头判定就是读取 BoneName,如果命中头部骨骼则乘以暴击系数。对于物理模拟,ImpactNormal 决定反弹方向。拾取检测里,Actor 和 Component 用来判断具体是哪个补给箱。
下面这段序列图展示了狙击枪开火后,输入处理、射线查询与命中结果解析的完整流程。
sequenceDiagram
participant Player as "玩家控制器"
participant Weapon as "武器组件"
participant World as "物理世界"
participant Target as "目标角色"
Player->>Weapon: 按下开火键
Weapon->>Weapon: 计算准星射线起点与方向
Weapon->>World: LineTraceSingleByChannel(Weapon)
World->>World: 遍历潜在碰撞体并过滤响应
World-->>Weapon: 返回 FHitResult
Weapon->>Weapon: 读取 Hit.Actor 与 BoneName
Weapon->>Target: 应用伤害与受击效果性能优化
Trace 查询是每帧高频率操作,性能敏感。UE5.8 提供了多种优化手段。
空间加速结构
虚幻的物理场景本身基于 Chaos 的空间划分,Trace 查询会优先进入可能相交的叶子节点。开发者能做的是减少查询范围。不要把 Trace 终点设得过远,能用短射线就别用长射线。对于大范围技能,先用预筛选缩小候选集,再对候选物体做精确检测。
异步查询
主线程同步 Trace 会阻塞游戏逻辑。UE5.8 支持把批量查询提交到 Chaos 的异步任务图,在下一帧或指定回调里取结果。异步 Trace 适合不依赖立即反馈的场景,例如 AI 的感知扫描、路径规划的碰撞预检。使用时要注意时序:异步结果返回时,目标位置可能已经变化。
批量查询
如果一帧内有很多同类查询,比如一百个弹片的命中判定,可以组织成批量射线一次性提交。Chaos 对批量射线有 SIMD 优化,比逐个调用单条射线更快。批量查询的另一个好处是便于统一排序和去重,减少后续逻辑分支。
通道与对象预过滤
查询时只指定必要的 Object Types 或 Channels。例如拾取检测只查 Pickup,不要查 WorldStatic。对象查询比通道查询在过滤阶段更直接,当目标类型明确时优先考虑对象查询。
性能优化的总原则是把 Trace 当成昂贵的操作,只在必要时发起,并尽量复用结果。很多项目里出现性能问题,不是因为 Trace 本身慢,而是因为每一帧对每个 AI 都发数十条射线,或者把持续性的范围检测做成了 Tick 查询。
常见踩坑
忽略 StartPenetrating
FHitResult 的 bStartPenetrating 为 true 时,表示查询起点已经在碰撞体内部。LineTraceSingle 在这种情况下可能不返回预期命中点。子弹从角色身体内部发射时尤其容易触发。常见处理方式是给射线一个极小偏移,或在代码里单独处理 penetration 情况。
混淆 Location 与 ImpactPoint
Location 是射线与碰撞体表面相交的位置,ImpactPoint 是实际接触位置。对于运动很快的物体或复杂碰撞,两者可能有细微差别。做特效生成时通常用 ImpactPoint,做后续射线延续时用 Location。
通道响应未正确配置
很多新手在 Project Settings 里改了响应矩阵,但忘记重新给已有 Actor 的 Preset 赋值,或者把自定义通道的默认响应设成了 Ignore。结果是场景里某些物体对射线完全不可见。养成习惯:新增通道后检查所有相关 Preset 的响应。
把 WorldDynamic 当成万能类型
遇到不确定该设什么 Object Type 时,有人会把所有可动物体都设成 WorldDynamic。这会让响应矩阵失去意义,因为所有东西对通道的响应都一样。应该按物体在游戏规则里的身份分类:Pawn、PhysicsBody、Vehicle、Projectile、Pickup 等。
未处理 Multi Trace 的重复命中
同一个 Actor 的多个碰撞组件可能被同一条射线命中多次。处理 Multi 结果时,建议按 Actor 去重,或者根据游戏逻辑决定是否需要多个命中点。
在 Tick 里做长距离 Trace
每帧对所有 AI 做长距离视线检测会很快拖垮性能。可以采用分时查询、基于距离的 LOD、或者使用 UE5.8 的 AI Perception 系统,把频繁检测交给专门的感知组件。
结语
回到文章开头那把诡异的影狙。我们把 Visibility 和 Weapon 通道拆开后,树叶对 Visibility 设为 Ignore、对 Weapon 设为 Block,墙面和躯干在两个通道都保持 Block,队友的 Weapon 响应在运行时按阵营动态调整。同时把瞬发子弹改成 LineTraceSingleByChannel,冰爆技能改成 SweepMultiByObjectType,拾取检测改成以脚底为中心的 SphereTrace。测试服里再也没有出现诡异的射偏和漏检。
碰撞通道和 Trace 查询本身并不复杂,核心就是三句话:先分清物体的身份,再决定通道如何响应,最后用合适的几何形状和查询方式去提问。把这套规则梳理清楚,很多物理相关的 bug 就能在配置阶段被预防,而不是在凌晨两点靠断点去追。