参考博客: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.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世界中的地位。
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 ClientConnections;
ServerConnection是客户端到服务器的连接,ClientConnections数组是服务器到客户端群的连接的数组。而在UNetConnnection中又有一个很重要的成员:
TMap<TWeakObjectPtr,class UActorChannel*> ActorChannels
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();
}
可以看到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" ) );
}
我在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& 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
}
首先前面的宏暂时可以忽略掉,
第一步,FGCArrayStruct* ArrayStruct = FGCArrayPool::Get().GetArrayStructFromPool();
UE将UObject的所有的强引用和弱引用都存储大ArrayStruct数据结构中,FGCArrayPool是UEGC的主要执行类
第二步,TArray& ObjectsToSerialize = ArrayStruct->ObjectsToSerialize;
分离UObject的强引用到ObjectsToSerialize 数组中。
这是FGCArrayStruct结构体的源码:
struct FGCArrayStruct
{
TArray ObjectsToSerialize;
TArray WeakReferences;
};
ObjectsToSerialize存储强引用,WeakReferences存储弱引用。
第三步,GObjectCountDuringLastMarkPhase.Reset();
重置对象的引用计数。
第四步,通过一个if判断标记可达对象,于是可达对象与不可达对象就被标记出来了,接下来便是GC清理。
GC的触发
UE的GC发生在游戏线程上,支持多线程GC,和大多数主流语言的GC一样支持自动触发和手动触发。
手动触发
手动触发UE也提供了两种方式,其一是通过C++函数:
GEngine->ForceGarbageCollection();
这里需要注意的是GEngine
在Engine.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"));
}
//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;
};
//AMyActor2
AMyActor2::AMyActor2()
{
PrimaryActorTick.bCanEverTick = true;
}
void AMyActor2::BeginPlay()
{
Super::BeginPlay();
a = UWorld()->SpawnActor();
a = NULL;
GEngine->ForceGarbageCollection();
}
void AMyActor2::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
}
OutputLog:
LogTemp: Warning: Actor1 Created
可以看到使用UWorld()->SpawnActor创建的Actor即使手动强制GC也没有被回收,因为这个Actor是可达对象。
自动触发
要想UE自动触发的GC能能够回收我们创建的对象,那么我们创建的对象就必须继承自UObject,至于加不加UPROPERTY()宏似乎不影响GC的回收,如下面的测试结果,还是以上面的例子为例,把BeginPlay函数改为如下:
//AMyActor2
AMyActor2::AMyActor2()
{
PrimaryActorTick.bCanEverTick = true;
}
void AMyActor2::BeginPlay()
{
Super::BeginPlay();
a = NewObject();
a = NULL;
GEngine->ForceGarbageCollection();
}
void AMyActor2::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
}
我们将MyActor2拖入场景中,运行,OutputLog输出,可以找到下面两句:
LogTemp: Warning: Actor1 Created
LogTemp: Warning: Actor1 Destryed
可以看到,没有使用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"));
}
//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;
};
//AMyActor2.cpp
void AMyActor2::BeginPlay()
{
Super::BeginPlay();
obj = NewObject();
}
使用UPROPERTY()宏的输出结果:
//在点击Play后输出结果
LogTemp: Warning: AMyActor2 Created
LogTemp: Warning: UMyObject Created
//再点击Stop后输出结果
LogTemp: Warning: UMyObject Destoryed
LogTemp: Warning: AMyActor2 Destroyed
不使用UPROPERTY()宏的输出结果:
//在点击Play后输出结果
LogTemp: Warning: AMyActor2 Created
LogTemp: Warning: UMyObject Created
LogTemp: Warning: UMyObject Destoryed
//再点击Stop后输出结果
LogTemp: Warning: AMyActor2 Destroyed
很明显在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"));
}
//AMyActor2
UCLASS()
class INSIDEUE4_API AMyActor2 : public AActor
{
GENERATED_BODY()
public:
AMyActor2();
AActor1* a;
TWeakObjectPtr p;
protected:
virtual void BeginPlay() override;
public:
virtual void Tick(float DeltaTime) override;
};
//AMyActor2.cpp/BeginPlay()
void AMyActor2::BeginPlay()
{
Super::BeginPlay();
a = NewObject();
p = a;
a = NULL;
GEngine->ForceGarbageCollection();
}
OutputLog:
LogTemp: Warning: Actor1 Created
LogTemp: Warning: Actor1 Destryed
可以看到,AActor1对象依旧被强制回收了。
而TWeakPtr则对于自定义类的弱指针。
注意:弱指针不可以被用来作为TSet或TMap的Key,因为一个对象被GC时无法通知一个容器的Key,但是可以用来作为容器的Value。
TSharedPtr、TSharedRef(自定义类的GC)
自定义类的GC,UE也贴心的提供了 TSharedPtr和TSharedRef对象来为自定义类支持GC,TSharedPtr本质上是一个被封装过的指针,使用形式上依然保留指针的风格。
创建TSharedPtr指针指向一个自定义类时,需要使用MakeShareable()
函数,如:
TSharedPtr p = MakeShareable(NewObject());
TSharedPtr f = MakeShareable(new FActor());
TSharedPtr和TSharedRef都可以为自定义类提供GC功能,二者的区别只在于TSharedPtr可以为null,而SharedRef不可以。我在网上查询发现有三种方法构建TSharedRef,分别为:
第一种:
TSharedRef ref(new FActor());
第二种:
TSharedRef ref = MakeShared(new FActor());
第三种:
TSharedPtr ptr = MakeShareable(new FActor());
TSharedRef ref = ptr.ToSharedRef();
其中第二种方法在编写时没有任何问题但在编译时无法通过,并提示:
The TSharedRef() constructor is for internal usage only for hot-reload purposes. Please do NOT use it.
使用的编译环境为: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);
}
};
然后,UObject对象就会在FActor对象析构时才被析构。
2.序列化
FObjectWriter和FObjectReader序列化对象到文件和从文件读取
FObjectWriter可以将对象数据序列化为二进制流,然后配合FFileHelper将流写入文件即可实现对象状态存储到文件。
void AOperatActor::SaveObject()
{
USerializationObj* obj= NewObject();
UE_LOG(LogTemp, Warning, TEXT("OldStr:%s"), *obj->str);
obj->str = TEXT("OperatActor");
TArray bytes;
FObjectWriter(obj, bytes);
FFileHelper::SaveArrayToFile(bytes, *FString("D:\Goulandis\UE4\MyProject\obj.txt"));
}
配合FFileHelper将文件中的对象状态读入字节数组,FObjectReader就可以将字节数组中的对象状态写入新的对象中。
USerializationObj* AOperatActor::LoadObject()
{
USerializationObj* newObj = NewObject();
TArray 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;
}
看一下运行结果:
可以看到,新创建的USerializationObj对象的状态是被修改过后的状态。
Actor的使用方式和UObject是一样的:
void AOperatActor::SaveActor()
{
ASerializationActor* actor = GetWorld()->SpawnActor();
UE_LOG(LogTemp, Warning, TEXT("OldStr:%s"), *actor->str);
actor->str = TEXT("NewActor");
TArray bytes;
FObjectWriter(actor, bytes);
FFileHelper::SaveArrayToFile(bytes, *FString("D:\Goulandis\UE4\MyProject\actor.txt"));
}
ASerializationActor * AOperatActor::LoadActor()
{
ASerializationActor* actor = GetWorld()->SpawnActor();
TArray 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;
}
运行结果:
3.反射
在使用UE4的反射时有一个基础概念是必须要清楚的,即UE4的反射系统是建立在一整套的宏的设计上的,也就是说,想要一个类、属性、方法、枚举、结构体等支持UE4的反射,那么类必须加UCLASS宏标识,属性必须加UPROPERTTY宏标识,方法必须加UFUNCTION宏标识,枚举必须加UENUM宏标识,结构体必须加USTRUCT宏标识,如果不加这些宏来标识对应的目标,那么这些目标对于UE4的反射系统来说就是不可见的。
搜索所有的Object
C++本身的反射系统RTTI相当薄弱,所以UE在C++的基础上借助UObject自己实现了一套反射系统,同时借鉴了C#的长处提供了一系列反射用的系统函数。
TArray result;
GetObjectsOfClass(UClass::StaticClass(), result); //获取所有的class和interface
GetObjectsOfClass(UEnum::StaticClass(), result); //获取所有的enum
GetObjectsOfClass(UScriptStruct::StaticClass(), result); //获取所有的struct
运行时创建对象
void AOperatActor::FindSerializationObj()
{
UClass* uclass = FindObject(ANY_PACKAGE, TEXT("SerializationObj"));
USerializationObj* obj = Cast(uclass->GetDefaultObject());
obj->PrintStr();
}
UE4提供FindObject模板函数来搜索指定的类的类型信息,返回的类型元素据通过UClass类型对象存储,UClass对象就是UE4专门用来存储元数据的类型,UClass中提供了大量的方法来操作元数据,UClass,这里使用GetDefaultObject函数调用默认的构造函数创建SerializationObj类型的对象,需要注意的是GetDefaultObject返回的是一个UObject对象,所以需要使用Cast来做类型转换。
遍历对象内所有的属性、函数
void AOperatActor::Foreach()
{
UClass* uclass = FindObject(ANY_PACKAGE, TEXT("SerializationObj"));
USerializationObj* obj = Cast(uclass->GetDefaultObject());
UE_LOG(LogTemp, Warning, TEXT("UProprty Start"));
for (TFieldIterator 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 i(obj->GetClass()); i; ++i)
{
UFunction* uf = *i;
UE_LOG(LogTemp, Warning, TEXT("UFunction:%s"), *uf->GetName());
}
UE_LOG(LogTemp, Warning, TEXT("UFunction End"));
}
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();
};
输出:
注意:
- 对于
UClass* uclass = FindObject(ANY_PACKAGE, TEXT("SerializationObj"));
需要注意的是UClass不能使用智能指针来装载,如:TSharedPtr uclass = MakeShared(FindObject(ANY_PACKAGE,TEXT("SerializationObje")))
,使用智能指针在编译阶段和运行阶段都没有问题,但是结束运行时会导致引擎崩溃(直接启动的引擎会崩溃,通过vs启动的引擎会报异常),根据崩溃的提示,原因视乎和GC有关,具体原因未明。 for (TFieldIterator i(obj->GetClass()); i; ++i)
的i的构造参数是UClass类型,而GetClass函数是一个实例函数,所以要取得一个类的UClass数据就不得不提供一个它的实例
此外由于静态变量无法被UPROPERTY宏标识,所以static属性对于UE4的反射系统来说也是不可见的,使用for (TFieldIterator i(obj->GetClass()); i; ++i)
遍历属性是可以发现其中没有静态属性的。
遍历类的继承的所有接口
void AOperatActor::Foreach()
{
UClass* uclass = FindObject(ANY_PACKAGE, TEXT("SerializationObj"));
USerializationObj* obj = Cast(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"));
}
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();
};
输出结果:
遍历枚举
UENUM()
enum TestEnum
{
A,
B,
C
};
void AOperatActor::Foreach()
{
UE_LOG(LogTemp, Warning, TEXT("Enum Start"));
UEnum* enumClass = StaticEnum();
for (int i = 0; i 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"));
}
输出结果:
遍历元数据
void AOperatActor::Foreach()
{
UClass* uclass = FindObject(ANY_PACKAGE, TEXT("SerializationObj"));
USerializationObj* obj = Cast(uclass->GetDefaultObject());
UE_LOG(LogTemp, Warning, TEXT("Meta Start"));
UMetaData* meta = obj->GetOutermost()->GetMetaData();
TMap* keyValues = meta->GetMapForObject(obj);
if (keyValues != nullptr && keyValues->Num() > 0)
{
for (TPair 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"));
}
需要注意的是,一个对象的UMetaData数据不能直接获取,而需要通过GetOutermost函数获取这个对象的UPakage对象再通过UPakage对象的GetMetaData函数来获取,由于UE4使用TMap
的数据结构来存储元数据,所以我们通过UMetaData对象的GetMapForObject函数获取的元数据需要使用一个TMap来存储,而TMap的元素又是一个TPair,所以遍历时可以使用一个范围for循环并使用
TPair结构来存储取出的
TMap元素。
对于元素据暂时没有深入去研究,总之如果我们只创建一个UObject类并且只往里面添加一些属性和函数,类的元数据都是空的,尝试过向UCLASS和UPROPERTY宏中添加meta内容,元数据依旧是空的,所以在使用TMap
时最好先判空。
这里有一个坑,就是UE_LOG不能打印FName类型的字符串,FName类型字符串必须通过ToString函数转换成FString才能被UE_LOG打印,更坑的是直接打印FName时,在编写代码时编辑器不会报错,只有在编译时才会报错。
遍历继承关系
void AOperatActor::Foreach()
{
UE_LOG(LogTemp, Warning, TEXT("SuperClass Start"));
TArray 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"));
}
输出结果:
将UClass换成UStruct最终效果也是一样的,因为UClass继承自UStruct。
void AOperatActor::Foreach()
{
UE_LOG(LogTemp, Warning, TEXT("SuperClass Start"));
TArray 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"));
}
遍历所有的子类
首先为USerializationObj类创建两个子类:
oid AOperatActor::Foreach()
{
UE_LOG(LogTemp, Warning, TEXT("DerivedClass Start"));
TArray 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"));
}
输出结果:
动态操作实例属性
UE4提供了一个通过名字来动态获取属性的方法
void AOperatActor::Invoke()
{
UClass* uclass = FindObject(ANY_PACKAGE, TEXT("SerializationObj"));
USerializationObj* obj = Cast(uclass->GetDefaultObject());
UE_LOG(LogTemp, Warning, TEXT("FindPropertyByName Start"));
UProperty* upro = obj->GetClass()->FindPropertyByName(FName(TEXT("str")));
FString* str = upro->ContainerPtrToValuePtr(obj);
check(str);
*str = TEXT("UProperty FString");
obj->PrintStr();
UE_LOG(LogTemp, Warning, TEXT("FindPropertyByName End"));
}
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();
};
运行结果:
UClass::FindPropertyByName()
函数可以通过名字来访问调用对象中的属性,而FindPropertyByName()返回的也不是直接可用的属性,而是包含这个属性信息的UProperty类,然后通过UProperty::ContainerPtrToValuePtr()
函数可以获取这个属性的指针,通过这个指针即可修改属性的值了。这个方法可直接修改实例中的任何属性,在测试修改const属性时发现了一个问题,即被UPROPERTY宏修饰的属性如果加上const那么程序将无法编译通过
除此之外也可通过遍历属性的方法来获取想要的属性,同样支持任何保护级
void AOperatActor::Invoke()
{
UClass* uclass = FindObject(ANY_PACKAGE, TEXT("SerializationObj"));
USerializationObj* obj = Cast(uclass->GetDefaultObject());
UE_LOG(LogTemp, Warning, TEXT("Private Property Start"));
for (TFieldIterator i(obj->GetClass()); i; ++i)
{
UProperty* up = *i;
if (up->GetName() == TEXT("priStr"))
{
FString* priStr = up->ContainerPtrToValuePtr(obj);
check(priStr);
*priStr = TEXT("UProperty PrivateString");
obj->PrintPrivateStr();
}
}
UE_LOG(LogTemp, Warning, TEXT("Private Property End"));
}
当然直接通过指针来操作属性在安全性上是不够的,大多数时候我们可能只是需要属性的一份值拷贝就够了,所以UE4针对FString类型的属性提供了两个更安全的操作函数:
void AOperatActor::Invoke()
{
UClass* uclass = FindObject(ANY_PACKAGE, TEXT("SerializationObj"));
USerializationObj* obj = Cast(uclass->GetDefaultObject());
UE_LOG(LogTemp, Warning, TEXT("ExportTextItem Start"));
FString outStr;
UProperty* outUpro = obj->GetClass()->FindPropertyByName(FName(TEXT("str")));
outUpro->ExportTextItem(outStr, outUpro->ContainerPtrToValuePtr(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(obj), PPF_None, obj);
UE_LOG(LogTemp, Warning, TEXT("InStr:%s"), *obj->str);
UE_LOG(LogTemp, Warning, TEXT("ExportTextItem End"));
}
输出结果:
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(ANY_PACKAGE, TEXT("SerializationObj"));
USerializationObj* obj = Cast(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"));
}
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);
};
输出结果:
事实上真正通过反射调用函数的方法是:ProcessEvent
,而有参函数和无参函数的调用又有所区别,首先需要通过UObject::FindFunctionChecked
函数通过函数名获取函数的元数据信息存储到UFunction类中,无参函数的调用就可以直接通过ProcessEvent(UFunction*,nullptr)
来调用了,第一个参数是存储了指定函数元数据信息的UFunction,由于没有参数所以传入函数参数的第二个参数直接设为nullptr即可。
而对于有参数有返回值的函数调用,则需要提前创建好存储函数参数和返回值的结构体,如上面例子中Fun_Params,名字可以随意取,但是结构体的成员类型、数量和顺序必须和对应的gen.cpp文件中UE4为这个函数创建的存储函数参数信息的结构体一直,我们可以看一下这个结构体的结构,位置在:项目根目录IntermediateBuildWin64UE4EditorIncMyProjectSerializationObj.gen.cpp,我这里类的名字是SerializationObj,所以文件叫SerializationObj.gen.cpp。
struct SerializationObj_eventExcternalInvokeFun_Parms
{
FString pam1;
bool pam2;
FString ReturnValue;
};
然后对应函数原型:
FString USerializationObj::ExcternalInvokeFun(FString pam1, bool pam2)
{
FString ret = TEXT("");
if (pam2)
{
ret = pam1 + TEXT("_True");
}
else
{
ret = pam1 + TEXT("_False");
}
return ret;
}
结构体的成员和函数的参数列表类型和顺序一一对应的,最后一个成员固定名字为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 bpActor(GetWorld()); bpActor; ++bpActor)
{
if (bpActor->GetName() == TEXT("MyBlueprint"))
{
for (TFieldIterator 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(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(FMemory_Alloca(fun->ParmsSize));
FFrame frame = FFrame(*bpActor, fun, buff);
fun->Invoke(*bpActor, frame, buff);
}
}
}
}
}
我们逐行分析:
for (TActorIterator bpActor(GetWorld()); bpActor; ++bpActor)
,遍历Level中所有的Actor,这里有一个坑,就是GetWorld()必须使用Actor自身的GetWorld()函数,不能使用GEngine->GetWorld(),否则运行时会提示资源被占用;
if (bpActor->GetName() == TEXT("MyBlueprint"))
,找到我们需要的蓝图;
for (TFieldIterator 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(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();
};
这里需要注意的是,如果需要用蓝图子类来实现父类函数的话,这个函数必须是public权限,且需要标识BlueprintImplementableEvent,这个标识符会告诉UE4这个函数可以在蓝图