开启辅助访问
 找回密码
 注册帐号

扫一扫,访问微社区

教程分享

关注:556

当前位置:游戏蛮牛 技术专区 教程分享

查看: 302|回复: 3

[基础知识] 《InsideUE4》UObject(一)开篇

[复制链接]  [移动端链接]
7日久生情
3207/5000
排名
2610
昨日变化

1246

主题

1247

帖子

3207

积分

Rank: 7Rank: 7Rank: 7Rank: 7

UID
132510
好友
17
蛮牛币
19530
威望
0
注册时间
2015-12-24
在线时间
241 小时
最后登录
2017-1-17
发表于 2016-12-12 16:33:18 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有帐号?注册帐号

x
本帖最后由 BobbyKim 于 2016-12-12 16:31 编辑

引言
在上个GamePlay专题,谈到UE创建游戏世界的时候(GamePlay架构(一)Actor和Component),简单的介绍了一下UObject的功能:

UObjectAndActor.jpg


藉着UObject提供的元数据、反射生成、GC垃圾回收、序列化、编辑器可见、Class Default Object等,UE可以构建一个Object运行的世界。(后续会有一个大长篇深挖UObject)

那么从本专题开始,我们将开始慢慢的填这个大坑。正所谓,千里之堤溃于蚁穴,万丈高塔始于垒土。在阅读分析游戏引擎源码的时候,又或者是想要扩展引擎功能,如果对于引擎底层对象创建的机制不太清楚,则常常会有点力不从心,因为功能模块的运行机制、数据流程、资源的加载释放时机,往往也都是依赖于对象的生命周期。而如果想要实现一个自己的游戏引擎,从一开始,也都得设计一个完善的对象管理机制,然后慢慢的在其上叠加功能,这个对象模型设计实现的如何,往往决定了一款引擎的基因。

愿景目标
本专题的内容主要是用于提升UE开发者,或者游戏引擎研究爱好者的内功,所以并不适合所有读者。在此需要明示本专题后续篇章(大概10~20篇)的目标愿景,以后不再赘述。
  • 专注于UE的UObject及其周边的系统,在介绍构建对象系统的时候,虽然会涉及到引擎的启动流程,但是并不详细介绍(留待后续专题探讨)。本专题的重点是介绍UE4中的UObject系统,理论上,读完该篇章,你应该可以心里比较有数的在别的引擎里也实现出一套类似的系统。
  • 本专题的内容对于你使用蓝图实现游戏逻辑几乎“无用”,对正常的表层C++开发也基本上提升不大。本专题只是让你安心一些而已。
  • 我,先声明,本专题为了详细的讨论机制,在91.233%的情况下我会显得比较罗嗦,会引用罗列大量的引擎源码。如同开篇所说,毕竟是在探讨最最底层的的源码,再粗旷的泛泛而谈也没啥意思。

前置知识
本专题的学习探讨门槛比较高,在开始本专题之前,需要你先掌握以下知识:
  • 优秀的C++语言能力。虽然不需要你“精通”C++,但是需要你熟练掌握C++11、模板、宏、对象内存模型和基本的各种规则机制。
  • 本专题也涉及到对象反射,所以也需要你对编译原理、元数据、程序结构有一定的了解。如果对其他编程语言(java,c#)的反射熟悉的话,对理解本专题也是一大助力。
  • 对多线程编程比较熟悉,知道并掌握各种线程同步机制。UE用多线程并行优化了很多任务,其中就包括GC,所以不可避免的需要你掌握相关的知识。
  • 对C++内存分配管理也比较熟悉,知道一些STL基本的内存分配器,也知道各种内存分配管理的意义和技术,如引用技术,GC等。
为了更好的理解本专题内容,有必要的时候,我也会在每一个篇章开头简单介绍一下C++的一些知识要点。不过还是需要你自己去搜索查阅学习其他更系统的C++资料。

对象模型
先问一个问题,为什么需要有一个统一的基类:Object?
甚至,我们在编程语言中也常常见到这种模式,比如Java中的object、C#的object,甚至一些纯对象的脚本语言(Ruby里连数字123都是对象)。刚接触UE的人,看到UE里的Object,可能觉得这没什么,好像就自然而然应该有应该是这样,但是做过游戏引擎的人就知道,这里面蕴含了很多设计思想和权衡。
大部分的游戏引擎底层都是C++,而C++作为一个下接操作系统硬件底层,上接用户逻辑的编程语言,为了适应各种环境,不为你不需要的东西付代价,C++是并没有提供原生GC的。STL库的那些智能指针更多只是在C++的语言层面上再提供一些小辅助。在最开始设计游戏引擎的时候,你不光要考虑该引擎所面对的用户群体和针对的游戏重点,更要开始考虑你所能利用到的都有什么内存管理方式。简单说一下其他游戏引擎在这方便的情况:

  • Cocos系列,最早是cocos-iphone扎根于objective-c,所以用的引用技术,后来有x了,为了照顾老用户的使用习惯,几乎是机械翻译了objective-c的内存管理机制,搞出了一个CCObject,里面只有Retain/Release,因为还是太过粗糙,也常常还是出现各种内存泄漏问题,用的时候也是得处处小心,漏掉一个就敢泄漏给你看,而且追查起来非常困难。没看人家objective-c自己后来都搞了一个ARC来减轻大家的工作了嘛。
  • Unity底层源码我没看过不得而知,不过上层脚本C#是基于Mono的已经实现完善的语言原生管理机制,对游戏对象的内存管理倒也确实是省心了非常多,缺点也是如果想要更精细的控制的时候就有点力不从心。
  • 其他引擎,用的还是C++提供了的那些,顶多自己再定制一些管理辅助类。比如KlayGE就是利用了boost的智能指针,CryEngine用的也是智能指针,我的Medusa引擎也是比较简单的采用的C++智能指针的方案。对象的分配释放也往往需要用户手动管理控制。
  • 在这就不得不提到Qt里的QObject,Qt虽然是处于跟UE不同的GUI框架领域,但是设计的思想却有些殊途同归,Qt里根据QObject基类构建出来的ObjectModel为UI的复杂构建和通信提供了许多了非常便利急需的功能。比如信号和槽的设计就常常让人们津津乐道,而且在编辑中也可以非常方便的查看对象的属性。Qt的流行,QObject应该功不可没。
  • UE的Object系统无疑是最强大的。实际上UE能实践出这么一套UObject是非常非常了不起的,更何况还有GC和HotReload的黑科技。在大型游戏引擎的领域尝试引入一整套UObject系统,对于整个业界也都是有非常大的启发。

那么引入一个Object的根基类设计到底有什么深远的影响,我们又付出了什么代价?

得到:
  • 万物可追踪。有了一个统一基类Object,我们就可以根据一个object类型指针追踪到所有的派生对象。如果愿意,我们都可以把当前的所有对象都遍历出来。按照纯面向对象的思想,万物皆是对象,所以有一个基类Object会大大方便管理。如果再加上一些机制,我们甚至可以把系统中的所有对象的引用图给展示出来。
  • 通用的属性和接口。得益于继承机制,我们可以在object里加上我们想应用于所有对象的属性和接口,包括但不限于:Equals、Clone、GetHashCode、ToString、GetName、GetMetaData等等。代码只要写一遍,所有的对象就都可以应用上了。
  • 统一的内存分配释放。实际上x里的CCObject的目的就是如此,可惜就是实现得不够好而已。用引用计数方案的话,你可以在Object上添加Retain+1/Release-1的接口;用GC的方案,你也有了一个统一Object可以引用,所以这也是为何几乎所有支持GC的语言都会设计出来一个Object基类的原因了。
  • 统一的序列化模型。如果想要让系统里的各种类型对象支持序列化,那么你要嘛针对各种类型分别写一套(如protobuf就是用程序生成了序列化代码),要嘛就得利用模板和宏各种标记识别(我自己Medusa引擎里实现的序列化模块Siren就是如此实现的),而如果有了一个Object基类,最差的我们就可以利用上继承机制把统一的序列化代码放到Object里面去。而如果再加上设计良好的反射机制,实现序列化就更加的方便了。
  • 统计功能。比如说我们想统计看看整个程序跑下来,哪种对象分配了最多次,哪种对象分配的时间最长,哪种对象存活的时间最长。等等其他很便利的功能,在有了可追踪和统一接口的基础上,我们也能方便的实现出来。
  • 调试的便利。比如对于一块泄漏了的内存数据,如果是多类型对象,你可能压根没法知道它是哪个对象。但是如果你知道它是Object基类下的一个子类对象,你可以把地址转换为一个Object指针,然后就可以一目了然的查看对象属性了。
  • 为反射提供便利。如果没有一个统一Object,你就很难为各种对象实现GetType接口,否则你就得在每个子类里都定义实现一遍,用宏也只是稍微缓解治标不治本。
  • UI编辑的便利。和编辑器集成的时候,为了让UI的属性面板控件能编辑各种对象。不光需要反射功能的支持,还需要引用一个统一Object指针。否则想象一下如果用一个void* Object,你还得额外添加一个ObjectType枚举用来转换成正确类型的C++对象,而且只能支持特定类型的C++类型对象。

代价:
  • 臃肿的Object。这算是继承的祖传老毛病了,我们越想为所有对象提供额外功能,我们就越会在Object里堆积大量的函数接口和成员属性。久而久之,这个Object身上就挂满了各种代码,可理解性就大大降低。Java和C#里的Object比较简单,看起来只有个位数的接口,那是因为有C++在JVM和CLR的背后默默的干着那些脏活累活,没显示出来给你看而已。而UE在原生的的C++基础上开始搭建这么一套系统,就是如今这么一个重量级的UObject了,大几十个接口,很少有人能全部掌握。
  • 不必要的内存负担。有时候有些属性并不是所有对象都用的到,但是因为不确定,为了所有对象在需要的时候就可以有,你还是不得不放在Object里面。比如说一个最简单的void* UserData,看起来为所有对象附加一个void*数据也挺合理的,用的时候设置取出就好了。但是其实有些类型对象可能一辈子都用不到,用不到的属性,却还占用着内存,就是浪费。所以在一个统一的Object里加数据,就得非常的克制,不然所有的对象都不得不得多一份占用。
  • 多重继承的限制。比如C多重继承于A和B,以前A和B都不是Object的时候还好,虽然大家对C++里的多重继承不太推荐使用,但是基本上也是不会有大的使用问题的。然后现在A和B都继承于Object了,现在让C想多重继承于A和B,就得面临一个尴尬的局面,变成菱形继承了!而甭管用不用得上全部用虚继承显然也是不靠谱的。所以一般有object基类的编程语言,都是直接限制多重继承,改为多重实现接口,避免了数据被继承多份的问题。
  • 类型系统的割裂。除非是像java和C#那样,对用户隐藏整个背后系统,否则用户在面对原生C++类型和Object类型时,就不得不去思考划分对象类型。两套系统在交叉引用、互相加载释放、消息通信、内存分配时采用的机制和规则也是大不一样的。哪些对象应该继承于Object,哪些不用;哪些可以GC,哪些只能用智能指针管理;C++对象里new了Object对象该怎么管理,Object对象里new了C++对象什么时候释放?这些都是强加给用户思考的问题。

著名的沃斯基·索德曾经说过,设计就是权衡的艺术。那些得到的UE已经想要攥在手里了,而那些代价我们也得想办法去尽量降低和规避:
  • 针对太过复杂的Object基类,虽然我常常夸UE的设计优雅卓越,但是我这里要黑一下UE,感觉UE的Object基类已经有点破罐子破摔了,能非常明显的感觉到了进化留下的痕迹,一个UObject你给我分了三层继承:(UObjectBase->UObjectBaseUtility->UObject),关键是头两层你还都没有子类。而Object相关的Flags常常竟然把32位都给占完了也是牛。念在UE提供了那么多的UObject功能模块实现,类声明里大几十个方法我们也只好忍了吧。这一块太过底层,估计也不敢大刀阔斧的整改,只能期待UE5再说了。
  • sizeof(UObject)==56。56个字节相对来说应该还是可以接受,关掉Stat的话还能再少一个指针大小。当然这里并没有考虑到外围Class系统的内存占用,但是光光一个对象基础的数据占用56字节起步的话,我觉得已经非常优秀了。10000个对象是546K,1百万个对象是53M。一方面游戏里的对象其实数量没有那么多,对于百万粒子那种也可以用原生的C++对象优化,另一方面现在各个平台内存也越来越宽裕了,所以这个问题已经解决得在可接受范围内了。
  • 规避多重继承,UE在BP里提供的也是多重继承Interface的方案。在C++层面上,我们只能尽量规避不要多重继承多个UObject子类,实在想要实现功能复用,也可以采用组合的组件模式,或者把共同逻辑写在C++的类型上,比如UE中众多的F开头的类就是如此的功能类。总之这个问题,好在我们可以用方式规避掉。
  • 只能多学习了。没办法,现实就是不完美的。越是设计精巧的系统就越是难以理解。不过一方面UE提倡在BP里实现游戏逻辑,C++充当BP的VM,就可以完全对用户隐藏掉复杂性。另一方面,UE在UObject上也提供了大量的辅助设计,如UCLASS等各种宏的便利,NewObject方便接口,UHT的自动分析生成代码,尽量避免用户直接涉及到UObject的内部细节。所以单从一个使用者的角度来说,如今的状态已经挺友好的了,Object工作的挺好,几乎不需要去操心或者帮它补漏。至于想理解的更深层次的话,就只能靠开发者们更用心的学习了。

权衡的结果大家也都知道了,UE下定雄心选择了开始搭建Object,提供了那么多我们日常使用的功能。我的Medusa引擎也是非常艳羡UE那么多便利的功能,但是让我从头开始去再去搭建一套,限于精力有限,我是不敢去做的。光一个GC就得有大量的算法权衡,多线程处理的各种细节和各种优化,更何况再融合了反射、序列化、CDO、统计,想实现得既优雅又性能优越就真的是一件非常不容易的事,代码写完之后还得需要大量的测试和修复才能慢慢稳定下来能用。信任感的建立是很难的,一旦出现对象被释放掉了或者没有释放,你第一时间怀疑的应该是你的使用有问题,而如果UE给你的印象是怀疑UE的Object实现内部有bug,那你就会逐渐的倾向于弃用UE的那一套,开始撸起袖子自己管理C++对象了。

总结
本文作为专题的开篇,唠了些书写背景的闲话,也闲聊了一下其他游戏引擎是怎么看待游戏内对象管理这回事的。每款游戏引擎都有自己的产生背景和侧重点,再加上设计的理念也不一样,所以就会产生各种各样的架构。接着探讨了设计一个Object系统有哪些好处和缺点,我不知道UE最初的UObject设计是从何而来的,但是如果没有UObject,没有了富饶的土壤,想要有繁茂的森林就比较困难了。各引擎的开发团队竞赛的时候,大家其实水平都差不了多少,同样想支持一个最新功能的时候,我利用上了统一的Object机制开发用了一周上线;你因为少了一些代码上的便利,还得自己手动管理内存,写序列化,再撸编辑器支持,代码写了两周,修复Bug用了2周,交付用户使用的时候,代码的接口因为不能反射也不是那么易用,慢慢的竞争优势就弱了。没那么方便调试统计,开发者修复bug起来就费劲,埋的Bug多了,用户觉得你越来越不稳定,引擎的生命力就是这么一步步一点点枯萎掉的。所以不要觉得引擎只要堆积功能就行了,一开始有个好的结构是重中之重。

闲话说完才可以开始之后的一个个功能的详细叙述。那么亲爱的读者们,请跟着我的脚步,重走UE曾经走过的路,让我们试着从头开始搭建一个Object系统,一步步的让她羽翼丰满多才多艺。

来源于网络


回复

使用道具 举报

4四处流浪
386/500
排名
4195
昨日变化
25

0

主题

22

帖子

386

积分

Rank: 4

UID
148958
好友
0
蛮牛币
619
威望
0
注册时间
2016-5-18
在线时间
148 小时
最后登录
2017-1-17
QQ
发表于 2016-12-12 18:24:27 | 显示全部楼层
有深度,赞!

回复

使用道具 举报

0

主题

1

帖子

1

积分

Rank: 1

UID
191924
好友
0
蛮牛币
8
威望
0
注册时间
2016-12-13
在线时间
1 小时
最后登录
2016-12-14
发表于 2016-12-13 18:09:55 | 显示全部楼层
我都写了谢绝转载,你剽窃过来,还删除我的原文链接。
https://zhuanlan.zhihu.com/p/24319968?refer=insideue4
道歉+删除!
[发帖际遇]: fjz13 发帖时在路边捡到 2 蛮牛币,偷偷放进了口袋. 幸运榜 / 衰神榜

回复 支持 反对

使用道具 举报

排名
21178
昨日变化
26

0

主题

8

帖子

21

积分

Rank: 1

UID
199491
好友
0
蛮牛币
22
威望
0
注册时间
2017-1-6
在线时间
5 小时
最后登录
2017-1-15
发表于 2017-1-6 13:28:10 | 显示全部楼层
6666666666666

回复

使用道具 举报

您需要登录后才可以回帖 登录 | 注册帐号

本版积分规则

快速回复 返回顶部 返回列表