UE5 C++(44-3):把射线检测的枚举整明白,ECC_Visibility 和 FCollisionObjectQueryParams 别再混用
做 UE5 的 C++ 开发,只要你碰过追踪类射击、AI 视线判定、物理探测,就一定绕不开射线检测。而射线检测里有两个东西是新手最容易卡住的地方:一个是ECC_Visibility这种碰撞通道,另一个是FCollisionObjectQueryParams::InitType这个枚举。很多人写LineTraceSingleByObjectType或者LineTraceSingleByChannel的时候,参数一头雾水,复制粘贴别人代码能跑,但一旦改需求就翻车。
这篇文章我基于项目UE5 C++(44-3):射线检测时需要的枚举类ECollisionChannel,ECC_Visibility。enum FCollisionObjectQueryParams::InitType完整梳理一遍交叉通道、物体类型查询参数背后的机制。不讲虚的,直接讲清楚他们分别是什么、怎么选、哪些坑我踩过,以及一份你能直接抄的完整 C++ 示例。
适合正在学 UE5 C++ 的初学者,也适合被LineTraceSingleByObjectType的FCollisionObjectQueryParams搞晕的老哥们。你要是愿意花十分钟把这篇文章看完,以后遇到射线检测的参数配置,基本不会再犯错。
1. 碰撞通道的本质:先理解 UE5 碰撞系统怎么分层
1.1 为什么 UE5 要搞出 ECollisionChannel 这一层枚举
很多人第一次看到ECollisionChannel会被一大堆ECC_WorldStatic、ECC_WorldDynamic、ECC_Pawn、ECC_Visibility、ECC_Camera吓到,觉得没必要搞这么复杂。等我解释完,你就明白了。
UE5 的碰撞系统本质是一张矩阵。横轴和纵轴都是碰撞通道,矩阵里填的是ECollisionResponse(三个值:Ignore、Overlap、Block)。任何一个 Actor 要参与碰撞检测,至少要指定两个信息:它自己属于哪个通道(Object Type),以及它和其他通道之间怎么响应(Response)。这就是ICollisionDataProvider背后那套经典模型。
ECollisionChannel就是给这条矩阵的行和列起的名字。它是uint8枚举,引擎默认定义了二十多个通道,前几个是引擎固定通道,后面从ECC_GameTraceChannel1开始是留给游戏项目自定义的。你可以在 Project Settings 的 Collision 面板里改它们的显示名,但 C++ 里仍然通过枚举值来引用。
这里的关键理解是:通道不仅仅是一种标签,它决定了碰撞查询的性能路径和过滤方式。物理引擎做射线检测时,如果只用某种类型的通道查询,就能提前跳过大量无需检测的 Actor,这正是ECollisionChannel存在的意义。
1.2 常用内建通道的区分:Visibility、Camera、Pawn、WorldStatic
内建通道里最常用的是几个:
ECC_WorldStatic:静态几何体,大部分场景静态网格底座都在这个通道。ECC_WorldDynamic:动态物体,比如可以被推动的箱子、移门。ECC_Pawn:玩家角色和 AIController 控制的 Pawn 默认属于这个通道。ECC_PhysicsBody:带物理模拟的物体。ECC_Visibility:专门用来做可见性检测的追踪通道。ECC_Camera:专门用来做相机防遮挡检测的追踪通道。
这里面特别容易困惑的是ECC_Visibility和ECC_Camera。它们不是实际的物体通道,而是“查询用的追踪通道”。什么意思呢?就是这条通道不是让你把一个 Actor 的 Object Type 设成 Visibility,而是让你在发起射线时把TraceChannel设成ECC_Visibility,然后检测这条射线碰到的物体。
引擎里默认的碰撞响应矩阵中,ECC_Visibility对ECC_WorldStatic是 Block,对ECC_Pawn是 Ignore。这个设计是故意的:Visibility 射线用来判断“我能不能看见这个位置”,而不希望被角色自己挡住。而ECC_Camera的默认矩阵则会把角色设成 Block,专门用于第三人称相机防止穿墙。
所以如果你希望射线能检测到角色,就不能默认用ECC_Visibility,得要么修改碰撞响应,要么使用FCollisionObjectQueryParams针对ECC_Pawn做对象查询。
1.3 Object Type 和 Trace Channel 的差别
碰撞通道里还有一对隐含概念:Object Type(对象类型)和 Trace Channel(追踪通道)。
Object Type 描述的是 Actor 本身的分类。每个可碰撞对象在创建时指定自己是哪个 Object Type,比如ECC_WorldDynamic。Trace Channel 描述的是查询方发起的追踪所使用的通道。一个射线检测函数会传入 Trace Channel,物理引擎拿这个通道去和场景里每个 Actor 的 Object Type 比对,再查询碰撞响应矩阵,得出这次检测是否命中。
换句话说,一个碰撞体有两个身份:它是什么(Object Type),以及别人发起的查询是否对它生效(由矩阵决定)。大多数新手犯的错误是把它们混为一谈,设置完一个 Actor 的 Object Channel 之后,却忘了检查查询时用的 Trace Channel 在矩阵里对这个 Object Channel 是不是 Block。
提示:修改 Project Settings 里的 Collision 面板可以自定义各通道之间的响应关系,但 C++ 查询里的通道枚举值不会变,修改的是矩阵行为,不是通道数量。
2. FCollisionObjectQueryParams 和它的 InitType 枚举
2.1 FCollisionObjectQueryParams 解决什么问题
刚才说的都是按 Trace Channel 查询。但实际开发里你常常会面临这样的需求:不想关心 Trace Channel 的矩阵配置,只想检测“射线碰到了哪些指定类型”。比如我想让射线只命中ECC_Pawn和ECC_WorldDynamic,不检测地面和静态网格;或者我又想检测静态,又不想检测 Pawn。
这时如果还通过 Trace Channel 和矩阵来做,你得在 Project Settings 里反复调矩阵,很容易影响到其他系统。UE5 提供了另一条路:Object Query。也就是直接指定需要检测的 Object Type 列表,物理引擎只对这些类型做检测。这个查询参数正是FCollisionObjectQueryParams。
这个结构体里保存了一个内部掩码,表示需要检测的 Object Type 集合。射线检测函数拿到它之后,会跳过不需要检测的类型,效率更高,也更灵活。
FCollisionObjectQueryParams的默认构造函数生成一个“空查询”,不会检测任何类型,因为内部的ObjectTypesToQuery值为0。所以如果你直接FCollisionObjectQueryParams QueryParams;然后传给LineTraceSingleByObjectType,大概率什么也检测不到。
2.2 enum InitType 的三种取值详解
这是FCollisionObjectQueryParams的核心构造逻辑。它有一个专门控制初始化方式的枚举:
enum InitType { AllObjects, AllStaticObjects, AllDynamicObjects };这个枚举不是用来直接赋值的普通枚举,而是给构造函数的第二个参数。构造函数原型大致如下:
FCollisionObjectQueryParams(enum InitType InType, EObjectTypeQuery InObjectType = EObjectTypeQuery::ObjectTypeQuery_MAX);也就是说你调用时是这么用的:
FCollisionObjectQueryParams(FClollisionObjectQueryParams::AllObjects); FCollisionObjectQueryParams(FClollisionObjectQueryParams::AllStaticObjects); FCollisionObjectQueryParams(FClollisionObjectQueryParams::AllDynamicObjects);AllObjects:对引擎里所有 Object Type 都进行检测。AllStaticObjects:只检测所有标记为静态的 Object Type,常见的是ECC_WorldStatic。AllDynamicObjects:只检测所有动态类型,比如 Pawn、WorldDynamic、PhysicsBody。
这三个选项背后对应的是FCollisionObjectQueryParams内部ObjectTypesToQuery掩码的预设值。选AllObjects时掩码全部置 1,选AllStaticObjects时只把静态相关类型的位打开,选AllDynamicObjects时只打开动态相关。
使用的时候要注意,EObjectTypeQuery参数是配合InitType使用的。如果你用了AllObjects,后面跟的InObjectType通常会忽略,因为所有类型都已经纳入了。如果你用AllStaticObjects或者AllDynamicObjects再配一个具体类型,实际上也是用预设的模式。更常见的是,直接用另一个构造函数重载来指定单类型或多类型组合:
FCollisionObjectQueryParams(EObjectTypeQuery InObjectType); FCollisionObjectQueryParams(const TArray<EObjectTypeQuery>& ObjectTypes);菜鸟常见误区是把InitType当成了独立枚举来用,比如写if (InitType == AllObjects),这是错的。它只是构造函数参数的一个标记,告诉结构体从哪种预设模式开始。
2.3 EObjectTypeQuery 和 ECollisionChannel 的关系
EObjectTypeQuery这是一个 UHT(Unreal Header Tool)支持的枚举,跟运行时引擎枚举ECollisionChannel是两个不同的东西。FCollisionObjectQueryParams的构造函数并不直接接收ECollisionChannel的通道值,而是接收EObjectTypeQuery。
你可能会觉得很绕,为什么不能直接传ECC_WorldDynamic?因为EObjectTypeQuery是蓝图可用的枚举,被标记了UENUM(BlueprintType),而ECollisionChannel是引擎底层广泛使用的枚举,两者数值定义不同。UE5 提供了一个转换函数:
UEngineTypes::ConvertToObjectType(ECollisionChannel Channel);例如:
EObjectTypeQuery MyQueryType = UEngineTypes::ConvertToObjectType(ECC_Pawn);拿到EObjectTypeQuery才能丢进FCollisionObjectQueryParams。同样也有反方向:
ECollisionChannel MyChannel = UEngineTypes::ConvertToCollisionChannel(MyQueryType);所以你看,其实整个系统是两套表述方式:
- 通道视角:
ECollisionChannel+ 碰撞响应矩阵。 - 对象查询视角:
EObjectTypeQuery+FCollisionObjectQueryParams掩码。
这两套方式最终面向的是同一个物理场景数据,只是查询引擎的路径不一样。后面实践部分我会分别展示。
3. 实战配置:Raycast 函数选型与代码完整示例
3.1 三条常用射线检测函数怎么选
UE5 的UKismetSystemLibrary和UWorld都提供了一系列射线检测。C++ 里通常会直接调UWorld的方法,常见的有这几个:
bool LineTraceSingleByChannel(const FVector& Start, const FVector& End, ECollisionChannel TraceChannel, const FCollisionQueryParams& Params, FHitResult& OutHit); bool LineTraceSingleByObjectType(const FVector& Start, const FVector& End, const FCollisionObjectQueryParams& ObjectQueryParams, const FCollisionQueryParams& Params, FHitResult& OutHit); bool LineTraceSingleByProfile(const FVector& Start, const FVector& End, FName ProfileName, const FCollisionQueryParams& Params, FHitResult& OutHit);它们的区别很好理解:
LineTraceSingleByChannel用 Trace Channel 发起查询,适合符合引擎默认碰撞设计、需要依赖配置矩阵的通用检测。比如你想要一次纯粹“符合项目碰撞规则”的检测,就用它。LineTraceSingleByObjectType用 Object Query 发起查询,适合只关心某种类型的场景,例如只检测Pawn,不检测墙壁。LineTraceSingleByProfile用碰撞配置文件(Collision Profile)发起查询,适合在 Project Settings 的 Profile 里定义好一批规则后按名字使用。
标题里提到的ECC_Visibility和FCollisionObjectQueryParams正好对应前两种。很多 AI 视觉、抓取判定、命中判定的逻辑,都需要这两条路协同。
3.2 完整示例:同时用 Visibility 通道和 Object Query 做双层检测
下面这段是我在项目里常用的一套逻辑:先做ECC_Visibility通道射线检测,排除遮挡物;然后用FCollisionObjectQueryParams只对ECC_Pawn做对象查询,判断目标是否是角色。
// RaycastUtils.h #pragma once #include "CoreMinimal.h" #include "Engine/World.h" #include "CollisionQueryParams.h" #include "Engine/EngineTypes.h" #include "GameFramework/Actor.h" #include "RaycastUtils.generated.h" UCLASS() class MYGAME_API URaycastUtils : public UBlueprintFunctionLibrary { GENERATED_BODY() public: UFUNCTION(BlueprintCallable, Category = "Raycast|Utils") static bool VisibilityCheckAndFindPawn( UWorld* World, const FVector& Start, const FVector& End, AActor* IgnoreActor, FHitResult& OutHitResult, APawn*& OutPawn) { if (!World) { return false; } FCollisionQueryParams Params; Params.bTraceComplex = false; Params.AddIgnoredActor(IgnoreActor); // 第一次检测:ECC_Visibility FHitResult VisibilityHit; bool bVisibilityBlocked = World->LineTraceSingleByChannel( VisibilityHit, Start, End, ECC_Visibility, Params ); if (bVisibilityBlocked) { // 说明中间有东西挡住了 OutHitResult = VisibilityHit; return false; } // 第二次检测:ObjectType 只查 Pawn FCollisionObjectQueryParams PawnQuery; PawnQuery.AddObjectTypesToQuery(ECC_Pawn); // 等价写法: // PawnQuery.AddObjectTypesToQuery(UEngineTypes::ConvertToObjectType(ECC_Pawn)); // 或者 FCollisionObjectQueryParams PawnQuery(TArray<EObjectTypeQuery>{ EObjectTypeQuery::ObjectTypeQuery_Pawn }); FHitResult PawnHit; bool bHitPawn = World->LineTraceSingleByObjectType( PawnHit, Start, End, PawnQuery, Params ); if (bHitPawn) { OutHitResult = PawnHit; OutPawn = Cast<APawn>(PawnHit.GetActor()); return true; } return false; } };这段逻辑非常典型:先用ECC_Visibility判断可见性,避免穿过墙壁直接命中墙壁后面的 Pawn;再用 Object Query 精确只命中角色。这在第三人称射击的准星检测、AI 索敌中都很实用。
注意FCollisionObjectQueryParams的AddObjectTypesToQuery方法,它接收ECollisionChannel参数,内部做转换。所以你也可以直接传ECC_Pawn,少写一次转换。
3.3 参数细节:bTraceComplex、IgnoreActor 和对象掩码的取舍
上面代码里Params.bTraceComplex = false是很多人忽略的细节。它表示射线检测只对简单碰撞体(Primitive Component 的简单碰撞形状)进行测试,而不是对每个三角形的复杂碰撞体做测试。绝大多数场景用 false 就够了,性能更好,而且结果是稳定的。
Params.AddIgnoredActor(IgnoreActor)用来忽略发起者自身或某个指定 Actor。在 FPS 里如果不忽略射手自己,射线很可能会命中自己的胶囊体,或者命中自己手上的武器。这里有一个容易遇到的问题:忽略 Actor 只会忽略这个 Actor 上所有 Primitive 组件,但如果你有多个组件被同一个物理查询命中,你需要FHitResult.Component来做进一步区分。
关于对象掩码,FCollisionObjectQueryParams支持一次性添加多个类型:
FCollisionObjectQueryParams QueryParams; QueryParams.AddObjectTypesToQuery(ECC_WorldDynamic); QueryParams.AddObjectTypesToQuery(ECC_Pawn); QueryParams.AddObjectTypesToQuery(ECC_PhysicsBody);这种写法比构造TArray<EObjectTypeQuery>再传入更直观。
3.4 InitType 的实际使用场景演示
InitType虽然构造参数简单,但在某些场景下很省事。例如做破坏效果检测时,我们可能只想访问所有动态物体:
FCollisionObjectQueryParams DynamicQuery(FCollisionObjectQueryParams::AllDynamicObjects); FHitResult Hit; bool bHitDynamic = World->LineTraceSingleByObjectType( Hit, Start, End, DynamicQuery, Params );或者做全场景扫描,在编辑器工具中统计所有可碰撞物体:
FCollisionObjectQueryParams AllQuery(FCollisionObjectQueryParams::AllObjects); TArray<FHitResult> Hits; World->LineTraceMultiByObjectType( Hits, Start, End, AllQuery, Params );这里最需要注意的是LineTraceMultiByObjectType返回的所有命中数组是未排序的,如果你想按距离排序,可以用FHitResult.Time或者手动重排。
4. 碰撞矩阵配置:Project Settings 里的坑与建议
4.1 默认矩阵下 ECC_Visibility 对 Pawn 是 Ignore
前面说过,ECC_Visibility默认对ECC_Pawn是 Ignore。这意味着你用LineTraceSingleByChannel(..., ECC_Visibility, ...)检测:
- 能命中地面、静态网格、部分动态障碍。
- 打不到场景里的其他角色(除非角色被修改过碰撞响应)。
这会在联机对战或 AI 索敌时造成很妖的问题:射线明明对准了敌人,可就是没命中。这不是射线写错,而是碰撞通道矩阵默认行为。
解决方案有三种:
- 在 Project Settings 的
Collision面板里,找到Visibility那一行,把对Pawn的响应从 Ignore 改为 Block。 - 用
FCollisionObjectQueryParams绕开矩阵,直接检测ECC_Pawn。 - 给 Pawn 单独挂一个额外碰撞体,放在自定义通道里,并让 Visibility 对它 Block。
方案 2 是最干净的,不污染全局配置,推荐优先使用。方案 1 适合整个项目统一“能看见Pawn”的语义,但要小心可能影响所有 Visibility 检测。
我碰到过一个真实翻车案例:项目早期发现墙壁后面的敌人能被狙到,就是因为美术同学在设置碰撞时顺便把 Visibility 对 Pawn 改成了 Block,导致射线穿透了墙体。所以全局矩阵的修改影响面很大,改之前一定要列一份所有使用该通道的系统清单。
4.2 自定义通道怎么加、怎么用、怎么命名
如果你需要新的碰撞通道,在 Project Settings 里往下拉,能看到Game Trace Channel 1到Game Trace Channel 18。默认情况下这些通道没有名字,只在数值上占用ECC_GameTraceChannel1到ECC_GameTraceChannel18。你可以双击它们的显示名称,改成Interaction、Bullet、View等等。
改完名字后,C++ 里可以直接用枚举常量吗?不行。ECC_GameTraceChannel1这种枚举值不变,但你可以用:
ECollisionChannel MyChannel = ECollisionChannel::ECC_GameTraceChannel1;虽然名字还是 Channel1,但你可以在注释里标明这代表Interaction通道。Blueprint 里使用时会显示你已经改好的名字,更方便。
如果要更语义化,项目里通常会封装一层全局函数:
static ECollisionChannel GetInteractionChannel() { return ECollisionChannel::ECC_GameTraceChannel1; }这样做的好处是,万一编辑器里通道顺序调整(比如从 Channel1 变成 Channel2),只需要改这一处封装。
4.3 多通道组合查询的注意事项
当你用FCollisionObjectQueryParams添加多个类型时,物理引擎会做按位与过滤。它的内部掩码最高支持 64 个通道(取决于 64 位机器上的FCollisionQueryFlag),所以通常不用担心位数不够。
但是有一个性能隐患:添加的类型越多,物理引擎需要测试的碰撞体越多。尤其是在射击游戏的子弹检测里,如果一次LineTraceMulti同时检测 Pawn、WorldDynamic、PhysicsBody、Destructible,命中数量会显著增多。你需要在逻辑层及时过滤掉无关结果。
我还建议:对于高频检测(例如每秒 60 次的准星射线),尽量使用 Object Query 而不是 Channel 查询,因为 Object Query 在内部可以直接跳过不匹配类型的物理体,实际性能表现通常更好。
另一个注意点是FCollisionObjectQueryParams::AllObjects不代表“能穿透一切障碍”,它代表“对所有 Object Type 都进行检测”。射线仍然会被第一个 Block 的碰撞体挡住,因为这是碰撞响应矩阵决定的。要穿透多个物体,你需要用LineTraceMultiByObjectType,然后自己从结果数组里取你需要的那一个。
5. 常见报错和诡异现象的排查实录
5.1 一直检测不到任何对象
如果你用FCollisionObjectQueryParams但怎么都检测不到,八成是构造函数用错了:
FCollisionObjectQueryParams WrongParams; // 默认掩码是 0这时候LineTraceSingleByObjectType大概率直接返回 false 或者命中无效结果。解决办法:初始化时传入AllObjects,或者调用AddObjectTypesToQuery。
还有一个很不起眼但经常翻车的点:你在调用LineTraceSingleByObjectType时把ECC_Visibility当成参数传给了FCollisionObjectQueryParams的构造函数。看一下构造函数签名,发现根本不匹配,编译器报错一长串。看仔细点,构造函数接收EObjectTypeQuery,不是ECollisionChannel。
5.2 初始化了 AllObjects 还是检测不到
有个人碰到过:FCollisionObjectQueryParams(AllObjects)配LineTraceSingleByObjectType依然什么都检测不到。最后发现是FCollisionQueryParams里设置了bTraceComplex为 true,且碰撞组件只开了复杂碰撞,而复杂碰撞体本身有问题导致实际几何体为空。
这个场景比较少见,但也提醒我们:当射线检测异常时,先逐步排查:
- 确认碰撞体存在且可见(可能被隐藏了但碰撞还在)。
- 确认
CollisionEnabled是QueryOnly或QueryAndPhysics。 - 确认
bGenerateOverlapEvents不直接影响射线,但检查bCallRigidBodySleepEvents这类选项没有副作用。 - 确认
Visibility通道不是被整体关闭了。
在引擎里临时开一下Show Collision命令(PIE界面按0,或者控制台vis collision相关指令),可以看到实际碰撞体,排查效率极高。
5.3 射线命中不准确、穿过缝隙或边缘抖动
有些地形是多个静态网格拼接的,加上碰撞体间隙,会导致射线在缝隙处穿透。解决办法:
- 用
bTraceComplex时对静态网格做复杂检测,能减少缝隙穿透,但性能开销大。 - 给关键地形块加上
Async物理体,或者使用Chaos的Accurate模式。 - 更实际的做法是:检测到命中后,对
FHitResult.ImpactPoint做一次小范围球体检测,确认周围接触面。
对于高速移动的子弹射线,抖动多半是因为使用了LineTraceSingleByChannel但碰撞体本身有细微的物理更新延迟。可以给子弹做SweepSingleByChannel用胶囊体或球体做扫掠,而不是纯射线。
5.4 多阶段查询的顺序陷阱
当你要先做ECC_Visibility检测,再对命中点做对象查询时,顺序很容易搞反。我的建议是:
- 先 Visibility 通道检测,得到遮挡信息。
- 如果被遮挡,再根据业务决定是否继续检测(比如墙壁后面的人也要报警)。
- 如果没被遮挡,再 Object Query 精确找 Pawn。
有些系统反而要先 Object Query 找到目标,再 Visibility 确认可见。这两种顺序在不同游戏交互里结果相似,但因为返回的FHitResult不一样,后续逻辑分支也会有差异。写代码前想清楚你要的是“可见且可命中”,还是“最近的目标是否可见”。
6. 性能优化与常用技巧
6.1 射线检测的频率控制
射线检测本身不贵,但高频跑(比如每一 tick 都做多次)依然可能吃性能。我通常给 AIController 或玩家准星射线加一个最小间隔:
float Timer = 0.0f; const float RayInterval = 0.05f; void Tick(float DeltaTime) { Timer += DeltaTime; if (Timer >= RayInterval) { Timer = 0.0f; PerformRaycast(); } }或者用FTimerManager设置定时器,避免每个 tick 都算。对竞技类射击这种要求极低延迟的,可以每帧跑,但要做好采样点合并。
6.2 使用 FCollisionQueryParams 的 bReturnPhysicalMaterial
如果你要做不同材质反馈,比如子弹打在金属上火花、木头上木屑,就需要bReturnPhysicalMaterial = true:
Params.bReturnPhysicalMaterial = true;拿到的FHitResult.PhysMaterial可以获取UPhysicalMaterial,再查它的属性来做音效和特效切换。这个开关会轻微增加开销,只在需要时打开。
6.3 使用 UWorld:: LineTraceSingleByChannel 和 AsyncTrace 的取舍
这里额外提一句异步追踪。UE5 提供:
FSingleLinearSweepAsyncUserData和各种AsyncTrace 支持,适合在物理模拟复杂、需要避免主线程卡顿的场景。但异步会引入回调时机问题,尤其是销毁对象时要特别小心回调里访问已失效的 Actor。我个人建议,除非你真的在 profiler 里看到红线,否则用同步射线就够了,简单可靠。
7. 从源码角度看一下 FCollisionObjectQueryParams 的实现机制
7.1 构造函数和掩码
打开引擎源码Engine/Source/Runtime/Engine/Classes/Engine/EngineTypes.h,能看到FCollisionObjectQueryParams定义。关键成员是:
FCollisionQueryFlag ObjectTypesToQuery;FCollisionQueryFlag本质上是一个可以和 64 位掩码互转的封装类型。构造函数:
FCollisionObjectQueryParams(enum InitType InType, EObjectTypeQuery InObjectType) { switch (InType) { case AllObjects: ObjectTypesToQuery = FCollisionQueryFlag::GetAllObjectsMask(); break; case AllStaticObjects: ObjectTypesToQuery = FCollisionQueryFlag::GetAllStaticObjectsMask(); break; case AllDynamicObjects: ObjectTypesToQuery = FCollisionQueryFlag::GetAllDynamicObjectsMask(); break; } }所以AddObjectTypesToQuery其实就是对ObjectTypesToQuery进行按位或操作。
7.2 从掩码到实际碰撞检测
物理引擎拿到ObjectTypesToQuery后,在FPhysScene的碰撞查询里使用QueryFilter进行过滤。简单说来就是:
- 对每个潜在碰撞体,先查它的 Object Type 是否在掩码位里。
- 不在则直接跳过,不参与后续碰撞响应检测。
- 在则继续做碰撞形状的相交检测。
这个机制让你可以放心地在高频率查询里指定少量类型,减少无效的几何体相交测试。
7.3 为什么要区分 UEngineTypes::ConvertToObjectType
EObjectTypeQuery在蓝图里被序列化为名字,和编辑器里的 Object Type 名称对应。而ECollisionChannel是完整矩阵通道。两者数值表示不一定相同,所以转换函数的存在是为了弥合 BP 和 C++ 两套体系的鸿沟。
一个有意思的细节是,EObjectTypeQuery的默认值对应ECC_WorldStatic的 “Object Type” 位,而ObjectTypeQuery_MAX常用来表示未指定。
8. 一些来自项目里的经验总结
8.1 关于 ECC_Visibility 的默认语义
在多人游戏里,玩家角色的 Pawn 默认对 Visibility 是 Ignore,这确实容易造成“能穿过队友狙击”的错觉。但如果你把 Visibility 对 Pawn 改成 Block,又要防止队友挡子弹,需要在子弹检测时单独忽略队友的通道或 Actor。这种博弈很常见,没有全局最优解,关键是把设计意图在代码注释里写清楚。
8.2 阅读FCollisionObjectQueryParams源码的收益
我建议每个 UE C++ 开发者都花一点时间读一遍EngineTypes.h里ECollisionChannel、ECollisionResponse、FCollisionObjectQueryParams这三个核心定义。读完你会发现:
- 碰撞系统不是魔法,是一张可预测的矩阵。
- 对象查询不是另一个物理系统,而是对同一套物理体的过滤视图。
- 所有复杂 bug 几乎都能归因到“矩阵没配好”或“掩码没设对”这两类。
8.3 写一个自己的调试工具
最后分享一个提升效率的小技巧:写一个DrawDebugLine封装的调试函数,在射线检测前把起点终点画出来,命中点画一个红点。这样你能在 PIE 里直观看到射线实际路径,排错效率翻倍。
void DrawDebugRay(UWorld* World, const FVector& Start, const FVector& End, const FHitResult* Hit, float Duration = 5.0f) { if (!World) { return; } DrawDebugLine(World, Start, End, FColor::Cyan, false, Duration, 0, 2.0f); if (Hit && Hit->bBlockingHit) { DrawDebugSphere(World, Hit->ImpactPoint, 8.0f, 12, FColor::Red, false, Duration, 0, 2.0f); } }把这类函数放在你的工具库中,任何射线问题都能快速可视化。
最后再纠正一个很多教程没讲透的概念:拿ECollisionChannel去推敲FCollisionObjectQueryParams::InitType的枚举值是不对路的。两者本质是不同查询路径的入口参数,InitType负责预设掩码,ECollisionChannel负责矩阵索引。把它们理解成两套并行的“碰撞口味选择器”,你的射线代码才能真正稳定可控。