前言
出于工作需要和个人好奇,本文对UE重叠事件更新的主要函数UpdateOverlaps从源码的角度进行了详细的分析,通过阅读源码,深入理解重叠事件是如何被触发和更新的。
解决问题
阅读本文,你将得到至少以下问题的答案:
-
BeginComponentOverlap和EndComponentOverlap事件是如何被触发的?
-
UE是如何保存和管理组件之间的碰撞的?
-
SetActorEnableCollision是如何起到作用的?
-
UE如何处理不同Actor,或者同一个Actor里重复碰撞的情况?
以上只是提出了几个很基础的问题,随着源码的不断解析,还会有更多新的问题会被提出。
说是深入探究,更像是笔者个人的学习笔记。其中一些笔者认为本应被了解的细节不会被提起,请读者至少对UE的重叠机制有基础的理解。
重叠更新的入口:USceneComponent::UpdateOverlaps
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,我们可以很轻松的获得本次碰撞查询中碰撞到的组件,以及碰撞的各种信息,例如碰撞坐标,法线等等。
接下来对参数列表中三个参数进行讲解,这几个参数还是挺重要的,后面会反复使用到这三个参数。
NewPendingOverlaps
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进行赋值。
也就是说,组件只做原地旋转的话,是不会有新的重叠开始事件的。
OverlapsAtEndLocation
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的根组件时才应该使用这个数组。
bDoNotifies
True to dispatch being/end overlap notifications when these events occur.
用于判断是否触发重叠事件。例如,当bDoNotifies为false时,OnBeginComponentOverlap、OnBeginActorComponentOverlap、OnEndComponentOverlap等相关委托都不会被触发。
目前看来OverlapsAtEndLocation和NewPendingOverlaps的关系挺微妙的,随着后面代码的分析,他们的作用会越来越清晰。
调用该函数的几种情况
那么什么情况下需要更新组件的重叠呢?
很明显,当组件产生任何Transform的变换时,都应该更新重叠以防止漏过任何一个事件。
除此以外,当组件的碰撞状态发生变化时,也应该及时更新重叠。笔者经过对一个Character进行不严谨的调试,找到了几个比较典型的调用方式:
1. UCharacterMovementComponent::PerformMovement
该函数是移动组件进行移动的主要函数,该函数会结合碰撞查询,计算出组件移动的目标位置,调用栈如下:
也就是说当你控制角色,使用移动组件进行移动时,每tick都会对重叠进行一次更新。
2.UPrimiticeComponent::MoveComponent
该函数用于更新Actor的transform时调用。例如SetActorRotation,SetActorPosition等函数,最终都会调用到MoveComponent函数,并对重叠进行更新
3.AActor::SetActorEnableCollision
这类函数用于改变组件的碰撞状态,同理还有设置组件的通道类型等函数。当组件的碰撞状态发生改变时,都会调用一次UpdateOverlaps以更新重叠。
值得一提的是,这类函数对UpdateOverlaps调用的传参都是默认的,即传入的两个数组都是空值。这意味着更新重叠时不会引入新的重叠,只会对当前已记录的重叠进行操作。
// update overlaps once after all components have been updated
UpdateOverlaps();
真正更新重叠的实现函数:UPrimitiveComponent::UpdateOverlapsImpl
篇幅有限,笔者不会去详细讲解碰撞是如何查询并产生结果的,也不会去讲解组件移动具体会发生什么事情(因为笔者也没来得及弄懂)。现在只需要知道一个前提:UE能通过某种方式获得当前的碰撞信息,并存入前面提到的函数参数中的两个数组中。根据这个前提,接下来将围绕UpdateOverlapsImpl
函数对整个重叠更新进行详细的讲解。
总所周知,USceneComponent为Actor提供了表达自身空间信息的能力,可以为开发者提供Transform等信息,而碰撞相关的信息则交给了其子类UPrimitiveComponent。也就是说,只有继承了UPrimitiveComponent的类才能拥有碰撞处理的能力,否则这个组件就是空间中的一个幽灵,无法与世界进行任何交互。
而作为第一个拥有碰撞能力的组件,它拥有着一个足以彰显其身份的成员:
TArray<FOverlapInfo> OverlappingComponents;
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 );
}
}
.........
GetGenerateOverlapEvents() && IsQueryCollisionEnabled()
是否生成重叠事件&是否允许碰撞。
IsQueryCollisionEnabled()可以通过SetActorEnableCollision改变其状态;
GetGenerateOverlapEvents()可以在蓝图里勾选“生成重叠事件”或者改变bool值bGenerateOverlapEvents进行修改。
补充一点,只有两个组件都能生成重叠事件,才会触发双方的BeginOverlap事件。
注意到有一个bIgnoreChildren变量,当self组件为根组件时其为true。这意味着根组件始终不会考虑子组件的影响。而子组件呢,默认下是会与本Actor的其他组件发生碰撞的,实际使用中我们很少会考虑这种问题,但这里可以作为一个小细节记一下。
UPrimitiveComponent::BeginComponentOverlap(const FOverlapInfo& OtherOverlap, bool bDoNotifies)
之后将对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数组,判断该组件是否已重叠,如果未重叠就执行后面的逻辑
- CanComponentsGenerateOverlap 判断双方是否都能生成重叠事件,如果其中一方不能重叠,函数到这也就结束了
- 判断两个Actor是否已重叠,如果已重叠,后续则不会触发ActorOverlap事件
- 添加新的重叠到自己的OverlappingComponents中
- 将自己添加到对方的OverlappingComponents中
- 触发双方的ComponentBeginOverlap委托
- 触发双方的ActorBeginOverlap委托
可以看到,组件通过检查自己的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更重要一些,会保存当前重叠的指针,让我们继续往后看。
Self组件没有移动的情况
// 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了,毕竟始终没有机会改变。
Self组件有移动的情况(或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的重叠。
EndComponentOverlap
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,本次重叠更新的主要内容就基本结束了。
Self组件没开启碰撞的情况
还记得前面提到的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函数。
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。
然后呢,看看在根组件经过前面一大串的逻辑后,在这个函数的末尾,是如何调用子组件的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)