• UE5: UpdateOverlap - 从源码深入探究UE的重叠触发


    前言

    出于工作需要和个人好奇,本文对UE重叠事件更新的主要函数UpdateOverlaps从源码的角度进行了详细的分析,通过阅读源码,深入理解重叠事件是如何被触发和更新的。

    解决问题

    阅读本文,你将得到至少以下问题的答案:

    1. BeginComponentOverlap和EndComponentOverlap事件是如何被触发的?

    2. UE是如何保存和管理组件之间的碰撞的?

    3. SetActorEnableCollision是如何起到作用的?

    4. 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.

    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 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);
    					}
    				}
    			}
    		}
    	}
    }
    

    逻辑并不难,主要做了以下几件事:

    1. 检查OverlappingComponents数组,判断该组件是否已重叠,如果未重叠就执行后面的逻辑
    2. CanComponentsGenerateOverlap 判断双方是否都能生成重叠事件,如果其中一方不能重叠,函数到这也就结束了
    3. 判断两个Actor是否已重叠,如果已重叠,后续则不会触发ActorOverlap事件
    4. 添加新的重叠到自己的OverlappingComponents中
    5. 将自己添加到对方的OverlappingComponents中
    6. 触发双方的ComponentBeginOverlap委托
    7. 触发双方的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 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 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)

  • 相关阅读:
    虹科方案 | 加州理工学院利用HK-TrueNAS开展地震研究
    Linux UOS统信系统安装并使用KVM
    vscode+phpstudy断点调试php程序
    ASTM标准涵盖哪些产品类别?出口美国的产品做ASTM认证需要注意哪些事项?
    多任务爬虫(多线程和多进程)
    英语四级核心词,记住这些就够了
    JVM探究
    Java类加载器
    带你认识工厂类设计模式——简单工厂&工厂方法&抽象工厂&简单抽象工厂&反射简单抽象工厂
    学之思第二天-调通登录功能
  • 原文地址:https://www.cnblogs.com/Qiu-Bai/p/17961318