当前位置:   article > 正文

【UE4】UE4GamePlay架构_gameplay框架

gameplay框架

参考博客:UE4官方文档大钊南京周润发带带大师兄yblackd董国政Ken_An张悟基paprika

这篇博文主要记录一些自己在学习GamePlay的过程中一些心得记录,最开始使用的是UE5源码学习,后来不知道不小心改了啥,UE5源码崩了,就换回了UE4.26所以源码部分可能会有一部分来自UE5有一部分来自UE4,会有点出入。

一、整体框架

首先来看一下整体框架:

红色部分为主体,从右往左为组合关系,至上而下为派生关系。

在整个UE宇宙的构成中,UEngine就类似化学元素,UObject就类似物质,物质通过演化便衍生出了物体—AActor和UActorComponent,AActor继续演化就出现了生物APawn,人—ACharacter,于是世界便有了信息—AInfo,规则—AGameMode,大量的物体、生物组合在一起便形成了大陆—ULevel,不同的大陆组合在一起便形成了世界—UWorld,世界有着自己的信息—FWorldContext和客观规律—UGameInstance。

而在UE这个宇宙有很多个Word,如编辑时的World,编辑时运行的World,运行时的World等等,查看源码就可知道UE宇宙有五大世界。

namespace EWorldType
{
	enum Type
	{
		None,		// An untyped world, in most cases this will be the vestigial worlds of streamed in sub-levels
		Game,		// The game world
		Editor,		// A world being edited in the editor
		PIE,		// A Play In Editor world
		Preview,	// A preview world for an editor tool
		Inactive	// An editor world that was loaded but not currently being edited in the level editor
	};
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

首先我们先了解一下这些类的具体作用,然后再细致的了解各个类。

1.UEngine

UEngine类是UE的基础,UEngine提供一些最底层的交互—与操作系统的交互,而根据不同的运行模式UE与操作系统的交互模式又有少许不同,所以UEngine又派生出了UGameEngine和UEditerEngine来负责不同运行模式下的交互模式。

其中有一个很重要的全局指针GEngine,通过GEngine可以访问各种UE的全局资源,同时GEngine还提供多线程访问能力。

关于UEngine的资料实在是太少了,官方文档中对UEngine的描述也就一句话,对UEngine的理解也就止步于此了。

2.UObject

UObject是构成UE世界最基础的物质,所以UObject提供供UE世界运行的最基本的功能:

  • Garbage collection:垃圾收集
  • Reference updating:引用自动更新
  • Reflection:反射
  • Serialization:序列化
  • Automatic updating of default property changes:自动检测默认变量的更改
  • Automatic property initialization:自动变量初始化
  • Automatic editor integration:和虚幻引擎编辑器的自动交互
  • Type information available at runtime:运行时类型识别
  • Network replication:网络复制

在之后再深入浅出的讲解各个功能。

3.AActor

AActor是派生自UObject的一个及其重要的类,AActor在UObject的基础上再进一步提供了:

  • Replication:网络复制
  • Spawn:动态创建
  • Tick:每帧运行

Replicatoin使AActor有了分裂复制的生育能力,Spawn使AActor在UE世界中出生,在UE4世界中死去,Tick使AActor有了心跳,AActor便组成了丰富多彩的UE世界。

AActor拥有一个庞大的子孙族群,ALevelScriptActor、ANavigationObjectBase、APawn、AController、AInfo这些都是AActor的直系后代,而这些后代也都各自拥有自己的庞大分支族群,构成了UE世界中最强大的种族AActor。

ALevelScriptActor

ALevelScriptActor在官方文档中的表述就是ULevelScriptBlueprint生成的类的基类,通过名称我们就很容易联想到关卡蓝图,没错ULevelScriptBlueprint就是我们最常用的关卡蓝图,ULevelScriptBlueprint继承自UObject,所以ULevelScriptBlueprint的子类是一个多继承的虚继承类,而ALevelScriptActor就为其提供AActor的能力。

在官方文档中有提及默认关卡蓝图是可以通过DefualtGame.ini配置文件替换成自定义关卡蓝图的,具体使用方法在后面在探讨。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ktqGj2Zw-1638946234139)(https://raw.githubusercontent.com/Goulandis/ImgLib/main/img/20210309211442.png)]

ANavigationObjectBase

ANavigationObjectBase的资料实在是少的可怜,就连官方文档也是没有一个字的描述,源码也是相当简单,总共就70行,由ANavigationObjectBase是APlayerState的基类,和它继承的接口INavAgentInterface可以猜测ANavigationObjectBase应该和网络复制有关,具体细节留到以后更熟悉UE4了再深入探讨吧。

APlayerStart

APlayerStart的作用就是记录APawn在游戏开始时生成的Position与Rotation信息,UE设计APlayerStart的初忠就是想让游戏的关卡设十师和场景设计师的工作分离开来,也就解耦合。那么,如果Level中不存在APlayerStart ,APawn 会出生在哪是呢?答案是世界原点(0,0,0)

APawn

APawn在AActor的基础上再度添加了:

  • 被Controller控制
  • PhysicsCollision:物理碰撞
  • MovementInput:移动响应接口

等能力,有了MovementInput接口APawn就拥有了可运动的能力,这里UE的逻辑划分十分精妙,UE将一个可运动的物体巧妙地划分成了APwan和AController,APawn重点表现在物体,而这个物体具备运动能力,但是自身不具备运动技巧;而AController这是控制APawn运动地大脑,用来控制APawn如何运动,如果把APawn比作是提线木偶,那么AController就是控制木偶运动地线。

到了APawn这一代,AActor的衍化之旅开始衍化出现于玩家间交互的能力,而这之中的佼佼者便是ACharacter。

ACharacter

ACharacter是APwan的特化加强版,在UE世界中可以称之为“人”,ACharacter是一个专门为人形角色定制的APawn,自带CharacterMovement组件,可以使人形角色像人一样行走。

ADefaultPawn

最初始的APawn使最基本的APawn类,只提供APawn的一些基本能力,而没有提供支持这些能力的组件,而在具体实际使用情况中我们使用的APawn应该还需要组合一些其他的能力,以适应不同的场景,如:我们知道APawn可以运动,但在实际场景中我们是要确定这个APawn是因该直立行走还是爬行,是用轮子行驶还是用翅膀飞行,APawn在玩家眼里应该长什么样子,是人还是蛇,是因该左球形碰撞还是应该做方形碰撞,这些都是APawn不具备的能力,这时ADefaultPawn便出现了,ADefaultPawn自带DefualtPawnMovement、CollisionComponent、StaticMeshCompnent三件套,为ADefaultPawn提供了默认的场景表现。

ASpectatorPawn

在游戏中存在一种特殊的玩家—观战玩家,这类玩家不需要具体表现形式,只需要一些相机的漫游能力,于是ASpectatorPawn出现了,ASpectatorPawn继承自ADefaultPawn,ASpectatorPawn提供了一个基本的USpectatorPawnMovement(不带重力漫游),并关闭了StaticMesh的显示,碰撞也设置到了“Spectator”通道。

AController

AController就是控制APawn运动的大脑了,ACtroller负责处理一些直接与玩家交互的控制逻辑,AController是从AActor派生的与APawn同级的子类,在UE的设计中,在同一时刻一个AController和一个APawn之间是1:1的关系,AController可以在多个APawn之间通过Possess/UnPossess切换。AController有两种控制APawn的方式,一种是AController直接附在APawn的身上控制APawn的移动,如驾驶汽车,一种是以上帝的视角控制APawn的移动,如控制第三人称的角色。

APlayerController

APlayerController是由AController派生出来专门用于负责玩家交互逻辑的AController,APlayerController提供了:

  • Camera管理
  • Input输入响应
  • UPlayer关联
  • HUD显示
  • Level切换
  • Voice音源监听

这些能力。

AAIController

在一个游戏中有玩家控制的角色也可以有NPC,那么NPC的行动逻辑有谁来控制呢?答案就是AAIController,AAIController与APlayerController完全不同,因为一个NPC不要管理Camera,不需要响应玩家的输入,不需要关联UPlayer,不需要显示HUD,不需要监听音源,只有Level切换可能会在少数情况下需要,那么AAIController因该做什么呢?UE为它设计的是这些事:

  • Navigation:自动寻路
  • AI Component:用于启动运行行为树,使用黑板数据
  • Task系统:让AI去完成一些任务

当然一个游戏中是至少需要一个APlayerController的,但是可以没有AAIController。

AInfo

AInfo是一些数据保存类的基类,AInfo不需要运动和碰撞,也不需要物理表现,仅仅只是保存数据,所以UE在AInfo中将这些功能都隐藏了,之所以不直接继承自UObject,而继承自AActor是因为游戏数据是需要具备网络复制的能力的,而UObject不具备这个能力

AWordSettings

AWordSetting继承自AInfo用来配置和保存一些Level配置,主要用于配置Level的GameMode信息,光照信息,导航系统,声音系统,LOD系统,物理加速度等关卡信息。由此可以知道一个Level对应一个AWordSetting,但是一个AWordSetting可以应用在多个Level上。

AGameMode

AGameMode就是用于配置AWorldSetting中的GameMode属性的。

在这里插入图片描述

在UE的设计中AGameMode就是游戏世界的逻辑,及整个游戏的玩法规则,而在实际情况中一个游戏既可以只有一个玩法也可以有多种玩法规则,所以AWordSetting与AGameMode的对应关系也是一个AWorldSetting只能对应一个AGameMode,而一个AGameMode可以对应多个AWorldSetting。那么AGameMode应该负责哪些逻辑呢?UE是这么规定的:

  • Class登记:记录GameMode中各种类的信息
  • Spawn:创建Pawn和PlayerController等
  • 游戏进度:游戏暂停重启的逻辑
  • 过场动画逻辑
  • 多人游戏的步调同步

AGameState

AGameState用于保存游戏数据,如任务进度,游戏活动等。

APlayerState

APlayerState是一个用于存储玩家状态的类,在一个游戏客户端,尤其是网络游戏客户端中是可以存在多个APlayerState对象的,不同的APlayerState保存不同玩家的状态,同时APlayerState也可以存在于服务器中。APlayerState的生命周期为一整个Level的生命周期。

到这是AActor家族下的几个重要成员的基本功能我们便有了一个大概的了解了,这里我们来捋一下这些成员之间的关系和在UE世界中的地位。
Alt

4.UActorComponent

UActorComponent是UE向U3D看齐的一个产物,虽然UE世界有了Actor就有了形形色色的物体生物,但是不同的生物拥有不同的技能,而同一个Actor可以会某个技能也可以不会,这种概念使用组合的方式组合到Actor下是最理想的,于是Component便出现了,UActorComponent直接继承自UObject,与AActor同级,Component既可以嵌套在Actor下,也可以嵌套在其他的Component下,但是需要注意的是,UActorComponent这一级是不提供互相嵌套的能力的,只有到其子类USceneComponent一级才提供互相嵌套能力。

USceneComponent

USceneComponent主要提供两大能力,一是Transform,二是SceneComponent的互相嵌套。一般我们直接在Level里创建的Actor都会默认带有一个SceneComponent组件。

UPrimitiveComponent

UPrimitiveComponent主要提供Actor用于物体渲染和碰撞相关的基础能力。

UMeshComponent

UMeshComponent由UPrimitiveComponent派生而来,主要提供具体的渲染显示方面的能力。

UChildActorComponent

从名字就可以窥探其功能一二了,UChildComponent在Actor中主要用于链接Actor与Component,提供Component和Actor的嵌套能力。

5.ULevel

ULevel可以看作是UE世界的大陆,是AActor的容器,前面提到的ALevelScriptActor便是ULevel默认带有的关卡蓝图,在这个关卡蓝图中编写便是这块大陆的逻辑,同时ULevel也默认带有一个AWorldSetting。

6.UWorld

在UE中所有的ULevel互相联系就构成了一个UWorld,ULevel构建UWorld的方式有两种,一种是以SubLevel的形式,像关卡流一样,一个关卡链接下一个关卡,来组成UWorld,一种是每一个ULevel就是这个大地图的UWorld中的一块地图,ULevel之间以相对位置衔接在一起,构成一个大地图来组成这个UWorld。无论是那种构成形式,在一个UWorld中都有一个PersistentLevel,PersistenetLevel就是主Level,是玩家最初始的出生地,这里用的是最初始而不是游戏开始,是因为,现在很多在游戏开始时玩家的出点可能不是PersistentLevel而是上一次玩家离线时的位置。

7.FWorldContext

FWorldContext不对开发者公开,是UE内部用来处理引擎UWorld上下文的类,比如当我们从编辑状态的EditorWorld点击播放切换到PIEWorld即运行状态时,这个过程中EditorWorld到PIEWorld之间的信息交换就是通过FWorldContext实现的。可以说FWorldContext处理的是UWorld级的通信。

8.UGameInstance

UGameInstance可以说是凌驾于所有AActor、UActorComponent、ULevel、UWorld之上的类,通常情况下一个Game中应该只有一个,这里的Game是UEngine中提到的所有World的总和,当然这不是绝对的,对于更高层次的开发者,UE也是提供了多个UGameInstance协同的扩展的。UGameInstance的生命周期就是从游戏进程启动到游戏进程结束。

所以UGameInstance主要处理:

  • UWorld、ULevel之间的切换
  • UPlayer的创建,这里的UPlayer又和前面的APlayerController有所不同,这一点在后面再介绍。
  • 全局配置
  • GameMode的切换

9.UNetDriver

从名字就可以略知一二,UNetDriver是UE处理网络同步相关的类,UNetDriver中有两个主要的成员:

class UNetConnection* ServerConnection;
TArray<class UNetConnection*> ClientConnections;
  • 1
  • 2

ServerConnection是客户端到服务器的连接,ClientConnections数组是服务器到客户端群的连接的数组。而在UNetConnnection中又有一个很重要的成员:

TMap<TWeakObjectPtr<AActor>,class UActorChannel*> ActorChannels
  • 1

ActorChannels是在服务器与客户端完成连接后用于实现Actor同步的对象。

10.UPlayer

UPlayer即玩家,ULevel可以切换,UWorld可以交替,但是尽管ULevel、UWorld如何变换,玩家还是那个玩家,所以UPlayer是和UGameInstance同一级别的存在,在整个GamePlay架构中UPlayer主要以GameModeBase中的一个属性出现。

在一个单机游戏中UPlayer是唯一的存在,但是在一个网络联级游戏中,表示同一实体的UPlayer即存在于玩家本地的客户端中,同时也存在于其他玩家的多个客户端中,那么玩家的输入就既要作用于本地的APawn上,同时在其他玩家的客户端中的表示这个实体的APawn也要做出响应的反应,于是UE便将UPlayer又派生出了两个子类,ULocalPlayer和UNetConnection。其中ULocalPlayer就是处理本地客户端的输入逻辑的类。

UNetConnection

UNetConnection就是处理其他玩家在本地客户端中的APawn的类,所以UNetConnection也是一个玩家。

11.USaveGame

前面提到了AGameState是一个保存游戏数据的类,这个保存是一个临时保存,所以当游戏程序关闭之后AGameState中数据也就不存在了,而USaveGame就是用来保存存档的类,USaveGame提供游戏数据永久性保存,我们只需要往USaveGame中添加我们要保存的属性字段,就可以直接调用USaveGame的接口直接将游戏数据序列化保存到本地文件中,相当的方便。

花了这么长的篇幅也就简要的介绍了一下GamePlay的整体框架,总共由这11个类组成,说起来不多,但是里面的门道却是相当深奥,这需要在以后的使用中慢慢学习消化。

那么接下来就开始各个类的详细使用学习了。

二、UObject

首先我们来看UObject提供的功能:

  • Garbage collection:垃圾收集
  • Reference updating:引用自动更新
  • Reflection:反射
  • Serialization:序列化
  • Automatic updating of default property changes:自动检测默认变量的更改
  • Automatic property initialization:自动变量初始化
  • Automatic editor integration:和虚幻引擎编辑器的自动交互
  • Type information available at runtime:运行时类型识别
  • Network replication:网络复制

1.垃圾回收

首先我们来研究研究UE4是如何进行垃圾回收的。

这里推荐两位大佬的博客:带带大师兄南京周润发

可以配合着看。

由于C++不提供GC功能,所有UE自己实现了一套GC功能,使用的也是最经典的标记-清理垃圾回收方式。

GC的过程

UEGC分为来两个阶段,第一个阶段UE从根集合开始遍历,遍历所有可达对象,于是UE就知道了哪些对象还在被引用,哪些对象已经不可被引用了。第二阶段UE会逐步的清理这些不可达对象,形式为分帧分批清理,为什么要这么做呢?想想我们卸载一次性Level时的感受就知道了,分批处理可以保证我们在使用UE时的顺滑而不卡顿。

UEGC的主要函数是在UObjectGlobals.h头文件中CollectGarbage函数

void CollectGarbage(EObjectFlags KeepFlags, bool bPerformFullPurge)
{
	// No other thread may be performing UObject operations while we're running
	AcquireGCLock();

	// Perform actual garbage collection
	CollectGarbageInternal(KeepFlags, bPerformFullPurge);

	// Other threads are free to use UObjects
	ReleaseGCLock();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

可以看到GC的整体流程很自然的划分成了三个阶段,获取GC锁、执行CollectGarbageInternal和释放GC锁。使用锁的原因是UEGC是多线程的,为了防止在GC的过程中对象被其他线程访问,以保证异步加载的稳定。而CollectGarbageInternal函数则进行垃圾回收和对象标记与清理,两个参数KeepFlags表示这些被标记的对象无论是否被引用都将被保留,bPerformFullPurge表示GC时进行全清理还是分帧分批清理。

那么GC又是如何进行对象标记的呢?还是看源码

/** 
 * Deletes all unreferenced objects, keeping objects that have any of the passed in KeepFlags set
 *
 * @param	KeepFlags			objects with those flags will be kept regardless of being referenced or not
 * @param	bPerformFullPurge	if true, perform a full purge after the mark pass
 */
void CollectGarbageInternal(EObjectFlags KeepFlags, bool bPerformFullPurge)
{
	SCOPE_TIME_GUARD(TEXT("Collect Garbage"));
	SCOPED_NAMED_EVENT(CollectGarbageInternal, FColor::Red);
	CSV_EVENT_GLOBAL(TEXT("GC"));
	CSV_SCOPED_TIMING_STAT_EXCLUSIVE(GarbageCollection);

	FGCCSyncObject::Get().ResetGCIsWaiting();

#if defined(WITH_CODE_GUARD_HANDLER) && WITH_CODE_GUARD_HANDLER
	void CheckImageIntegrityAtRuntime();
	CheckImageIntegrityAtRuntime();
#endif

	DECLARE_SCOPE_CYCLE_COUNTER( TEXT( "CollectGarbageInternal" ), STAT_CollectGarbageInternal, STATGROUP_GC );
	STAT_ADD_CUSTOMMESSAGE_NAME( STAT_NamedMarker, TEXT( "GarbageCollection - Begin" ) );

	// We can't collect garbage while there's a load in progress. E.g. one potential issue is Import.XObject
	check(!IsLoading());

	// Reset GC skip counter
	GNumAttemptsSinceLastGC = 0;

	// Flush streaming before GC if requested
	if (GFlushStreamingOnGC)
	{
		if (IsAsyncLoading())
		{
			UE_LOG(LogGarbage, Log, TEXT("CollectGarbageInternal() is flushing async loading"));
		}
		FGCCSyncObject::Get().GCUnlock();
		FlushAsyncLoading();
		FGCCSyncObject::Get().GCLock();
	}

	// Route callbacks so we can ensure that we are e.g. not in the middle of loading something by flushing
	// the async loading, etc...
	FCoreUObjectDelegates::GetPreGarbageCollectDelegate().Broadcast();
	GLastGCFrame = GFrameCounter;

	{
		// Set 'I'm garbage collecting' flag - might be checked inside various functions.
		// This has to be unlocked before we call post GC callbacks
		FGCScopeLock GCLock;

		UE_LOG(LogGarbage, Log, TEXT("Collecting garbage%s"), IsAsyncLoading() ? TEXT(" while async loading") : TEXT(""));

		// Make sure previous incremental purge has finished or we do a full purge pass in case we haven't kicked one
		// off yet since the last call to garbage collection.
		if (GObjIncrementalPurgeIsInProgress || GObjPurgeIsRequired)
		{
			IncrementalPurgeGarbage(false);
			FMemory::Trim();
		}
		check(!GObjIncrementalPurgeIsInProgress);
		check(!GObjPurgeIsRequired);

#if VERIFY_DISREGARD_GC_ASSUMPTIONS
		// Only verify assumptions if option is enabled. This avoids false positives in the Editor or commandlets.
		if ((GUObjectArray.DisregardForGCEnabled() || GUObjectClusters.GetNumAllocatedClusters()) && GShouldVerifyGCAssumptions)
		{
			DECLARE_SCOPE_CYCLE_COUNTER(TEXT("CollectGarbageInternal.VerifyGCAssumptions"), STAT_CollectGarbageInternal_VerifyGCAssumptions, STATGROUP_GC);
			const double StartTime = FPlatformTime::Seconds();
			VerifyGCAssumptions();
			VerifyClustersAssumptions();
			UE_LOG(LogGarbage, Log, TEXT("%f ms for Verify GC Assumptions"), (FPlatformTime::Seconds() - StartTime) * 1000);
		}
#endif

		// Fall back to single threaded GC if processor count is 1 or parallel GC is disabled
		// or detailed per class gc stats are enabled (not thread safe)
		// Temporarily forcing single-threaded GC in the editor until Modify() can be safely removed from HandleObjectReference.
		const bool bForceSingleThreadedGC = !FApp::ShouldUseThreadingForPerformance() || !FPlatformProcess::SupportsMultithreading() ||
#if PLATFORM_SUPPORTS_MULTITHREADED_GC
		(FPlatformMisc::NumberOfCores() < 2 || GAllowParallelGC == 0 || PERF_DETAILED_PER_CLASS_GC_STATS);
#else	//PLATFORM_SUPPORTS_MULTITHREADED_GC
			true;
#endif	//PLATFORM_SUPPORTS_MULTITHREADED_GC

		// Perform reachability analysis.
		{
			const double StartTime = FPlatformTime::Seconds();
			FRealtimeGC TagUsedRealtimeGC;
            //-----------------------------------------------------------
			TagUsedRealtimeGC.PerformReachabilityAnalysis(KeepFlags, bForceSingleThreadedGC);
            //-----------------------------------------------------------
			UE_LOG(LogGarbage, Log, TEXT("%f ms for GC"), (FPlatformTime::Seconds() - StartTime) * 1000);
		}

		// Reconstruct clusters if needed
		if (GUObjectClusters.ClustersNeedDissolving())
		{
			const double StartTime = FPlatformTime::Seconds();
			GUObjectClusters.DissolveClusters();
			UE_LOG(LogGarbage, Log, TEXT("%f ms for dissolving GC clusters"), (FPlatformTime::Seconds() - StartTime) * 1000);
		}

		// Fire post-reachability analysis hooks
		FCoreUObjectDelegates::PostReachabilityAnalysis.Broadcast();
		
		{
			FGCArrayPool::Get().ClearWeakReferences(bPerformFullPurge);

			GatherUnreachableObjects(bForceSingleThreadedGC);

			if (bPerformFullPurge || !GIncrementalBeginDestroyEnabled)
			{
				UnhashUnreachableObjects(/**bUseTimeLimit = */ false);
				FScopedCBDProfile::DumpProfile();
			}
		}

		// Set flag to indicate that we are relying on a purge to be performed.
		GObjPurgeIsRequired = true;
		// Reset purged count.
		GPurgedObjectCountSinceLastMarkPhase = 0;
		GObjCurrentPurgeObjectIndexResetPastPermanent = true;

		// Perform a full purge by not using a time limit for the incremental purge. The Editor always does a full purge.
		if (bPerformFullPurge || GIsEditor)
		{
			IncrementalPurgeGarbage(false);
		}

		if (bPerformFullPurge)
		{
			ShrinkUObjectHashTables();
		}

		// Destroy all pending delete linkers
		DeleteLoaders();

		// Trim allocator memory
		FMemory::Trim();
	}

	// Route callbacks to verify GC assumptions
	FCoreUObjectDelegates::GetPostGarbageCollect().Broadcast();

	STAT_ADD_CUSTOMMESSAGE_NAME( STAT_NamedMarker, TEXT( "GarbageCollection - End" ) );
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148

我在PerformReachabilityAnalysis函数处做了标记,GC时UE就是通过这个函数进行对象标记的,PerformReachabilityAnalysis函数会做多线程实时的分析对象的引用关系,然后标记出可达与不可达对象。标记是如何进行的还得深入到PerformReachabilityAnalysis函数,再上源码

/**
	 * Performs reachability analysis.
	 *
	 * @param KeepFlags		Objects with these flags will be kept regardless of being referenced or not
	 */
	void PerformReachabilityAnalysis(EObjectFlags KeepFlags, bool bForceSingleThreaded = false)
	{
		LLM_SCOPE(ELLMTag::GC);

		SCOPED_NAMED_EVENT(FRealtimeGC_PerformReachabilityAnalysis, FColor::Red);
		DECLARE_SCOPE_CYCLE_COUNTER(TEXT("FRealtimeGC::PerformReachabilityAnalysis"), STAT_FArchiveRealtimeGC_PerformReachabilityAnalysis, STATGROUP_GC);

		/** Growing array of objects that require serialization */
		FGCArrayStruct* ArrayStruct = FGCArrayPool::Get().GetArrayStructFromPool();
		TArray<UObject*>& ObjectsToSerialize = ArrayStruct->ObjectsToSerialize;

		// Reset object count.
		GObjectCountDuringLastMarkPhase.Reset();

		// Make sure GC referencer object is checked for references to other objects even if it resides in permanent object pool
		if (FPlatformProperties::RequiresCookedData() && FGCObject::GGCObjectReferencer && GUObjectArray.IsDisregardForGC(FGCObject::GGCObjectReferencer))
		{
			ObjectsToSerialize.Add(FGCObject::GGCObjectReferencer);
		}

		{
			const double StartTime = FPlatformTime::Seconds();
			MarkObjectsAsUnreachable(ObjectsToSerialize, KeepFlags, bForceSingleThreaded);
			UE_LOG(LogGarbage, Verbose, TEXT("%f ms for Mark Phase (%d Objects To Serialize"), (FPlatformTime::Seconds() - StartTime) * 1000, ObjectsToSerialize.Num());
		}

		{
			const double StartTime = FPlatformTime::Seconds();
			PerformReachabilityAnalysisOnObjects(ArrayStruct, bForceSingleThreaded);
			UE_LOG(LogGarbage, Verbose, TEXT("%f ms for Reachability Analysis"), (FPlatformTime::Seconds() - StartTime) * 1000);
		}
        
		// Allowing external systems to add object roots. This can't be done through AddReferencedObjects
		// because it may require tracing objects (via FGarbageCollectionTracer) multiple times
		FCoreUObjectDelegates::TraceExternalRootsForReachabilityAnalysis.Broadcast(*this, KeepFlags, bForceSingleThreaded);

		FGCArrayPool::Get().ReturnToPool(ArrayStruct);

#if UE_BUILD_DEBUG
		FGCArrayPool::Get().CheckLeaks();
#endif
	}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47

首先前面的宏暂时可以忽略掉,

第一步,FGCArrayStruct* ArrayStruct = FGCArrayPool::Get().GetArrayStructFromPool();UE将UObject的所有的强引用和弱引用都存储大ArrayStruct数据结构中,FGCArrayPool是UEGC的主要执行类

第二步,TArray<UObject*>& ObjectsToSerialize = ArrayStruct->ObjectsToSerialize;分离UObject的强引用到ObjectsToSerialize 数组中。

这是FGCArrayStruct结构体的源码:

struct FGCArrayStruct
{
	TArray<UObject*> ObjectsToSerialize;
	TArray<UObject**> WeakReferences;
};
  • 1
  • 2
  • 3
  • 4
  • 5

ObjectsToSerialize存储强引用,WeakReferences存储弱引用。

第三步,GObjectCountDuringLastMarkPhase.Reset();重置对象的引用计数。

第四步,通过一个if判断标记可达对象,于是可达对象与不可达对象就被标记出来了,接下来便是GC清理。

GC的触发

UE的GC发生在游戏线程上,支持多线程GC,和大多数主流语言的GC一样支持自动触发和手动触发。

手动触发

手动触发UE也提供了两种方式,其一是通过C++函数:

GEngine->ForceGarbageCollection();
  • 1

这里需要注意的是GEngineEngine.h头文件下。

手动触发的使用场景一般是在卸载某些资源后,手动触发GC回收这些资源在使用过程中的无用对象。

其二是蓝图节点:

在这里插入图片描述

手动调用这两个函数,UE会跳过GC算法,在下一次Tick时直接进行GC。

这里有一点需要注意,在大多数情况下,手动GC一般只能回收NewObject函数创建的对象,而UWorld()->SpawnActor函数创建的对象无论如何调用都无法销毁,这是因为,当UE创建一个Actor之后在UWorld中就已经保存了这个Actor的引用,所以无论我们如何释放Actor的引用,这个Actor的引用计数都不会归零,所以要销毁一个Actor还是需要通过Actor->Destroy()函数。

我们可以个一个例子:

//AACtor.cpp
AActor1::AActor1()
{
	PrimaryActorTick.bCanEverTick = true;
	UE_LOG(LogTemp, Warning, TEXT("Actor1 Created"));
}

AActor1::~AActor1()
{
	UE_LOG(LogTemp, Warning, TEXT("Actor1 Destryed"));
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
//AMyActor.h
UCLASS()
class INSIDEUE4_API AMyActor2 : public AActor
{
	GENERATED_BODY()
	
public:	
	AMyActor2();
	AActor1 *a;//注意这里没有加UPROPERTY()宏

protected:
	virtual void BeginPlay() override;

public:	
	virtual void Tick(float DeltaTime) override;
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
//AMyActor2
AMyActor2::AMyActor2()
{
	PrimaryActorTick.bCanEverTick = true;
}

void AMyActor2::BeginPlay()
{
	Super::BeginPlay();
	a = UWorld()->SpawnActor<AActor1>();
	a = NULL;
	GEngine->ForceGarbageCollection();
}

void AMyActor2::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

OutputLog:

LogTemp: Warning: Actor1 Created
  • 1

可以看到使用UWorld()->SpawnActor创建的Actor即使手动强制GC也没有被回收,因为这个Actor是可达对象。

自动触发

要想UE自动触发的GC能能够回收我们创建的对象,那么我们创建的对象就必须继承自UObject,至于加不加UPROPERTY()宏似乎不影响GC的回收,如下面的测试结果,还是以上面的例子为例,把BeginPlay函数改为如下:

//AMyActor2
AMyActor2::AMyActor2()
{
	PrimaryActorTick.bCanEverTick = true;
}

void AMyActor2::BeginPlay()
{
	Super::BeginPlay();
	a = NewObject<AActor1>();
	a = NULL;
	GEngine->ForceGarbageCollection();
}

void AMyActor2::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

我们将MyActor2拖入场景中,运行,OutputLog输出,可以找到下面两句:

LogTemp: Warning: Actor1 Created

LogTemp: Warning: Actor1 Destryed
  • 1
  • 2
  • 3

可以看到,没有使用UPROPERTY()宏的变量a依旧在手动GC时被回收了,这里为了效果明显点使用了手动强制回收,其实使用自动GC也是一样的。

这里有提个疑问:

当我们在一个继承自UObject的类组合一个继承自UObject的对象,如果在这个对象定义前没有使用UPROPERTY()宏,那么在Play后UE会调用一次这个对象的析构函数,但是这个对象依然可以被使用,而如果在定义这个对象前使用了UPROPERTY()宏,那么这对象将和组合类被析构时一起被析构。疑问为什么UE会调用一次被组合对象的析构且析构后依然可以使用这个对象。如:

//UMyObject.cpp
UMyObject::UMyObject()
{
	UE_LOG(LogTemp, Warning, TEXT("UMyObject Created"));
}

UMyObject::~UMyObject()
{
	UE_LOG(LogTemp, Warning, TEXT("UMyObject Destoryed"));
}

void UMyObject::Fun()
{
	UE_LOG(LogTemp, Warning, TEXT("UMyObject"));
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
//AMyActor2.h
UCLASS()
class INSIDEUE4_API AMyActor2 : public AActor
{
	GENERATED_BODY()
	
public:	
	AMyActor2();
	UPROPERTY()
	UMyObject* obj;
protected:
	virtual void BeginPlay() override;

public:	
	virtual void Tick(float DeltaTime) override;
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
//AMyActor2.cpp
void AMyActor2::BeginPlay()
{
	Super::BeginPlay();
	obj = NewObject<UMyObject>();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

使用UPROPERTY()宏的输出结果:

//在点击Play后输出结果
LogTemp: Warning: AMyActor2 Created
LogTemp: Warning: UMyObject Created
//再点击Stop后输出结果
LogTemp: Warning: UMyObject Destoryed
LogTemp: Warning: AMyActor2 Destroyed
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

不使用UPROPERTY()宏的输出结果:

//在点击Play后输出结果
LogTemp: Warning: AMyActor2 Created
LogTemp: Warning: UMyObject Created
LogTemp: Warning: UMyObject Destoryed
//再点击Stop后输出结果
LogTemp: Warning: AMyActor2 Destroyed
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

很明显在Play后UMyObject对象的析构函数被调用了,但是此时如果继续访问UMyObject里的成员依旧可以访问。

TWeakObjectPtr、TWeakPtr(既保存引用又可GC)

有时我们可能需要在一个类里面临时保存一些对象,但是一旦保存了引用,就需要手动释放才能保证这些对象可以被GC自动回收,关于这个方面UE也贴心的为我们提供了 TWeakObjectPtr指针,当然,这也是C++弱指针的UE魔改办罢了,使用这个指针既可以引用对象,但是又不会造成引用计数+1。可以通过一个例子很好的看出来。

//AACtor1.cpp
AActor1::AActor1()
{
	PrimaryActorTick.bCanEverTick = true;
	UE_LOG(LogTemp, Warning, TEXT("Actor1 Created"));
}

AActor1::~AActor1()
{
	UE_LOG(LogTemp, Warning, TEXT("Actor1 Destryed"));
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
//AMyActor2
UCLASS()
class INSIDEUE4_API AMyActor2 : public AActor
{
	GENERATED_BODY()
	
public:	
	AMyActor2();
	AActor1* a;
	TWeakObjectPtr<AActor1> p;

protected:
	virtual void BeginPlay() override;

public:	
	virtual void Tick(float DeltaTime) override;
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
//AMyActor2.cpp/BeginPlay()
void AMyActor2::BeginPlay()
{
	Super::BeginPlay();
	a = NewObject<AActor1>();
	p = a;
	a = NULL;
	GEngine->ForceGarbageCollection();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

OutputLog:

LogTemp: Warning: Actor1 Created

LogTemp: Warning: Actor1 Destryed
  • 1
  • 2
  • 3

可以看到,AActor1对象依旧被强制回收了。

而TWeakPtr则对于自定义类的弱指针。

注意:弱指针不可以被用来作为TSet或TMap的Key,因为一个对象被GC时无法通知一个容器的Key,但是可以用来作为容器的Value。

TSharedPtr、TSharedRef(自定义类的GC)

自定义类的GC,UE也贴心的提供了 TSharedPtr和TSharedRef对象来为自定义类支持GC,TSharedPtr本质上是一个被封装过的指针,使用形式上依然保留指针的风格。

创建TSharedPtr指针指向一个自定义类时,需要使用MakeShareable()函数,如:

TSharedPtr<UMyObject> p = MakeShareable(NewObject<UMyObject>());
TSharedPtr<FActor> f = MakeShareable(new FActor());
  • 1
  • 2

TSharedPtr和TSharedRef都可以为自定义类提供GC功能,二者的区别只在于TSharedPtr可以为null,而SharedRef不可以。我在网上查询发现有三种方法构建TSharedRef,分别为:

第一种:

TSharedRef<FActor> ref(new FActor());
  • 1

第二种:

TSharedRef<FActor> ref = MakeShared<FActor>(new FActor());
  • 1

第三种:

TSharedPtr<FActor> ptr = MakeShareable<FActor>(new FActor());
TSharedRef<FActor> ref = ptr.ToSharedRef();
  • 1
  • 2

其中第二种方法在编写时没有任何问题但在编译时无法通过,并提示:

The TSharedRef() constructor is for internal usage only for hot-reload purposes. Please do NOT use it.
  • 1

使用的编译环境为:UE4.22 + VS2017

FGCObject(在自定义类中控制UObject对象的GC)

当我们在一个自定义类中组合一个UObject对象时,如果不做特殊处理也会出现GC触发中发现的疑问,在自定义类没有被析构时,UObject的对象的析构函数就被调用了,但是对象依然可以被使用。目前没有发现这种情况会导致什么样的后果,但是作为一个合格的UE程序还是应该尽量避免这种情况的发生,那么在一个自定义类中组合一个UObject对象,应该如何控制UObject对象的GC呢?

UE4提供了一个叫做FGCObject的类,位于GCObject.h头文件中,我们需要使自定义类继承自FGCObject类,然后再实现AddReferencedObjects函数,并在函数中通过Collector.AddReferencedObject()函数将所有的UObject对象UE4自动管理即可。

如:

class INSIDEUE4_API FActor : FGCObject
{
public:
	FActor();
	~FActor();
	UMyObject* obj;

	virtual void AddReferencedObjects(FReferenceCollector& Collector) override
	{
		Collector.AddReferencedObject(obj);
	}
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

然后,UObject对象就会在FActor对象析构时才被析构。

2.序列化

FObjectWriter和FObjectReader序列化对象到文件和从文件读取

FObjectWriter可以将对象数据序列化为二进制流,然后配合FFileHelper将流写入文件即可实现对象状态存储到文件。

void AOperatActor::SaveObject()
{
	USerializationObj* obj= NewObject<USerializationObj>();
	UE_LOG(LogTemp, Warning, TEXT("OldStr:%s"), *obj->str);
	obj->str = TEXT("OperatActor");
	TArray<uint8> bytes;
	FObjectWriter(obj, bytes);	
	FFileHelper::SaveArrayToFile(bytes, *FString("D:\\Goulandis\\UE4\\MyProject\\obj.txt"));	
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

配合FFileHelper将文件中的对象状态读入字节数组,FObjectReader就可以将字节数组中的对象状态写入新的对象中。

USerializationObj* AOperatActor::LoadObject()
{
	USerializationObj* newObj = NewObject<USerializationObj>();
	TArray<uint8> bytes;
	FFileHelper::LoadFileToArray(bytes, *FString("D:\\Goulandis\\UE4\\MyProject\\obj.txt"));
	FObjectReader reader(newObj, bytes);
	UE_LOG(LogTemp, Warning, TEXT("NewStr:%s"), *newObj->str);
	return newObj;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

看一下运行结果:

可以看到,新创建的USerializationObj对象的状态是被修改过后的状态。

Actor的使用方式和UObject是一样的:

void AOperatActor::SaveActor()
{
	ASerializationActor* actor = GetWorld()->SpawnActor<ASerializationActor>();
	UE_LOG(LogTemp, Warning, TEXT("OldStr:%s"), *actor->str);
	actor->str = TEXT("NewActor");
	TArray<uint8> bytes;
	FObjectWriter(actor, bytes);
	FFileHelper::SaveArrayToFile(bytes, *FString("D:\\Goulandis\\UE4\\MyProject\\actor.txt"));
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
ASerializationActor * AOperatActor::LoadActor()
{
	ASerializationActor* actor = GetWorld()->SpawnActor<ASerializationActor>();
	TArray<uint8> bytes;
	FFileHelper::LoadFileToArray(bytes, *FString("D:\\Goulandis\\UE4\\MyProject\\actor.txt"));
	FObjectReader reader(actor,bytes);
	UE_LOG(LogTemp, Warning, TEXT("NewStr:%s"), *actor->str);
	return actor;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

运行结果:

3.反射

在使用UE4的反射时有一个基础概念是必须要清楚的,即UE4的反射系统是建立在一整套的宏的设计上的,也就是说,想要一个类、属性、方法、枚举、结构体等支持UE4的反射,那么类必须加UCLASS宏标识,属性必须加UPROPERTTY宏标识,方法必须加UFUNCTION宏标识,枚举必须加UENUM宏标识,结构体必须加USTRUCT宏标识,如果不加这些宏来标识对应的目标,那么这些目标对于UE4的反射系统来说就是不可见的。

搜索所有的Object

C++本身的反射系统RTTI相当薄弱,所以UE在C++的基础上借助UObject自己实现了一套反射系统,同时借鉴了C#的长处提供了一系列反射用的系统函数。

TArray<UObject*> result;
GetObjectsOfClass(UClass::StaticClass(), result);   //获取所有的class和interface
GetObjectsOfClass(UEnum::StaticClass(), result);   //获取所有的enum
GetObjectsOfClass(UScriptStruct::StaticClass(), result);   //获取所有的struct
  • 1
  • 2
  • 3
  • 4

运行时创建对象

void AOperatActor::FindSerializationObj()
{
	UClass* uclass = FindObject<UClass>(ANY_PACKAGE, TEXT("SerializationObj"));
	USerializationObj* obj = Cast<USerializationObj>(uclass->GetDefaultObject());
	obj->PrintStr();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

UE4提供FindObject模板函数来搜索指定的类的类型信息,返回的类型元素据通过UClass类型对象存储,UClass对象就是UE4专门用来存储元数据的类型,UClass中提供了大量的方法来操作元数据,UClass,这里使用GetDefaultObject函数调用默认的构造函数创建SerializationObj类型的对象,需要注意的是GetDefaultObject返回的是一个UObject对象,所以需要使用Cast来做类型转换。

遍历对象内所有的属性、函数

void AOperatActor::Foreach()
{
	UClass* uclass = FindObject<UClass>(ANY_PACKAGE, TEXT("SerializationObj"));
	USerializationObj* obj = Cast<USerializationObj>(uclass->GetDefaultObject());
    UE_LOG(LogTemp, Warning, TEXT("UProprty Start"));
	for (TFieldIterator<UProperty> i(obj->GetClass()); i; ++i)
	{
		UProperty* up = *i;
		UE_LOG(LogTemp, Warning, TEXT("UProperty:%s"), *up->GetName());
	}
	UE_LOG(LogTemp, Warning, TEXT("UProprty End"));
	UE_LOG(LogTemp, Warning, TEXT("UFunction Start"));
	for (TFieldIterator<UFunction> i(obj->GetClass()); i; ++i)
	{
		UFunction* uf = *i;
		UE_LOG(LogTemp, Warning, TEXT("UFunction:%s"), *uf->GetName());
	}
	UE_LOG(LogTemp, Warning, TEXT("UFunction End"));
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

USerializationObj头文件内容:

UCLASS(BlueprintType)
class MYPROJECT_API USerializationObj : public UObject
{
	GENERATED_BODY()
public:
	UPROPERTY(BlueprintReadWrite)
	FString str = "Init String";
	int a = 0;

	USerializationObj();
	UFUNCTION()
	void PrintStr();
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

输出:

在这里插入图片描述

注意:

  • 对于UClass* uclass = FindObject<UClass>(ANY_PACKAGE, TEXT("SerializationObj"));需要注意的是UClass不能使用智能指针来装载,如:TSharedPtr<UClass> uclass = MakeShared(FindObject<UClass>(ANY_PACKAGE,TEXT("SerializationObje"))),使用智能指针在编译阶段和运行阶段都没有问题,但是结束运行时会导致引擎崩溃(直接启动的引擎会崩溃,通过vs启动的引擎会报异常),根据崩溃的提示,原因视乎和GC有关,具体原因未明。
  • for (TFieldIterator<UProperty> i(obj->GetClass()); i; ++i)的i的构造参数是UClass类型,而GetClass函数是一个实例函数,所以要取得一个类的UClass数据就不得不提供一个它的实例

此外由于静态变量无法被UPROPERTY宏标识,所以static属性对于UE4的反射系统来说也是不可见的,使用for (TFieldIterator<UProperty> i(obj->GetClass()); i; ++i)遍历属性是可以发现其中没有静态属性的。

遍历类的继承的所有接口

void AOperatActor::Foreach()
{
    UClass* uclass = FindObject<UClass>(ANY_PACKAGE, TEXT("SerializationObj"));
	USerializationObj* obj = Cast<USerializationObj>(uclass->GetDefaultObject());
	UE_LOG(LogTemp, Warning, TEXT("Interfaces Start"));
	for (FImplementedInterface& i : obj->GetClass()->Interfaces)
	{
		UClass* inter = i.Class;
		UE_LOG(LogTemp, Warning, TEXT("Interface:%s"), *inter->GetName());
	}
	UE_LOG(LogTemp, Warning, TEXT("Interfaces End"));
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

USerializationObj头文件内容:

UCLASS(BlueprintType)
class MYPROJECT_API USerializationObj : public UObject,public ITestInterface1,public ITestInterface2
{
	GENERATED_BODY()
public:
	UPROPERTY(BlueprintReadWrite)
	FString str = "Init String";
	int a = 0;

	USerializationObj();
	UFUNCTION()
	void PrintStr();
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

输出结果:

在这里插入图片描述

遍历枚举

UENUM()
enum TestEnum
{
	A,
	B,
	C
};

void AOperatActor::Foreach()
{
	UE_LOG(LogTemp, Warning, TEXT("Enum Start"));
	UEnum* enumClass = StaticEnum<TestEnum>();
	for (int i = 0; i < enumClass->NumEnums() - 1; i++)
	{
		FString enumStr = enumClass->GetValueAsString(TestEnum(enumClass->GetValueByIndex(i)));
		UE_LOG(LogTemp, Warning, TEXT("Enum:%s"), *enumStr);
	}
	UE_LOG(LogTemp, Warning, TEXT("Enum End"));
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

输出结果:

在这里插入图片描述

遍历元数据

void AOperatActor::Foreach()
{
    UClass* uclass = FindObject<UClass>(ANY_PACKAGE, TEXT("SerializationObj"));
	USerializationObj* obj = Cast<USerializationObj>(uclass->GetDefaultObject());
    UE_LOG(LogTemp, Warning, TEXT("Meta Start"));
	UMetaData* meta = obj->GetOutermost()->GetMetaData();
	TMap<FName, FString>* keyValues = meta->GetMapForObject(obj);
	if (keyValues != nullptr && keyValues->Num() > 0)
	{
		for (TPair<FName, FString> p : *keyValues)
		{
			FString key = p.Key.ToString();
			FString vuale = p.Value;
			UE_LOG(LogTemp, Warning, TEXT("Meta:Key=%s,Value=%s"),*key,*vuale);
		}	
	}
	UE_LOG(LogTemp, Warning, TEXT("Meta End"));
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

需要注意的是,一个对象的UMetaData数据不能直接获取,而需要通过GetOutermost函数获取这个对象的UPakage对象再通过UPakage对象的GetMetaData函数来获取,由于UE4使用TMap<FName,FString>的数据结构来存储元数据,所以我们通过UMetaData对象的GetMapForObject函数获取的元数据需要使用一个TMap<FName,FString>来存储,而TMap的元素又是一个TPair,所以遍历时可以使用一个范围for循环并使用TPair<FName,FString>结构来存储取出的TMap<FName,FString>元素。

对于元素据暂时没有深入去研究,总之如果我们只创建一个UObject类并且只往里面添加一些属性和函数,类的元数据都是空的,尝试过向UCLASS和UPROPERTY宏中添加meta内容,元数据依旧是空的,所以在使用TMap<FName,FString>时最好先判空。

这里有一个坑,就是UE_LOG不能打印FName类型的字符串,FName类型字符串必须通过ToString函数转换成FString才能被UE_LOG打印,更坑的是直接打印FName时,在编写代码时编辑器不会报错,只有在编译时才会报错。

遍历继承关系

void AOperatActor::Foreach()
{
	UE_LOG(LogTemp, Warning, TEXT("SuperClass Start"));
	TArray<FString> className;
	className.Add(obj->GetClass()->GetName());
	UClass* super = obj->GetClass()->GetSuperClass();
	while (super)
	{
		className.Add(super->GetName());
		super = super->GetSuperClass();
	}
	FString superClassStr = FString::Join(className, TEXT("->"));
	UE_LOG(LogTemp, Warning, TEXT("SuperClass:%s"), *superClassStr);
	UE_LOG(LogTemp, Warning, TEXT("SuperClass End"));
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

输出结果:

将UClass换成UStruct最终效果也是一样的,因为UClass继承自UStruct。

void AOperatActor::Foreach()
{
	UE_LOG(LogTemp, Warning, TEXT("SuperClass Start"));
	TArray<FString> className;
	className.Add(obj->GetClass()->GetName());
	UStruct* super = obj->GetClass()->GetSuperUStruct();
	while (super)
	{
		className.Add(super->GetName());
		super = super->GetSuperUStruct();
	}
	FString superClassStr = FString::Join(className, TEXT("->"));
	UE_LOG(LogTemp, Warning, TEXT("SuperClass:%s"), *superClassStr);
	UE_LOG(LogTemp, Warning, TEXT("SuperClass End"));
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

遍历所有的子类

首先为USerializationObj类创建两个子类:

oid AOperatActor::Foreach()
{
    UE_LOG(LogTemp, Warning, TEXT("DerivedClass Start"));
	TArray<UClass*> res;
	GetDerivedClasses(USerializationObj::StaticClass(), res, false);
	for (UClass* uc : res)
	{
		UE_LOG(LogTemp, Warning, TEXT("SubClass:%s"),*uc->GetName());
	}
	UE_LOG(LogTemp, Warning, TEXT("DerivedClass End"));   
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

输出结果:
在这里插入图片描述

动态操作实例属性

UE4提供了一个通过名字来动态获取属性的方法

void AOperatActor::Invoke()
{
    UClass* uclass = FindObject<UClass>(ANY_PACKAGE, TEXT("SerializationObj"));
	USerializationObj* obj = Cast<USerializationObj>(uclass->GetDefaultObject());
	UE_LOG(LogTemp, Warning, TEXT("FindPropertyByName Start"));
	UProperty* upro = obj->GetClass()->FindPropertyByName(FName(TEXT("str")));
	FString* str = upro->ContainerPtrToValuePtr<FString>(obj);
	check(str);
	*str = TEXT("UProperty FString");
	obj->PrintStr();
	UE_LOG(LogTemp, Warning, TEXT("FindPropertyByName End"));
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

SerializationObj类:

UCLASS(BlueprintType,meta=(DiaplayName="Obj"))
class MYPROJECT_API USerializationObj 
{
	GENERATED_BODY()
public:
	UPROPERTY(BlueprintReadWrite,meta=(EditCondition="bCanNamePropertyShow"))
	FString str = "Init String";
	int a = 0;
private:
    UPROPERTY()
	FString priStr = TEXT("Private String");
public:
	USerializationObj();
	UFUNCTION()
	void PrintStr();
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

运行结果:

在这里插入图片描述

UClass::FindPropertyByName()函数可以通过名字来访问调用对象中的属性,而FindPropertyByName()返回的也不是直接可用的属性,而是包含这个属性信息的UProperty类,然后通过UProperty::ContainerPtrToValuePtr()函数可以获取这个属性的指针,通过这个指针即可修改属性的值了。这个方法可直接修改实例中的任何属性,在测试修改const属性时发现了一个问题,即被UPROPERTY宏修饰的属性如果加上const那么程序将无法编译通过

除此之外也可通过遍历属性的方法来获取想要的属性,同样支持任何保护级

void AOperatActor::Invoke()
{
    UClass* uclass = FindObject<UClass>(ANY_PACKAGE, TEXT("SerializationObj"));
	USerializationObj* obj = Cast<USerializationObj>(uclass->GetDefaultObject());
	UE_LOG(LogTemp, Warning, TEXT("Private Property Start"));
	for (TFieldIterator<UProperty> i(obj->GetClass()); i; ++i)
	{
		UProperty* up = *i;
		if (up->GetName() == TEXT("priStr"))
		{
			FString* priStr = up->ContainerPtrToValuePtr<FString>(obj);
			check(priStr);
			*priStr = TEXT("UProperty PrivateString");
			obj->PrintPrivateStr();
		}
	}
	UE_LOG(LogTemp, Warning, TEXT("Private Property End"));
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

当然直接通过指针来操作属性在安全性上是不够的,大多数时候我们可能只是需要属性的一份值拷贝就够了,所以UE4针对FString类型的属性提供了两个更安全的操作函数:

void AOperatActor::Invoke()
{
    UClass* uclass = FindObject<UClass>(ANY_PACKAGE, TEXT("SerializationObj"));
	USerializationObj* obj = Cast<USerializationObj>(uclass->GetDefaultObject());
	UE_LOG(LogTemp, Warning, TEXT("ExportTextItem Start"));
	FString outStr;
	UProperty* outUpro = obj->GetClass()->FindPropertyByName(FName(TEXT("str")));
	outUpro->ExportTextItem(outStr, outUpro->ContainerPtrToValuePtr<FString*>(obj), nullptr, (UObject
		*)obj, PPF_None);
	UE_LOG(LogTemp, Warning, TEXT("OutStr:%s"),*outStr);
	outStr = TEXT("NewFString");
	UE_LOG(LogTemp, Warning, TEXT("OutStr:%s"), *outStr);
	UE_LOG(LogTemp, Warning, TEXT("OutStr:%s"), *obj->str);
	FString inStr = TEXT("NewFString");
	outUpro->ImportText(*inStr, outUpro->ContainerPtrToValuePtr<FString*>(obj), PPF_None, obj);
	UE_LOG(LogTemp, Warning, TEXT("InStr:%s"), *obj->str);
	UE_LOG(LogTemp, Warning, TEXT("ExportTextItem End"));
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

输出结果:

在这里插入图片描述

UProperty::ExportTextIte函数返回的是一个FString,而非FString*,所以获取到的是一份FString的拷贝,可以看到我们对outStr做修改是不会影响到到obj中的str的,同时UE4也提供拷贝设值UProperty::ImportText函数,将inStr的值拷贝赋值到obj的str中,之后obj的str的值就发生了变化。

动态调用实例函数

void AOperatActor::InvokeFunction()
{
	UE_LOG(LogTemp, Warning, TEXT("InvokeFunction Start"));
	struct Fun_Params
	{
		FString pam1;
		bool pam2;
		FString ret;
	};
	UClass* uclass = FindObject<UClass>(ANY_PACKAGE, TEXT("SerializationObj"));
	USerializationObj* obj = Cast<USerializationObj>(uclass->GetDefaultObject());
	UFunction* fun = obj->FindFunctionChecked("ExcternalInvokeFun");
	Fun_Params pams;
	pams.pam1 = TEXT("Invoke ExcternalInvokeFun");
	pams.pam2 = true;
	obj->ProcessEvent(fun, &pams);
	UE_LOG(LogTemp, Warning, TEXT("InvokeFunction:ret=%s"), *pams.ret);
	UFunction* fun_none = obj->FindFunctionChecked("PrintStr");
	obj->ProcessEvent(fun_none, nullptr);
	UE_LOG(LogTemp, Warning, TEXT("InvokeFunction End"));
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

SerializationObj头文件内容:

UCLASS(BlueprintType,meta=(DiaplayName="Obj"))
class MYPROJECT_API USerializationObj : public UObject,public ITestInterface1,public ITestInterface2
{
	GENERATED_BODY()
public:
	UPROPERTY(BlueprintReadWrite,meta=(EditCondition="bCanNamePropertyShow"))
	FString str = "Init String";
	int a = 0;

	static FString staticStr;
private:
	UPROPERTY()
	FString priStr = TEXT("Private String");
public:
	USerializationObj();
	UFUNCTION()
	void PrintStr();
	UFUNCTION()
	void PrintPrivateStr();
	static void Print();
	UFUNCTION()
	FString ExcternalInvokeFun(FString pam1, bool pam2);
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

输出结果:

在这里插入图片描述

事实上真正通过反射调用函数的方法是:ProcessEvent,而有参函数和无参函数的调用又有所区别,首先需要通过UObject::FindFunctionChecked函数通过函数名获取函数的元数据信息存储到UFunction类中,无参函数的调用就可以直接通过ProcessEvent(UFunction*,nullptr)来调用了,第一个参数是存储了指定函数元数据信息的UFunction,由于没有参数所以传入函数参数的第二个参数直接设为nullptr即可。

而对于有参数有返回值的函数调用,则需要提前创建好存储函数参数和返回值的结构体,如上面例子中Fun_Params,名字可以随意取,但是结构体的成员类型、数量和顺序必须和对应的gen.cpp文件中UE4为这个函数创建的存储函数参数信息的结构体一直,我们可以看一下这个结构体的结构,位置在:项目根目录\Intermediate\Build\Win64\UE4Editor\Inc\MyProject\SerializationObj.gen.cpp,我这里类的名字是SerializationObj,所以文件叫SerializationObj.gen.cpp。

struct SerializationObj_eventExcternalInvokeFun_Parms
{
	FString pam1;
	bool pam2;
	FString ReturnValue;
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

然后对应函数原型:

FString USerializationObj::ExcternalInvokeFun(FString pam1, bool pam2)
{
	FString ret = TEXT("");
	if (pam2)
	{
		ret = pam1 + TEXT("_True");		
	}
	else
	{
		ret = pam1 + TEXT("_False");
	}
	return ret;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

结构体的成员和函数的参数列表类型和顺序一一对应的,最后一个成员固定名字为ReturnValue用于存储函数的返回值。

所以我们在调用有参有返回值的函数时需要创建一个对应这种结构的结构体,使用这个结构体的变量来传递参数和接收返回值,如上面例子中的:pams。

相较于C#中Invoke函数,将参数和返回值直接装箱至object中,UE4却没有办法这么做,因为UE4的UObject系统和原生C++可以算是两套系统,UE4的UObject没办法像C#那样将所有的类型都装箱到UObject中,索性把装箱的操作直接交给开发者做了,所以才有创建存储参数返回值的结构体的步骤。

C++通过反射调用蓝图函数和事件

由于蓝图函数和事件在编译后也是以UFunction的元数据存储的,所以通过反射是可以实现C++调用蓝图函数和事件的。

首先创建一个继承自Actor的蓝图MyBlueprint,并在蓝图中新增函数PrintStr和自定义事件PrintWorld:

在这里插入图片描述

然后在C++中增加调用蓝图函数和事件的代码:

void AOperatActor::InvokeBPFunction()
{
	for (TActorIterator<AActor> bpActor(GetWorld()); bpActor; ++bpActor)
	{
		if (bpActor->GetName() == TEXT("MyBlueprint"))
		{
			for (TFieldIterator<UFunction> bpFun(bpActor->GetClass()); bpFun; ++bpFun)
			{
				if (bpFun->HasAnyFunctionFlags(FUNC_BlueprintEvent) && bpFun->HasAnyFunctionFlags(FUNC_BlueprintCallable) && bpFun->GetName() == TEXT("PrintStr"))
				{
					UFunction* fun = *bpFun;
					uint8* buff = static_cast<uint8*>(FMemory_Alloca(fun->ParmsSize));
					FFrame frame = FFrame(*bpActor, fun, buff);
					fun->Invoke(*bpActor, frame, buff);
				}
				if (bpFun->HasAnyFunctionFlags(FUNC_BlueprintEvent) && bpFun->HasAnyFunctionFlags(FUNC_BlueprintCallable) && bpFun->GetName() == TEXT("PrintWorld"))
				{
					UFunction* fun = *bpFun;
					uint8* buff = static_cast<uint8*>(FMemory_Alloca(fun->ParmsSize));
					FFrame frame = FFrame(*bpActor, fun, buff);
					fun->Invoke(*bpActor, frame, buff);
				}
			}			
		}
	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

我们逐行分析:

for (TActorIterator<AActor> bpActor(GetWorld()); bpActor; ++bpActor),遍历Level中所有的Actor,这里有一个坑,就是GetWorld()必须使用Actor自身的GetWorld()函数,不能使用GEngine->GetWorld(),否则运行时会提示资源被占用;

if (bpActor->GetName() == TEXT("MyBlueprint")),找到我们需要的蓝图;

for (TFieldIterator<UFunction> bpFun(bpActor->GetClass()); bpFun; ++bpFun),遍历蓝图中的所有的函数和事件,蓝图函数和事件在底层元数据都是以UFunction的形式存储的,所以遍历的时候可以同时遍历函数和事件;

if (bpFun->HasAnyFunctionFlags(FUNC_BlueprintEvent) && bpFun->HasAnyFunctionFlags(FUNC_BlueprintCallable) && bpFun->GetName() == TEXT("PrintStr")),找到蓝图中名字为PrintStr的函数HasAnyFunctionFlags()函数用于判断当前函数是否拥有某个标记,如:FUNC_BlueprintEvent—函数时蓝图事件,FUNC_BlueprintCallable—函数是蓝图可调用函数即蓝图函数;

UFunction* fun = *bpFun;获取函数的元素据存储到UFunction中;

uint8* buff = static_cast<uint8*>(FMemory_Alloca(fun->ParmsSize));,为函数栈申请内存空间,FMemory_Alloca申请自动内存的宏,fun->ParmsSize函数的总变量大小;

FFrame frame = FFrame(*bpActor, fun, buff);,创建函数栈;

fun->Invoke(*bpActor, frame, buff);,通过函数栈执行函数

这种方式调用蓝图函数虽然很灵活方便,但是效率实在堪忧,能不用还是尽量别用吧。

C++通过子类重写调用蓝图函数

通过C++父类申明函数,蓝图子类实现函数,C++父类调用函数的方式也可以实现C++调用蓝图函数,虽然这种方式不属于反射的范畴了,不过想起来了还是记录一下吧。

首先对于C++类AOperActor创建一个给蓝图来实现的函数BPPrint

UCLASS()
class MYPROJECT_API AOperatActor : public AActor
{
	GENERATED_BODY()
	
public:	
	AOperatActor();
protected:
	virtual void BeginPlay() override;

public:	
	virtual void Tick(float DeltaTime) override;
	UFUNCTION(BlueprintImplementableEvent)
	void BPPrint();
};

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

这里需要注意的是,如果需要用蓝图子类来实现父类函数的话,这个函数必须是public权限,且需要标识BlueprintImplementableEvent,这个标识符会告诉UE4这个函数可以在蓝图中被当作事件来使用,并且会对这个函数进行默认实现,也就是实现一个空函数体,这就是为什么即使我们不在子类里实现这个函数直接调用也不会报错的原因。

然后我们创建一个继承自AOperator类的蓝图类并在蓝图类里实现BPPrint函数

在这里插入图片描述

这里实现BPPrint函数的方式有两个,一个是直接右键搜索BPPrint就像调用事件一样,直接调出实现,另一个是在Function中重写BPPrint,最终的结果和表现形式是一样的。

然后最关键的一点就是,OperatorActorInherit这个实现了BPPrint函数的蓝图类必须要在场景中函数调用才能生效,我们在AOperatorActor类的BeginPlay函数中调用

void AOperatActor::BeginPlay()
{
	Super::BeginPlay();
	BPPrint();
}
  • 1
  • 2
  • 3
  • 4
  • 5

结果:

在这里插入图片描述

我这里选择使用一个Actor来做C++通过继承调用蓝图函数的例子而不是Object,也正是因为实现函数的蓝图必须在场景里调用才生效,而Object是不能存在于场景中的。

上面说到蓝图VM会为被BlueprintImplementableEvent标识的函数生成默认实现,事实上UE4也提供了函数的自定义默认实现的,即使用BlueprintNativeEvent标识就可以自定义函数的默认实现了,且必须要实现,否则编译不能通过,更重要的是函数名还有所变化,如:我们要自定义BPPrint的默认实现,那么BPPrint的实现应该如下:

void AOperatActor::BPPrint_Implementation()
{
	GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Red, FString::Printf(TEXT("BPPrint")));
}
  • 1
  • 2
  • 3
  • 4

后缀_Implementation是必须要加的,否则编译无法通过。

此时如果我们不在子类中重写这个函数那么调用是默认调用父类的函数实现,如果我们在子列中重写这个函数的实现那么调用的就是子类的函数实现了。如:

不在子类中重写:

在这里插入图片描述

在子类中重写:

这里有一个问题,就是父类的实现会被多调用一次,原因未知。

除了通过重写父类函数然后直接通过调用父类函数的形式在C++中调用子类的蓝图函数的调用方式外,UE4还提供了直接通过函数名字来调用子类的任意函数的接口:

C++通过CallFunctionByNameWithArguments调用蓝图函数

大部分操作和前面的C++通过子类重写调用蓝图函数一样,需要一个继承自父类的蓝图子类,不同的是子类不需要重写父类的函数,父类可以直接通过CallFunctionByNameWithArguments接口使用函数名调用子类蓝图中任意函数。

子类蓝图中的PrintHello函数

在这里插入图片描述

注意这个函数是直接由子类创建的。

然后就可以直接在父类里调用了,我这里直接在BeginePlay里调用

void AOperatActor::BeginPlay()
{
	Super::BeginPlay();
	FString cmd = FString::Printf(TEXT("PrintHello HHHHHH"));
	FOutputDeviceDebug de;
	CallFunctionByNameWithArguments(*cmd, de, NULL, true);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

输出结果:

这里有几点需要注意,FString::Printf中的字符串使用空格隔开,一个字符串为要调用的函数名,之后的字符串为参数,各个参数之间也是用空格隔开,FOutputDeviceDebug来自头文件OutputDeviceDebug.h

当然CallFunctionByNameWithArguments接口也有通过子类重写来调用蓝图的方式一样需要通过子类来调用蓝图,所以一样需要这个子类蓝图要存在于场景中,否则调用一样无效,所以有一样的局限性,就是只支持Actor类型。

三、AActor

1.Actor网络同步

Actor的网络同步可以参考另一篇博文

2.GameMode

GameMode的执行过程

这里引用Ken_An大佬总结的一张精髓图片

GameMode只运行在服务器当中,对于单机游戏来说,由于UE4的服务器代码和客户端代码是一体的所以单机游戏本身可以算是自己的服务器,对网络游戏来说,GameMode只存在于服务器当中,在客户端中只拥有GameMode的一些副本,GameMode存在于ULevel中,当游戏切换Level时,当前GameMode会随着Level的切换而被销毁,并在新的Level加载之后产生新的GameMode。

GameMode的创建到Pawn的生成过程:

  • 游戏进程开始运行,此时UE创建GameInstance,GameInstance初始化WorldSetting中设置的GameMode,事实上在UE创建GameInstance时还创建UEngine和UWorld;

  • UE调用UGameEngine::Start函数,Start函数调用UEngine::Browse函数,再由Browse函数调用UEngine::LoadMap函数,由LoadMap函数来加载Map,创建新的World,并调用AGameInstance::CreateGameModeForURL创建GameMode;

  • SetGameMode函数主要是确保GameMode只能在Server端创建,并调用AGameInstance::CreateGameModeForURL函数创建GameMode,而CreateGameModeForURL就是实际直接调用SpawnActor创建GameMode的函数了;

  • CreateGameModeForURL函数会去读取WorldSetting的配置,并配置到新创建GameMode中;

  • Client发送连接请求:Client通过ClientTravel函数向服务器请求连接;

  • Server处理Client的请求连接:如果Server接受Client的连接,则发送配置的Server Default Map给Client;

  • Client加载地图成功之后,Server调用AGameModeBase::PreLogin函数,如果Server不想某个Client接入游戏,可以在PreLogin中拒绝;

  • 如果Server接受Client加入游戏,则调用AGameModeBase::Login,如果不接受则不调用:每当有一个Client加入游戏,Login函数就会创建一个PlayerController并复制一份到对应的Client中替换Client的本地PlayerController,此时Client和Server就通过PlayerController建立起了通信连接,RPC调用就生效了,但是按官方的说法此时调用RPC还是不安全的,应该在AGameModeBase::PostLogin函数执行完之后再调用;

    疑问:按照官方的说法,PreLogin在Login之前调用,且源码中也是一个公有的虚函数,我在自定义的GameMode中重写的PreLogin函数在游戏运行时并没有调用而重写的Login和PostLogin会调用,关于这方面的资料实在是太过于匮乏,目前尚不知道原因何在。

  • PostLogin调用HandleStartingNewPlayer:HandleStartingNewPlayer函数是可以被蓝图重写的;

  • HandleStartingNewPlayer调用RestartPlayer:RestartPlayer为蓝图可调用函数,但UE不允许RestartPlayer函数被蓝图重写,允许被C++重写;

  • RestartPlayer函数会通过FindPlayerStart函数为将要Spawn的Pawn选取出生位置,然后调用RestartPlayerAtPlayerStart函数在在指定位置生成Pawn;

  • RestartPlayerAtPlayerStart则会调用SpawnDefaultPawnFor函数实际生成Pawn并设置生成位置,然后提供InitStartSpot函数在引擎认为完成Pawn的生成之前来调整Pawn的出生位置,InitStartSpot在源码中是一个空函数,可以被蓝图重写,然后RestartPlayerAtPlayerStart会调用FinishRestartPlayer函数来设置Controller的朝向,并通知引擎确认Pawn的生成;

  • SpawnDefaultPawnFor函数也是一个蓝图可重写函数,会初始化Pawn生成的Transform,然后调用SpawnDefaultPawnAtTransform函数在指定的Transform生成Pawn;

  • SpawnDefaultPawnAtTransform函数则是实际调用SpawnActor函数来创建Pawn的最底层函数了,SpawnDefaultPawnAtTransform也是一个蓝图可重写函数;

    至此从GameMode生成到Pawn的生成过程就结束了。

AGameMode与AGameModeBase

AGameMode继承自AGameModeBase,AGameModeBase提供基础的游戏玩法规则,角色控制链中各种类的注册,游戏进度的暂停与重启,过场动画等,而AGameMode则在AGameModeBase的基础上加上了多人联机匹配的机制,如AGameMode提供了联机时的各种状态(等待加入,等待准备,游戏中等等),当游戏中的玩家断开连接时,AGameMode提供挂起玩家并存储玩家状态,待玩家重返游戏时恢复的机制。根据官方文档中描述AGameMode的产生在AGameModeBase之前,而AGameModeBase是在UE4.14之后才加入的,目的是在AGameMode上面再添加一个层级,以便UE4后续对GameMode的扩展。这些功能都在AGameMode的源码中有所反应,如下面截取的部分源码片段:

/*
*...
*/

	/** What match state we are currently in */
	UPROPERTY(Transient)
	FName MatchState;

	/** Updates the match state and calls the appropriate transition functions */
	virtual void SetMatchState(FName NewState);

	/** Overridable virtual function to dispatch the appropriate transition functions before GameState and Blueprints get SetMatchState calls. */
	virtual void OnMatchStateSet();

	/** Implementable event to respond to match state changes */
	UFUNCTION(BlueprintImplementableEvent, Category="Game", meta=(DisplayName="OnSetMatchState", ScriptName="OnSetMatchState"))
	void K2_OnSetMatchState(FName NewState);

	// Games should override these functions to deal with their game specific logic

	/** Called when the state transitions to WaitingToStart */
	virtual void HandleMatchIsWaitingToStart();

	/** Returns true if ready to Start Match. Games should override this */
	UFUNCTION(BlueprintNativeEvent, Category="Game")
	bool ReadyToStartMatch();

	/** Called when the state transitions to InProgress */
	virtual void HandleMatchHasStarted();

	/** Returns true if ready to End Match. Games should override this */
	UFUNCTION(BlueprintNativeEvent, Category="Game")
	bool ReadyToEndMatch();

	/** Called when the map transitions to WaitingPostMatch */
	virtual void HandleMatchHasEnded();

	/** Called when the match transitions to LeavingMap */
	virtual void HandleLeavingMap();

	/** Called when the match transitions to Aborted */
	virtual void HandleMatchAborted();
	
/*
*...
*/
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46

4.GameState

按照官方的说法GameState是用来保存游戏全局数据的,如任务进度,NPC状态等,GameState在服务器产生并会备份一份到所有的客户端,并且GameState对所有客户端可见,与PlayerState相对,PlayerState用于保存客户端自身的状态。GameState有GameMode创建。

只有服务器上GameState在状态发生改变时才会自行同步备份到所有的客户端,客户端的GameState副本自身不会自行同步GameState的状态到Server,如一个客户端触发了一个NPC的状态,修改了这个客户端中GameState备份的NPC状态,这个GameState备份不会将修改过的状态同步的服务器和其他的客户端,但如果修改状态的逻辑在服务器中执行,修改的GameState时服务器上的GameState,则这个状态的修改会自行同步到所有的客户端,所以对GameState的修改应该在服务器中进行。

GameState属于GameMode配置的一部分所以会跟随着GameMode的产生而产生,销毁而销毁。

GameState的创建过程

void AGameModeBase::PreInitializeComponents()
{
	Super::PreInitializeComponents();

	FActorSpawnParameters SpawnInfo;
	SpawnInfo.Instigator = GetInstigator();
	SpawnInfo.ObjectFlags |= RF_Transient;	// We never want to save game states or network managers into a map									
											
	// Fallback to default GameState if none was specified.
	if (GameStateClass == nullptr)
	{
		UE_LOG(LogGameMode, Warning, TEXT("No GameStateClass was specified in %s (%s)"), *GetName(), *GetClass()->GetName());
		GameStateClass = AGameStateBase::StaticClass();
	}

	UWorld* World = GetWorld();
	GameState = World->SpawnActor<AGameStateBase>(GameStateClass, SpawnInfo);
	World->SetGameState(GameState);
	if (GameState)
	{
		GameState->AuthorityGameMode = this;
	}

	// Only need NetworkManager for servers in net games
	AWorldSettings* WorldSettings = World->GetWorldSettings();
	World->NetworkManager = WorldSettings->GameNetworkManagerClass ? World->SpawnActor<AGameNetworkManager>(WorldSettings->GameNetworkManagerClass, SpawnInfo) : nullptr;

	InitGameState();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29

在GameMode构造的时候会初始化GameState的类型为AGameStateBase,GameMode在AGameModeBase::PreInitializeComponents函数中通过SpawnInfo来确定GameMode指定的GameState类型,然后调用SpawnActor创建GameState对象,然后调用InitGameState配置GameState的一些属性。

在AGameModeBase::InitGameState中

void AGameModeBase::InitGameState()
{
	GameState->GameModeClass = GetClass();
	GameState->ReceivedGameModeClass();

	GameState->SpectatorClass = SpectatorClass;
	GameState->ReceivedSpectatorClass();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

GameState获取了当前GameMode对象的引用和当前SpectatorPawn的对象,SpectatorPawn是旁观者类。

5.PlayerState

和GameState相对PlayerState用于保存玩家数据,和GameState一样PlayerState也首先在Server中生成并同步副本到所的Client中,一个Client的当前Level中会保存所有加入这局游戏的玩家的PlayerState。

在这里插入图片描述

如,我们有三个玩家加入游戏,那么在运行时Level下就出现了三个PlayerState。

PlayerSate存在于Controller和Pawn中,Controller保存PlayerState的源对象,Pawn保存PlayerState的引用,即PlayerState的生命周期跟着Controller走,这也比较符合PlayerState的定位,PlayerState保存的是玩家数据而不是角色数据,因为一局游戏中一个玩家可以操控多个角色。

和GameState一样,PlayerState也在Server中的PlayerSate状态发生变化时会自动同步状态到所有客户端中对应的PlayerState副本,而Client中PlayerState副本状态发生改变时不会自动同步到Server,所以对PlayerState的修改也应该在服务器中进行。

PlayerState的创建过程

  • GameMode在Login函数中调用SpawnPlayerController函数;
  • SpawnPlayerController会根据配置情况调用不通的函数来创建PlayerController;
  • PlayerController调用PostInitializeComponents函数进行初始化,PostInitializeComponents是APlayerController继承自AActor的函数,在Actor所有组件初始化后初始化自己时调用;
  • PostInitializeComponents函数调用InitPlayerState函数创建PlayerState实例,InitPlayerState函数是APlayerController继承自AController的函数;
  • InitPlayerState函数通过SpawnInfo确定创建的PlayerState的类型,然后调用SpawnActor创建PlayerState实例。

6.WorldSettings

WorldSettings的资料着实是太少太少了,连官方论坛中都很少提及,官方文档也就了了一句话,WorldSettings主要做的就是对游戏世界的一系列配置,如:大地图的动态加载与卸载,世界光照,声音系统,边界检查,导航系统,AI系统,世界重力模拟等等,具体的一些选项功能可以查看Im-JC的博文。

WorldSettings是蓝图不可见的,如果我们需要动态的获取WorldSettings里的一些配置则需要通过GetActorsWithClass来获取。

默认WorldSettings是可以更换的,在ProjectSettings/Engine/GeneralSetttings/DefualtClass下

在这里插入图片描述

可以看到不仅WorldSettings可以配置,GameViewportClient、LocalPlayer、LevelScriptActor,PhysicsCollisionHandler等都可以自定义配置,UE是真的强大,连关卡蓝图、UI显示,物理碰撞等都给予了我们自定义能力。

我们在编辑器中打开的WorldSettings视图并不是WorldSettings,而是由WorldSettings提供一个可视化编辑界面。

WorldSettings的创建过程

在UE源码中好一阵找,发现WorldSettings的创建有四个地方,分别是

UEditorLevelUtils::AddLevelToWorld_Internal

UEditorEngine::CreateTransLevelMoveBuffer

UWorld::RepairWorldSettings

UWorld::InitializeNewWorld

UEditorLevelUtils和UEditorEngine都是和编辑器相关的,不在GamePlay的框架内,这里就不讨论了,我们重点看一下UWorld中的。

InitializeNewWorld函数是实际创建WorldSettings的地方,而RepairWorldSettings按照源码的解释就是用于确保游戏中切实有一个可用的WorldSettings的功能函数,RepairWorldSettings在UWorld::PostLogin时被调用。

  • GameInstance在InitializeStandalone中调用UWorld::CreateWorld函数;
  • UWorld::CreateWorld函数调用InitializeNewWorld来创建WorldSettings;
  • UWorld::InitializeNewWorld函数就是实际创建WorldSettings的函数,InitializeNewWorld会先读取ProjectSettings中的WorlSettings的配置,如果配置了则创建对应的WorldSettings类实例,否则创建默认的WorldSettings实例。

7.ALevelScriptActor

ALevelScriptActor就是我们常说的关卡蓝图,ALevelScriptActor是一个在关卡中的隐藏Actor,在Level列表里是看不到的。

自定义关卡蓝图

既然关卡蓝图也是一个Actor那么理论上关卡蓝图也是可以自定义的,经过一番研究UE4还真提供了自定义关卡蓝图的功能。

ALevelScriptActor不是一个蓝图类,所以我们直接去创建蓝图是找不到一个ALevelScriptActor基类可供继承的,所以我们只能先用C++去创建一个继承自ALevelScriptActor的自定义C++类,然后再修改关卡蓝图的父类为自定义的ALevelScriptActor类。

这里我创建了一个LSPLevelScriptActor类并重写了BeginPlay函数,在BeginPlay函数里只打印一串字符。

//.h
#include "CoreMinimal.h"
#include "Engine/LevelScriptActor.h"
#include "LSPLevelScriptActor.generated.h"

UCLASS()
class LSPTETRISCLIENT_API ALSPLevelScriptActor : public ALevelScriptActor
{
	GENERATED_BODY()
	void BeginPlay() override;
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
//.cpp
#include "LSPLevelScriptActor.h"
#include "Engine.h"

void ALSPLevelScriptActor::BeginPlay()
{
    GEngine->AddOnScreenDebugMessage(-1, 10, FColor::Red, "LSPLevelScriptActor::BeginPlay");
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

然后我们打开想要自定义关卡蓝图的关卡,并打开关卡蓝图,在ClassSettings/ClassOptions/ParentClass设置成为自定义的LSPLevelScriptActor,那么当我们运行BeginPlay事件时,就会在屏幕上打印LSPLevelScriptActor::BeginPlay

在这里插入图片描述

8.APlayerController

APlayController与AIController相对,专用于给玩家操作的角色控制器,在AGameMode章节有说过APlayerController是由AGameModeBase::Login函数创建。APlayerController在游戏运行时是不可视的,主要负责接收外部输入,如鼠标键盘、游戏手柄等,并根据输入按一定的逻辑来控制与其绑定的APwan。APlayerController可以通过Possess函数来获取一个APawn的控制权,也可以通过UnPossess函数还放弃一个APawn的控制权。

在UE4的设计里,APwan和APlayerController都是可以接收外部输入的,如InputAxis事件既可以放在APawn里对APwan进行控制,也可以APlayerController里对指定APawn进行控制,那么二者对外部输入的处理有什么不同呢?事实上,APlayerController在逻辑上的层级要高于APawn的,也就是说外部输入要先进入APlayerController再由APlayerController传递给APawn,这就使得APlayController可以对APawn的输入进行拦截。

既然APawn和APlayerController都可以接收外部输入,那么对输入逻辑的处理应该放在APawn里还是放在APlayerController里呢?

个人理解是人应该放在APawn里,为什么呢?因为在一个游戏里,同一个玩家是可以操作多种类型的角色的,如GTA5里面,玩家既可以控制人型角色,也可以开各种车辆,还可以还飞机。各种角色对接收的输入和对输入的处理都是不一样的,如当玩家按下键盘s时,如果APawn是一个人,那么角色应该向后走,如果APawn是一辆车,那么角色应该减速,如果APawn是一个架飞机,那么角色因该下降。这么多中不同的对同一输入的处理不因该由一个APlayerController来出来,而是将之拆分到不同的APawn中处理。

输入顺序

UE4里可以接收输入的有4种类,APlayController、APawn、ALevelScriptActor和普通Actor

Actor只要设置EnableInput或AutoReceiveInput就可以接收输入了

在这里插入图片描述

UE4的可接收输入对象的输入优先级:

Actor>APlayerController>ALevelScriptActor>APawn

输入栈

UE4对输入的接收有一个输入栈的概念,在游戏一开始时,UE4会对所有的可接收输入的对象进行入栈处理,UE4通过入栈顺序来对可接收输入对象的输入优先级进行分级,先直接上一段源码。

void APlayerController::BuildInputStack(TArray<UInputComponent*>& InputStack)
{
	// Controlled pawn gets last dibs on the input stack
    //获取当前控制的APawn
	APawn* ControlledPawn = GetPawnOrSpectator();
	if (ControlledPawn)
	{
		if (ControlledPawn->InputEnabled())
		{
			// Get the explicit input component that is created upon Pawn possession. This one gets last dibs.
            //获取APawn的输入组件
			if (ControlledPawn->InputComponent)
			{
                //首先将APawn的输入组件入栈
				InputStack.Push(ControlledPawn->InputComponent);
			}

			// See if there is another InputComponent that was added to the Pawn's components array (possibly by script).
			for (UActorComponent* ActorComponent : ControlledPawn->GetComponents())
			{
				UInputComponent* PawnInputComponent = Cast<UInputComponent>(ActorComponent);
				if (PawnInputComponent && PawnInputComponent != ControlledPawn->InputComponent)
				{
					InputStack.Push(PawnInputComponent);
				}
			}
		}
	}

	// LevelScriptActors are put on the stack next
    //将拥有输入的关卡蓝图入栈
	for (ULevel* Level : GetWorld()->GetLevels())
	{
		ALevelScriptActor* ScriptActor = Level->GetLevelScriptActor();
		if (ScriptActor)
		{
			if (ScriptActor->InputEnabled() && ScriptActor->InputComponent)
			{
				InputStack.Push(ScriptActor->InputComponent);
			}
		}
	}
	//将PlayerController自身入栈
	if (InputEnabled())
	{
		InputStack.Push(InputComponent);
	}

	// Components pushed on to the stack get priority
    //将拥有输入的Actor入栈,CurrentInputStach会保存所有拥有InputComponent组件的Actor的InputComponent组件的引用
	for (int32 Idx=0; Idx<CurrentInputStack.Num(); ++Idx)
	{
		UInputComponent* IC = CurrentInputStack[Idx].Get();
		if (IC)
		{
			InputStack.Push(IC);
		}
		else
		{
			CurrentInputStack.RemoveAt(Idx--);
		}
	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63

为了验证输入优先级,我创建了一个具有APawn,APlayerController,具有输入的ALevelScriptActor,具有输入的Actor的关卡。

我在APawn中加入了前后左右滚动的事件InputAxisMoveForward和InputAxisMoveRight,同时在APlayerController,ALevelScriptActor,Actor中分别都加入一个InputAxisMoveRight,且只进行文字打印操作。

先来直接看一下结果

在这里插入图片描述

可以看到字符串的输出顺序为Actor Input->Controller Input->ServerMap Input->APawn Input

此时键盘输入依旧会一次从栈顶的Actor一直传递到栈底的APawn,但是APawn的InputAxisMoveRight已经被其上层的InputComponent截断,所以APawn只能进行前后移动而无法左右移动。

输入流程

先上一张从张悟基大佬哪里盗来的流程图。

  • 在UE4LaunchEngineLoop.h文件中有一个专门处理引擎循环的类FEngineLoop,UE4在FEngineLoop::Tick()函数中处理每帧获取设备输入,主要处理逻辑。

    FSlateApplication& SlateApp = FSlateApplication::Get();
    {              				QUICK_SCOPE_CYCLE_COUNTER(STAT_FEngineLoop_Tick_PollGameDeviceState);
    SlateApp.PollGameDeviceState();
     }
    
    • 1
    • 2
    • 3
    • 4

    其中使用了大量的宏,以本小菜的水平当前还看不懂

    声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/IT小白/article/detail/261015
推荐阅读
相关标签