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中的内存管理

没错,除了地板,一个玩家起始地,以及天空以外什么都没有。那么那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<uanimsequence> Itr; Itr; ++Itr){
 
            FString animName = Itr->GetName();
 
            int64 exclusiveResSize = Itr->GetResourceSize(EResourceSizeMode::Exclusive);
 
} </uanimsequence>


UE4中的内存管理

可视引用查看器

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

obj refs name=AS_HorseCart_idle

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

static ConstructorHelpers::FObjectFinder<UMaterial>

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

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

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

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

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

TSubclassOf<UAnimInstance> mAnimBPClass01;

TSubclassOf<UAnimInstance> mAnimBPClass02;

TSubclassOf<UAnimInstance> mAnimBPClass03;

以上各行应变成:

TAssetSubclassOf<UAnimInstance> mAnimBPClass01;

TAssetSubclassOf <UAnimInstance> mAnimBPClass02;

TAssetSubclassOf <UAnimInstance> 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中的内存管理

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

UE4中的内存管理

 

纹理Mip偏离(bias) 

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

UE4中的内存管理

UE4中的内存管理

 

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

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

  • 3
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
    游戏引擎之所以要做内存管理,一个是加快内存分配速度,另一个就是处理内存泄漏问题。     1.先简单说处理内存泄漏这个问题,一般的引擎在debug 模式下 都有一个记录内存分配的结构体,每分配一段内存就记录这段内存的信息,包括大小,分配时间,是否是数组,前后越界的标记等等吧,其实这些都不是那么重要,因为你只知道这些,一旦泄漏出现,你虽然知道泄漏,但无法定位。相反如果你知道堆栈的调用信息,就能准确定位。我以前的实现,在debug下,只记录当前调用new的时候的行号和文件,也就是内部的__FILE__ __LINE___.。我看了同事那个能记录堆栈调用过程,简直觉得很牛逼(其实不是调用堆栈,只是打印出调用过程,继续往下看你就知道了),以前自己也想过,但不知道怎么去实现。如果U3里面也加入这个功能,那就更牛叉了。思想很简单,就是核心东西在一个函数,这个是系统函数,提供当前这行指令所在的地址,它会打印出来这行指令的文件名和行号。先详细说下数据在内存的分配     最早的计算机数据段和代码段区分的很严格,现在似乎没有这么严格了!对于全局变量和静态变量它的分配完全在数据段分配,知道运行结束才会收回内存!而对于自动变量(包括函数参数和函数定义的变量)则在堆栈分配!一般的分配情形是这样的:从栈下到栈顶依次是函数参数,函数结束后下一条指令的地址,寄存器保护,然后是函数定义的变量!
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值