出于工作需要和个人好奇,本文对UE重叠事件更新的主要函数UpdateOverlaps从源码的角度进行了详细的分析,通过阅读源码,深入理解重叠事件是如何被触发和更新的。
阅读本文,你将得到至少以下问题的答案:
BeginComponentOverlap和EndComponentOverlap事件是如何被触发的?
UE是如何保存和管理组件之间的碰撞的?
SetActorEnableCollision是如何起到作用的?
UE如何处理不同Actor,或者同一个Actor里重复碰撞的情况?
以上只是提出了几个很基础的问题,随着源码的不断解析,还会有更多新的问题会被提出。
说是深入探究,更像是笔者个人的学习笔记。其中一些笔者认为本应被了解的细节不会被提起,请读者至少对UE的重叠机制有基础的理解。
Queries world and updates overlap tracking state for this component
当一个组件需要更新当前重叠状态时,就会调用这个函数。
这个函数定义在USceneComponent,表明只有场景组件的子类才能调用该函数。并且它不是一个虚函数,更新重叠相关的具体实现放在一个叫``UpdateOverlapsImpl的虚函数中。因此可以将UpdateOverlaps视作为重叠更新的总入口,然后调用子类的
UpdateOverlapsImpl`从而执行具体的更新逻辑。
观察该函数的声明:
bool UpdateOverlaps(const TOverlapArrayView* PendingOverlaps = nullptr, bool bDoNotifies = true, const TOverlapArrayView* OverlapsAtEndLocation = nullptr);
其中,TOverlapArrayView
就是经过了typedef的TArrayView<const FOverlapInfo>
.
FOverlapInfo
是对FHitResult
的一个简单封装,FHitResult
相信大家都很熟悉了,通过使用FHitResult,我们可以很轻松的获得本次碰撞查询中碰撞到的组件,以及碰撞的各种信息,例如碰撞坐标,法线等等。
接下来对参数列表中三个参数进行讲解,这几个参数还是挺重要的,后面会反复使用到这三个参数。
An ordered list of components that the MovedComponent overlapped during its movement (eg. generated during a sweep). Only used to add potentially new overlaps.
Might not be overlapping them now.
移动组件在移动过程中重叠的有序组件列表(例如:在扫描过程中生成)。仅用于添加潜在的新重叠。
说人话就是,本次碰撞查询中检测到的将要碰到的重叠。之后在UpdateOverlapsImpl
中,将会使用该数组调用BeginComponentOverlap.
值得一提的是,即使我们当前的组件(后续我们就叫它Self组件吧)已经在其他组件的重叠里了,此时如果有移动行为的话,该数组仍会把已经重叠的组件保存进去,至于会不会重复触发BeginOverlap,后续当然有相关的逻辑处理,这里先按下不表。
如果当前没有移动,只是简单的对组件进行了旋转,那么这个数组将会是空的,可以查阅UPrimitiveComponent::MoveComponentImpl
, 其中有这么一段代码:
TInlineOverlapInfoArray OverlapsAtEndLocation; bool bHasEndOverlaps = false; if (bRotationOnly) { bHasEndOverlaps = ConvertRotationOverlapsToCurrentOverlaps(OverlapsAtEndLocation, OverlappingComponents); } else { bHasEndOverlaps = ConvertSweptOverlapsToCurrentOverlaps(OverlapsAtEndLocation, PendingOverlaps, 0, GetComponentLocation(), GetComponentQuat()); } TOverlapArrayView PendingOverlapsView(PendingOverlaps); TOverlapArrayView OverlapsAtEndView(OverlapsAtEndLocation); UpdateOverlaps(&PendingOverlapsView, true, bHasEndOverlaps ? &OverlapsAtEndView : nullptr);
有个bRotationOnly变量,如果只有旋转的话,不会对PendingOverlaps进行赋值。
也就是说,组件只做原地旋转的话,是不会有新的重叠开始事件的。
If non-null, the given list of overlaps will be used as the overlaps for this component at the current location, rather than checking for them with a scene query.
Generally this should only be used if this component is the RootComponent of the owning actor and overlaps with other descendant components have been verified.
(机翻)如果非空,则给定的重叠列表将用作该组件在当前位置的重叠,而不是使用场景查询来检查它们。
一般来说,只有当这个组件是拥有Actor的RootComponent,并且与其他子组件的重叠已经被验证时,才应该使用这个组件。
说人话就是,这个数组将会存有Self组件当前位置(查询末端位置)的所有重叠,并且只有self组件是Actor的根组件时才应该使用这个数组。
True to dispatch being/end overlap notifications when these events occur.
用于判断是否触发重叠事件。例如,当bDoNotifies为false时,OnBeginComponentOverlap、OnBeginActorComponentOverlap、OnEndComponentOverlap等相关委托都不会被触发。
目前看来OverlapsAtEndLocation和NewPendingOverlaps的关系挺微妙的,随着后面代码的分析,他们的作用会越来越清晰。
那么什么情况下需要更新组件的重叠呢?
很明显,当组件产生任何Transform的变换时,都应该更新重叠以防止漏过任何一个事件。
除此以外,当组件的碰撞状态发生变化时,也应该及时更新重叠。笔者经过对一个Character进行不严谨的调试,找到了几个比较典型的调用方式:
该函数是移动组件进行移动的主要函数,该函数会结合碰撞查询,计算出组件移动的目标位置,调用栈如下:
也就是说当你控制角色,使用移动组件进行移动时,每tick都会对重叠进行一次更新。
该函数用于更新Actor的transform时调用。例如SetActorRotation,SetActorPosition等函数,最终都会调用到MoveComponent函数,并对重叠进行更新
这类函数用于改变组件的碰撞状态,同理还有设置组件的通道类型等函数。当组件的碰撞状态发生改变时,都会调用一次UpdateOverlaps以更新重叠。
值得一提的是,这类函数对UpdateOverlaps调用的传参都是默认的,即传入的两个数组都是空值。这意味着更新重叠时不会引入新的重叠,只会对当前已记录的重叠进行操作。
// update overlaps once after all components have been updated UpdateOverlaps();
篇幅有限,笔者不会去详细讲解碰撞是如何查询并产生结果的,也不会去讲解组件移动具体会发生什么事情(因为笔者也没来得及弄懂)。现在只需要知道一个前提:UE能通过某种方式获得当前的碰撞信息,并存入前面提到的函数参数中的两个数组中。根据这个前提,接下来将围绕UpdateOverlapsImpl
函数对整个重叠更新进行详细的讲解。
总所周知,USceneComponent为Actor提供了表达自身空间信息的能力,可以为开发者提供Transform等信息,而碰撞相关的信息则交给了其子类UPrimitiveComponent。也就是说,只有继承了UPrimitiveComponent的类才能拥有碰撞处理的能力,否则这个组件就是空间中的一个幽灵,无法与世界进行任何交互。
而作为第一个拥有碰撞能力的组件,它拥有着一个足以彰显其身份的成员:
Set of components that this component is currently overlapping.
含义很明显,保存了所有与当前组件重叠且能生成重叠事件的其他组件。记住这个组件,可以说一个组件的重叠更新始终是围绕着这个组件完成的。
一开始是一些简单的判断。如果Actor还没有beginPlayer,将不会继续后续的逻辑。
紧随其后的,就是对NewPendingOverlaps数组进行处理,相关代码如下:
// first, dispatch any pending overlaps if (GetGenerateOverlapEvents() && IsQueryCollisionEnabled()) { bCanSkipUpdateOverlaps = false; if (MyActor) { const FTransform PrevTransform = GetComponentTransform(); // If we are the root component we ignore child components. Those children will update their overlaps when we descend into the child tree. // This aids an optimization in MoveComponent. const bool bIgnoreChildren = (MyActor->GetRootComponent() == this); if (NewPendingOverlaps) { // Note: BeginComponentOverlap() only triggers overlaps where GetGenerateOverlapEvents() is true on both components. const int32 NumNewPendingOverlaps = NewPendingOverlaps->Num(); for (int32 Idx=0; Idx < NumNewPendingOverlaps; ++Idx) { BeginComponentOverlap( (*NewPendingOverlaps)[Idx], bDoNotifies ); } } .........
是否生成重叠事件&是否允许碰撞。
IsQueryCollisionEnabled()可以通过SetActorEnableCollision改变其状态;
GetGenerateOverlapEvents()可以在蓝图里勾选“生成重叠事件”或者改变bool值bGenerateOverlapEvents进行修改。
补充一点,只有两个组件都能生成重叠事件,才会触发双方的BeginOverlap事件。
注意到有一个bIgnoreChildren变量,当self组件为根组件时其为true。这意味着根组件始终不会考虑子组件的影响。而子组件呢,默认下是会与本Actor的其他组件发生碰撞的,实际使用中我们很少会考虑这种问题,但这里可以作为一个小细节记一下。
之后将对NewPendingOverlaps进行一次完整的遍历。前面提到,NewPendingOverlaps可能包含已经重叠的组件,也可能包含还未重叠的组件。这些组件将在这个函数中进行统一处理,忽略已经重叠的组件,而未重叠的组件则调用双方的OnComponentBeginOverlap委托。
void UPrimitiveComponent::BeginComponentOverlap(const FOverlapInfo& OtherOverlap, bool bDoNotifies) { // If pending kill, we should not generate any new overlaps if (!IsValid(this)) { return; } const bool bComponentsAlreadyTouching = (IndexOfOverlapFast(OverlappingComponents, OtherOverlap) != INDEX_NONE); if (!bComponentsAlreadyTouching) { UPrimitiveComponent* OtherComp = OtherOverlap.OverlapInfo.Component.Get(); if (CanComponentsGenerateOverlap(this, OtherComp)) { GlobalOverlapEventsCounter++; AActor* const OtherActor = OtherComp->GetOwner(); AActor* const MyActor = GetOwner(); const bool bSameActor = (MyActor == OtherActor); const bool bNotifyActorTouch = bDoNotifies && !bSameActor && !AreActorsOverlapping(*MyActor, *OtherActor); // Perform reflexive touch. OverlappingComponents.Add(OtherOverlap); // already verified uniqueness above AddUniqueOverlapFast(OtherComp->OverlappingComponents, FOverlapInfo(this, INDEX_NONE)); // uniqueness unverified, so addunique const UWorld* World = GetWorld(); const bool bLevelStreamingOverlap = (bDoNotifies && MyActor->bGenerateOverlapEventsDuringLevelStreaming && MyActor->IsActorBeginningPlayFromLevelStreaming()); if (bDoNotifies && ((World && World->HasBegunPlay()) || bLevelStreamingOverlap)) { // first execute component delegates if (IsValid(this)) { OnComponentBeginOverlap.Broadcast(this, OtherActor, OtherComp, OtherOverlap.GetBodyIndex(), OtherOverlap.bFromSweep, OtherOverlap.OverlapInfo); } if (IsValid(OtherComp)) { // Reverse normals for other component. When it's a sweep, we are the one that moved. OtherComp->OnComponentBeginOverlap.Broadcast(OtherComp, MyActor, this, INDEX_NONE, OtherOverlap.bFromSweep, OtherOverlap.bFromSweep ? FHitResult::GetReversedHit(OtherOverlap.OverlapInfo) : OtherOverlap.OverlapInfo); } // then execute actor notification if this is a new actor touch if (bNotifyActorTouch) { // First actor virtuals if (IsActorValidToNotify(MyActor)) { MyActor->NotifyActorBeginOverlap(OtherActor); } if (IsActorValidToNotify(OtherActor)) { OtherActor->NotifyActorBeginOverlap(MyActor); } // Then level-script delegates if (IsActorValidToNotify(MyActor)) { MyActor->OnActorBeginOverlap.Broadcast(MyActor, OtherActor); } if (IsActorValidToNotify(OtherActor)) { OtherActor->OnActorBeginOverlap.Broadcast(OtherActor, MyActor); } } } } } }
逻辑并不难,主要做了以下几件事:
可以看到,组件通过检查自己的OverlappingComponents数组来判断是否是已经触发的重叠,来规避重叠事件的重复触发。另外,主动触发重叠的一方会直接触发双方的重叠事件,因为重叠更新通常是在运动中触发的,如果其中一方不移动,只触发主动方的事件的话将会漏掉对方的重叠事件。
由于该函数会自动规避已重叠的组件,因此我们就不用费心思考虑是否会重复触发重叠开始事件了,这个后面也会用到。
在重叠开始事件中往往会存在各种各样的逻辑,其中包括移动、销毁、添加其他Actor等等逻辑,这些都是不可预测的,UE很明显考虑到了这一点,在重叠开始事件结束后,还需要再次检查当前的状态是否和之前有所改变。
另外,我们还需要考虑本次重叠更新调用时,是否有旧的重叠已失效,比如我们走出了重叠的范围,或是别的组件自己关闭了碰撞。
// now generate full list of new touches, so we can compare to existing list and determine what changed TInlineOverlapInfoArray OverlapMultiResult; TInlineOverlapPointerArray NewOverlappingComponentPtrs;
因此,代码里新定义了两个临时数组,其中OverlapMultiResult将会保存在新的位置重新重叠检测的结果;NewOverlappingComponentPtrs更重要一些,会保存当前重叠的指针,让我们继续往后看。
// Might be able to avoid testing for new overlaps at the end location. if (OverlapsAtEndLocation != nullptr && bAllowCachedOverlapsCVar && PrevTransform.Equals(GetComponentTransform())) { const bool bCheckForInvalid = (NewPendingOverlaps && NewPendingOverlaps->Num() > 0); if (bCheckForInvalid) { // BeginComponentOverlap may have disabled what we thought were valid overlaps at the end (collision response or overlap flags could change). GetPointersToArrayDataByPredicate(NewOverlappingComponentPtrs, *OverlapsAtEndLocation, FPredicateFilterCanOverlap(*this)); } else { GetPointersToArrayData(NewOverlappingComponentPtrs, *OverlapsAtEndLocation); } }
筛选OverlapsAtEndLocation,将当前能触发重叠事件的组件指针存入NewOverlappingComponentPtrs。
这里使用bCheckForInvalid做了一个小优化,如果NewPendingOverlaps为空,就意味着没有任何BeginOverlap事件,就不需要筛选OverlapsAtEndLocation了,毕竟始终没有机会改变。
else { SCOPE_CYCLE_COUNTER(STAT_PerformOverlapQuery); UE_LOG(LogPrimitiveComponent, VeryVerbose, TEXT("%s->%s Performing overlaps!"), *GetNameSafe(GetOwner()), *GetName()); UWorld* const MyWorld = GetWorld(); TArray<FOverlapResult> Overlaps; // note this will optionally include overlaps with components in the same actor (depending on bIgnoreChildren). FComponentQueryParams Params(SCENE_QUERY_STAT(UpdateOverlaps), bIgnoreChildren ? MyActor : nullptr); Params.bIgnoreBlocks = true; //We don't care about blockers since we only route overlap events to real overlaps FCollisionResponseParams ResponseParam; InitSweepCollisionParams(Params, ResponseParam); ComponentOverlapMulti(Overlaps, MyWorld, GetComponentLocation(), GetComponentQuat(), GetCollisionObjectType(), Params); for (int32 ResultIdx=0; ResultIdx < Overlaps.Num(); ResultIdx++) { const FOverlapResult& Result = Overlaps[ResultIdx]; UPrimitiveComponent* const HitComp = Result.Component.Get(); if (HitComp && (HitComp != this) && HitComp->GetGenerateOverlapEvents()) { const bool bCheckOverlapFlags = false; // Already checked above if (!ShouldIgnoreOverlapResult(MyWorld, MyActor, *this, Result.OverlapObjectHandle.FetchActor(), *HitComp, bCheckOverlapFlags)) { OverlapMultiResult.Emplace(HitComp, Result.ItemIndex); // don't need to add unique unless the overlap check can return dupes } } } // Fill pointers to overlap results. We ensure below that OverlapMultiResult stays in scope so these pointers remain valid. GetPointersToArrayData(NewOverlappingComponentPtrs, OverlapMultiResult); }
当Self组件在BeginOverlap中发生了坐标的变化,那么我们就需要重新进行碰撞查询。这段代码看着复杂,其实也就只做了这一件事:调用ComponentOverlapMulti
函数进行重叠查询,然后将新查询到的重叠的指针放入NewOverlappingComponentPtrs中。
另外,这里再次用到了bIgnoreChildren,说明UE真的很不想让根组件更新到子组件的重叠,据说是为了优化MoveComponent的流程?大概吧,但是这并不意味着子组件不会和根组件发生重叠事件,当子组件主动更新重叠时,仍会检测到根组件,并触发双方的重叠事件。
这里埋下了一个伏笔,这段函数还有一个触发条件,就是OverlapsAtEndLocation为空的情况。本以为是一个不起眼的判断,却为子组件的重叠更新埋下了伏笔。
整理出可能存在的新的重叠后,我们还需考虑旧的重叠是否已经失效,因此需要对比新旧重叠,来获取新增的和过时的重叠。
总之先把旧的重叠缓存一下吧,很显然,直到前面调用重叠开始事件之前,OverlappingComponents数组里都是“旧重叠”。
这里的代码定义了OldOverlappingComponentPtrs数组,缓存了旧重叠的指针,对应前面的NewOverlappingComponentPtrs数组。之后将OverlappingComponents的元素以指针的方式拷贝到OldOverlappingComponentPtrs中。
// If we have any overlaps from BeginComponentOverlap() (from now or in the past), see if anything has changed by filtering NewOverlappingComponents if (OverlappingComponents.Num() > 0) { TInlineOverlapPointerArray OldOverlappingComponentPtrs; if (bIgnoreChildren) { GetPointersToArrayDataByPredicate(OldOverlappingComponentPtrs, OverlappingComponents, FPredicateOverlapHasDifferentActor(*MyActor)); } else { GetPointersToArrayData(OldOverlappingComponentPtrs, OverlappingComponents); }
那么怎么判断哪些重叠是过时的,哪些重叠是新的呢?
我们现在手里有两个数组,一个是NewOverlappingComponentPtrs,保存了当前所有有效的重叠;另一个是OldOverlappingComponentPtrs,保存了曾经有效的重叠。
那么去除这两个数组重复的部分,我们就可以筛选出过时的重叠和新的需要触发重叠事件的重叠。他们之间的关系如下图所示。
// Now we want to compare the old and new overlap lists to determine // what overlaps are in old and not in new (need end overlap notifies), and // what overlaps are in new and not in old (need begin overlap notifies). // We do this by removing common entries from both lists, since overlapping status has not changed for them. // What is left over will be what has changed. // 去除重复的部分 for (int32 CompIdx=0; CompIdx < OldOverlappingComponentPtrs.Num() && NewOverlappingComponentPtrs.Num() > 0; ++CompIdx) { // RemoveAtSwap is ok, since it is not necessary to maintain order const bool bAllowShrinking = false; const FOverlapInfo* SearchItem = OldOverlappingComponentPtrs[CompIdx]; const int32 NewElementIdx = IndexOfOverlapFast(NewOverlappingComponentPtrs, SearchItem); if (NewElementIdx != INDEX_NONE) { NewOverlappingComponentPtrs.RemoveAtSwap(NewElementIdx, 1, bAllowShrinking); OldOverlappingComponentPtrs.RemoveAtSwap(CompIdx, 1, bAllowShrinking); --CompIdx; } }
最终,OldOverlappingComponentPtrs就只剩下了过时的,需要调用EndOverlap的重叠;NewOverlappingComponentPtrs剩下了新增的,需要调用BeginOverlap的重叠。
const int32 NumOldOverlaps = OldOverlappingComponentPtrs.Num(); if (NumOldOverlaps > 0) { // Now we have to make a copy of the overlaps because we can't keep pointers to them, that list is about to be manipulated in EndComponentOverlap(). TInlineOverlapInfoArray OldOverlappingComponents; OldOverlappingComponents.SetNumUninitialized(NumOldOverlaps); for (int32 i=0; i < NumOldOverlaps; i++) { OldOverlappingComponents[i] = *(OldOverlappingComponentPtrs[i]); } // OldOverlappingComponents now contains only previous overlaps that are confirmed to no longer be valid. for (const FOverlapInfo& OtherOverlap : OldOverlappingComponents) { if (OtherOverlap.OverlapInfo.Component.IsValid()) { EndComponentOverlap(OtherOverlap, bDoNotifies, false); } else { // Remove stale item. Reclaim memory only if it's getting large, to try to avoid churn but avoid bloating component's memory usage. const bool bAllowShrinking = (OverlappingComponents.Max() >= 24); const int32 StaleElementIndex = IndexOfOverlapFast(OverlappingComponents, OtherOverlap); if (StaleElementIndex != INDEX_NONE) { OverlappingComponents.RemoveAtSwap(StaleElementIndex, 1, bAllowShrinking); } } } }
具体EndComponentOverlap发生了什么,基本和BeginCompoentOverlap反着来,笔者就不赘述了。
之后再将新的重叠遍历调用BeginCompoentOverlap,本次重叠更新的主要内容就基本结束了。
还记得前面提到的GetGenerateOverlapEvents() && IsQueryCollisionEnabled()条件判断吗?对于调用了SetActorEnableCollision关闭Actor碰撞的情况,这里当然也是有考虑的。
// first, dispatch any pending overlaps if (GetGenerateOverlapEvents() && IsQueryCollisionEnabled()) //TODO: should modifying query collision remove from mayoverlapevents? {....} else { // GetGenerateOverlapEvents() is false or collision is disabled // End all overlaps that exist, in case GetGenerateOverlapEvents() was true last tick (i.e. was just turned off) if (OverlappingComponents.Num() > 0) { const bool bSkipNotifySelf = false; ClearComponentOverlaps(bDoNotifies, bSkipNotifySelf); } }
当OverlappingComponents数组里还有重叠,我们需要将这些重叠全部处理掉,也就是一一调用EndComponentOverlap,UE在这里将其写成了一个ClearComponentOverlaps函数。
void UPrimitiveComponent::ClearComponentOverlaps(bool bDoNotifies, bool bSkipNotifySelf) { if (OverlappingComponents.Num() > 0) { // Make a copy since EndComponentOverlap will remove items from OverlappingComponents. const TInlineOverlapInfoArray OverlapsCopy(OverlappingComponents); for (const FOverlapInfo& OtherOverlap : OverlapsCopy) { EndComponentOverlap(OtherOverlap, bDoNotifies, bSkipNotifySelf); } } }
在讲解这部分之前,必须强调很重要的一点:
前面提到的移动过程产生的重叠更新,是不会直接通过子组件调用的,必须通过根组件先调用UpdateOverlap,然后经过循环递归调用,才能触发子组件的UpdateOverlap。
然后呢,看看在根组件经过前面一大串的逻辑后,在这个函数的末尾,是如何调用子组件的UpdateOverlap的:
// now update any children down the chain. // since on overlap events could manipulate the child array we need to take a copy // of it to avoid missing any children if one is removed from the middle TInlineComponentArray<USceneComponent*> AttachedChildren; AttachedChildren.Append(GetAttachChildren()); for (USceneComponent* const ChildComp : AttachedChildren) { if (ChildComp) { // Do not pass on OverlapsAtEndLocation, it only applied to this component. bCanSkipUpdateOverlaps &= ChildComp->UpdateOverlaps(nullptr, bDoNotifies, nullptr); } }
先说一个小细节:在遍历子组件之前,先缓存了一份子组件,是因为子组件更新重叠的过程中,可能会自己脱离父组件,导致循环出现BUG,这点大家平时写代码的时候要注意一下。
我们可以看到最后调用了这样一行代码:
ChildComp->UpdateOverlaps(nullptr, bDoNotifies, nullptr);
然后发现传入的两个数组都是nullptr。
what?两个数组都是空指针的话,那么子组件还怎么更新重叠?
现在回过去看 Self组件有移动的情况(或OverlapsAtEndLocation为空的情况)这一节,会发现子组件会直接走这段逻辑,也就是现场判断组件在场景中的重叠的方式,之后再进行后面的逻辑。
至此,UpdateOverlap的流程就基本结束了。
断点调试发现,根组件在调用UpdateOverlaps的NewPendingOverlaps数组中,并没有任何子组件,哪怕子组件碰撞全开。
往上追溯,才发现UPrimitiveComponent::MoveComponentImpl里在重叠检测时还藏了一手:
FComponentQueryParams Params(SCENE_QUERY_STAT(MoveComponent), Actor); FCollisionResponseParams ResponseParam; InitSweepCollisionParams(Params, ResponseParam); Params.bIgnoreTouches |= !(GetGenerateOverlapEvents() || bForceGatherOverlaps); Params.TraceTag = TraceTagName; bool const bHadBlockingHit = MyWorld->ComponentSweepMulti(Hits, this, TraceStart, TraceEnd, InitialRotationQuat, Params);
FComponentQueryParams Params的第二个参数就是要忽略的Actor,这里的Actor指的就是本身,所以检测的结果自然就没有自己的子组件了。
不过即便如此,如果子组件在碰撞上允许和根组件生成重叠事件时,在子组件的UpdateOverlaps还是不可避免地与根组件发生重叠关系。不过UE的注释里都提到了,这都是为了优化MovementCompoennt的移动流程。
角色移动组件 | 虚幻引擎文档 (unrealengine.com)
UE4的移动碰撞 - 知乎 (zhihu.com)