诚如之前所说,虚幻4基本的一些特性都是由UObject穿针引线在一起的,想把虚幻玩到比較深的程度。UObject是迟早要面对、回避不得的问题,所以,准备在其他主题之前,先把UObject好好弄一下。UObject主要完毕了哪些工作呢?私以为:
反射系统
UObject体系构建了整个虚幻反射系统的核心。每一个UObject都来自于一个UClass,这个Class能够是Unreal Header Tool(以后统一遵循官网命名:UHT)生成的,也能够是来自于Blueprint生成的(UBlueprintGeneratedClass)。反射能够说是如今主流引擎的构建基础。对国内多数人而言,可能更熟悉的是Unity透过Mono构建出来的反射,它的重要性不言而喻。
反射非常大一坨的东西,详细就不说了,它最大的作用,相当于在执行时动态生成代码,能够省掉非常多手写代码的工作量。否则像UE这样复杂的界面。所有Hardcode。100人是绝对不够的。改一次所需的时间也是无法接受的。有了反射之后,剩下的非常多就是非常好理解的一条路就顺下来了:属性编辑器自己主动生成、自己主动消息包收发、自己主动序列化、自己主动生成BP节点、BP和C++的自己主动接口交互、自己主动浅拷贝深拷贝、甚至依照设定规则来进行拷贝……不胜枚举。
共通性都是一样:Get Class。Get Property,或者Get Function,分析Property和Function的属性。然后,设值、获取值、Invoke函数……
垃圾回收和生命期管理
UObject构建了虚幻的垃圾回收(GC)系统。GC这东西众说纷纭。但博主本人持乐观态度。
近期的公司业务就遇到这么个事儿:脚本里须要发动态包,于是就须要在脚本里手动生成一个动态包。并挂接在包上面。
为了完毕这个目的,我就必须在脚本中制作一个生成动态包的节点。然后问题来了。我们必须还得要一个回收动态包的节点,否则这个动态包就无法回收……于是最后放弃了,回到了包里加各种Reserved的老路上去……有了GC,多数情况下都不用管这事儿……你让一个策划去理解回收这样的事情,就是在给他们添麻烦,那本不是他们业务内的范畴。
GC的存在价值,并不是是让事情变得简单这么简单,很多其它时候它能让你节省下非常多编程心力,把精力花在真正该关注的地方。
真GC出问题时去查错所带来的成本。未必比忘写delete带来的成本要大,说不定反而更小。
虚幻的垃圾回收系统,基本上就是从Root開始。不断遍历全部的Property。标记其为使用中。最后再遍历一遍。确认哪些Object没有标使用中就给它删掉。基本上,你不须要管这个过程。由于反射的作用,所以相关的信息都是UE自己主动就帮我们处理好的。有几个要注意的:
"Singleton"。须要一直存在的,直接AddToRoot。
F类本身是不走垃圾回收的,可是F 类内部又有U类。这样的情况下你须要注意AddReferencedObjects。
把F类内部的U类给增加到GC树上。
Classes里的类,标UPROPERTY的UObject属性会被自己主动增加到当前类的GC列表里,但不标的会不会。没有详细跟。反正习惯随手写个即可了。
TArray和TMap里面的UObject会被自己主动增加GC列表,可是假设写的是std::vector和std::map,则应该是不会的。须要手动用AddReferencedObjects加进去。
资源管理
UObject最后一个作用是构建了虚幻的资源管理体系。包含资源的搜索、资源之间的引用管理。以下具体展开。
首先要先说一下虚幻的Object命名,因为资源也都是UObject,所以其命名与UObject是同一个标准。依照如今的要求,是[类名']路径名/路径名/Asset名.[包内路径.]Object本名:[属性名]['](通常是Object所在类名+一个数字后缀)。
比方:
Brush'/Script/Engine.Default__Brush'
BillboardComponent'/Script/Engine.Default__TextRenderActor:Sprite'
/Engine/TemplateResources/MI_Template_BaseGray_03_Metal.MI_Template_BaseGray_03_Metal
这个名称解析跟UE3和UDK略有不同,UE3因为基于upk来对包进行管理,而又限制Content文件夹下的Upk包不能重名。所以不须要前面的路径名。UE4基于Asset,Content不同子文件夹下能够有同名UAsset,所以路径名就是不可或缺的了。除此之外。Asset跟UPK没有太多不同。我们后面说包。也是指的UAsset,尽管看起来这个不像包。
理论上,全部的UObject都能够交由StaticLoadObject来载入(其实也确实是这么做的),可是非常多类是有基于LoadObject的特殊实现的,比方UClass(Blueprint Class),就必须用StaticLoadClass,而地图必须使用LoadMap,LevelStreaming这样地图相关的载入流程。这些变种的主要差别是会针对对应的情况做一些特殊的处理和操作。
可是核心都绕不开StaticLoadObject。
所以搞明确这个StaticLoadObject,实际上就搞明确了虚幻基本的资源组织结构。
基本的流程例如以下:
解析路径,找到相应包(UAsset或者UPK)。假设还没载入则载入包。
推断Object是否已经载入,假设已经载入则直接返回。
对资源包的载入。会把整个资源包的所有Object所有预载入的(创建并调用PreLoad。对于资源等须要PostLoad的调用PostLoad)。同一时候,载入包时创建ULinkerLoad,这个LinkerLoad会自己主动分析每一个包与其他包之间的关联,通过Imports来记录本包对其他包的引用。通过Exports记录本包内的Object。
载入后,看看目标对象是不是个Redirector。假设是Redirector,则说明"以前有个包在这里,可是被移动到新地方去了",就重定位到必要的地方。
说了这么多,事实上你明确原理就非常easy:虚幻全部对象都是依照一个包括了路径、包内路径、对象类名的唯一名称来命名的。而虚幻全部的包都是会记录对其他包的引用的。
所以一旦一个包的路径、对象的包内路径、以及对象类名本身发生变化,都可能会导致旧有资源的丢失和重定向。
当然。相关也都有一些机制能够帮助你事后修正(比方Redirector,Engine.ini里的Redirector config),可是,那都是补救措施。不能100%保证成功补救。
好的情况。又一次定位一下资源引用什么的就能够解决,可是最糟糕的情况下,有可能会导致数据丢失(当中最easy发生的就是由于BP类名改动,导致子BP类无法找到父BP类而导致子类无法正常使用仅仅能删了重来……)
所以。在做UE4的包路径转移、资源名改动之前,一定要做好备份工作。最好是把全部原型迭代完成后。统一进行类似操作,并常常存档或发SVN、GIT。
相关的注意事项:
Redirector须要提醒一点。虚幻里进行资源文件从一个目录到还有一个目录的移动,一定要在编辑器中进行。由于虚幻资源之间的引用关系是通过前面说的Object命名来保证的。而路径名又是Object的一部分,名称不正确等非常easy发生故障。而编辑器移动资源后,有时候会发现移动前所在的路径下多了一个1KB字节左右小尾巴,这个小尾巴就是Redirector。相同不要手动删除,而是要在资源查看器里,通过对Redirector(须要Filter开启)的Fix up命令来进行删除。
先把Redirector打开
然后Fixup,或者
右键直接目录,Fix up Redirectors in Folder。
因为包是"Link Load"的,载入过程中会分析引用,所以假设包比較碎,这里就会因为做了很多其它的文件訪问而导致速度变慢。单假设单个包内数据量较大。则也会导致载入单个包时速度变慢的情况。归根究竟。就是权衡啊权衡。
(一般说来。10个1k > 1个10k)
Transient对象不会被存盘。Transient包(GetTransientPackage)是一个特殊的包,全部暂时对象都应该创建在这个包里面。
异步实际并不是真正异步。
LoadObject载入过程中有一系列的全局变量,并且这些全局变量维护时没有不论什么锁,所以也无法真正做到异步载入。所以尽管您看到接口上有LoadPackageAsync,但那个的实现是在主线程每帧区分时间片来实现的。只是话说回来,多线程读包真的有必要吗?机械硬盘的訪问速度本身是最大的限制因素,读包过程中的多线程,CPU事实上帮不上不论什么忙。
真正大量磁盘或网络数据的异步载入能够參考Texture Streaming(为何仅仅有Texture做了Streaming就是由于这玩意儿如今是游戏最吃资源的了,十个模型的资源量不见得比得上一张贴图啊),先把Object和少量基本信息当作占位符载入进来。Object的实际数据则放到其它线程里慢慢载入。
假设您有类似需求,能够考虑这个方法。
只是感觉是不是必需,比方我们游戏经常使用的角色异步载入什么的,事实上走主线程时间片全然够了。
编辑器中的资源会被标记为Standalone。在无引用时仍然存在。其他另一系列不会被GC的情况。须要注意。