1、图块库的结构 图块库由图块索引和图块内容两部分构成。因为图块的字节长度是不固定的。 图块索引的结构:每个索引的长度为一个 int (4 bytes); 图块内容的结构: (1)8 bytes的图块信息头 typedef struct { short blkwidth; //图块的宽度(以点阵来计算) short blkhigh; //图块的高度(以点阵来计算) short blkorx; //图块在X方向相对于绝对位置的偏移量 short blkory; //图块在Y方向相对于绝对位置的偏移量 }BlkInfo;
(2)长度不固定的图块内容 其结构是以行为单位,进行处理的。它的每一行的结构 1 byte n bytes 画这一行需要阅读多少字节 不定长度的线段内容
它的每个线段内容的结构 1 byte 1 byte n bytes 相对于上次位置的偏移量 这个线段共需要画多少个字节 这个线段的具体内容 阅读图块的C语言算法 void drawblk(char * dest) /*输入参数:dest - 图块将展开成像素格式到这个目的缓冲区内*/ { BlkInfo mInfo; //包含了这个图块的图块信息头 UCHAR readnum ; //画这一行应该阅读多少字节 UCHAR drawnum ; //每个线段所用掉的字节数 UCHAR usednum ; //目前画过的线段已经用掉的字节数 char mx ; //当前的线段应该画在的位置(相对于本图块的绝对位置) int offset ;//因为dest是连续的内存,不分宽度和高度的,需要 //一个变量来计算画点到dest的偏移量。 LPSTR lpBlk ; //指向本图块的开始位置的指针 short y=mInfo.blkhigh-1;//实际需要画的行数 int bufWidth; //dest接收的宽度(是为了兼容标准位图的标准格式) //在这里可以认为等于 mInfo.blkwidth int i ; for(i=0;i<mInfo.blkhigh;i++) { readnum=*lpBlk; //取得画这一行应该阅读多少字节 lpBlk++; //指针指向了第一个线段的偏移量
usednum=0; mx=0; drawnum=0; //局部变量的初始化
while(usednum<readnum) //进行循环直到该画的字节都画完 { mx = * lpBlk + mx + drawnum; //取得即将画出的线段的偏移量 lpBlk ++; //指针指向了即将画出的线段的 //实际字节数 drawnum= * lpBlk ; //取得即将画出的线段的实际字节数 lpBlk++; //指针指向了即将画出的线段的 //实际内容 offset=y*bufWidth+mx; //计算应该拷贝到dest的那个位置 memcpy(lpDIBits+offset,lpBlk,drawnum); //到dest的内存拷贝
usednum=usednum+drawnum+2; //计算已经画出的线段用了多少 //字节 lpBlk=lpBlk+drawnum; //指向下一个即将画出的线段 } y--; //标准位图是从最后一行开始算起的 } }
注意:只是为了说明图块的解码方法。对于转化成标准内存位图,需要宽度边界的判断。对于直接写入屏幕缓冲区,还需要进行是否超过缓冲区边界的判断。
2、阅读图块库的类 - CBlkLibApi 的数据和函数成员
class CBlkLibApi { public: CBlkLibApi(); virtual ~CBlkLibApi(); protected: CPalette* pPal; //char* blkBuffer; HBLK blkHandle; //UINT * idxBuffer; HBDX idxHandle; CList <UINT,UINT> idxList; int blkNumber; CString palFileName; public: BOOLEAN ExportBlk(int index,CFile&file); //把指定编号的图块输出成标准位图格式 BOOLEAN LoadBlkLibFile(LPCTSTR lpszBlkName); //装载图块库到内存 void CloseBlkLibFile(); //关闭图块库 BOOLEAN SaveBlkLibFile(LPCTSTR lpszBlkName); //图块库文件存盘 void SetPalFileName(LPCTSTR lpszPalName); //设置图块所用到的调色板文件 CPalette * GetBlkPal(); //取得当前图库所用的调色板 int GetBlkNumber(); //取得当前图库中图块的数量 BOOLEAN BlkiToIcon(CDC * dcWnd,int wDest,int hDest, CBitmap& mBitmap,int index); //把指定编号的图块输出成指定大小的标准位图(缩放) HANDLE BlkiToBitmap(LPBITMAPINFO& mbInfo,int index); //把指定编号的图块输出成与原来图块大小相同的标准位图 LPBITMAPINFO GetBlkiInfo(BlkInfo& mInfo, int index); //取得指定编号的图块的信息 BOOLEAN InsertBlk(int position,CBitmap mBitmap,CBitmap mMask, int blkOrx,int blkOry); //把一个标准的位图加入图块库中,成为一个图块 BOOLEAN DeleteBlk(int position); //从图块库中删除指定位置的图块 protected: LPBITMAPINFO mDibInfo; BOOLEAN ReadBlkLibFile(LPCTSTR lpszBlkName); BOOLEAN ReadBlkIdxFile(LPCTSTR lpszIdxName); BOOLEAN ReadBlkPalFile(); void GetBlkIdxFileName(LPCTSTR lpszBlkName,CString& idxName); void InitDibInfo(); }; 需要改进的地方: 原来是由这个类自己管理一个调色板文件。在使用DirectX后,需要改成兼容的调色板结构。 增加两个图块库之间的大量图块的直接拷贝。 提供把图块直接画在由指针指定的内存中的功能。就象直接画在屏幕上一样。 实际上传递给它的指针,很有可能就是屏幕的背后缓存的指针。
3、地图数组文件的结构(内部) 地图数组文件由场景索引和场景地图两个部分组成。场景地图的长度可以是固定的,也可以是不固定的。场景索引结构:每个场景索引的长度为一个 int (4 bytes);场景地图结构:地图分成几个层来画,一是为人物的移动提供了判断依据,二是为使某些物体具有透明的属性,同时也造成了三维空间的假象:即下面的层会被 上面的层遮盖。
(1)地面:将把这一层的数组全部填满。因为地面是一直铺满整个地图的。包括草 地,土地,水,但是不包括任何阻挡人物行走的石头和草。
(2)建筑?包括房屋,花草树木,桌椅板凳任何带有高度的物体。在这一层中,有 物体的地方,将填写相关物体的图块索引号,没有物体的地方将用 0字节来填写。 最终所有的人物,在计算了移动位置以后,也将并入这一层。根据人物将移动的方 向的字节是否为零,来判断人物移动的可能性。如果增加某些有生气的动物,也将并入这一层(同样是计算了位置改变以后)。
(3)摆设 包括桌子上摆的东西,墙上挂的装饰品。仅仅是为了画面的华丽程度。加入这一层的原因,是因为摆设物品的位置和所依附的物体的坐标往往是相同的。也许 只是为了减少一些图块?但是增加了两个很大的矩阵!
(4)建筑图块的偏移量因为所有的图块,并不是很规则的,为了在画面上使它们紧凑地排列在一起,而加入了这个数组。
(5)摆设物品的偏移量作用同建筑图块的偏移
(6)动画和事件触发标志定义了在地图上需要进行动画和什么地方将有事件发生的数组。在地图里,它们的标志是相同的。有这种标志的地方,填写了事件的索引号,没有的地方,用-1来填写(因为事件的索引号有可能是0)。因为地图是随着人物 的走动和时钟的进行,不断改变的(范围和某些动画),这些标志将作为动画处理和事 件发生的判断依据。无论如何,事件的发生是和地图想关联的,它们都有自己固定的位置。不断打算盘的客栈老板,和飘扬的旗帜,流动的水,都将来源于这些标志。
改进建议 数组的大量填零,造成空间的很大浪费。对于物体的显示,可以采用绝对位置加索引号来表示,显示倒不成问题,但是对于人物移动可能性的判断,就变得很 困难。是否可以采用某种链表结来进行判断,总的说来,是要用时间来换取空间,还是用空间来换取时间。如果在链表结构上处理得非常杰出,时间损失很小 ,是最好不过的了!
物体的偏移量的信息是否可以包含在图块本身的数据结构里?似乎影响不是很大。 经过仔细观察,带有偏移量的原因有两个:一是对于规则大小的图块,需要 摆在图块的中央,显得好看。二是一些不规则的家具,一件家具往往是由几个图 块拼凑起来的。如果没有适当的偏移,会出现裂缝。但是为什么是拼凑起来的?因为一个家具可能会在不同方位出现不同的人物。比如坐在桌子旁边的令狐冲的形象,是和桌子的那一角画在一起的。反正,认为能够很自由地在桌子前面站起来,坐 下,于是很大的难题。原来游戏采取屏幕变黑来处理,偷懒了!!
动画和事件的触发?能不能分开来写?这些标志是和判断人物行走无关的,也许优化的可能性是最大的,算法相对简单一点。
地图显示和运行的基本算法 这部分的程序应该是比较关键的。程序的大部分时间都将在地图的显示,移动,和变 化状态中。原来我的程序结构是把事件的处理加入了地图显示的循环,就是坏┯惺录?⑸??赝急旧淼难?方?岵??6佟J欠裼Ω貌扇》⑺拖?⒌姆?/FONT>式,还值得仔细 探讨。
unsigned short * allsinbuf; //指向所有数组的指针-这些数组连续放在一起 unsigned short * groundbuf; //指向地面数组的指针 unsigned short * buildbuf; //指向建筑数组的指针 unsigned short * toolsbuf; //指向摆身数组的指针 unsigned short * actorbuf; //指向动画和事件标志的指针 unsigned short * buildoff; //指向建筑物体偏移的指针 unsigned short * toolsoff; //指向摆设物体偏移的指针 sinkey * skeybuf; //指向动画和事件解释文件的指针 //提供给外部以下三个函数
int SceenInInit(unsigned idx) //指定场景的初始化 /*输入参数 idx - 场景的编号*/ { 打开并读入需要的图块文件; 打开并读入需要的图块索引文??/FONT> 打开并读入指定场景号码的地图数组文件; 打开并读入指定场景号的动画和事件解释文件?//格式随后介绍 } int SceenInRun(unsigned idx) //指定场景的运行 /*输入参数 idx - 场景的编号*/ { int mx,my; //以自己扮演的角色为标准的人物位置 int m; //角色需要显示的当前桢(包括了面对的方向) int outFlag=SceenLoop; //程序循环和退出场景的标志 short allDef;
i=0; j=0; //场景在屏幕左上角的那个图块在地图数组 //中的位置。 m=需要显示的当前桢; //一般为零。 mx=人物初始位置; my=人物初始位置; //根据场景的不同而不同
SceenInDraw(0,0,mx,my,m); //根据初始人物位置和场景位置画出场景 while(!outFlag) //循环直到接到退出指令 { GetEvent(&eventMsg); //获取用户的输入,转化成标准消息结构 outFlag=DispatchEvent(&eventMsg,&i,&j,&mx,&my,&m);//处理消息事件 SceenInDraw(i,j,mx,my,m); //重新刷新屏幕 ClearEvent(&eventMsg); //清除消息队列 } return 1; }
void SceenInClose(void) //关闭指定场景 { clearv( ); //清除屏幕 closesinall( ); //关闭所有文件,释放所有内存 }
//以下函数是上面三个函数需要调用的不公开函
int SceenInDraw(unsigned int x,unsigned y,unsigned mx, unsigned my,uchar picnum) //根据角色的位置和时钟重新刷新屏幕 /*输入参数 x,y,- 场景在屏幕左上角的那个图块在地图数组中的位置 mx,my - 人物的当前位置 picnum - 人物需要显示的当前桢 { int i; clearvb( ); //清除屏幕背后缓存区 dispground(x,y); //画地面 dispmainact(mx,my); //把主角合并到建筑那一层 dispactor(x,y,picnum); //把其他角色合并到建筑那一层 dispbuild(x,y); //显示建筑层 disptools(x,y); //显示摆设层 /*calc if outFlag=1*/ //计算是否走到了出口 VideoReflash( ); //把背后缓存传送到屏幕 return 0; }
int DispatchEvent(&eventMsg,&i,&j,&mx,&my,&m) //处理消息队列,来计算位置的变化 { if(eventMsg.type&键盘事件) //处理键盘事件 { switch(eventMsg.key) { case 方向键: //处理人物的移动 if(moveable(mx,my,0))?//如果可以移动(没有障碍) { 计算位置; 计算方向 ; 计算当前显示桢; } break; case 激活菜单: //处理用户呼叫菜单 MenuInit( ); if(MenuRun( )) outFlag=SceenQuit; break; case 决定: if((allDef=eventhappend(mx,my,MActorDir))!=-1) SceenDefRun(allDef); //如果存在事件,则执行它 break; default: break; } } if(eventMsg.type&时钟刷新) { m++; if(m==12) //计算动画的显示 m=0; } }
存在的问题 对于多个物体时间有差别的动画问题。是不是把时钟的控制单位定小一点??给每物体单独分配一个控制时间的变量?搜索链表? 对于时间是否包含在场景循环内,还是以发送消息的方式,交给其他的函数来处理原来的方式,是对应每个场景,都有一个单独的图块库。如果采取所有场景共用一个图块库,应该如何在初始化的时候,加载当前场景所需要的图块?如果是另外记载需要的图块,装入内存以后,是否要重新建立索引?
设想,共有的图块库为 { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10,..............} 当前场景需要的图块为 { 1, 5, 13, 18,............ }
在加载的时候,重新建立一个对应于当前内存中图块库的索引,在一边挑选图块的时候,这个索引就一边建立起来了。 在图形部分需要做的工作: 尽快指定合理的解决方案,解决上面的问题。 一个可以重用的 CBlkApi 类。 一个图块编辑器。 一个地图编辑器。 场景运行程序的基本构架。 一个可重用的 CSceenMaps 类。 “盗亦有道”--- 如何从别人的游戏学习经验 选定一个游戏,来对他的文件组织结构进行比较深入的分析,无疑是得到这一类型的游戏制作经验的捷径。据说有名的《赤壁》就是前导公司仔细剖析了《魔兽争霸》做出来的!以下结合对智冠出品的“金庸群侠传”的剖析,介绍一下我是怎么样从别人的游戏里学习经验的。 也许是因为智冠许诺给河洛制作组的工作是时间太短的原因,我从来没有见到过文件格式如此透明的游戏了。能够遇到这样一个游戏,并从中学到很多有关 RPG游戏的编程经验,也为剖析其他游戏积累经验,当属于三生有幸。 但是剖析游戏切忌陷入“牛角尖”的误区,时刻牢记我们剖析游戏的目的和初衷:我们仅仅是为了从中学习一些经验,并不是真的要把这个游戏的每一比特的含义都搞懂。 而且深入剖析一个游戏以后,等于已经掌握了它记录图象信息存储的文件格式,对于那些暂时找不到美工,又急于制作引擎的业余游戏编程爱好者说,也暂时提供了用来实验的图象素材,而且比用屏幕捕捉程序截取的图片完整得多。 剖析“金庸群侠传”的步骤: 1、确定每个文件的用途: a、查看游戏文件目录,根据自己的找出自己熟悉的文件。比如,一看到SETSOUN -D.EXE马上就明白了:是声卡的设置程序嘛!凡是在 DOS4GW下做的游戏,大多数有这个文件。同时也就确定了*.MDI,*.DIG是声卡的MIDI和声音的驱动程序,*.XMI是游戏的MIDI音乐,*.WAV是程序的音效。还有一些是根据自己的编程经验一眼就能看出来的:768个字节的那个文件,肯定是记录的系统 256色调色板的!这样就可以排除掉很多文件,剩下的就是游戏开发小组自己定义的文件格式了。 b、有一些文件的文件名起得非常好:好到一眼就可以确定他是干什么的程度。比如,ALLSIN.GRP 和 ALLSIN.IDX,就是 ALL SCENERY IN 嘛!所有的内部场景。IDX 嘛,当然就是索引了!HDGRP.GRP和HDGRP.IDX,当然是人物的头像了!至于是否想法正确, 可以用下几步的方法来验证。 c、利用改变文件名的方法,来确定一些文件的作用:把等待验证用途的文件改成其他名字,而人为地给引擎制造一些错误。对于一个好的游戏引擎,应该有比较好的容错性和稳固性,或者会给出XXXX文件找不到的信息。这种方法对那些在游戏初始化已经完成,后期才加载的文件尤其灵验。但是用这种方法有时可能会死机( 为了祖国的游戏事业,RESET几次有啥子大不了的嘛)。那么我们就坐下来大骂这个游戏的引擎做得太差劲了好了。比如,我们在游戏目录下看到如下的文件:BUILDING.002 BUILDX.002 BUIL-DY.002 EARTH.002 SURFACE.002,我们根据名字猜测它们是和大地图地图有关的文件,那么我们就把EARTH.002 改名成 EARTH.003,然后运行游戏,看看会发生什么错误。谢天谢地!没有死机。但是走出去一看,大地图上所有的地面(不包括道路,植物和建筑都变成单一的蓝色了,这就说明EARTH.002 这个文件,记录的是大地图上地面的图块的排列。 d、利用同类文件交换文件名来确定文件的用途:在这个游戏里,有SMP0??和SDX0-?? 这样的文件,文件名比较简单,很难确定它到底是保存的图块,还是场景中图块的排列顺序。但是正好这种同类的文件很多,这个时候可以采取这种办法。把SMP001,SD-X001的文件名与SMP000,SDX000 对调一下(具体过程不用说了吧)。然后运行游戏,咦?没有变化?一个不小心,走进了“河洛客栈”,噎?“河洛客栈”什么时候重新装修过了?布局没有改变,只是构成场景的图块改变了,这就说明这几个文件,记录的是图块本身的信息。(用这种办法同样容易死机,别怪我没有事先警告你)。 2、猜测、验证每个文件的结构: 对每个文件在游戏里的作用搞清楚以后,接下来就是最艰巨的任务了:搞明白每一 个文件的格式。 a、根据索引查找规律:把要搞懂的文件用 PCTOOLS或者其他可以查看二进制文件的工具显示在屏幕上,看看有没有什么规律。可以看到,凡是索引文件,都是由很有规律的整数或者长整数按顺序排列的,根据这些索引,可以确定其相应被索引的文件的每段数据。然后,再仔细比较被索引文件的每一段数据有什么不同,就可以找到其中的规律了。 b、把文件用零填充来查找规律:有些数据的单元长度不好确定,可以采取这种方法。比如记录图块的文件,我们并不知道图块的每一行应该有多少个字节,这时我们可以修改指定位置的图块的数据,尝试用 "0" 来填写几个字节,然后运行游戏观察结果。 c、用修改文件数据来查找规律:用法和第二项相同,只是不填成"0",而是用其他的数字来填充,有时是一个关键的字节(比如代表位置的),有时是一串字节,来寻找每个字段的含义。为了效果更显著,可以把数字改得夸张一点,但是不要怕死机哦! 3、用自己编写的程序来调用游戏的数据 为了验证数量很多的文件格式的正确性,就要自己写一些程序,因为我们不能老是一个字节一个字节地来寻找,验证规律啊!比如在初步对图块文件的格式有所了解之后,就可以用自己的程序把它们显示在屏幕上! 4、尝试用自己编写的引擎来做个类似的游戏当你对一个游戏了解到这种程度以后,有什么想法?......对了!那就是尝试一下,自己做一个类似的游戏。什么?你说还是有很多的文件格式没有彻底搞懂?请不要忘记我们原来的初衷:我们只是想通过剖析它,来看一下这种游戏到底是怎么做出来的。比如在这个游戏中的 KDEF.GRP 是记录游戏所有发生的事件的文件,而我只搞懂了几种事件的记录方式,但是我们也可以自己来定义自己的事件文件啊!干吗非要把它搞得那么透彻?反正最后的事件文件还不是要重新来写的。找不到美工,音乐吗?暂时先用它原来的场景和音乐凑合一下吧!如果你的引擎真的已经达到了原来引擎的水平,还发愁没有好的美工音乐编剧和你合作?
|