UE5: UpdateOverlap - 从源码深入探究UE的重叠触发
前言
出于工作需要和個(gè)人好奇,本文對(duì)UE重疊事件更新的主要函數(shù)UpdateOverlaps從源碼的角度進(jìn)行了詳細(xì)的分析,通過閱讀源碼,深入理解重疊事件是如何被觸發(fā)和更新的。
解決問題
閱讀本文,你將得到至少以下問題的答案:
-
BeginComponentOverlap和EndComponentOverlap事件是如何被觸發(fā)的?
-
UE是如何保存和管理組件之間的碰撞的?
-
SetActorEnableCollision是如何起到作用的?
-
UE如何處理不同Actor,或者同一個(gè)Actor里重復(fù)碰撞的情況?
以上只是提出了幾個(gè)很基礎(chǔ)的問題,隨著源碼的不斷解析,還會(huì)有更多新的問題會(huì)被提出。
說是深入探究,更像是筆者個(gè)人的學(xué)習(xí)筆記。其中一些筆者認(rèn)為本應(yīng)被了解的細(xì)節(jié)不會(huì)被提起,請(qǐng)讀者至少對(duì)UE的重疊機(jī)制有基礎(chǔ)的理解。
重疊更新的入口:USceneComponent::UpdateOverlaps
Queries world and updates overlap tracking state for this component
當(dāng)一個(gè)組件需要更新當(dāng)前重疊狀態(tài)時(shí),就會(huì)調(diào)用這個(gè)函數(shù)。
這個(gè)函數(shù)定義在USceneComponent,表明只有場(chǎng)景組件的子類才能調(diào)用該函數(shù)。并且它不是一個(gè)虛函數(shù),更新重疊相關(guān)的具體實(shí)現(xiàn)放在一個(gè)叫``UpdateOverlapsImpl的虛函數(shù)中。因此可以將UpdateOverlaps視作為重疊更新的總?cè)肟冢缓笳{(diào)用子類的UpdateOverlapsImpl`從而執(zhí)行具體的更新邏輯。
觀察該函數(shù)的聲明:
bool UpdateOverlaps(const TOverlapArrayView* PendingOverlaps = nullptr, bool bDoNotifies = true, const TOverlapArrayView* OverlapsAtEndLocation = nullptr);
其中,TOverlapArrayView就是經(jīng)過了typedef的TArrayView<const FOverlapInfo>.
FOverlapInfo是對(duì)FHitResult的一個(gè)簡(jiǎn)單封裝,FHitResult相信大家都很熟悉了,通過使用FHitResult,我們可以很輕松的獲得本次碰撞查詢中碰撞到的組件,以及碰撞的各種信息,例如碰撞坐標(biāo),法線等等。
接下來對(duì)參數(shù)列表中三個(gè)參數(shù)進(jìn)行講解,這幾個(gè)參數(shù)還是挺重要的,后面會(huì)反復(fù)使用到這三個(gè)參數(shù)。
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.
移動(dòng)組件在移動(dòng)過程中重疊的有序組件列表(例如:在掃描過程中生成)。僅用于添加潛在的新重疊。
說人話就是,本次碰撞查詢中檢測(cè)到的將要碰到的重疊。之后在UpdateOverlapsImpl中,將會(huì)使用該數(shù)組調(diào)用BeginComponentOverlap.
值得一提的是,即使我們當(dāng)前的組件(后續(xù)我們就叫它Self組件吧)已經(jīng)在其他組件的重疊里了,此時(shí)如果有移動(dòng)行為的話,該數(shù)組仍會(huì)把已經(jīng)重疊的組件保存進(jìn)去,至于會(huì)不會(huì)重復(fù)觸發(fā)BeginOverlap,后續(xù)當(dāng)然有相關(guān)的邏輯處理,這里先按下不表。
如果當(dāng)前沒有移動(dòng),只是簡(jiǎn)單的對(duì)組件進(jìn)行了旋轉(zhuǎn),那么這個(gè)數(shù)組將會(huì)是空的,可以查閱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);
有個(gè)bRotationOnly變量,如果只有旋轉(zhuǎn)的話,不會(huì)對(duì)PendingOverlaps進(jìn)行賦值。
也就是說,組件只做原地旋轉(zhuǎn)的話,是不會(huì)有新的重疊開始事件的。
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.
(機(jī)翻)如果非空,則給定的重疊列表將用作該組件在當(dāng)前位置的重疊,而不是使用場(chǎng)景查詢來檢查它們。
一般來說,只有當(dāng)這個(gè)組件是擁有Actor的RootComponent,并且與其他子組件的重疊已經(jīng)被驗(yàn)證時(shí),才應(yīng)該使用這個(gè)組件。
說人話就是,這個(gè)數(shù)組將會(huì)存有Self組件當(dāng)前位置(查詢末端位置)的所有重疊,并且只有self組件是Actor的根組件時(shí)才應(yīng)該使用這個(gè)數(shù)組。
bDoNotifies
True to dispatch being/end overlap notifications when these events occur.
用于判斷是否觸發(fā)重疊事件。例如,當(dāng)bDoNotifies為false時(shí),OnBeginComponentOverlap、OnBeginActorComponentOverlap、OnEndComponentOverlap等相關(guān)委托都不會(huì)被觸發(fā)。
目前看來OverlapsAtEndLocation和NewPendingOverlaps的關(guān)系挺微妙的,隨著后面代碼的分析,他們的作用會(huì)越來越清晰。
調(diào)用該函數(shù)的幾種情況
那么什么情況下需要更新組件的重疊呢?
很明顯,當(dāng)組件產(chǎn)生任何Transform的變換時(shí),都應(yīng)該更新重疊以防止漏過任何一個(gè)事件。
除此以外,當(dāng)組件的碰撞狀態(tài)發(fā)生變化時(shí),也應(yīng)該及時(shí)更新重疊。筆者經(jīng)過對(duì)一個(gè)Character進(jìn)行不嚴(yán)謹(jǐn)?shù)恼{(diào)試,找到了幾個(gè)比較典型的調(diào)用方式:
1. UCharacterMovementComponent::PerformMovement
該函數(shù)是移動(dòng)組件進(jìn)行移動(dòng)的主要函數(shù),該函數(shù)會(huì)結(jié)合碰撞查詢,計(jì)算出組件移動(dòng)的目標(biāo)位置,調(diào)用棧如下:
也就是說當(dāng)你控制角色,使用移動(dòng)組件進(jìn)行移動(dòng)時(shí),每tick都會(huì)對(duì)重疊進(jìn)行一次更新。
2.UPrimiticeComponent::MoveComponent
該函數(shù)用于更新Actor的transform時(shí)調(diào)用。例如SetActorRotation,SetActorPosition等函數(shù),最終都會(huì)調(diào)用到MoveComponent函數(shù),并對(duì)重疊進(jìn)行更新
3.AActor::SetActorEnableCollision
這類函數(shù)用于改變組件的碰撞狀態(tài),同理還有設(shè)置組件的通道類型等函數(shù)。當(dāng)組件的碰撞狀態(tài)發(fā)生改變時(shí),都會(huì)調(diào)用一次UpdateOverlaps以更新重疊。
值得一提的是,這類函數(shù)對(duì)UpdateOverlaps調(diào)用的傳參都是默認(rèn)的,即傳入的兩個(gè)數(shù)組都是空值。這意味著更新重疊時(shí)不會(huì)引入新的重疊,只會(huì)對(duì)當(dāng)前已記錄的重疊進(jìn)行操作。
// update overlaps once after all components have been updated
UpdateOverlaps();
真正更新重疊的實(shí)現(xiàn)函數(shù):UPrimitiveComponent::UpdateOverlapsImpl
篇幅有限,筆者不會(huì)去詳細(xì)講解碰撞是如何查詢并產(chǎn)生結(jié)果的,也不會(huì)去講解組件移動(dòng)具體會(huì)發(fā)生什么事情(因?yàn)楣P者也沒來得及弄懂)。現(xiàn)在只需要知道一個(gè)前提:UE能通過某種方式獲得當(dāng)前的碰撞信息,并存入前面提到的函數(shù)參數(shù)中的兩個(gè)數(shù)組中。根據(jù)這個(gè)前提,接下來將圍繞UpdateOverlapsImpl 函數(shù)對(duì)整個(gè)重疊更新進(jìn)行詳細(xì)的講解。
總所周知,USceneComponent為Actor提供了表達(dá)自身空間信息的能力,可以為開發(fā)者提供Transform等信息,而碰撞相關(guān)的信息則交給了其子類UPrimitiveComponent。也就是說,只有繼承了UPrimitiveComponent的類才能擁有碰撞處理的能力,否則這個(gè)組件就是空間中的一個(gè)幽靈,無法與世界進(jìn)行任何交互。
而作為第一個(gè)擁有碰撞能力的組件,它擁有著一個(gè)足以彰顯其身份的成員:
TArray<FOverlapInfo> OverlappingComponents;
Set of components that this component is currently overlapping.
含義很明顯,保存了所有與當(dāng)前組件重疊且能生成重疊事件的其他組件。記住這個(gè)組件,可以說一個(gè)組件的重疊更新始終是圍繞著這個(gè)組件完成的。
對(duì)新加入的重疊進(jìn)行處理
一開始是一些簡(jiǎn)單的判斷。如果Actor還沒有beginPlayer,將不會(huì)繼續(xù)后續(xù)的邏輯。
緊隨其后的,就是對(duì)NewPendingOverlaps數(shù)組進(jìn)行處理,相關(guān)代碼如下:
// 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改變其狀態(tài);
GetGenerateOverlapEvents()可以在藍(lán)圖里勾選“生成重疊事件”或者改變bool值bGenerateOverlapEvents進(jìn)行修改。
補(bǔ)充一點(diǎn),只有兩個(gè)組件都能生成重疊事件,才會(huì)觸發(fā)雙方的BeginOverlap事件。
注意到有一個(gè)bIgnoreChildren變量,當(dāng)self組件為根組件時(shí)其為true。這意味著根組件始終不會(huì)考慮子組件的影響。而子組件呢,默認(rèn)下是會(huì)與本Actor的其他組件發(fā)生碰撞的,實(shí)際使用中我們很少會(huì)考慮這種問題,但這里可以作為一個(gè)小細(xì)節(jié)記一下。
UPrimitiveComponent::BeginComponentOverlap(const FOverlapInfo& OtherOverlap, bool bDoNotifies)
之后將對(duì)NewPendingOverlaps進(jìn)行一次完整的遍歷。前面提到,NewPendingOverlaps可能包含已經(jīng)重疊的組件,也可能包含還未重疊的組件。這些組件將在這個(gè)函數(shù)中進(jìn)行統(tǒng)一處理,忽略已經(jīng)重疊的組件,而未重疊的組件則調(diào)用雙方的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數(shù)組,判斷該組件是否已重疊,如果未重疊就執(zhí)行后面的邏輯
- CanComponentsGenerateOverlap 判斷雙方是否都能生成重疊事件,如果其中一方不能重疊,函數(shù)到這也就結(jié)束了
- 判斷兩個(gè)Actor是否已重疊,如果已重疊,后續(xù)則不會(huì)觸發(fā)ActorOverlap事件
- 添加新的重疊到自己的OverlappingComponents中
- 將自己添加到對(duì)方的OverlappingComponents中
- 觸發(fā)雙方的ComponentBeginOverlap委托
- 觸發(fā)雙方的ActorBeginOverlap委托
可以看到,組件通過檢查自己的OverlappingComponents數(shù)組來判斷是否是已經(jīng)觸發(fā)的重疊,來規(guī)避重疊事件的重復(fù)觸發(fā)。另外,主動(dòng)觸發(fā)重疊的一方會(huì)直接觸發(fā)雙方的重疊事件,因?yàn)橹丿B更新通常是在運(yùn)動(dòng)中觸發(fā)的,如果其中一方不移動(dòng),只觸發(fā)主動(dòng)方的事件的話將會(huì)漏掉對(duì)方的重疊事件。
由于該函數(shù)會(huì)自動(dòng)規(guī)避已重疊的組件,因此我們就不用費(fèi)心思考慮是否會(huì)重復(fù)觸發(fā)重疊開始事件了,這個(gè)后面也會(huì)用到。
在重疊開始事件中往往會(huì)存在各種各樣的邏輯,其中包括移動(dòng)、銷毀、添加其他Actor等等邏輯,這些都是不可預(yù)測(cè)的,UE很明顯考慮到了這一點(diǎn),在重疊開始事件結(jié)束后,還需要再次檢查當(dāng)前的狀態(tài)是否和之前有所改變。
另外,我們還需要考慮本次重疊更新調(diào)用時(shí),是否有舊的重疊已失效,比如我們走出了重疊的范圍,或是別的組件自己關(guān)閉了碰撞。
// now generate full list of new touches, so we can compare to existing list and determine what changed
TInlineOverlapInfoArray OverlapMultiResult;
TInlineOverlapPointerArray NewOverlappingComponentPtrs;
因此,代碼里新定義了兩個(gè)臨時(shí)數(shù)組,其中OverlapMultiResult將會(huì)保存在新的位置重新重疊檢測(cè)的結(jié)果;NewOverlappingComponentPtrs更重要一些,會(huì)保存當(dāng)前重疊的指針,讓我們繼續(xù)往后看。
Self組件沒有移動(dòng)的情況
// 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,將當(dāng)前能觸發(fā)重疊事件的組件指針存入NewOverlappingComponentPtrs。
這里使用bCheckForInvalid做了一個(gè)小優(yōu)化,如果NewPendingOverlaps為空,就意味著沒有任何BeginOverlap事件,就不需要篩選OverlapsAtEndLocation了,畢竟始終沒有機(jī)會(huì)改變。
Self組件有移動(dòng)的情況(或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);
}
當(dāng)Self組件在BeginOverlap中發(fā)生了坐標(biāo)的變化,那么我們就需要重新進(jìn)行碰撞查詢。這段代碼看著復(fù)雜,其實(shí)也就只做了這一件事:調(diào)用ComponentOverlapMulti函數(shù)進(jìn)行重疊查詢,然后將新查詢到的重疊的指針放入NewOverlappingComponentPtrs中。
另外,這里再次用到了bIgnoreChildren,說明UE真的很不想讓根組件更新到子組件的重疊,據(jù)說是為了優(yōu)化MoveComponent的流程?大概吧,但是這并不意味著子組件不會(huì)和根組件發(fā)生重疊事件,當(dāng)子組件主動(dòng)更新重疊時(shí),仍會(huì)檢測(cè)到根組件,并觸發(fā)雙方的重疊事件。
這里埋下了一個(gè)伏筆,這段函數(shù)還有一個(gè)觸發(fā)條件,就是OverlapsAtEndLocation為空的情況。本以為是一個(gè)不起眼的判斷,卻為子組件的重疊更新埋下了伏筆。
整理出可能存在的新的重疊后,我們還需考慮舊的重疊是否已經(jīng)失效,因此需要對(duì)比新舊重疊,來獲取新增的和過時(shí)的重疊。
對(duì)比新舊重疊
緩存舊重疊
總之先把舊的重疊緩存一下吧,很顯然,直到前面調(diào)用重疊開始事件之前,OverlappingComponents數(shù)組里都是“舊重疊”。
這里的代碼定義了OldOverlappingComponentPtrs數(shù)組,緩存了舊重疊的指針,對(duì)應(yīng)前面的NewOverlappingComponentPtrs數(shù)組。之后將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);
}
篩選新舊重疊
那么怎么判斷哪些重疊是過時(shí)的,哪些重疊是新的呢?
我們現(xiàn)在手里有兩個(gè)數(shù)組,一個(gè)是NewOverlappingComponentPtrs,保存了當(dāng)前所有有效的重疊;另一個(gè)是OldOverlappingComponentPtrs,保存了曾經(jīng)有效的重疊。
那么去除這兩個(gè)數(shù)組重復(fù)的部分,我們就可以篩選出過時(shí)的重疊和新的需要觸發(fā)重疊事件的重疊。他們之間的關(guān)系如下圖所示。
// 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.
// 去除重復(fù)的部分
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就只剩下了過時(shí)的,需要調(diào)用EndOverlap的重疊;NewOverlappingComponentPtrs剩下了新增的,需要調(diào)用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發(fā)生了什么,基本和BeginCompoentOverlap反著來,筆者就不贅述了。
之后再將新的重疊遍歷調(diào)用BeginCompoentOverlap,本次重疊更新的主要內(nèi)容就基本結(jié)束了。
Self組件沒開啟碰撞的情況
還記得前面提到的GetGenerateOverlapEvents() && IsQueryCollisionEnabled()條件判斷嗎?對(duì)于調(diào)用了SetActorEnableCollision關(guān)閉Actor碰撞的情況,這里當(dāng)然也是有考慮的。
// 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);
}
}
當(dāng)OverlappingComponents數(shù)組里還有重疊,我們需要將這些重疊全部處理掉,也就是一一調(diào)用EndComponentOverlap,UE在這里將其寫成了一個(gè)ClearComponentOverlaps函數(shù)。
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);
}
}
}
調(diào)用子組件的UpdateOverlap
在講解這部分之前,必須強(qiáng)調(diào)很重要的一點(diǎn):
前面提到的移動(dòng)過程產(chǎn)生的重疊更新,是不會(huì)直接通過子組件調(diào)用的,必須通過根組件先調(diào)用UpdateOverlap,然后經(jīng)過循環(huán)遞歸調(diào)用,才能觸發(fā)子組件的UpdateOverlap。
然后呢,看看在根組件經(jīng)過前面一大串的邏輯后,在這個(gè)函數(shù)的末尾,是如何調(diào)用子組件的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);
}
}
先說一個(gè)小細(xì)節(jié):在遍歷子組件之前,先緩存了一份子組件,是因?yàn)樽咏M件更新重疊的過程中,可能會(huì)自己脫離父組件,導(dǎo)致循環(huán)出現(xiàn)BUG,這點(diǎn)大家平時(shí)寫代碼的時(shí)候要注意一下。
我們可以看到最后調(diào)用了這樣一行代碼:
ChildComp->UpdateOverlaps(nullptr, bDoNotifies, nullptr);
然后發(fā)現(xiàn)傳入的兩個(gè)數(shù)組都是nullptr。
what??jī)蓚€(gè)數(shù)組都是空指針的話,那么子組件還怎么更新重疊?
現(xiàn)在回過去看 Self組件有移動(dòng)的情況(或OverlapsAtEndLocation為空的情況)這一節(jié),會(huì)發(fā)現(xiàn)子組件會(huì)直接走這段邏輯,也就是現(xiàn)場(chǎng)判斷組件在場(chǎng)景中的重疊的方式,之后再進(jìn)行后面的邏輯。
至此,UpdateOverlap的流程就基本結(jié)束了。
根組件跳過子組件的重疊查詢
斷點(diǎn)調(diào)試發(fā)現(xiàn),根組件在調(diào)用UpdateOverlaps的NewPendingOverlaps數(shù)組中,并沒有任何子組件,哪怕子組件碰撞全開。
往上追溯,才發(fā)現(xiàn)UPrimitiveComponent::MoveComponentImpl里在重疊檢測(cè)時(shí)還藏了一手:
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的第二個(gè)參數(shù)就是要忽略的Actor,這里的Actor指的就是本身,所以檢測(cè)的結(jié)果自然就沒有自己的子組件了。
不過即便如此,如果子組件在碰撞上允許和根組件生成重疊事件時(shí),在子組件的UpdateOverlaps還是不可避免地與根組件發(fā)生重疊關(guān)系。不過UE的注釋里都提到了,這都是為了優(yōu)化MovementCompoennt的移動(dòng)流程。
參考
角色移動(dòng)組件 | 虛幻引擎文檔 (unrealengine.com)
UE4的移動(dòng)碰撞 - 知乎 (zhihu.com)
總結(jié)
以上是生活随笔為你收集整理的UE5: UpdateOverlap - 从源码深入探究UE的重叠触发的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 你真的会用 npx 吗❓❓❓
- 下一篇: IPv6通过公网共享文件(Windows