第五回 对象描述信息(Object Description)--使对象使用起来更方便
一个c++项目里会使用各种各样的对象,但对象的使用往往并不是件轻松的事,当用一个c++类去封装一个对象的时候,程序员往往要做以下这些事
*.对象的成员变量的初始化,一般在构造函数里实现
*.对象的内容的清除,一般在析构函数里实现
*.对象的复制,把一个对象赋值给另一个对象,一般要实现一个operator =(..)
*.对象的比较,最常用的比较就是比较两个对象的内容是否一样了,一般通过实现operator==(..)来实现
*.对象的序列化,也就是存储/读取的工作,一般需要为这个对象实现Save()/Load()的函数.
*.对象序列化的时候,一个比较讨厌的问题就是格式兼容问题,当你修改/添加/删除了这个对象的一些成员变量后,旧有的存储数据就与当前的格式不兼容了,这就需要存储时多存储一个版本号,并在读取的时候作判断,来保持格式的兼容
*.此外,如果用户需要对这个对象进行编辑(这在3D engine中是很普遍的),那就会比较麻烦了,你可能会需要写一个编辑对话框,为每一个对象的成员变量加一个控件,把这些控件排版成好看的样子(不过我要说这也是件很有成就感的一件事),然后写一大堆消息处理函数,来对对象进行编辑.
如果仅仅针对某一个类做这些事情,其实并不算太痛苦的事,但当类的数量急剧增加时,这就变得越来越难以忍受了.在相当漫长的一段时间内,我都忍受着这些痛苦,机械地实现着这些没什么技术含量,但相当繁琐的代码--直到在开发这个引擎过程中,我终于考虑比较彻底的解决这个问题.
我看过一些其它的engine的代码,我发现一种比较常用的方法,就是用一个脚本文件来定义你的对象(比如说unreal),然后在脚本的分析代码里,提取出这个对象的各个成员变量的描述信息(比如类型,初始值,编辑方式等),然后再通过这些信息使用统一的方式来完成种种的操作.我没有能力写一个像unreal那样的有效率的虚拟机(曾经想过),所以最后还是使用c++的方式来实现了.
实现的思路基本上是这样,在写一个c++类的时候,除了定义各种成员变量以外,再为每一个变量定义一套额外的描述信息,这些信息以这个类的静态变量的形式存放.这些描述信息主要包含:
*.这个成员变量在对象中的相对位置,也就是相对于对象起始地址的偏移量
*.这个成员变量的名字及文字描述.
*.这个成员变量的一个版本号
*.这个成员变量的语义(sementic),决定了编辑器对它的编辑方式
*.以及,最关键的,这个成员变量的操作器,操作器为某种特定类型的变量提供各种操作,比如初始化,清除,复制,Save/Load,以及编辑手段等.常用的变量类型有:
**.简单变量,这个变量的所有内容可以被包含在一块连续的内存中.
**.简单变量的定长数组
**.简单变量的变长数组
**.字符串
**.对象
**.对象的定长数组
**.对象的变长数组
**.等等...
下面这个例子列出了这些变量类型:
class CSubObj
{
public:
int v1;//简单变量
int v2[20];//简单变量的定长数组
};
class CObj
{
public:
POINT v3;//简单变量
std::vector<POINT> v4;简单变量的变长数组
std::string v5;//字符串
CSubObj v6;//对象
std::vector<CSubObj> v7;//对象的变长数组
CSubObj v8[10];//对象的定长数组
};
对于这些变量类型的各种操作彼此不同,所以,需要为每一种变量类型写一个对应的操作器,比如对于简单变量的变长数组,它的各种操作是这么实现的:
*.初始化:已经由vector的构造函数完成了,不需要做任何事
*.清除:调用vector的clear()函数
*.复制:调用vector的assign()函数
*.比较:比较两个vector包含的内存块
*.Save/Load:将vector包含的内存块作save/load.
*.编辑:写一个编辑数组的控件(比如写一个list box,并且提供编辑每一个item的功能)
可以为每个成员变量都添加上述的这些描述信息,最后一个类的所有成员变量的描述信息被记录在一个链表里,这个链表的头记录在这个类的静态成员变量里.有了这些信息,你就可以使用一些统一的方式来对一个对象的实例(实际上是对这个对象实例的各个成员变量)来进行各种操作了.
下面是引擎中这套系统的一个例子,希望你能够通过它对这套系统的使用方法有个直观的了解:
首先有一组宏,这些宏用来定义对象描述信息
//定义对象描述信息开始
BEGIN_OBJ_DESC(classtype,version)
//以下这些宏分别对应了成员变量的各种操作器:
GELEM_VAR_INIT(type,name,initv)
GELEM_STRING_INIT(name,initstr)
GELEM_VARVECTOR_INIT(type,name,initv)
GELEM_VARARRAY_INIT(type,name,initv)
GELEM_STRINGARRAY_INIT(name,initv)
GELEM_OBJ(type,name)
GELEM_OBJVECTOR(type,name)
GELEM_OBJARRAY(type,name)
//以下下定义一些额外的信息
GELEM_VERSION(version)//成员变量的版本号
GELEM_EDITVAR(name,vartype,varsem,desc)//成员变量的编辑相关信息
GELEM_EDITOBJ(name,desc)//对象成员变量的编辑相关信息
//定义对象描述信息结束
END_OBJ_DESC()
然后就可以使用这些宏来为一个对象定义它的各个成员变量了,以下是一个例子,屋子里面放椅子
struct Chair
{
DWORD color;//椅子的颜色
int nLegs;//有几条腿
BOOL bBackrest;//有没有靠背
//对象描述信息
BEGIN_OBJ_DESC(TestData,1);
GELEM_VAR_INIT(DWORD,color,0x0000ff);//初始化为蓝色
GELEM_EDITVAR("颜色",GVT_UNSIGNED,GSem_Color,"椅子的颜色");
GELEM_VAR_INIT(int,nLegs,4);//有4条腿
GELEM_EDITVAR("腿的数量",GVT_SIGNED,GSem_Number,"椅子腿的数量");
GELEM_VAR_INIT(BOOL,bBackrest,1);//初始化为有靠背
GELEM_EDITVAR("靠背",GVT_SIGNED,GSem_Boolean,"椅子是否有靠背");
END_OBJ_DESC();
};
struct Room
{
std::vector<Chair> chairs;//屋里的椅子
std::string ownername;//屋子主人的名字
//对象描述信息
BEGIN_OBJ_DESC(Room,1);
GELEM_OBJVECTOR(Chair,chairs);
GELEM_EDITOBJ("椅子","屋子里的椅子");
GELEM_STRING_INIT(ownername,"Jack");//注意没有为这个变量定义编辑信息,所以它不会出现在编辑窗口中
END_OBJ_DESC();
};
有了以上这些定义,就可以方便的使用这些对象了, 比如:
void main()
{
//初始化
Room room1,room2,room3;
assert(room1.area==SIZE(20,10));
room1.chairs.resize(4);
for(int i=0;i<4;i++)
assert(room1.chairs[0].color==0x0000ff);
//比较,赋值
assert(!(room2==room1));
room2=room1;
assert(room1==room2);
//存储,读取
room1.GSaveToFile("room1.bin");
room2.GLoadFromFile("room2.bin");
//清除对象数据
room1.GClear();
assert(room1==room3);
}
注意:上面的GSaveToFile(),GLoadFromFile(),GClear(),以及operator=(),operator==()是由BEGIN_OBJ_DESC()/END_OBJ_DESC()这两个宏在对象里添加的函数
此外,可以在对象的存储信息里包含额外的控制数据,使得可以很方便的在对象里增加/删除/修改成员变量,而保证原来的存储数据仍旧可以正确的读出.
最后,可以使用一个通用的编辑窗口来显示/编辑这些对象,如下图:
我想我并没有把这套系统的实现细节讲得很清楚,除了思路,我只是列出了这套系统的最终使用方法,希望你能看得明白.出于保密原因,我也不方便贴出源代码,不过我想有时候知道做什么往往比知道怎么做重要的多,我的实现并不是很优雅(有1500多行代码,大多数是模板类和宏),描述信息的定义也略显笨拙,希望能有更加完美,更加简洁的实现.不过这种类型的系统的确可以大大简化对象的使用和编辑,减少出错的概率,使得应用程序可以将大规模的对象直接暴露给用户,而这在没有这套系统的情况下是不太可能的.
最后记录一下目前的实现的一些优点和缺陷:
*.(优点)可以为任何对象添加描述信息,而不要求这个对象派生自一个固定的基类(有一些实现好像要求这么做)
*.(优点)使用起来比较方便,只需要include一个头文件
*.(缺点)存储时为了控制版本信息,数据量有些大,读取时的效率也不是很高
*.(缺点)目前并不支持所有的变量类型,比如map,list等,当然可以避免使用它们.
*.想不出来了,想到再加.