串行化的机制和原理
今天终于把Seralization 的基本框架搭好了,简单的测试了一下,存储没问题,读取好像还有点问题.由于现在还没有写由Object派生出来的类,测试不出什么东西,等把场景管理的部分完成后再回来改.
由于整个Seralization的机制比较烦琐,我今天把整个思路整理一下,一是作为交流,另外也作为CoagelEngine的开发文档.
Seralization 又叫串行化,简单的讲,就是一种保存当前运行程序的状态,下次运行程序时可以将被保存的状态提取出来,这样就可以从上次保存的状态开始往后运行.在游戏设计中,这也就是所谓的存档/读档功能.
那么Seralization如何才能实现呢. 在结构化的程序设计中,函数(方法)和数据是相互独立的.
我们需要将当前内存块中的每个变量数据都保存到文件(后者是一个内存块,这点在下文提到)中,然后下次运行时在将这些数据都提取出来,付给每个变量. 比如说我的程序有两个全局变量Ea,Eb.有两个局部变量a,b. 其中a,b都是函数Fun 的局部变量.那么我需要保存这4个变量的值. 并且我要知道a,b是Fun的局部变量,Ea,Eb是全局变量. 并且我需要知道程序执行到哪一步了. 这是个复杂且难以完成的工作. 因此Seralization 都是在OOP 面向对象的原则下进行的. 甚至有的人直接称其为类的串行化.
基于OOP原则的程序, 类是数据和方法的集合. 我们只需要保存当前生存着的每个类中的数据, 类与类之间的关系, 当然如果有全局变量也需要保存. 下次还原时我们还原被保存的这些信息, 就还原了程序的状态.
这里就引入了几个问题.
1. 我们知道类继承机制中的虚函数机制可以动态的决定调用哪个函数. 比如:
Class A
{
Public:
Visual void fun() { return 1;};
}
Class B : public A
{
Public:
Virtual void fun(){ return 2;};
}
我定义一个指针 A* p = new B;
注意这里虽然p是一个A* 类的指针,但他实际上指向的是一个class B.
因此调用p->func 其实调用的是 B::func();
回到刚才的话题.类的成员函数如果被声明是虚函数的话,可以动态的决定调用哪个类的函数.但是类的创建必须要写成 A p = new A, 这里我们不能利用虚函数来实现动态的决定创建的是什么类.因为创建时程序还不知道p到底值向什么类.创建A类就必须写成new A,创建B类就必须写成new B. 这就是问题所在了.这个问题又被分为两个方面.首先,我们必须在保存类的信息时同时保存该类的类型
(是class A,还是 class B).这点可以利用RTTI的机制来实现.RTTI的原理很简单,我们在每个类中都添加一个字符串,这个字符串记录的这个类的类型名字.比如class A的字符串就是”A”,class B的字符串就是”B”.当我们保存的时候,我们把这个字符串也保存起来.这样当我们读取的时候就知道现在读取的数据是属于什么类的了.
然后我们需要解决的问题就是如果创建这个类了.前面说了.我们不能利用new 来创建,因为这里的创建是程序执行时动态决定的.我们事先不知道哪些类需要创建.这个问题我们利用object factory的思想来解决.我们为每种类都定义一个static factoryFunc();由于是static
的,所以该类的所以实例都共享这个方法,同时这个factoryFunc()是类相关的,不是实例相关的,因此在类还没被实例化之前就存在了(可以想像成全局的,从程序运行开始一直存在到程序结束). 我们将每个类的factoryFunc和该类的类型名(A,B)一起添加到一个map
容器中,一个类型名对应一个factoryFunc. factoryFunc封装了类的new 操作.这样我读取存档的时候.我只需要先读取类的类型名,然后到map里去查找这个类型名对应的factoryFunc,然后调用它来创建这个类就行了.伪代码如下
Read(stream, classname);
factoryFunc = Map->find(classname)
object* p = factoryFunc();
这样就实现了类的动态创建.
2.如何保存类之间的关系
这里我说类之间的关系就是指类的指针成员指向的其它类.由于我设计的引擎是以OOP为基础的.我定义了一个Object 类,并且认为其它一切的类都是这个类的派生类.注意最开始我们提到的保存机制保存了类的指针.但是如果我们新创建了一个类,那这个类的指针是系统自动分配的,和上次保存的地址是不一样的.因此不能简单的将当前类的指针成员的值保存下来,然后在读取出来重新付给它(因为现在这个值代表的内存块里的数据和上次不一样了).我们需要一个映射机制,能将旧的指针值和新的指针值对应起来.我这里也是利用的map来实现.
整个Seralization机制流程如下:
主要是2个类.Object , Stream
其中Stream里面有一个vector<object*> top,用来保存场景中的主要物体.这里需要解释一下.我的设计思路是场景中的物体是有关联关系的.比如一辆汽车,它有4个子物体――轮子.如果这辆汽车没有父节点,那汽车就是一个主要物体.而轮子不是主要物体.这个容器的作用是用来递归的调用Object的方法.
Stream的第2个容器是static map<string,factortFunc*> factory, 就是上面提到的,将factoryFunc和类的类型名对应起来.
Stream的第3个容器是map<Object*,Link*> mLink ,Link是一个自定义的类,它有一个object*
,用来表示它是哪个实例的Link,还有一个vector<object*>,里面保存这个实例的指针成员数据.
Stream的第4个容器是vector<Object*> order,用来保存场景中所有的实例指针.
Stream提供Save和Load两个方法.
Save: 首先将order清空,然后对top中的每个实例都调用object::register.这个方法是将该实例的指针添加到order中,并递归的调用这个实例的子实例.这样order就保存了所有实例的指针值了.
然后对每个实例都调用Object::Save 当然,这里Save 和 register都是虚函数.
Save的作用是将类的类型名,类的指针,类的名字,类的数据成员,类的成员指针都保存起来.如果这个实例是主物体,那就在类的类型名前加一个”Top”.
Load: 首先将order, mLink清空,依次读出每一块数据,根据数据中类的类型名,去factory中找到对应的方法,创建这个类,然后把类的名字,类的数据成员都从数据块中读出付给新的类.接下来是重点了,创建这个类(实例)以后,要把它跟一个Link类联系起来.把这个实例现在的指针付个Link的Object*,并且将数据块中的关于成员指针的数据都加入到Link的verctor中.注意这里vector中还是旧的指针数据哦.最后读出数据块中这个类的指针值,将它作为关于这个类的标示,和Link一起放入mLink中.于是mLink就成了用旧指针值作索引,用保存了这个实例的新指针值和这个实例的就指针成员的Link作值的hash表.
上面的步骤结束后.我们从头查找m_link,对每一个Link中Object* ,我们都调用虚函数
Object::Link(),并把该Link作为参数传给Link().Object::Link会根据Link的vector里面的每个值作索引,去m_link里面找到他对应的Link,并把Link的object* 传回来,作为该Object新的成员指针.
大体的流程就是这样,过几天有空我会画一张详细的流程图