UE4中的内存管理

http://gad.qq.com/program/translateview/7182050

大家好!

呃,我有一段时间没有更新博客了;我和Vincent以及同事程序员Marc-Andre一直在Frima忙着更新我们的Oculus Connect 2演示。现在我们的游戏中有了人物的面部动画以及强大的配音,将其提高到了一个崭新的层次。Polygon上还有一篇关于FATED的文章,一切开始变得令人激动了!点这里阅读文章。

由于项目中的程序员只有我们两个人,而且我们俩基本上需要负责游戏的所有技术方面,有些时候事情会变得凌乱。FATED的内存管理就是个例子。因为我们俩都是虚幻4的新手,我们没有花时间研究什么东西被载入到内存中以及它们是什么时候被载入的,因此犯了许多关键性的错误。 

当我们匆忙将OC2演示制作完成后,载入演示场景差不多花了75秒(!)。这对于任何游戏来说都是不正常的。是时候该仔细研究一下到底什么东西需要花这么长时间载入,也许可以将其中一些改为异步(asynchronous)载入。所以虽然这篇文章对于虚幻引擎的老手来说可能有些小儿科,但是我觉得对于技术欠佳的开发者会很有帮助。 

问题的根源究竟是什么? 

首先找出问题的症结所在。在虚幻引擎中你可以炫酷地使用指令从各个方面审查你的游戏,当然内存统计也不例外。最有用的指令之一就是“memreport-full”。“full”参数会给出更多有用的但不是必须的信息。运行这条指令会在你的“Saved\Profiling\MemReports”项目文件夹中创建一个.memreport文件。这是一个自定义的文件拓展名,但实际上它就是一个文本文件,用来表示游戏的当前内存状态。

也许你想要先看一下对象列表,它列出了所有载入的UObject以及它们占据的内存空间大小。对于每种对象你都可以看到类似于以下的内容: 

AnimeSequence       36        16628K           16634K           7578K       7578K

Texture2D                 150            212K         219K         260996K         96215K

AnimSequence(或者Texture2D)是对象的类名,而它旁边的数字(36或150)是该类的实例数量。需要特别关注的是后四个数字中的第一个和最后一个。第一个是对象的实际大小而最后一个是这些对象直接引用的专有资源(asset)的大小。例如,作为存放纹理元数据(212K)的Texture2D对象没有占用太多的内存,而它专门引用的实际纹理数据(意味着没有其他对象引用那个数据)大小则为96215K。

以上两行数据是从这个场景的memreport中抽取出来的:

UE4中的内存管理_第1张图片

没错,除了地板,一个玩家起始地,以及天空以外什么都没有。那么那36个动画和150个纹理都来自于何处呢?

寻找资源引用 

由于我知道这里很明显出现了一些错误,我必须找出都有哪些资源被载入了。内存中的确有36个动画序列,但是它们是什么且为什么存在呢?再一次,UE4可以简单地告诉你答案。

memreport指令实际上是由一系列的其它指令组成的,这些指令被依次处理形成一个游戏“完整的”报告。其中一条指令是“obj list(对象列表)”。它列出了所有的对象,以及实例的数量,但是这个指令还可以使用一些参数,其中一个是对象的类。

objlist Class=AnimSequence 

这会输出当前内存内所有的动画序列,按占用内存的大小排序(降序)。像这样:

AnimSequence/Game/Character/Horse/Animation/AS_HorseCart_idle.AS_HorseCart_idle 

491K   491K   210K   210K

现在,如果你和我一样,也许你会想使它看上去整洁些,幸运的是你可以很容易地按自己的需求设计Exec函数来准确地输出你想要的内容。例如,它可以按专用资源的大小而不是对象的大小进行排序。以下代码会帮助你更好地了解该怎样做: 

1
2
3
4
5
6
7
8
9
for (TObjectIterator Itr; Itr; ++Itr){
 
            FString animName = Itr->GetName();
 
            int64 exclusiveResSize = Itr->GetResourceSize(EResourceSizeMode::Exclusive);
 
}


UE4中的内存管理_第2张图片

可视引用查看器

现在我可以准确地看到被载入的对象,因此能够更加轻松地找出那个看似空荡的空间内什么东西引用了它们。在虚幻引擎中有两种方法可以查看对象的引用。首先,在编辑器中有一个可视引用查看器(意味着你能看到以上图片);这会显示所有潜在的引用,并不一定是当前实际载入到内存中的资源。当然,还有一种简单的方法可以使用另一个控制台指令得到当前最短的引用。

obj refs name=AS_HorseCart_idle

这会输出被载入内存中(会花一些时间)的内容的一个引用链,通常沿着这条链我们会得到问题的症结所在。在我们的情况中,我们犯了一个错误因此产生了许多这样不严谨的资源引用:我们在一些本地类的构造器中直接引用了资源。例如像这样:

static ConstructorHelpers::FObjectFinder

TextMaterial(TEXT(“Material’/Game/UI/Materials/UITextMat.UITextMat'”));

在以上例子中,这是一个对于材料的直接引用:改材料会一直出现在内存中。其实仔细想想挺有道理的,因为构造器不仅仅会在一个类的实例被创建时被调用,还会在类的静态版本被创建时被用来设置默认性质。所以避免这些你不总需要的东西出现在内存中!相反,我们加入一个可以在该类的蓝图版本中被分配的UPROPERTY。这样如果场景中没有那个对象的实例被载入,那么它就不会出现在内存中。

使用可流化管理器(Streamable Manager)的异步载入

资源的错误引用并不是演示载入时间过长的唯一原因,所以我们还需做一番研究工作。我们还剩下一些临时的资源,一些非常大的纹理(我将在下面具体讲解)但是更重要的是我们仍然有很多要载入的内容而且我们打算将它们一次性载入。让我们看看如何使用被虚幻引擎称为可流化管理器的东西异步载入资源。

比如说,如果你有一个引用多个动画蓝图的actor,那么当你实例化该actor时所有的动画蓝图都会被载入,即使你一次只使用一个。

TSubclassOf mAnimBPClass01;

TSubclassOf mAnimBPClass02;

TSubclassOf mAnimBPClass03;

以上各行应变成:

TAssetSubclassOf mAnimBPClass01;

TAssetSubclassOf mAnimBPClass02;

TAssetSubclassOf mAnimBPClass03; 

完成这个改动后,mAnimBPClass01引用的动画蓝图只会在你指定要求它载入时才会被载入。这样做很简单:你需要使用FStreamableManager对象。只需记得将它声明为总会存在于内存之中,不会被删除(比如就像你游戏的GameInstance)。在我的情况中,我为它专门使用了一个特殊的“管理器”对象,会在游戏的开始被创建而永远不会被删除。它处理我们游戏中所有动态载入的内容。 

UPROPERTY()

FStreamableManager mStreamableManager;

有不止一种方法可以异步载入一项资源,这是其中一种:将mArrayOfAssetToLoad作为FStringAssetReference的一个TArray。 

mStreamableManager.RequestAsyncLoad(mArrayOfAssetToLoad,

FStreamableDelegate::CreateUObject(this,&UStreamingManager::LoadAssetDone));

在虚幻引擎的内部文件系统中,FStringAssetReference代表了资源完整路径的字符串。使用一个TAssetSubclassOf<>指针你可以通过对其调用ToStringReference()得到它。

另外,如果你使用Wwise管理你的音频,Audio Banks会需要很长的载入时间。幸运的是,英明地(懂了吗?)对库进行细分并对UAkAudioBank使用LoadAsync()代替Load()可以解决这个问题。记得在编辑器中不要勾选声音库的AutoLoad选项!另外,出于某种原因LoadAsync()函数没有被暴露在蓝图中,所以你需要在本地代码中暴露它或者在蓝图中手动暴露它。 

关卡构成器(Level Composition)

异步载入资源是需要做的一件事,但是也许你还希望将你的关卡分为不同的部分分别载入。我们可以使用虚幻引擎中的“关卡构成器”功能。过去关卡的载入是在引擎的主要游戏线程上完成的,但是现在可以使用不同的线程分别载入。

在你项目中的DefaultEngine.ini文件里添加以下内容:

[Core.System]

AsyncLoadingThreadEnabled=True

我们使用的仍是虚幻4.82版本,但是根据我的调查在4.9中这可能已经是默认的了。无论怎样,这应该使得关卡的异步载入更加平滑。但是如果你启用了这个功能,你需要注意类构造器中的内容,因为某些操作并不是线程安全的,使用它们也许会导致锁死甚至游戏的崩溃。

UE4中的内存管理_第3张图片

流关卡功能可以使用体积或者一个简单的距离参数“自动”完成,但是在我们的情况中我们决定手动进行。在以上图片中,取消勾选Streaming Distance将允许所有连接到该层的子关卡被手动载入。这可以在蓝图中使用Load Stream Level或者在C++中使用UGameplayStatics:::LoadStreamLevel()来完成。

UE4中的内存管理_第4张图片

 

纹理Mip偏离(bias) 

几乎在所有的游戏中,纹理最终会占用许多空间。几乎在所有游戏中,另有一些过大的纹理远超出最佳的像素。幸运的是,相对于重新导入每个纹理,UE4提供了一个简单的方法不需要重新导入任何内容以除去多余的部分:LOD偏离。

UE4中的内存管理_第5张图片

UE4中的内存管理_第6张图片

 

你可以看到当我们降低一个mip时资源大小的差别是非常显著的,尤其当纹理为4096x4096时!当然,我们不能对所有纹理都这样做,但是的确有很多纹理不需要4096那么大。

当然内存优化和内存管理还有很多内容,但是基本上这些是我们将演示的载入时间从75+秒降到10秒左右所做的全部工作了。虚幻引擎是一个很棒的工具,我一直在学习它并不断进步。我希望这篇文章可以帮助那些正在创作优秀作品的人。如果你有任何问题或者评论的话,我将很高兴回答它们!同时,我还会回到FATED的努力工作中。更多信息敬请期待!

你可能感兴趣的:(game)