《大象无形:虚幻引擎程序设计浅析》

转载:https://blog.csdn.net/qq826364410/article/details/102717636

 

一、对象销毁

 

纯C++类销毁

尽量不要使用new、delete方案,可以使用智能指针。智能指针会使用引用计数来完成自动的内存释放。

使用MakeShareable函数可以来转化普通指针为智能指针。

TSharedPtr YourClassPtr = MakeShareable(new YourClass());

 

UObject类

无法使用智能指针来管理UObject对象。

UObject类采用自动垃圾回收机制。当一个类的成员变量包含指向UObject的对象,同时又带有UPROPERTY宏定义,那么这个成员变量将会触发引用计数机制。

垃圾回收器会定期从根节点Root开始检查,当一个UObject没有被别的任何UObject引用,就会被垃圾回收。可以通过AddToRoot函数来让一个UObject一直不被回收。

Actor类

Actor类对象可以通过调用Destroy函数来请求销毁。

 

二、从C++到蓝图

 

UPROPERTY

当需要将一个UObject类的子类的成员变量注册到蓝图中时,只需要借助UPROPERTY宏即可。

UPROPERTY(BlueprintReadWrite, VisibleAnywhere,Category="Object")

 

  • BlueprintReadWrite 可从蓝图读取或写入此属性。此说明符与 BlueprintReadOnly 说明符不兼容。
  • VisibleAnywhere 说明此属性在所有属性窗口中可见,但无法被编辑。此说明符与“Edit”说明符不兼容。
  • Blueprintable - 该类可以由蓝图扩展。
  • BlueprintReadOnly - 该属性可以从蓝图读取,但不能写入蓝图。
  • EditAnywhere - 该属性可以在原型和实例上的属性窗口中编辑。
  • Category - 定义该属性将出现在编辑器“细节(Details)”视图下面的哪个部分。这对于整理结构而言十分有用。
  • BlueprintCallable - 该函数可以从蓝图调用。仅用于组播委托。应公开属性在蓝图代码中调用。

 

UFUNCTION

UFUNCTION(BlueprintCallable, Category="Object")

可选的还有:

BlueprintImplementEvent:表示这个成员函数有其蓝图的子类实现,不应该尝试在C++中给出函数的实现,这会导致错误。

BlueprintNativeEvent:表示这个成员函数提供一个“C++的默认实现”,同时也可以被蓝图重载。需要提供一个“函数名_Implement”的函数实现,放置于.cpp中。

 

说明符众多,不便在此一一列出,但可以参考下面的链接:

UCLASS说明符列表

UPROPERTY说明符列表

UFUNCTION说明符列表

USTRUCT说明符列表

 

三、引擎相关的类

 

1. FPaths类

FPaths::GameDir() 游戏根目录

FPaths::FileExists() 文件是否存在

FPaths::ConvertRelativePathToFull() 转变相对路径为绝对路径

 

2.使用Log类

FString dir = FPaths::ProjectPluginsDir(); UE_LOG(LogTemp, Log, TEXT("%s"), *dir);

 

第一个参数是Log的分类,第二个参数是Log的类型,分为Log,Warning,Error。Log灰色,Warning黄色,Error红色。具体输出内容为TEXT宏,有三种常用符号:

1.%s字符串(Fstring)

2.%d整形数据(int32)

3.%f浮点型(float)

 

3.文件的读写与访问

FString str = FPaths::ProjectDir() + TEXT("Setting.xml"); if (FPlatformFileManager::Get().GetPlatformFile().FileExists(*str)) { }

FPlatformFileManager::Get().GetPlatformFile()获得一个IPlatformFile类型的引用,这个接口提供了通用的文件访问接口。

 

4.字符串处理

FName 是无法被修改的字符串,不管出现多少次,在字符串表中只被存储一次。

FText 提供本地化支持。

FString 是唯一提供修改操作的字符串类。

 

5.ImageWrapper 

 

图片文件自身的数据是压缩后的数据,称为CompressedData

图片对应的真正的RGBA数据,是没有压缩的,与格式无关,称为RawData

所有的图片格式,都可以抽象为一个CompressedData和RawData的组合。

 

读取JPG图片:

  1. 从文件中读取为TArray的二进制数据
  2. 使用SetCompressData填充为压缩数据
  3. 将压缩后的数据借助ImageWrapper的GetRaw转换为RGB数据
  4. 填充RGB数据到UTexture的数据中

 

转换PNG图片到JPG:

  1. 从文件中读取为TArray的二进制数据
  2. 使用SetCompressData填充为压缩数据
  3. 将压缩后的数据借助ImageWrapper的GetRaw转换为RGB数据
  4. SetRaw将RGB数据填充到JPG类型的ImageWrapper中
  5. 使用GetCompressData获得压缩后的JPG数据
  6. 最后用FFileHelper写入到文件中

 

6.模块机制

虚幻源码目录包含四大部分:Runtime,Development,Editor,Plugin

每个部分有包含了很多个模块。

一个模块文件夹包含:Public文件夹,Private文件夹,模块构建文件.build.cs

只有通过XXX_API宏暴露的类和成员函数才能被其他模块访问。

 

(1). 模块包含文件结构:

 

模块名.build.cs (模块构建文件,告知UBT如何配置自己的编译和构建环境)

public文件夹:

模块名.h

private文件夹:

模块名.cpp

模块名PrivatePCH.h (模块预编译头文件,加速代码的编译,当前模块公用的头文件可以放置于这个头文件中,当前模块所有的.cpp文件,都需要包含预编译头文件)

 

(2). 引入模块

对于游戏模块,引入当前模块的方式是在游戏工程目录下的Source文件夹中,找到工程名.Target.cs文件。修改SetupBinaries函数,添加引入的模块。

 

对于插件模块,修改当前插件的.uplugin文件,在Modules数组中引入新的模块。

 

7.虚幻引擎初始化

 

(1).初始化分为预初始化和初始化。

  • 首先加载的是FPlatformFileModule,读取文件
  • 调用FEngineLoop::PreInit,预初始化PreInit:
  1. 设置路径:当前程序路径,当前工作目录路径,游戏工程路径
  2. 设置标准输出:GLog系统输出
  3. 初始化游戏主线程GameThread,把当前线程设置为主线程
  4. 初始化随机数系统
  5. 初始化TaskGraph任务系统,并按照当前平台核心数量设置TaskGraph的工作线程数量
  • 加载核心模块,FEngineLoop::LoadCoreModules,加载CoreUObject
  • 在初始化引擎之前,加载模块,FEngineLoop::LoadPreInitModules,包括:引擎模块、渲染模块、动画蓝图、Slate渲染模块、Slate核心模块、贴图压缩模块和地形模块。
  • 加载这些模块后,AppInit函数会被调用,进入引擎正式的初始化阶段。
  • 所有被加载到内存中的模块,如果有PostEngineInit函数,都会被调用从而初始化。借助IProjectManager完成。

 

(2).游戏主循环

GEngine->Tick:最重要的任务是更新当前的World。无论是编辑器中正在编辑的World,还是游戏模式下只有一个的World。此时所有World中持有的Actor都会被得到更新。

很多任务无法在一次Tick中完成,就会分在多次Tick函数中完成。

 

8.虚幻内存分配

《游戏引擎架构》书中,对内存分配方案重点提到两个方面:

1. 通过内存池降低malloc消耗

2. 通过对齐降低缓存命中失败消耗。

 

在虚幻引擎中,主要使用还是Intel TBB内存分配器提供的scalable_allocator:不在同一个内存池中分配内存,解决多线程竞争带来的无谓消耗;cache_aligned_allocator:通过缓存对齐,避免假共享。

 

9.多线程

 

FRunnable

1. 声明一个继承自FRunnable的类FRunnableTestThread,并实现三个函数:Init、Run和Exit。

2. 借助FRunnableThread的Create方法,第一个参数传入FRunnable对象,第二个参数传入线程的名字。

FRunnableThread::Create(new FRunnableTestThread(0), TEXT("TestThread"));

 

Task Graph任务系统

由于采用的是模板匹配,不需要每个Task继承自一个指定的类FTestTask,只要具有指定的几个函数,就能够让模板编译通过。

GetTaskName:静态函数,返回当前Task的名字

GetStatId:静态函数,返回当前Task的ID记录类型,可以借助RETURN_QUICK_DECLARE_CYCLE_STAT宏快速定义一个并返回。

GetDesiredThread:指定Task在哪个线程执行

GetSubsequentsMode:用来进行依赖检查的前置标记

DoTask:最重要的函数,Task的执行代码

 

TGraphTask::CreateTask(NULL, ENamedThreads::GameThread). ConstructAndDispatchWhenReady(0);

 

Std::Thread

C++11的特性

 

 

10.UObject

对象初始化分为:内存分配和对象构造阶段。

《大象无形:虚幻引擎程序设计浅析》_第1张图片

 

(1).内存分配阶段:

获取当前UObject对象对应的UClass类的信息,根据类成员变量的总大小,加上内存对齐,然后在内存中分配一块合适的区域存放。

(2).对象构造阶段:

获取FObjectInitializer对象持有的、指向刚刚构造出来的UObject指针,调用以FObjectInitializer为参数的构造函数(ClassConstructor),完成对象构造。

 

11.序列化

虚幻引擎序列化每个继承自UClass类的默认值,然后序列化对象与类默认对象的差异。节约了大量子类对象序列化后的存储空间。

反序列化:先实例化对应类的对象,然后还原原始对象数据

如果成员变量没有被UPROPERTY标记,不会被序列化。

如果成员变量与默认值一致,也不会进行序列化。

 

12.垃圾回收

(1).垃圾回收算法

引用计数法

给每个东西保持一个引用计数。用时加1,不用减1。一旦减为0,进行回收。

优点:

引用计数不用暂停,是逐渐完成的。将垃圾回收的过程分配到运行的过程中。

缺点:

指针操作开销,每次使用都要调整引用计数,频繁使用的物品,频繁修改计数是很大的一笔开销。

环形引用,互相引用的对象,锅与锅盖配套,互相引用,导致两者引用计数都是1。但是实际上需要把锅和锅盖一起垃圾回收。

 

标记-清扫算法

是追踪式GC的一种。追踪式引用计数算法会寻找整个对象引用网络,从应用程序的root出发,利用相互引用关系,遍历其在Heap(堆)上动态分配的所有对象,没有被引用的对象不被标记,即成为垃圾;存活的对象被标记,即维护成了一张“根-对象可达图”。

优点:

没有环形引用问题,即使锅盖和锅互相引用,也可以垃圾回收。

缺点:

必须暂停,执行完垃圾回收算法后,才能继续做其他事情,导致系统有延迟。

如果只是丢掉垃圾而不整理,就会导致可用空间越来越细碎,最终导致大对象无法被分配。

(2).整理内存:

C#中,启用Compact算法,对内存中存活的对象进行移动,修改它们的指针,使之在内存中连续,这样空闲的内存也就连续了,这就解决了内存碎片问题,当再次为新对象分配内存时,CLR不必在充满碎片的内存中寻找适合新对象的内存空间,所以分配速度会大大提高。但是大对象(large object heap)除外,GC不会移动一个内存中巨无霸,因为它知道现在的CPU不便宜。通常,大对象具有很长的生存期,当一个大对象在.NET托管堆中产生时,它被分配在堆的一个特殊部分中,移动大对象所带来的开销超过了整理这部分堆所能提高的性能。

Compact算法除了会提高再次分配内存的速度,如果新分配的对象在堆中位置很紧凑的话,高速缓存的性能将会得到提高,因为一起分配的对象经常被一起使用(程序的局部性原理),所以为程序提供一段连续空白的内存空间是很重要的。

 

(3).虚幻引擎的智能指针系统

采用引用计数算法,使用弱指针方案来(部分)解决环形引用问题。

问题:往往忘记去判断哪些是强指针,哪些是弱指针,从而导致内存释放的问题。

tips:智能指针系统管理非UObject对象。

 

《大象无形:虚幻引擎程序设计浅析》_第2张图片

《大象无形:虚幻引擎程序设计浅析》_第3张图片

 

UObject的标记清扫算法

UClass包含了类的成员变量信息,类的成员变量包含了“是否是指向对象的指针”,因此具备选择精确式GC的客观条件。利用反射系统,完成对每一个被引用的对象的定位。故采用追踪式GC。

虚幻在回收过程中,没有搬迁对象,应该是考虑到对象搬迁过程中修正指针的庞大成本。

选择了一个非实时但是渐进式的垃圾回收算法,将垃圾回收的过程分步、并行化,以削弱选择追踪式GC带来的暂停等消耗。

 

虚幻引擎的GC

是追踪式、非实时、精确式,非渐近、增量回收(时间片)。

 

垃圾回收函数 CollectGarbage

  1. 锁定
  2. 回收:标记和清除
  3. 解锁

 

虚幻的GC入口是CollectGarbage()

 
  1. COREUOBJECT_API void CollectGarbage(EObjectFlags KeepFlags, bool bPerformFullPurge = true);

  2. void CollectGarbage(EObjectFlags KeepFlags, bool bPerformFullPurge)

  3. {

  4. // No other thread may be performing UOBject operations while we're running

  5. GGarbageCollectionGuardCritical.GCLock();

  6.  
  7. // Perform actual garbage collection

  8. CollectGarbageInternal(KeepFlags, bPerformFullPurge);

  9.  
  10. // Other threads are free to use UObjects

  11. GGarbageCollectionGuardCritical.GCUnlock();

  12. }

锁定/解锁

借助GGarbageCollectionGuardCritical.GCLock/GCUnLock函数,在垃圾回收期间,其他线程的任何UObject操作都不会工作,从而避免出现一边回收一边操作导致各种问题。

回收

回收过程对应函数CollectGarbageInternal中的FRealtimeGC::PerfomReachablilityAnanlysis函数,可以看做两个步骤:标记和清除。不过,增加了簇和增量清除,簇是为了提高回收效率,增量清除是为了避免垃圾回收时导致的卡顿。

 

标记过程:全部标记为不可达,然后遍历对象引用网络来标记可达对象。

清除过程:直接检查标记,对没有被标记可达的对象调用ConditionalBeginDestroy函数来请求删除。

 

标记过程的实现原理:

全部标记为不可达:虚幻引擎的MarkObjectsAsUnreachable函数就是用来标记不可达的。借助FRawObjectIterator遍历所有的Object,然后设置标记为Unreachable即可。

MarkObjectsAsUnreachable(ObjectsToSerialize, KeepFlags);

遍历对象引用网络来标记可达对象:

 
  1. FGCReferenceProcessor ReferenceProcessor;

  2. TFastReferenceCollector

  3. ReferenceCollector(ReferenceProcessor, FGCArrayPool::Get());

  4. ReferenceCollector.CollectReferences(ObjectsToSerialize, bForceSingleThreaded);

 

这里有几个重要的对象TFastReferenceCollector、FGCReferenceProcessor、以及FGCCollector,分别介绍一下。

 

TFastReferenceCollector:用于可达性分析。

如果是单线程就调用ProcessObjectArray()函数,遍历UObject的记号流(token stream)来查找存在的引用,如果没有记号流,调用UClass::AssembleReferenceTokenStream()函数就是用生成记号流(token steam,其实就是记录了什么地方有UObject引用),用CLASS_TokenStreamAssembled来保存。

如果是多线程,创建几个FCollectorTask来处理,最终还是调用ProcessObjectArray()函数来处理。

 

UClass::AssembleReferenceTokenStream()函数

如果没有创建token stream,那么就会遍历当前UClass的所有UProperty,对每个UProperty调用EmitReferenceInfo()函数,这是一个虚函数,如果它有父类,那么就会调用父类的AssembleReferenceTokenStream()函数,并把父类添加到数组的前面,最后加上GCRT_EndOfStream到记号流中,并设置CLASS_TokenStreamAssembled来保存。

 

FGCReferenceProcessor

处理由TFastReferenceCollector查找得到的UObject引用。

如果Object->IsPendingKill()的返回值为true且允许引用消除,那么把Object的引用设置为NULL

否则,调用ThisThreadAtomicallyClearedRFUnreachable()清除不可达标记,标记为可达,如果这个UObject是簇的根,调用MarkReferencedClustersAsReachable函数,把当前簇引用的其他簇标记为可达,当这个UObject簇根不可达,整个簇都会被回收。

 

基于簇的垃圾回收

其中跟Cluster相关的几个函数在UObjectBaseUtility中,如下图所示: 

《大象无形:虚幻引擎程序设计浅析》_第4张图片

 

用于加速Cook后的对象的回收,所以编辑器下不会使用簇来GC。能够作为簇根的为UMaterial和UParticleSystem,基本上所有的类都可以在簇中。当垃圾回收阶段检查到一个簇根不可达,整个簇都会被回收,加速回收的效率,节省了再去处理簇的子对象的时间。

 

FGCCollector

继承自FReferenceCollector,HandleObjectReference()和HandleObjectReferences()都调用了FGCReferenceProcessor的HandleObjectReference()方法来进行UObject的可达性分析。

 

清除过程的实现原理:

为了减少卡顿,虚幻增加了增量清除的概念(IncrementalPurgeGarbage()函数),就是一次删除只占用固定的时间片,一段段进行销毁的触发。

  • 需要注意的是,由于会在两次清除时间内产生新的UObject,故在每次进入清除时,需要检查GObjCurrentPurgeObjectIndexNeedsReset,如果为true,那么重新创建一个FRawObjectIterator用于遍历所有的UObject。
  • 通过GObjCurrentPurgeObjectIndex来循环遍历所有的FUObjectItem(记录了UObject相关的信息,比如ClusterIndex,Flags等),如果对象不可达且IsReadyForFinishDestroy()为true,那么我们就调用ConditionalFinishDestroy();
  • 而如果IsReadyForFinishDestroy()为false,那么把它添加到GGCObjectPendingDestruction中去。待到下一次增量清除时,如果GGCObjectPendingDestructionCount不为0且IsReadyForFinishDestroy()为true,那么我们就调用ConditionalFinishDestroy()。
  • 当然如果有时间限制,到了时间限制,也会退出。


 

你可能感兴趣的:(UE4)