在UE5里做射线检测,最容易被忽视但是影响最大的一个点是什么?我自己的答案是:通道(Channel)。很多朋友第一次写LineTraceSingleByChannel,参数直接填ECC_Visibility,然后往场景里打一条射线,发现什么都没有命中;换成了ECC_WorldDynamic,又发现角色、物理物件、掉落物全都被打中了,毫无筛选逻辑。这种情况十有八九就是没搞清楚ECollisionChannel和FCollisionObjectQueryParams::InitType这两个枚举到底在碰撞系统中扮演什么角色。
这期内容我准备把这套东西彻底讲明白。不光是枚举值列表,还包括它们在碰撞预设(Collision Preset)里的关联、基于通道和基于对象类型的两种射线检测写法、以及我在实际项目里踩过的那些“射线明明穿过物体却不命中”的坑。适合正在用C++写玩家交互、AI视线、武器射击、物体拾取这类功能的开发者参考,照着抄就能少走弯路。
1. 先把两个枚举的身份搞清楚:ECollisionChannel与InitType
1.1 ECollisionChannel:给场景里的物体发“身份证”
ECollisionChannel是UE5整个碰撞系统的基础分类机制。它本质上是一个UENUM枚举,用来标记一个Actor或组件“属于什么类型”。例如静态网格属于ECC_WorldStatic,可移动的Actor属于ECC_WorldDynamic,角色胶囊体属于ECC_Pawn,物理模拟物体属于ECC_PhysicsBody。
这里的重点在于:通道不仅仅是“身份标签”,它还参与碰撞响应矩阵的判定。每个带碰撞组件的Actor,都会维护一张“响应表格”,里面记录了它对这个世界的所有通道分别采取什么行为——Ignore是直接无视,Overlap是重叠但不阻挡,Block是阻挡并产生碰撞事件。射线检测在底层做碰撞查询时,就是要去看“你查询的这个通道,命中物体的响应到底是不是Block”。
所以当你设置Actor的碰撞预设时,看到的Object Type下拉框,本质上就是在选择一个ECollisionChannel枚举值;下面那一大串Collision Responses矩阵,就是对这个Actor在收到各种通道查询时的响应策略。这个机制设计出来的意义很直接:让不同物体天然具备不同的碰撞特性,而不需要每次都写一堆条件判断。入门阶段记不住那么多枚举不要紧,只要先抓住一点——射线检测是否命中一个Actor,取决于该Actor的碰撞组件对射线查询的通道是否响应为Block。
1.2 FCollisionObjectQueryParams::InitType:基于对象类型筛选的查询开关
FCollisionObjectQueryParams这个结构体是另一套查询体系的核心参数。它与普通通道查询不同:通道查询是拿着一个通道去“问”场景里的物体“你阻挡我吗”,而对象类型查询是反过来,先列出一批物体类型,场景里所有属于这些类型的物体都会被纳入候选范围。
InitType枚举就是用来快速构造查找范围的一个开关。它提供了三个预设值:AllObjects表示查询所有类型的物体,AllStaticObjects表示只查静态物体(集中在ECC_WorldStatic),AllDynamicObjects表示查询除了静态物体之外的可动类型(比如ECC_WorldDynamic、ECC_Pawn、ECC_PhysicsBody)。
实际项目中,我们经常配合LineTraceSingleByObjectType使用。我见过不少刚转C++的人把这两个查询体系混着用,结果要么查不到、要么查出来一堆不想要的东西。简单的区分方法:如果你要“只检测角色”,用对象类型查询传入ECC_Pawn,比通道查询更精准;如果你要“判断有没有东西遮挡视线”,那通道查询用ECC_Visibility就是行业惯例。
1.3 ECC_Visibility凭什么成为射线检测的默认主角
ECC_Visibility这个通道被设计出来,本质就是为“可见性判断”服务的。AI索敌、狙击镜头遮挡、弹道穿透判断这些功能,都需要一条射线从起点到终点,检测中间是否有会挡住视线的物体。你会发现项目默认的碰撞预设里,很多静态网格、墙体、地面默认对ECC_Visibility都是Block响应,这就意味着射线只要碰到它们就能被拦下来。
但它也不是万能的。因为很多Actor的默认预设并不会对ECC_Visibility做Block响应,例如某个人形角色的默认碰撞预设是Pawn,其响应矩阵里对ECC_Visibility往往是Overlap,导致你用Visibility通道根本砸不到它。这也是新手最常见的“为什么射线穿过角色身体”的原因之一。在后面的实操章节,我会给出具体的配置解决办法。
2. 通道全景图:从内置枚举到自定义通道的配置要点
2.1 内置通道逐个拆解:从ECC_WorldStatic到ECC_MAX
按UE5源码里ECollisionChannel的定义顺序,内置通道可以分成这几类用途:
ECC_WorldStatic:静态世界物体,比如地形、建筑、静态网格。这类物体运行时一般不会移动,碰撞成本低,适合做射线检测的“兜底目标”。ECC_WorldDynamic:动态世界物体,比如可破坏物、可移动的门、机关。它与ECC_WorldStatic的边界在实际项目中经常需要自己定义。ECC_Pawn:玩家和AI角色的胶囊体通道。交互类射线检测、近战范围检测都很依赖它。ECC_Visibility:可见性检测通道,我上面已经强调过,它是视线遮挡类功能的默认通道。ECC_Camera:摄像机通道。主要用于第三人称摄像机防穿墙,通常独立配置,避免影响其他游戏逻辑。ECC_PhysicsBody:物理模拟物体的通道,例如被子弹击飞的小道具、掉落物、载具碎片。ECC_Vehicle:载具专用通道,配合载具移动组件使用。ECC_Destructible:可破坏物通道,主要对应APEX Destruction Mesh等可破碎网格。ECC_EngineTraceChannel1到ECC_EngineTraceChannel6:引擎级追踪通道,面数不多,通常是给某些官方插件或内部系统保留的。普通项目尽量不要占用。ECC_GameTraceChannel1到ECC_GameTraceChannel18:游戏自定义通道,项目里自己取名、自己用。ECC_Overlap_Deprecated:遗留的Overlap通道,基本废弃,不要使用。ECC_MAX:这不是真正的通道,是枚举的边界值,用于校验和遍历。
实际开发里,我最常用的组合就是ECC_Visibility做视线检测、ECC_Pawn做近战判定、ECC_WorldDynamic做可交互机关的命中判定,再留两个自定义通道给武器穿透和体积物拾取。记住这些通道之间的边界不是绝对的,策划和程序要在项目初期统一碰撞预设规范。
2.2 自定义通道:ECC_GameTraceChannel1~18的配置入口
自定义通道在代码里虽然还是ECC_GameTraceChannel1这样的名字,但你在编辑器里看得见的是你命名的字符串。入口在项目设置这个位置:Project Settings -> Engine -> Collision。
点击右侧的New Object Channel或New Trace Channel按钮,会弹出一个对话框,里面可以填写Name,系统会分配一个未使用的ECC_GameTraceChannel编号。我建议命名规范里带上用途前缀,比如Trace_Weapon、Object_Collectible,这样在碰撞矩阵里一眼就能认出用途。
这里有个非常重要的区分:Object Channel会占“身份类型”的坑,意味着你可以把某个Actor的Object Type设置为这个自定义对象类型;而Trace Channel不参与Object Type身份,它只会出现在碰撞响应矩阵里,专门用来做射线查询。所以做射线专用通道时,优先创建Trace Channel,省得干扰其他对象的类型判断。
我一般在项目里固定创建两个Trace Channel:一个叫WeaponTrace,用于子弹和近战武器的命中检测;一个叫InteractionTrace,用于玩家与场景物件的交互拾取。然后在武器武器命中时需要被命中的墙体、箱子、可破坏物预设里,把这两个通道都设为Block,人物角色反而设为Ignore,这样子弹不会误中自己或队友。
2.3 碰撞预设与通道响应矩阵的关联
碰撞预设就是一套可复用的通道响应配置。你可以在Project Settings -> Collision -> Presets里看到UE自带的那些:Default、BlockAll、Pawn、Visibility、QueryOnly等等。每个预设都包含三部分:该物体的Object Type、对所有通道的响应方式、以及一些如True的碰撞开关开关。
当你创建Actor时,Mesh组件的Collision Preset一旦选好,就自动获得了那一整套响应矩阵。例如预设Visibility的定义通常是:Object Type为ECC_WorldDynamic,对ECC_Visibility是Block,对其他大多数通道是Ignore或Overlap。所以射线用ECC_Visibility打在这种物体上,是能命中的。
但你要知道,预设只是快捷方式。真正起决定性作用的是碰撞组件里那张矩阵表。你在检测时踩到“为什么射线穿模”的坑时,一定要去查看目标Actor的实际响应矩阵,而不是只盯着预设名字。我整理过一个规律,排查时不自觉就按这个思路找:
- 确认目标Actor的Object Type是什么
- 确认目标的响应矩阵里,对查询通道是Ignore、Overlap还是Block
- 确认射线起点是否已经在目标内部、或与目标距离极近
整个排查习惯建立起来之后,射线检测的命中率能提升一个量级。
2.4 Trace Channel与Object Channel的分工逻辑
在UE5的碰撞体系里,两类通道是平行存在的。Trace Channel主要用于射线、扫描这类查询操作,Object Channel用于确定一个物体属于什么类型。查询时,如果你的函数叫LineTraceSingleByChannel,那传入的ECC_XXX一般指Trace Channel;如果你的函数叫LineTraceByObjectType,那传入的类型就要是某个Object Channel的通道。
很多人不理解为什么要有Object Channel这一套。举个实际例子:当你想让一束射线“只打到可拾取物体,跳过普通墙面、角色、载具”时,如果用Trace Channel,你得给所有“墙”配Ignore,给可拾取物配Block。而用Object Channel,你只需要在查询参数里填入可拾取物的Object Type,其他类型自动被排除,不需要去动碰撞矩阵。所以对象类型查询在筛选精确目标时很明显更高效,但前提是你得理解FCollisionObjectQueryParams的构造方式与InitType预设。这就引出了下一章的核心内容。
3. 基于Channel的射线检测实操:LineTraceSingleByChannel
3.1 函数签名与关键参数逐个说明
先看一个基础接口,UE5的C++函数原型大致是这样的:
bool UWorld::LineTraceSingleByChannel( FHitResult& OutHit, const FVector& Start, const FVector& End, ECollisionChannel TraceChannel, const FCollisionQueryParams& Params, const FCollisionResponseParams& ResponseParams ) const;重要的参数就几个:OutHit是结果结构体,所有命中数据都会塞进去;Start和End分别是射线起点和终点;TraceChannel就是你要拿哪个通道去撞物体;FCollisionQueryParams用来做忽略、过滤、调试等高级控制。
我建议第一行就要搞清楚一个事:这个函数的返回值是布尔值,表示“是否命中”,但真正有信息量的内容全在FHitResult里。举个例子,你判断命中了一个Actor,但你要知道具体命中哪个组件、哪个骨骼、命中点在哪个位置,这些只有FHitResult能给你。所以我写项目时,几乎从不直接拿返回值做业务判断,而是先看HitResult.bBlockingHit,再取组件和骨骼名。
起点和终点用世界坐标。如果起点在场景网格内部,射线会直接检测不到那个网格——这是很重要的一个细节。武器射击检测时,如果枪口模型的World Transform有偏差,射线起点插进墙里,就会出现“向墙射击永远命中不了墙”的诡异问题。排查时可以在编辑器里用DrawDebugLine画出射线,一眼就能看出起点是否合理。
3.2 命中结果FHitResult里有哪些信息值得读
FHitResult的常用字段我整理成一张表,方便随时查阅:
| 字段 | 含义 | 实际用途 |
|---|---|---|
bBlockingHit | 是否产生阻挡命中 | 判断射线是否被拦截 |
HitActor | 被命中的Actor对象 | 拿到目标引用,比如转义成AEnemy |
HitComponent | 被命中的组件 | 判断打到的是身体的哪个组件 |
BoneName | 被命中的骨骼名称 | 处理躯干/头部的不同动画反馈 |
ImpactPoint | 碰撞发生的世界坐标点 | 生成特效、播放音效 |
ImpactNormal | 命中点的法线方向 | 计算贴花朝向、判断命中角度 |
Distance | 从起点到命中点的距离 | 计算伤害衰减 |
实战中,最常用的是HitActor和ImpactPoint。要特别注意:HitActor可能是Actor的某个子对象,比如可破坏网格的ADestructibleActor内嵌组件。拿了HitActor后,我习惯用GetOwner进一步追溯真正的游戏逻辑Actor,避免对着一块碎块判断。
FHitResult还有个容易混淆的地方:它既能用于射线,也能用于扫描。在LineTrace中HitActor基本只可能是场景里实际存在的Actor,但在重叠事件里它可能是任意目标。所以拿到FHitResult后,先检查bBlockingHit再检查HitActor != nullptr,两个条件都满足再进业务逻辑。
3.3 FCollisionQueryParams的忽略与调试技巧
FCollisionQueryParams这个结构体一开始容易被人忽略,但它能帮你避开一堆烦人的自伤问题。最常见的一个操作:
FCollisionQueryParams QueryParams; QueryParams.AddIgnoredActor(MyCharacter); QueryParams.AddIgnoredComponent(MyWeaponMesh); QueryParams.bTraceComplex = false; QueryParams.TraceTag = TEXT("WeaponTrace");AddIgnoredActor会把传入的Actor从命中候选里剔除,用于防止射线打到自己。射击检测里,持枪角色通常要被忽略,否则从枪口射出的射线第一帧就可能撞上自己的胶囊体。还有一种情况是为了防止多段射线检测互相干扰:你射击检测上面一层已经处理了某个Actor,下面一层还想同一个方向再检测一次,就可以用AddIgnoredActor把刚才那个Actor加入忽略。
bTraceComplex这个参数控制是否用复杂碰撞体进行检测。复杂碰撞体是Mesh实际的三角面,精准但慢;简单碰撞体是胶囊体、盒体、球体这些代理形状,速度快但细节少。常见策略:武器射击用true保证穿透点精确;大面积环境扫描用false提升性能。要注意骨骼网格默认的简单碰撞形状其实是胶囊体组合,和可视化Mesh有差异,如果发现射线明明很精准却穿模,大概率是简单碰撞体的贴合度不够。
TraceTag配合ShowCollision命令可以可视化调试。具体操作是在引擎控制台输入ShowCollision,或者在代码里用DrawDebugLine。我给武器系统设置TraceTag后,每次射击立刻能看到射线路径、被命中物体的角点,极大缩短了排错时间。
3.4 一个能直接用的角色视线检测代码
结合以上内容,我给一个具体的例子:实现一个“判断视野内是否能看到指定敌人”的功能。这个在AI警戒、无人机索敌、狙击镜头里都很常用。
bool UMyFunctionLibrary::HasLineOfSight(AActor* Observer, AActor* Target, float MaxDistance) { if (!Observer || !Target) { return false; } FVector Start = Observer->GetActorLocation(); FVector End = Target->GetActorLocation(); if (FVector::DistSquared(Start, End) > MaxDistance * MaxDistance) { return false; } FCollisionQueryParams QueryParams; QueryParams.AddIgnoredActor(Observer); QueryParams.AddIgnoredActor(Target); QueryParams.bTraceComplex = true; QueryParams.TraceTag = TEXT("LineOfSight"); FHitResult HitResult; bool bHit = GetWorld()->LineTraceSingleByChannel( HitResult, Start, End, ECC_Visibility, QueryParams ); // 如果什么障碍物都没挡住,就算能看到 return !bHit; }这个函数我把Observer和Target都从查询里去掉了,避免射线起点在角色内部导致误判。实际项目中视线检测往往还要考虑瞄准点,不能直接用Actor的中心点,而是要取头部、胸口这些视觉关键点。这时可以拿到USkeletalMeshComponent的某个SocketLocation来作为目标坐标,比Actor中心更符合游戏表现。
另外,MaxDistance限制放前面是一层性能优化:距离一旦超过最大观察距离,根本不执行射线查询。如果场景里敌人很多,还可以先做一次粗略的OverlapMultiByChannel或者距离排序,把候选集缩小之后再逐一对每个敌人做射线检测,能省不少CPU开销。
4. 基于ObjectType的射线检测:FCollisionObjectQueryParams::InitType实战
4.1 什么时候需要换用ObjectType查询
通道查询虽然方便,但在一种场景下特别难受:你只想让射线命中一个特定类别的物体,比如“只命中可拾取的药水包”“只命中AI角色”,对其他东西一律无视。如果用通道查询,你得让所有非目标物统一配置Ignore响应,既侵入性强,又容易漏改。
对象类型查询完美解决了这个问题。它不关心每个物体对某个通道的响应是什么,只看物体的Object Type是否被包含在查询参数里。举个例子,LineTraceSingleByObjectType传入一个FCollisionObjectQueryParams,里面只包含ECC_Pawn,那么这条射线只会看到Pawn类型的目标,墙体、掉落物、载具这些统统无视。
另一个典型场景是“物理穿透检测”。玩家用念力控制物体时,射一条线想寻找可移动的物理物件,此时用AllDynamicObjects预设就能把静态墙体和地形全部排除,只留下动态物体。这种逻辑用通道查询处理会非常麻烦,因为墙面和角色对通道响应是复杂的,用对象类型查询一两行就搞定了。
4.2 InitType三种模式源码视角与使用场景
FCollisionObjectQueryParams::InitType的真实定义在引擎源码里是这样的:
enum InitType { AllObjects, AllStaticObjects, AllDynamicObjects };构造函数会依据传入的InitType来填充内部的ObjectTypes数组:
AllObjects:把所有可用的Object Channel都加入查询数组。这是最粗暴的用法,相当于不筛选任何类型。AllStaticObjects:只加入ECC_WorldStatic。用于寻找场景里的静态建筑、地形。AllDynamicObjects:加入除ECC_WorldStatic以外的所有对象类型,包括ECC_WorldDynamic、ECC_Pawn、ECC_PhysicsBody、ECC_Vehicle等。
有一个重点要提醒:AllDynamicObjects并不等于“只用动态关卡里的Actor”。它的判断依据完全是碰撞预设中的Object Type。如果某个物体的Object Type被设成了ECC_WorldStatic,就算这个Actor在运行时会移动,AllDynamicObjects也查不到它。反过来,如果一个物体Object Type是ECC_Pawn,哪怕它根本不移动,也会被AllDynamicObjects命中。所以正确理解应该基于配置类型,而不是物理运动状态。
代码里初始化时可以这样写:
// 只查所有动态物体 FCollisionObjectQueryParams ObjectParams(FCollisionObjectQueryParams::AllDynamicObjects); // 还可以先初始化成AllObjects,再手动剔除某些类型 FCollisionObjectQueryParams CustomParams(FCollisionObjectQueryParams::AllObjects); CustomParams.AddObjectTypesToQuery(ECC_WorldStatic); // 还是要确认一下是否需要注意一个细节:构造函数FCollisionObjectQueryParams(InitType InType)是显式构造。如果你直接写LineTraceSingleByObjectType(HitResult, Start, End, FCollisionObjectQueryParams::AllDynamicObjects),很多编译器会抱怨类型不匹配,因为AllDynamicObjects只是枚举值,需要先构造成FCollisionObjectQueryParams。所以要么写全构造过程,要么用局部变量接收。
4.3 多对象类型组合查询与通道过滤
有时候不想要AllObjects那么宽,也不想只针对单一类型。这时可以用数组构造方式或手动追加类型:
FCollisionObjectQueryParams ObjectParams; ObjectParams.AddObjectTypesToQuery(ECC_Pawn); ObjectParams.AddObjectTypesToQuery(ECC_WorldDynamic); ObjectParams.AddObjectTypesToQuery(ECC_PhysicsBody);这等价于“只查询Pawn、动态世界物体和物理物体”,排除了静态地形。我把这个模式叫做“白名单查询”。在多物体射击游戏里,玩家的子弹要能打中敌人、可破坏物和物理碎片,但不要被普通墙面表面的装饰物挡住,白名单查询就非常合适。
既然说是白名单,选中了哪些类型,最终命中结果就只可能是这些类型里的物体。FHitResult的HitActor一定符合查询范围内某个类型。如果你在结果里看到奇怪的StaticMesh,先检查它的碰撞预设Object Type是不是你设置的类型之一。
组合查询还有一个性能优势:引擎在筛选候选时,可以提前剔除不在ObjectTypes里的组件,减少碰撞检测的计算量。尤其大场景里,一个通道查询可能命中很多候选物体,再用代码去过滤,浪费了大量的底层计算。换成白名单对象查询,很多物体根本不会进入候选列表。
4.4 完整的射击射线检测示例
结合上面内容,给一个更贴近真实项目的例子:武器射线射击,只检测玩家、敌方Pawn、可破坏物理物体,不检测自己的身体和普通地形。
void AWeapon::FireWithObjectQuery(const FVector& StartLocation, const FVector& EndLocation) { FCollisionObjectQueryParams ObjectParams; ObjectParams.AddObjectTypesToQuery(ECC_Pawn); ObjectParams.AddObjectTypesToQuery(ECC_WorldDynamic); ObjectParams.AddObjectTypesToQuery(ECC_PhysicsBody); FCollisionQueryParams QueryParams; QueryParams.AddIgnoredActor(GetInstigator()); QueryParams.AddIgnoredComponent(WeaponMesh); QueryParams.bTraceComplex = true; QueryParams.TraceTag = TEXT("ShootTrace"); FHitResult HitResult; bool bHit = GetWorld()->LineTraceSingleByObjectType( HitResult, StartLocation, EndLocation, ObjectParams, QueryParams ); if (bHit) { AActor* HitActor = HitResult.GetActor(); FVector ImpactPoint = HitResult.ImpactPoint; FVector ImpactNormal = HitResult.ImpactNormal; // 处理伤害、特效、音效 ApplyDamage(HitActor, ImpactPoint, ImpactNormal); SpawnHitEffect(ImpactPoint, ImpactNormal); } }注意GetInstigator()的目的是忽略发射者,防止子弹射线起点在角色胶囊体内部时就把自己命中。还要记得在目标Actor的碰撞预设里确认:敌方角色的Object Type必须是ECC_Pawn,否则这个白名单永远查不到敌人。我遇到过的典型错误是:AI角色虽然默认Object Type是ECC_Pawn,但策划为了某个行为改了预设,导致整个伤害系统失效。排查方法是打印命中结果或先在编辑器中临时调整Object Type验证。
5. 射线检测踩坑实录:通道配置与查询失效的排查手册
5.1 穿过物体不命中的三类原因
这个问题是新手高频问题之一。我把它拆成三个主要原因:
第一个原因:目标物体的碰撞组件没有正确生成。例如一个角色的骨骼网格体挂着Physics Asset但没启用碰撞查询,或者StaticMesh组件的Collision Enabled被设成了NoCollision。此时射线直接穿过你看起来明明有Mesh的地方,像打在空气上。检查方法是点击目标Actor,查看Mesh组件的Collision Enabled属性,确认是否Query Only或Physics Only。
第二个原因:响应矩阵中对查询通道是Ignore或Overlap。这对应前面强调的响应矩阵判断。例如,你拿ECC_Visibility去砸一个角色,但角色的碰撞预设对Visibility是Overlap,于是射线穿过。要修改矩阵中Visibility通道为Block,或者换一个角色也会响应的通道。
第三个原因:射线起点在目标内部。如果射线起点已经嵌进碰撞体内部,UE碰撞系统默认不会检测出自己内部包含的物体。典型场景是枪口模型本身穿过墙体,或玩家角色头部在墙体内部时对墙进行检测。用DrawDebugLine画线后,如果起点已经压进去了,调整起点位置或用FMath::Clamp把起点拉回安全距离。
5.2 ECC_Visibility误命中:响应矩阵没收紧
和“射线不命中”相对的问题是“不该命中的全命中了”。常见于用ECC_Visibility做视线判断时,整个场景里的悬崖边缘、路牌、地形小起伏全部被算成阻挡,AI因此无法索敌。这是因为太多物体默认对Visibility是Block响应。
解决办法是把游戏世界的“视线阻挡层”收敛到一个自定义Trace Channel,或者收敛到几个固定预设里。具体做法:对大多数可交互但不应挡视线的物体,在碰撞预设里把ECC_Visibility响应改成Ignore;对真正阻挡视线的实体墙、门、柱子,保持Block。
有个更成熟的实践:不直接用ECC_Visibility做AI视线,而是新建一个Trace_AIView通道,专门标记“哪些物体会阻挡AI视野”。这样不干扰原来的Visibility通道,也方便策划在关卡里批量调整。在我看来,前期规范比后期反复排查高效得多。
5.3 AllDynamicObjects漏检静态场景的坑
使用对象类型查询时,最容易漏检的就是静态地形和静态网格。原因就是我前面强调的:AllDynamicObjects只包含动态类型。很多游戏里玩家踩的桥、迷宫墙这些家具其实是静态网格,但它们挂着可破坏部件,或者被策划改成可交互对象。
如果你需要“从射线里找出场景里所有会导致碰撞的物体,无论动静”,直接用AllObjects。这个预设会把ECC_WorldStatic也包含进去,能覆盖静态地形和动态物件。注意,如果你想忽略地形但保留动态物体,就不能用AllObjects,要手动列出动态类型。
还有一个隐藏陷阱:某种物体虽然是动态生成,但它的碰撞预设没有设置为动态类型,而是沿用了ECC_WorldStatic。例如动态生成的栅栏沿用了项目的Static预设,那么AllDynamicObjects一样查不到。排查时要一并检查Object Type配置,而不要只盯着“是否运动”。
5.4 联机与蓝图事件重复触发的排查
多人游戏里,射线检测的执行端经常会造成混乱。我通常建议:射线检测只在服务器端执行,客户端通过RPC请求服务器做判定,服务器将结果广播回来。这样做可以避免跨端碰撞参数不一致导致“同一发子弹在不同客户端打中不同人”的问题。
还有一个常见现象:蓝图里同时绑定了LineTraceSingleByChannel节点和C++里的检测逻辑,事件被重复触发了两遍。排查时打开TraceTag可视化,看看是否每一次伤害都伴随两根调试射线——两根射线往往意味着两个检测逻辑并存。
基于版本差异,不同UE5小版本里LineTraceByChannel和LineTraceByObjectType的函数签名可能略有出入,比如某些版本增加了碰撞响应参数。如果编译报错,优先查阅当前引擎头文件,而不是强行套用老版本文档。这点在社区里很多人问过,我建议大家记住“以引擎源码为准”。
5.5 快速自查速查表
| 现象 | 首要怀疑点 | 排查建议 |
|---|---|---|
| 射线穿过明显有Mesh的物体 | 碰撞组件未启用或响应矩阵是Ignore/Overlap | 查看目标的Collision Enabled和响应矩阵 |
| 用ECC_Visibility砸不到角色 | 角色预设对Visibility不是Block | 修改矩阵或换成ECC_Pawn |
| 射线命中但伤害没触发 | HitActor为空、Obstructor逻辑未走 | 打印HitActor与HitComponent |
| ObjectType查询漏检墙体 | 墙体Object Type不是白名单里的类型 | 检查Object Type或改用AllObjects |
| 武器射线打到自己 | 忽略了持枪角色 | 添加AddIgnoredActor/AddIgnoredComponent |
| 调试射线看不到 | TraceTag没设置或未开启ShowCollision | 设置TraceTag并执行ShowCollision命令 |
最后再说一个我习惯保留的经验:不要迷信某一个通道是“万能通道”。ECC_Visibility很常用,但它的语义是“可见性”,不是“物理阻挡”,更不是“交互目标”。在一个稍微复杂的项目里,我都会花十几分钟在项目初始化时把这些通道的分类与预设整理成文档,分发给策划和TA。等后面加新功能时,所有人能一眼看出该把物体设置成什么类型、对哪个通道响应Block,整个团队能省下大量联调时间。这个前期投入,是相关代价最小的。