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