给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了。

相关推荐
暮志未晚Webgl16 小时前
115. UE5 GAS RPG 实现角色死亡后从存档点复活
数码相机·ue5
书鸢123617 小时前
UE5 Does Implement Interface 节点
ue5
暮志未晚Webgl2 天前
113. UE5 GAS RPG 实现传送点切换地图
ue5
暮志未晚Webgl2 天前
112. UE5 GAS RPG 制作高亮接口
android·ue5
Zhichao_972 天前
【UE5 C++】判断两点连线是否穿过球体
c++·算法·ue5
一碗情深2 天前
UE5 打包报错 Unknown structure 的解决方法
ue5
异次元的归来3 天前
UE5相机系统初探(二)
ue5·游戏引擎·camera
Zhichao_973 天前
【UE5 C++课程系列笔记】06——定时器的基本使用
笔记·ue5
零一数创6 天前
现代化水库可视化管理平台:提升水库运行效率与安全保障
安全·3d·unity·ue5·游戏引擎