给UE5优化一丢丢编辑器性能

背后的原理

先看FActorIterator的定义

c++ 复制代码
/**
 * Actor iterator
 * Note that when Playing In Editor, this will find actors only in CurrentWorld
 */
class FActorIterator : public TActorIteratorBase<FActorIterator>
{
    //.....
}

找到基类TActorIteratorBase

c++ 复制代码
/**
 * Template class used to filter actors by certain characteristics
 */
template <typename Derived>
class TActorIteratorBase
{
public:
	/**
	 * Iterates to next suitable actor.
	 */
	void operator++()
	{
		// Use local version to avoid LHSs as compiler is not required to write out member variables to memory.
		AActor*           LocalCurrentActor      = nullptr;
		int32             LocalIndex             = State->Index;
		TArray<UObject*>& LocalObjectArray       = State->ObjectArray;
		TArray<AActor*>&  LocalSpawnedActorArray = State->SpawnedActorArray;
		const UWorld*     LocalCurrentWorld      = State->CurrentWorld;
		while(++LocalIndex < (LocalObjectArray.Num() + LocalSpawnedActorArray.Num()))
		{
			if (LocalIndex < LocalObjectArray.Num())
			{
				LocalCurrentActor = static_cast<AActor*>(LocalObjectArray[LocalIndex]);
			}
			else
			{
				LocalCurrentActor = LocalSpawnedActorArray[LocalIndex - LocalObjectArray.Num()];
			}
			State->ConsideredCount++;
			
			ULevel* ActorLevel = LocalCurrentActor ? LocalCurrentActor->GetLevel() : nullptr;
			if ( ActorLevel
				&& static_cast<const Derived*>(this)->IsActorSuitable(LocalCurrentActor)
				&& static_cast<const Derived*>(this)->CanIterateLevel(ActorLevel)
				&& ActorLevel->GetWorld() == LocalCurrentWorld)
			{
				// ignore non-persistent world settings
				if (ActorLevel == LocalCurrentWorld->PersistentLevel || !LocalCurrentActor->IsA(AWorldSettings::StaticClass()))
				{
					State->CurrentActor = LocalCurrentActor;
					State->Index = LocalIndex;
					return;
				}
			}
		}
		State->CurrentActor = nullptr;
		State->ReachedEnd = true;
	}
//.....省略剩余代码
}

可以看到TActorIteratorBase基本就是在遍历FActorIteratorState里的两个数组。为啥有两个数组?因为遍历的过程中可能又Spawn了新的Actor。

接着看FActorIteratorState的代码

c++ 复制代码
/*-----------------------------------------------------------------------------
	Iterator for the editor that loops through all selected actors.
-----------------------------------------------------------------------------*/

/**
 * Abstract base class for actor iteration. Implements all operators and relies on IsActorSuitable
 * to be overridden by derived class.
 * Note that when Playing In Editor, this will find actors only in CurrentWorld.
 */
class FActorIteratorState
{
public:
	/** Current world we are iterating upon						*/
	const UWorld* CurrentWorld;
	/** Results from the GetObjectsOfClass query				*/
	TArray<UObject*> ObjectArray;
	/** index of the current element in the object array		*/
	int32 Index;
	/** Whether we already reached the end						*/
	bool	ReachedEnd;
	/** Number of actors that have been considered thus far		*/
	int32		ConsideredCount;
	/** Current actor pointed to by actor iterator				*/
	AActor*	CurrentActor;
	/** Contains any actors spawned during iteration			*/
	TArray<AActor*> SpawnedActorArray;
	/** The class type we are iterating, kept for filtering		*/
	UClass* DesiredClass;
	/** Handle to the registered OnActorSpawned delegate		*/
	FDelegateHandle ActorSpawnedDelegateHandle;

	/**
	 * Default ctor, inits everything
	 */
	FActorIteratorState(const UWorld* InWorld, const TSubclassOf<AActor> InClass) :
		CurrentWorld( InWorld ),
		Index( -1 ),
		ReachedEnd( false ),
		ConsideredCount( 0 ),
		CurrentActor(nullptr),
		DesiredClass(InClass)
	{
		check(IsInGameThread());
		check(CurrentWorld);

#if WITH_EDITOR
		// In the editor, you are more likely to have many worlds in memory at once.
		// As an optimization to avoid iterating over many actors that are not in the world we are asking for,
		// if the filter class is AActor, just use the actors that are in the world you asked for.
		// This could be useful in runtime code as well if there are many worlds in memory, but for now we will leave
		// it in editor code.
		if (InClass == AActor::StaticClass())
		{
			// First determine the number of actors in the world to reduce reallocations when we append them to the array below.
			int32 NumActors = 0;
			for (ULevel* Level : InWorld->GetLevels())
			{
				if (Level)
				{
					NumActors += Level->Actors.Num();
				}
			}

			// Presize the array
			ObjectArray.Reserve(NumActors);

			// Fill the array
			for (ULevel* Level : InWorld->GetLevels())
			{
				if (Level)
				{
					ObjectArray.Append(Level->Actors);
				}
			}
		}
		else
#endif // WITH_EDITOR
		{
			constexpr EObjectFlags ExcludeFlags = RF_ClassDefaultObject;
			GetObjectsOfClass(InClass, ObjectArray, true, ExcludeFlags, EInternalObjectFlags::Garbage);
		}

		const auto ActorSpawnedDelegate = FOnActorSpawned::FDelegate::CreateRaw(this, &FActorIteratorState::OnActorSpawned);
		ActorSpawnedDelegateHandle = CurrentWorld->AddOnActorSpawnedHandler(ActorSpawnedDelegate);
	}

	~FActorIteratorState()
	{
		CurrentWorld->RemoveOnActorSpawnedHandler(ActorSpawnedDelegateHandle);
	}

	/**
	 * Returns the current suitable actor pointed at by the Iterator
	 *
	 * @return	Current suitable actor
	 */
	FORCEINLINE AActor* GetActorChecked() const
	{
		check(CurrentActor);
		checkf(!CurrentActor->IsUnreachable(), TEXT("%s"), *CurrentActor->GetFullName());
		return CurrentActor;
	}

private:
	void OnActorSpawned(AActor* InActor)
	{
		if (InActor->IsA(DesiredClass))
		{
			SpawnedActorArray.AddUnique(InActor);
		}
	}
};

着重看红色框中的代码

可以看到在Editor下当InClass是AActor::StaticClass()的时候,会遍历当前World中所有的Actor。

再看FActorIterator的构造函数

可以看到FActorIterator只传一个构造参数的时候,InClass会默认是AActor::StaticClass(),也就会遍历场景中所有的Actor。当我们明确知道自己想要遍历的是Actor子类时,却因为少传了一个参数而被迫轮询了一遍所有Actor!

再来看ForEachObjectOfClass的代码。

c++ 复制代码
void ForEachObjectOfClass(const UClass* ClassToLookFor, TFunctionRef<void(UObject*)> Operation, bool bIncludeDerivedClasses, EObjectFlags ExclusionFlags, EInternalObjectFlags ExclusionInternalFlags)
{
	TRACE_CPUPROFILER_EVENT_SCOPE(ForEachObjectOfClass);

	// Most classes searched for have around 10 subclasses, some have hundreds
	TArray<const UClass*, TInlineAllocator<16>> ClassesToSearch;
	ClassesToSearch.Add(ClassToLookFor);

	FUObjectHashTables& ThreadHash = FUObjectHashTables::Get();
	FHashTableLock HashLock(ThreadHash);

	if (bIncludeDerivedClasses)
	{
		RecursivelyPopulateDerivedClasses(ThreadHash, ClassToLookFor, ClassesToSearch);
	}

	ForEachObjectOfClasses_Implementation(ThreadHash, ClassesToSearch, Operation, ExclusionFlags, ExclusionInternalFlags);
}

RecursivelyPopulateDerivedClasses是查找ClassToLookFor所有的子类。

ForEachObjectOfClasses_Implementation则是在遍历所有这些子类的实例对象。代码如下:

c++ 复制代码
FORCEINLINE void ForEachObjectOfClasses_Implementation(FUObjectHashTables& ThreadHash, TArrayView<const UClass* const> ClassesToLookFor, TFunctionRef<void(UObject*)> Operation, EObjectFlags ExcludeFlags /*= RF_ClassDefaultObject*/, EInternalObjectFlags ExclusionInternalFlags /*= EInternalObjectFlags::None*/)
{
	TRACE_CPUPROFILER_EVENT_SCOPE(ForEachObjectOfClasses_Implementation);

	// We don't want to return any objects that are currently being background loaded unless we're using the object iterator during async loading.
	ExclusionInternalFlags |= UE::GC::GUnreachableObjectFlag;
	if (!IsInAsyncLoadingThread())
	{
		ExclusionInternalFlags |= EInternalObjectFlags::AsyncLoading;
	}

	TBucketMapLock ClassToObjectListMapLock(ThreadHash.ClassToObjectListMap);

	for (const UClass* SearchClass : ClassesToLookFor)
	{
		auto List = ThreadHash.ClassToObjectListMap.Find(SearchClass);
		if (List)
		{
			for (auto ObjectIt = List->CreateIterator(); ObjectIt; ++ObjectIt)
			{
				UObject* Object = static_cast<UObject*>(*ObjectIt);
				if (!Object->HasAnyFlags(ExcludeFlags) && !Object->HasAnyInternalFlags(ExclusionInternalFlags))
				{
					if (UE::GC::GIsIncrementalReachabilityPending)
					{
						UE::GC::MarkAsReachable(Object);
					}
					Operation(Object);
				}
			}
		}
	}
}

解决方法及建议

  • 对于Actor子类的遍历
    建议使用TActorIterator。示例如下:
c++ 复制代码
for (TActorIterator<AInstancedFoliageActor> It(InWorld); It; ++It)
{
    AInstancedFoliageActor* IFA = *It;
    if (IFA->GetLevel() == InLevel)
    {
        IFA->PostApplyLevelOffset(InOffset, bWorldShift);
    }
}
  • 对于UActorComponent或UObject子类
    建议使用ForEachObjectOfClass或者GetObjectsOfClass。示例如下:
c++ 复制代码
TArray<UWorld*, TInlineAllocator<4>> WorldsToEOFUpdate;
ForEachObjectOfClass(UWorld::StaticClass(), [&WorldsToEOFUpdate](UObject* WorldObj)
{
    UWorld* World = CastChecked<UWorld>(WorldObj);
    if (World->HasEndOfFrameUpdates())
    {
        WorldsToEOFUpdate.Add(World);
    }
});
  • UGameplayStatic一众方法
    推荐使用UGameplayStatics::GetAllActorsOfClassWithTag、UGameplayStatics::GetAllActorsOfClass,尽量避免使用UGameplayStatics::GetAllActorsWithTag、UGameplayStatics::GetAllActorsWithInterface。

赶紧去检查自己项目有没有类似问题吧!

引擎代码提交

提交的PR,部分已经Merge到官方Github了。

相关推荐
mengzhi啊1 小时前
ue5 在一个蒙太奇的上半身插槽放两段动画,用片段1,2作为区分。播放动画蒙太奇,自由选择片段1,2
ue5
子燕若水1 天前
UE5 开启“Python Remote Execution“
ue5
我要吐泡泡了哦1 天前
虚幻5路径追踪渲染器(PT和DF的效果差别与PT速度性能)-李文磊-技术分享笔记
笔记·ue5·图形渲染
ue星空7 天前
模之屋模型导入到UE5
ue5·蓝图
mengzhi啊7 天前
ue5 设置角色属性(生命值,蓝条值,能量值)c++
ue5
ue星空7 天前
UE5游戏性能优化指南
游戏·性能优化·ue5·蓝图
电子云与长程纠缠8 天前
在UE5中使用视差贴图
学习·缓存·ue5·编辑器·贴图
子燕若水9 天前
Unreal Engine 5 (UE5) Metahuman 的头部材质
前端·ue5·材质
ue星空9 天前
UE材质节点Fresnel
ue5·材质
ue星空9 天前
UE材质控制UV
ue5·材质·uv