
UE5里做碰撞检测绕不开BeginOverlap和EndOverlap这两兄弟。这期是系列第27篇的第1小篇主要讲C里怎么把OnComponentBeginOverlap和OnComponentEndOverlap这两个事件通过代理绑定到自己的回调函数上顺便把FComponentBeginOverlapSignature这种签名宏的使用逻辑先捋个大概。标题里写了“宏定义推导...以后再整理”那这篇就把“够用”的那层讲透底层宏展开细节我后面单开一篇再深挖。先说结论BeginOverlap适合处理“进入某个区域触发逻辑”比如拾取道具、进入伤害区域、踩到机关、走进视野范围EndOverlap适合处理“离开区域时清理状态”比如解除锁定、停止掉血、关闭提示UI。一套配合下来角色就像长了感知触角。这里有个常见的误区——有人觉得用蓝图节点连线不就完了何必写C。但项目一旦上规模几十个触发器的逻辑全堆蓝图节点里改需求时你就知道什么叫“连线的地狱”了。C里绑定代码可读性强还能挂版本控制做review换分支也不容易丢节点。这篇文章适合两类人一是从蓝图转C想在代码里复刻蓝图事件的开发者二是刚接触UE5 C网上抄了一段AddDynamic代码但编译不过的初学者。我会把从声明、配置、绑定到排查的完整链路都过一遍按步骤操作即可复现。1. 先搞清楚“代理绑定”到底在绑什么1.1 从回调函数到委托的进化原生C里最常见的事件通知方式是函数指针。但函数指针在UE的框架下有明显的短板没法做序列化、蓝图识别不了、对象销毁后指针悬空也没人管。UE给出的替代方案叫委托Delegate相当于给回调函数包了一层安全外壳。Delegate内部帮你管理了函数指针的存储和调用还支持绑定到UObject成员函数对象销毁时委托内部会自动清掉无效绑定。BeginOverlap用到的具体委托类型是动态多播委托。动态是指绑定不再依赖函数指针的裸地址而是靠FName函数名字符串来找目标这个设计是为了让蓝图反射系统能参与进来多播意味着一个事件可以同时挂多个回调函数Broadcast调用时会按绑定顺序全部执行一遍。所以你在一个组件上绑两个回调函数是合理的只要参数签名一致两个都会收到通知。碰撞相关的委托定义在Components/PrimitiveComponent.h里组件默认就带了两个公开委托对象OnComponentBeginOverlap和OnComponentEndOverlap。物理引擎每帧做碰撞检测时发现两个Component产生了重叠就会调OnComponentBeginOverlap.Broadcast(...)把你写好的所有回调函数逐个拉出来执行。你做的绑定动作本质就是把自定义函数挂到这个事件的通知列表上。1.2 组件级委托和Actor级委托怎么选很多初学者会在C里找OnActorBeginOverlap来绑定发现写起来和蓝图里看到的节点不一样原因是你找错层级了。OnActorBeginOverlap挂在AActor上而OnComponentBeginOverlap挂在UPrimitiveComponent上。蓝图里那个“On Actor Begin Overlap”事件节点底层其实是把Actor身上所有PrimitiveComponent的委托统一汇总之后再抛出来的封装。具体开发中我建议优先绑组件级委托。原因很简单Actor级委托只告诉你“我这个Actor和别的Actor重叠了”但你要是想知道“是角色的左脚踩进了陷阱还是整个身体进来了”Actor级委托给不了这个粒度。组件级委托会把OverlappedComponent和OtherComp都传给你你可以直接判断哪个碰撞体触发的。另外一个Actor身上挂了好几个碰撞体时组件级委托可以分别绑定不同的回调逻辑上更清晰。1.3 参数多的吓人但实际上很好记FComponentBeginOverlapSignature一共六个参数OverlappedComponent当前组件也就是你绑定前那个组件OtherActor重叠过来的那个ActorOtherComp重叠过来的Actor身上的组件OtherBodyIndexOtherComp上的第几个碰撞体索引bFromSweep是否由扫动Sweep检测出来的重叠SweepResult如果是Sweep产生的这里会有命中信息EndOverlap少了最后两项只有五个参数。记住一个技巧参数顺序永远是“自己、对方Actor、对方组件、组件索引”后面再挂Sweep信息。这条规则在整个UE碰撞回调里是统一的记住了就会用了。2. 绑定前想清楚碰撞三件套缺一不可2.1 为什么你的绑定代码没反应我见过太多人把AddDynamic写好了、编译也通过了结果进游戏发现回调函数就是不执行。排除代码错误之后百分之八十的案发现场都在碰撞配置上。触发组件重叠事件需要三个条件同时满足碰撞体启用了查询功能、两个组件的碰撞响应是Overlap、两个组件都勾选了Generate Overlap Events。一个都不能少。第一个条件是Collision Enabled形状组件默认是Query OnlyMesh组件默认则是Query and Physics。如果你把静态网格体的碰撞直接关掉了那就没有后续了。第二个条件碰撞响应有三类Ignore、Overlap、Block。只有Overlap才会触发BeginOverlap回调Block触发的是Hit相关的HitResult回调。第三个条件Generate Overlap Events默认是开启的但有些美术素材导入时为了性能会把它关掉尤其是一些从其他引擎导进来的模型经常出现这种问题。2.2 碰撞预设Collision Presets怎么配碰撞预设本质是一套“预设好的碰撞响应矩阵”。窗口右侧Details面板里的Collision Presets下拉菜单选的是引擎内置的几种组合预设名启用情况默认响应适用场景NoCollision查询和物理全关忽略全部纯装饰物不值得参与碰撞计算QueryOnly仅查询Block世界静态Overlap移动物体触发器、可拾取物、伤害区域PhysicsActor查询物理WorldDynamic响应Block可被物理推动的物体OverlapAll仅查询Overlap所有通道纯重叠检测比如通关检测门BlockAll查询物理Block所有通道静态墙壁、地面实际项目里我更习惯关闭预设自己在代码里逐通道设置。预设表格只能应付简单原型真实玩法里通道往往要按项目自定义比如“敌方攻击判定只和PlayerPawn重叠”。预设写死的话后面换通道名就得全场景排查工作量很大。2.3 用代码保证“三件套”完整在C里给形如BoxComponent的碰撞组件做配置时直接看代码比面板点选好排查。一个典型的配置代码长这样TriggerBox-SetCollisionEnabled(ECollisionEnabled::QueryOnly); TriggerBox-SetCollisionResponseToAllChannels(ECR_Ignore); TriggerBox-SetCollisionResponseToChannel(ECC_Pawn, ECR_Overlap); TriggerBox-SetGenerateOverlapEvents(true);这里有几个关键点。SetCollisionResponseToAllChannels先把所有通道清成Ignore再把需要的通道单独设成Overlap。这样即使引擎升级后增加了通道也不会误触发。GenerateOverlapEvents必须显式设成true别依赖默认值——编辑器版本差异会导致默认值不同代码里写死最稳妥。我遇到过一种特殊情况两个组件在同一个Actor身上一个设成OverlapAll、一个设成BlockAll然后这个Actor的碰撞检测视角下物理引擎会把两个响应综合之后判定最终结果导致部分区域不触发重叠。这种“一个物体多个碰撞体响应不一致”的坑排查时要特别注意必要时用碰撞调试可视化把每个组件的实际响应画出来看。3. 核心实操在C里把事件绑到函数上3.1 头文件声明组件、回调函数一个都不能少先定义一个BoxComponent挂到Actor上同时声明两个回调函数。回调函数必须加UFUNCTION()这点很多人会忘不加的话AddDynamic宏在编译阶段就会报“函数不是蓝图可调用”之类的错误。UCLASS() class MYGAME_API AMyTriggerActor : public AActor { GENERATED_BODY() public: AMyTriggerActor(); protected: UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category Trigger) class UBoxComponent* TriggerBox; UFUNCTION() void OnOverlapBegin(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult SweepResult); UFUNCTION() void OnOverlapEnd(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex); };有个细节UPROPERTY()修饰的TriggerBox在声明时不加VisibleAnywhere的话面板检查就成了玄学调试时你想看它挂没挂上还得翻代码。习惯上所有组件都用VisibleAnywhere暴露出来省得后面排查时还要盲猜。3.2 构造函数里创建组件并完成代理绑定实现文件里先CreateDefaultSubobject创建盒子组件然后在构造函数的结尾绑定事件AMyTriggerActor::AMyTriggerActor() { PrimaryActorTick.bCanEverTick false; TriggerBox CreateDefaultSubobjectUBoxComponent(TEXT(TriggerBox)); RootComponent TriggerBox; TriggerBox-SetCollisionEnabled(ECollisionEnabled::QueryOnly); TriggerBox-SetCollisionResponseToAllChannels(ECR_Ignore); TriggerBox-SetCollisionResponseToChannel(ECC_Pawn, ECR_Overlap); TriggerBox-SetGenerateOverlapEvents(true); TriggerBox-OnComponentBeginOverlap.AddDynamic(this, AMyTriggerActor::OnOverlapBegin); TriggerBox-OnComponentEndOverlap.AddDynamic(this, AMyTriggerActor::OnOverlapEnd); }这里有一个很多教程没说的经验绑定动作放在构造函数里是官方推荐做法。因为组件创建完当场绑定就不存在BeginPlay执行顺序问题。如果你在BeginPlay里绑定就要小心同一帧里别的Actor先触发碰撞事件时回调还没绑上的竞态条件尽管概率很小但确实会发生。3.3 回调函数体判断目标然后做业务事件来了之后第一步必须是判空。你别小看这一步物理引擎在角色销毁瞬间仍然可能再补发一帧重叠回调没判空的话直接野指针崩溃。这也是引擎行为躲不开只能自己兜住void AMyTriggerActor::OnOverlapBegin(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult SweepResult) { if (!OtherActor || OtherActor this) { return; } ACharacter* Character CastACharacter(OtherActor); if (Character) { UE_LOG(LogTemp, Warning, TEXT(Character entered trigger zone: %s), *Character-GetName()); // 在这里编写进入区域后的逻辑 } } void AMyTriggerActor::OnOverlapEnd(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex) { if (!OtherActor || OtherActor this) { return; } ACharacter* Character CastACharacter(OtherActor); if (Character) { UE_LOG(LogTemp, Warning, TEXT(Character left trigger zone: %s), *Character-GetName()); // 在这里编写离开区域后的逻辑 } }这里我说一下为什么用Cast 而不是直接判断类名。Cast走的是引擎反射系统类型不匹配时返回空指针安全又高效。直接比较GetClass()或者类名字符串也行但遇到蓝图子类时Cast能正确识别继承关系而GetClass()ACharacter::StaticClass()只能匹配基类本身。做定制游戏时你的角色大概率是蓝图子类用Cast最稳。4. 宏定义推导FComponentBeginOverlapSignature 的前世今生4.1 直接从PrimitiveComponent.h里找源头在UE源码的Components/PrimitiveComponent.h文件里你能看到这样两行宏定义DECLARE_DYNAMIC_MULTICAST_DELEGATE_SixParams(FComponentBeginOverlapSignature, UPrimitiveComponent*, OverlappedComponent, AActor*, OtherActor, UPrimitiveComponent*, OtherComp, int32, OtherBodyIndex, bool, bFromSweep, const FHitResult, SweepResult); DECLARE_DYNAMIC_MULTICAST_DELEGATE_FiveParams(FComponentEndOverlapSignature, UPrimitiveComponent*, OverlappedComponent, AActor*, OtherActor, UPrimitiveComponent*, OtherComp, int32, OtherBodyIndex);这段宏展开后会生成一个新的C类类名就是第一个参数“FComponentBeginOverlapSignature”。这个类里带了一个委托调用列表InvocationList以及Broadcast、IsBound、Clear等操作委托的方法。我们在前面写的AddDynamic和引擎内部的Broadcast都发生在由这个宏生成的类身上。4.2 DECLARE_系列宏展开后做了什么以DECLARE_DYNAMIC_MULTICAST_DELEGATE_SixParams为例宏内部会通过装饰符生成一个继承自TBaseDynamicMulticastDelegate_六参数模板的特化类。这个模板类上定义了AddDynamic / RemoveDynamic绑定与解绑Broadcast执行所有已绑定回调IsBound判断是否有人监听Clear清空所有绑定这里有个底层细节特别有意思模板参数里每个参数都要写两次第一次写类型、第二次写变量名。比如UPrimitiveComponent*, OverlappedComponent就是一对。这个设计是预处理器宏做不到“遍历形参列表”的妥协方案所以UE用固定的1~N个参数模板硬编码出了N种宏变体。这也是为什么你在源码里能看到DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams、_ThreeParams一直到_Params这种不断增加参数数量的宏定义。理解了这一点你以后再看到带个位数后缀的委托宏就不会觉得它神秘了。4.3 AddDynamic宏到底展开成什么在Delegate.h里能找到定义内容很短#define AddDynamic(UserObject, FuncName) \ this-__Internal_AddDynamic(UserObject, FuncName, FName(#FuncName))展开之后就干了两件事第一取回调函数的真实指针地址FuncName编译期会做类型检查参数签名对不上就编译报错这是静态期保护第二把函数名字符串化#FuncName转成FName为的是运行期内通过反射系统按照名字找到这个函数的UFunction对象。所以回调函数必须加UFUNCTION()的原因就清楚了因为AddDynamic最终要通过FName去引擎的反射系统里查这个函数没进反射系统的函数索引表里查不到绑定直接失效。这不是什么玄学而是动态委托设计上就定好的规则。4.4 “以后再整理”不等于“现在可以不理解”这句话给所有看到“复杂宏定义”头大的人。我自己的经验是用UE的委托你可以把宏当成“编译器替你在写样板代码”的语法糖写代码时不需要背宏展开的每一步但当报错信息出现“TBaseDynamicMulticastDelegate”之类字样时你要能认出来这是委托模板的底层类快速把问题定位到绑定环节。至于宏展开怎么从预处理器一步步生成类定义那确实值得单独写一篇但那是“进阶”而不是“入门”的地图。这篇先把能跑通的绑定逻辑吃下来宏展开细节咱后面慢慢盘。5. 常见问题与排查技巧实录5.1 完全不触发回调从这三个角度排查我在项目里帮别人排查过不少“绑定不生效”的问题。总结下来从现象倒推原因基本逃不出这张表现象可能原因排查方式物体穿模但无回调Collision Enabled被设成NoCollision面板看Collision Presets是否正常物体相撞但只有Hit没Overlap碰撞响应是Block而非Overlap把通道响应改成Overlap再测试一方是Mesh不触发Mesh组件Collision Complexity有问题勾选Use Complex Collision As Simple试试Actor销毁时漏了EndOverlapActor销毁不保证回调在Destroy前手动释放逻辑蓝图里能看到事件C里不触发回调函数没加UFUNCTION检查函数声明处是否加了UFUNCTION()逐个说。第一类“穿模无回调”最常见的陷阱是Mesh组件的碰撞体类型。StaticMeshComponent如果没有在资产里生成Simple Collision凸包或图元组成的简单碰撞体那无论你怎么设碰撞预设它都只能参与复杂碰撞而复杂碰撞在默认情况下不触发Overlap。这就是为什么开发者总喜欢在Mesh旁边另外加一个BoxComponent专门做检测省心。第二类“重叠判定失败”要理解Overlap和Block互斥。同一个物体、同一个通道要么Overlap要么Block不能两个都期望。你想做墙同时又想触发重叠事件这不符合UE碰撞模型必须拔掉一边。合理方案是把墙设成Block在墙前单独放一个触发器负责Overlap检测。第三类生命周期问题几乎人人都会踩。Actor的销毁和物理子系统的结算不在同一个时机你角色的Actor销毁瞬间物理引擎可能还握着对它的引用如果这时候发生重叠事件而你正在回调里访问对方对象的变量就撞上野指针了。我的习惯是一切回调函数体第一行先判空第二行再IsValid()确认对象可用。5.2 重复绑定导致回调执行多次新手很容易犯的错把AddDynamic写在BeginPlay里而BeginPlay在编辑器PIE和运行时各触发一次测试时同一帧内回调被执行两次甚至四次。现象就是拾取道具加了两倍金币。解决方法是把绑定统一放构造函数或者用RemoveDynamic先解绑再绑定// 安全绑定方式先移除再添加 TriggerBox-OnComponentBeginOverlap.RemoveDynamic(this, AMyTriggerActor::OnOverlapBegin); TriggerBox-OnComponentBeginOverlap.AddDynamic(this, AMyTriggerActor::OnOverlapBegin);对动态多播委托来说同一个函数绑定多次是允许的引擎不做去重所以这是全凭自觉的规范。我自己的项目规范里明确写了动态委托绑定只允许出现在构造函数和OnConstruction里BeginPlay里要绑必须先Remove。5.3 回调里拿到的是对方组件但不知道碰撞体是谁有一种很微妙的情况两个Shape组件叠加在同一个Actor上比如一个胶囊体一个盒子。回调触发时你拿到的OtherComp可能是胶囊体也可能是盒子但你的业务逻辑其实只想针对某一种碰撞体做处理。这时不要试图用GetName()去猜组件名虽然一般是固定的但蓝图里手动复制组件后名字会带后缀猜名不靠谱。正确的解法是在类的定义里把需要区分的组件单独保存成成员变量回调里比较OtherComp 某个成员组件来判断是哪一个if (OtherComp LeftHandCollider) { // 左手触发的逻辑 } else if (OtherComp RightHandCollider) { // 右手触发的逻辑 }这里要小心一个引用比较的前提判断前先确认OtherComp非空。5.4 Sweep和Overlap的边界情况bFromSweep这个参数代表着这次重叠事件是由Sweep动作比如角色胶囊体移动时扫过场景检测出来的。普通的重叠比如缩放组件、主动移动物体bFromSweep是false。调试时打印它一眼就能看出事件来源。SweepResult则是扫动命中时的详细Hit信息里面有撞击法线、距离、时间等物理数据。我踩过的一个坑是在BeginOverlap回调里用SweepResult.Location来记录“玩家进入区域时最初位置”。看似合理但bFromSweep为false时SweepResult是默认值Location是不准的。正确做法只有bFromSweep为true时才读取SweepResult里的数据否则用OtherActor-GetActorLocation()。6. 实战扩展用重叠事件做一个可拾取道具6.1 完整道具类代码把上面的知识揉一起最典型的应用是做一个可拾取道具。下面这个类在场景里放一个旋转的球体玩家走进去就拾取走进来和走出去都有日志输出道具销毁前手动解绑并调用EndOverlap逻辑UCLASS() class MYGAME_API APickupItem : public AActor { GENERATED_BODY() public: APickupItem(); UFUNCTION() void OnOverlapBegin(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult SweepResult); UFUNCTION() void OnOverlapEnd(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex); UFUNCTION() void OnPickupTimer(); protected: UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category Pickup) class USphereComponent* PickupSphere; UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category Pickup) class UStaticMeshComponent* PickupMesh; FTimerHandle PickupTimerHandle; };实现文件里构造函数创建组件并绑定这里我额外做了一个设置碰撞体只和Pawn通道重叠其他一律忽略避免玩家还没靠近就被NPC或者小动物触发APickupItem::APickupItem() { PrimaryActorTick.bCanEverTick true; PickupSphere CreateDefaultSubobjectUSphereComponent(TEXT(PickupSphere)); RootComponent PickupSphere; PickupMesh CreateDefaultSubobjectUStaticMeshComponent(TEXT(PickupMesh)); PickupMesh-SetupAttachment(RootComponent); PickupMesh-SetCollisionEnabled(ECollisionEnabled::NoCollision); PickupSphere-SetCollisionEnabled(ECollisionEnabled::QueryOnly); PickupSphere-SetCollisionResponseToAllChannels(ECR_Ignore); PickupSphere-SetCollisionResponseToChannel(ECC_Pawn, ECR_Overlap); PickupSphere-SetGenerateOverlapEvents(true); PickupSphere-OnComponentBeginOverlap.AddDynamic(this, APickupItem::OnOverlapBegin); PickupSphere-OnComponentEndOverlap.AddDynamic(this, APickupItem::OnOverlapEnd); }这里有个关键细节PickupMesh的碰撞体设成NoCollision否则玩家走进来会和网格体发生Block看起来就是撞在空气墙上了和拾取逻辑矛盾。外部展示用的Mesh正常是不需要参与碰撞运算的让专门的Sphere碰撞体来做检测职责分离。回调逻辑里Begin时判断是不是玩家角色是的话启动一个定时器延迟0.1秒后执行拾取销毁这样给视觉一个反应时间。End时清除定时器玩家离开后就不拾取void APickupItem::OnOverlapBegin(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult SweepResult) { if (!OtherActor || CastACharacter(OtherActor) nullptr) { return; } UE_LOG(LogTemp, Warning, TEXT(PickupItem: Player entered overlap area.)); // 延迟触发拾取防止玩家只是擦边路过 GetWorldTimerManager().SetTimer(PickupTimerHandle, this, APickupItem::OnPickupTimer, 0.1f, false); } void APickupItem::OnOverlapEnd(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex) { if (!OtherActor || CastACharacter(OtherActor) nullptr) { return; } UE_LOG(LogTemp, Warning, TEXT(PickupItem: Player left overlap area, cancel pickup.)); GetWorldTimerManager().ClearTimer(PickupTimerHandle); } void APickupItem::OnPickupTimer() { UE_LOG(LogTemp, Warning, TEXT(PickupItem: Pickup confirmed. Destroying.)); Destroy(); }6.2 给道具增加旋转动画你把道具放在场景里它不动的话看起来就是个静态物体。给Mesh加一个旋转在Tick里更新相对旋转void APickupItem::Tick(float DeltaSeconds) { Super::Tick(DeltaSeconds); if (PickupMesh) { FRotator NewRotation PickupMesh-GetComponentRotation(); NewRotation.Yaw 45.0f * DeltaSeconds; PickupMesh-SetWorldRotation(NewRotation); } }旋转这个细节带来的体验提升很大玩家能一眼看出这东西“活着”可以交互。而且旋转是纯可视化逻辑不影响碰撞体碰撞体始终保持直立姿态拾取判定是一致的。6.3 道具销毁前注意的事Destroy()之后委托并不会立刻解除绑定但动态委托在UObject销毁后会自动从所有委托列表里清除自己这是引擎内部引用清理的机制。不过我们要面对一个现实EndOverlap很可能在销毁瞬间不会被触发因为销毁动作不会先走一遍碰撞离开流程。所以如果你在EndOverlap里写了“道具被拿走时播放音效”销毁前要先手动调用你的EndOverlap逻辑或者干脆就是让Destroy前先调用解绑、再手动清定时器。这是我在实际项目里印象最深的一个坑道具拿了、音效也播了但男角色上的状态没清掉因为在PickupTimer里直接Destroy没有走EndOverlap的清理逻辑。后来我在制作者文档里写了一条规则凡是通过Destroy结束生命的Actor业务清理动作必须在Destroy前显式完成不要依赖EndOverlap兜底。6.4 从单个道具扩展到批量关卡触发器学会了单个道具的做法场景里有几百个陷阱、机关、安全区本质都是同一套模板的变体。你只需要把TriggerBox换成CapsuleComponent、SphereComponent或者直接在静态网格体上开启GenerateOverlapEvents然后按同样的方式绑定回调函数即可。真到我开发RTS类玩法时大量单位同时进出区域碰撞回调每帧触发上千次这时候有几个性能优化点不要在每个回调里都写耗时的Cast或FindComponentByClass尽量在BeginPlay时把常用引用缓存成成员变量Overlap回调里尽量避免蓝图函数调用C直接完成逻辑需要频繁进出的区域考虑用间隔检测代替逐帧重叠判定。碰撞回调本身非常轻量但业务逻辑写的太重帧率照样掉。结尾留几句实在话我自己最早接触这个系列时也被那串长长的宏定义吓住过。后来才发现真正让自己成长的不是背下那一堆模板展开而是把“AddDynamic到底发生了什么”“为什么必须UFUNCTION”“为什么碰撞不触发”这三个问题逐个击破的过程。UE的宏体系很复杂它把C的预处理器用到了极限但这恰恰是引擎给开发者做减法的手段——你只需要写一个带UFUNCTION的普通函数剩下的脏活累活交给宏和反射系统。如果你今天照着这篇文章敲了一遍代码并且跑通了那你已经把UE5 C碰撞事件这条主线拿下了。下次再遇到OnHit、OnActorHit之类的事件你会感觉它们是老朋友绑定套路都是通的。至于FComponentBeginOverlapSignature的宏展开细节、UHT的生成代码长什么样这些我确实打算单独整理一篇但不会作为你上手的前置门槛。先把手里的角色变得能踩机关、能捡金币、能进出区域那些底层的事等你在实战里碰到具体报错再去啃效率会高得多。