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

2024/1/13 1:02:25

本文主要是介绍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<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);
					}
				}
			}
		}
	}
}

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

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

UE4的移动碰撞 - 知乎 (zhihu.com)



这篇关于UE5: UpdateOverlap - 从源码深入探究UE的重叠触发的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程