自动序列化,学习和实践

来源:互联网 发布:淘宝林俊杰国际歌友会 编辑:程序博客网 时间:2024/05/01 03:56

GC本次重构中,唯一不需要大动的或许就是自动序列化了,这个思想算是从U3借鉴过来的。

U3里给我最大的感觉,就是序列化这里几乎不需要太动脑筋,只要是uc系的Object,基本上都已经处理好了自动序列化,版本更新时,数千个类却不需要合并一行序列化相关的代码。

Every Thing Is Automatic。如果游戏能这么做出来该多好!

当然也有人说U3这个系统设计的很烂,费效率,耽误时间,难查错,等等等等。这里面有些是自动序列化必然会存在的问题,也有些是U3本身的假设导致的,从系统论的观点而言,一个组件的错误有可能不是因为组件本身,而是因为它所在的环境,这些废话就不细细展开了。

对于GC而言,资源系统是独立于整个渲染构架的,因此资源的存储也是由这些外部系统来处理的。而外部系统的重构、重写、改变也可能是最多的——因为他们跟需求最相关,最直接相关。面对着未知次数的重写,也就大胆试了试自动序列化——我可不想焦头烂额的时候还去处理序列化这种劳动密集型的问题。

自动序列化的基本思路是:利用语言系统提供的反射机制(C/C++靠宏和Template自己写,.NET靠.NET本身提供的反射),分析每个对象需要序列化的成员,在写文件的时候自动去写这些成员,读的时候自动去把他们读出来。

“说起来简单”的东西一般写起来都不那么简单。我们先来看看这个系统要面临的几个基本目的吧:

1,存档时,记录下那些对用户有用的信息,抛弃那些对用户无意义的信息。

2,读档时,正确的读取记录完毕的信息。同时,最重要的:正确读取所有被当前对象直接引用或者间接引用的物体。

还有几个高级目的,能实现最好,不能实现不妨碍使用,这几个是从U3总结出来的,GC并没有完全做到:

1,可插接的,用户可以插接新的Formatter来提供新的格式(例如加密、压缩、网络、文档)。

2,可以很方便的跟已经有的系统共存,几乎不需要增加太多的操作,就可以为一个新的类提供一个自动序列化。

3,版本的兼容问题,按照旧类存储的数据,新类如果已经没有了这个成员,那就不需要去读。

4,有效降低存储和读取开销。

 

基本的问题其实很好解决:

存档时的步骤如下:

  1.  
    1. 1 分析有多少个物体需要存储。
    2. 2 对每个物体:
    3. 3 分析有多少个成员需要存储。
    4. 4 对每个成员:
    5. 5 如果成员是一个POD或引用对象,直接存储。
    6. 6 如果成员是一个数组,则记录数组大小,然后把数组中的每个成员当作一个成员,从第4步开始。
    7. 7 如果成员是一个.NET对象,把这个对象当作一个物体,从第2步开始。
    8. 8 如果成员是一个RawDataBuffer,记录这个RawDataBuffer的大小和数据。

 

读取的步骤如下:

  1. 1 定位当前要读取的是哪个物体。
  2. 2 对当前物体,读取所有的成员列表。
  3. 3 对每个成员,读取成员里面记录的数据。
  4. 4 如果成员是一个POD或者引用对象,直接读取。
  5. 5 如果成员是一个数组,读取数组大小,然后读取数组中的每个成员,从第3步开始。
  6. 6 如果成员是一个.NET对象,从第2步开始递归。
  7. 7 如果成员是一个RawDataBuffer,读取这个RawDataBuffer。

 

有人说,这玩意儿怎么没见过?看看.NET的Formatter吧,特别是Soap Formatter。看看使用了这些Formatter的程序吧,那个简单啊……让人心里痒痒的。

痒痒归痒痒,但BinaryFormatter却仍然没有用上,这主要是因为能控制的东西太少了,而且可定制性并不强。从Formatter一层继承过来看起来是比较直接一些,但是可用的信息也不多,权衡了半天,最后决定干脆自己从头写。那句话怎么说来着——“学习走路的时候,要懂得离开妈妈的怀抱。”

 

首当其冲的问题是,引用。

比如材质里面一般都要引用纹理,存储的时候,哪些物体有引用,哪些引用要存储,这些相对还好办(反射,存唯一名,存GUID),但是,引用在读的时候该怎么处理呢?很显然的,当我载入一个材质的时候,这个材质相关的纹理也是应该载入的。

问题就来了——我怎么知道这个纹理在哪个文件夹下呢?当然,我可以拿着一个名字去Search整个资源系统(甚至可能是文件系统),然后去读取这个纹理。这样的话,我就必须在一开始,将所有纹理的信息建立到这个Hash中。这可能是最简单的实现这个需求的方法。然后,我需要一个模型,这个模型需要一个材质,然后我要Cache所有的材质。然后我需要一个场景,这个场景需要一个模型,然后我要Cache所有的模型。然后我需要一个世界,这个世界有一个场景,然后我要Cache所有的场景……不错,还挺押韵。

当然,不排除这个做法是一个的做法,也不排除我会在以后使用这个做法,还是那句话,没有好的,没有坏的,没有对的,没有错的——眼睛里只有对错的人往往看不到真正的对和错——关心目的可能更好一点。因为外包的特性,与他们商量后,我为GC挑选了唯一命名系统——这个系统的描述相对而言,给用户的感觉相对更好一些,名字看起来像是给非程序员看的。命名系统其实不仅仅是程序员要用的,随着项目规模的越来越大,给非程序人员使用的资源系统是一个庞大的系统,甚至树形的管理都不够用啊!

在这个唯一命名系统中,每个资源都有自己分配到的一个唯一的,独一无二的命名,其他的不重要,这个资源可以来自内存,可以来自硬盘,也可以来自光驱和火星,这是他的自由,但他有一个唯一的身份证号码,只需要知道这个就足够了。你看身份证是怎么排的?前六位指定您的省市自治区,然后是您的生日,最后一位区别码。只需要一个号码就可以知道这个人的基本信息,我认为这就是信息系统编码的一个很典型的正面例子。唯一命名也是这样的意图,具体的实现其实是无所谓的。

这样,引用的问题就解决了,当发现这个成员是一个引用的时候,我存下它的全名,然后剩下的就不用担心了。读取的时候,根据这个全名,我能够分析出来所对应的包、文件系统、路径、读取器,好了,剩下的工作就是不断的重复和递归读取过程即可。

 

接下来的问题,就是格式。

用以构成我们资源的文件格式何止千变万化,有些信息,我们从XML读比较容易,有些信息,我们又希望用二进制来保存。用户希望自己的格式不要让别人随便破解,跟网络关系密切的用户希望自己的资源能跟网络亲密接触。

GC这里的解决方法是,把所有的成员抽象成为Property,除了POD Type外,还支持了下面的Type:

List,直接存储一个Array或List。

Ref Packable,存储一个引用对象。

Struct,存储一个原生的.NET对象。

RawDataBuffer,存储一个BYTE[]数组。

这样,可以允许用户自己来写不同的Formatter,来存储和读取这些Property。

目前GC实现的是一套Binary的Formatter,其实如果不是因为时间紧迫,我还是倾向于XML ^_^。

 

基本上解决了这些问题,整个系统也就折腾的差不多了吧,GC为此大约编码了1000行左右,3、4个晚上的功夫,看来我的夜生活也还算有效率:P

把一些思路总结出来,希望能抛砖引玉~~欢迎天外飞砖。 ^_^

当然,还是那样,这个东西并不是任何时候都值得使用的,GC以后准备对于复杂的上层系统的资源组织使用这个系统来降低维护成本,解决版本更新问题。对于对性能和速度要求比较高的系统,这套系统并不合适,因为频繁造访Reflection,小数据量的磁盘传输,降低硬盘Cache命中率,大量的Hash搜索,如此等等,可能都是本系统带来的负面影响。

原创粉丝点击