同人游戏开发手记(三) - 第二章 守护者之剑系列 (2.1 ~ 2.2)

来源:互联网 发布:网络财经视频直播 编辑:程序博客网 时间:2024/04/28 20:22

by.六十六(http://blog.csdn.net/fosly).互粉啊~~.

注意:由于没有接触VB编程太久了,所以后面出现的VB代码中可能有一些细节不是很科学, 贴博客的时候才偶然看到, 比如声明两个整型变量, 那时我可能这么写

Dim nW, nH as Integer, 实际上这么写是不对的, 这样写第一个nW是无类型的, 活活活活.

当然用C或者C++或者其它的什么语言写的东西肯定也有好多其它的错误, 因为没有接触其它语言编程也太久了...写这个文章的时候我的工作还是送盒饭和小区临时保安呢, 低保收入, 没那么多零用钱买东西补脑子...请看的人原谅我的不细心吧.

文章中提到的附件可以到我的资源什么什么栏那里下载:

 http://download.csdn.net/detail/fosly/4884212

 

第二章 守护者之剑系列

2.1 游戏简介

         这里说的实际上是《守护者之剑》的一个外传,游戏的名字叫《无尽的宿命》,讲述了两个女孩和一根圆柱变成的男人,完后有时候好快乐,有时候也会痛的故事……

2.2 游戏分析

2.2.1 寻找突破口

    我喜欢《守护者之剑外传-无尽的宿命》这款游戏里面乌乌公主的造型,所以,就先拿这款游戏开刀吧。

    在我看来,要制作一款同人游戏,第一步要做的,应该就是先想办法把这款可爱的游戏残忍的肢解掉,把它的动画,声音,图片文件统统的拆分出来,然后为我所用。当然这其实是他(和谐)妈一种相当不雅的行为,因为原作者大都不喜欢您这样做。所以咱们做的时候,心中要默念“开发大神好”,可以保平安。

    

图2.2-1 游戏开场画面截图

    多么唯美的开场啊,男女主角就这样在下水道里分别了。原风雷小组的2D游戏,画面真的是一级棒,不管之后的迷宫有多么的变态 ,恶心,不可理喻,作为一个美工,我还是会专心的玩到底的。

    这款游戏采用了四方向卷轴式显示方法,鼠标右键可以控制人物上下左右移动,左键控制游戏选项。从游戏的第一印象看来,它基本上包括了以下几种元素:动画文件(都在CD2上面,直接拷出来归档吧),声音(这当然是必不可少的),背景(应该是一副完整的大图),人物连续动画的图片,人物头像的图片,还有其他窗口,鼠标动画等等杂七杂八的小图。

    我们现在要做的是,尽快得到这些游戏元素,把它们变成可以很容易被我们使用的文件形式。(如果观众大人您是80后,应该肯定玩过这款游戏吧,即便是在那个年代,卖的也不贵,还记得小珍那个可爱的大脑袋包装不……如果没玩过也不要紧,那么http://www.91danji.com/game/5187.html,先把游戏翻版吧,哎,可怜的经典,这个网络下载版的貌似没有过场动画- -!)

    游戏的安装方式比较简单,经过分析,安装程序好像就是单纯的把光盘下的游戏文件直接拷贝到硬盘上,这样的话,为了方便操作直接把CD1上面Gs_saga的游戏目录拷贝到硬盘上吧,然后打开(图2.2.2)。

图2.2-2 游戏根目录

 

    可见,这些文件夹除了anp\不知是何物外,其它的命名都不是很费解。

    Bto 应该是战斗场景(battle occasion)的缩写

    Cursor 鼠标动画

    Face 人物头像

    I_face 估计也是人物头像之类的

    Item 游戏物品

    Map 地图

    Wav 声音/音效

    Anp 难道是All Npc Pictures,呵呵

    声音文件貌似都在wav\目录中,这个直接可以留作备用,其他的文件都有什么呢?我们可以用Windows自带的文件搜索功能,搜索*.*,然后查看方式选择“列表”,按类型排序,看一看这款游戏程序里面都有什么类型的文档,或许文件的命名,扩展名可以给我们一些提示。

    经过搜索,发现该游戏共有如下文件类型:

    .15,.pcx,.anp,.btm,.bto,.lar,.mls,.pal,.sat,.scn,.stb等等,还有Face\目录下面的所有文件都没有扩展名。

.pcx是Windows98时代的一种常见的图像格式,这个就不用分析了,.pal应该是调色板数据(pallet),而其它的都不敢断言。

    那怎么办呢,最后我决定尝试从出现的最多的anp格式下手,因为从文件大小和文件命名上可以隐隐的感觉到它们和图像数据有关。

    我选择了cursor\csr_push.anp文件开始尝试,之所以选择它,一是因为估摸着它应该是鼠标按键动画,二是因为它只有1KB左右大小,如果ANP格式所有的文件存储方式相同,并且它又是图像数据的话,那么这小东西就成了“麻雀虽小,五脏俱全”的标准突破口。

    先进入游戏,然后给游戏截个图(这里推荐使用金山游侠),把鼠标的形状从截图中抠出来。

 

图2.2-3 鼠标截图

 

    然后看一看图像宽度是24像素,高度是37像素。接着用十六进制编辑器打开csr_push.anp文件,看到一大堆乱七八糟的数字(图2.2-4),不管它,现在假设这些数字全部都是颜色代码,并且假设我们估计出来的图像大小是对的,写一段显示颜色的程序出来,将这些数字都变成颜色吧。

 

    图2.2-4 csr_push.anp

 

结果总是令人惊喜,从最后显示出来的图像可以看出,我们的猜想是正确的。(如图2.2-5)屏

幕上依稀的出现了一幅类似于鼠标箭头的奇怪图案,虽然颜色和形状多少有些对不上……

    这样我们就找到了一个突破口,暂时可以假设,所有的以.anp扩展名结尾的文件都是含有图片的。

  图2.2-5 直接读点 

2.2.2 图像替换大法

    既然知道.anp文件中存有图像,那么有什么软件可以让我们快速看到.anp文件中存储的到底是什么数据呢?作为一个程序员,也许您首先想到分析图像格式然后编码显示,但是作为一个厨子,我现在就是纯粹的想看一看那里面究竟是啥,编码根本无从下手啊。

    其实,想来想去,貌似最标准的图像查看器就是游戏的主程序本身,有时候它会帮助您做很多事情。前言中我已经提醒过您(这里还是用敬语吧,因为马上您就该骂我了),这不是一本专业的游戏开发教程,下面我要告诉您,我是这样知道anp里的真相的:

    把游戏根目录里面的anp文件一个一个的删掉,删了以后会出错的,就留着。可恶啊,不断删文件然后运行的时候,经常出现莫名其妙的错误,而且一不小心就自动关闭了。(请游戏开发者不要诅咒我,我知道没有一个正常人会这样打游戏的)

    一下午过去了,最后游戏被删改的面目全非,不过这样就方便以后的操作了(最精简版的运行程序放到了本系列附件目录下的2.2.2文件夹)。

    您可以将任意一个想要查看的.anp文件命名为jsa01p.anp,放到anp\目录下,然后运行游戏,读取第二个存档查看。如果嫌背景碍眼,可以将map\day\的map001d.pal_纯灰或map001d.pal_纯绿文件重命名,将扩展名改为.pal然后替换原有的map001d.pal文件,将背景换为纯灰色或绿色。

    这样替换下来,很快我们就又有了新的发现。原来一个anp文件中不但存储了图像,而且是包含声音,图像序列,动画显示方式,甚至特效,特殊操作的一个集成包,看来日后的提取工作任务艰巨啊!

    当然这种查看方式肯定不是对所有游戏都适用的,但是制作同人游戏,本身就不是什么正规的开发,因此我们从不正规的角度得到预期结果的方法是无可非议的。

    接下来要做的就是要给游戏开苞了,然而,毕竟咱们走的不是从正规渠道,第一次往往是快乐中夹杂着痛苦。

 

2.2.3 显示第一幅图片

    还是从csr_push.anp文件做文章,既然知道它里面有图像数据,那么我们就想办法将它搞出来。经过简单的分析,可以大体推算出,文件第12H字节开始处的1A 00 00 00四字节,和文件16H字节开始处的27 00 00 00四字节,应该是图像的宽度和高度。

    这里要说明的是,数值在文件中的储存方式,一般都是由低位向高位存储,假如有一个长整型数值000012DFH,存储到文件中,就变成了DF 12 00 00,所以,csr_push.anp中的那幅鼠标图案的宽度应该是26像素,高度是39像素。那么图像所占的字节数应该是二者相乘,也就是1014。

好巧,1014的十六进制表示为03F6,正好文件中紧跟着又出现了03F6(如图2.2-6中间方框圈中的)。

图2.2-6 标记

那就说明,这个F6 03 00 00四字节很可能用来存储图像存在内存中的实际大小。

    可是,很快就出现了新的问题啊,这个文件的总大小才738字节,所以如果图像数据的实际大小是1014的话,它应该是经过压缩后存档的。提到图像数据压缩,莫名的感到这似乎是一个很高深的问题,不过好在游戏图像为了保证画质无损,一般应该是采用“行程压缩”的办法。举个简单的例子,如果图像中连续出现了16个红色(颜色号假设是BE),那么存储到文件中的形式就可能是10 BE,这样就省去了14个字节。

    分析这些图像文件,最重要的除了有耐心,还必须细心,我试着打开了其它的一些同类型文件,发现F00FAA77这四个字节很是碍眼,几乎每个文件中都有,而且总有一个是在图存储像实际大小的那四个字节之前,而紧跟着出现的,又很可能是代表着图像数据的结束。那么我们暂且不管它是代表着什么,就暂且当它是每一幅图像数据开始和结束的标记吧。

    请看图2.2-6中紫色标记的四个字节,它代表十进制的636,而636又恰好是到下一个F00FAA77标记之前的字节数,那么我们几乎可以打保票,右边方框圈中的就是之后的图像数据存储大小,而中间方框圈中的就代表图像实际像素数。

    暂时还不清楚它是怎么压缩的,当然我之前也不了解那些常用的行程压缩规范。也许专业的程序员看到这些数据以后,马上就拍案而起,“这不就是标准的RLE存储嘛,这样这样就可以搞定了”。——事实上WOW还火的时候,我曾到WOW的群中问过,求真相帝赐我图像读法!但是众神们除了二楼的沙发和三楼回复“自己试吧”之外,后面的跟帖全是“兰州烧饼”……等等,现在的切糕吧也许有线索。

    这时最好的办法就是把这段数据打印出来,坐在一个角落里慢慢的想象,这是一个大侦探游戏,然后一点一点的找规律。

如果是行程压缩,应该会碰到这两种情况,一种是颜色值有重复,那么就存为颜色数量紧跟颜色编号,如果是孤独的一个颜色点,之前之后都没有重复的配对,那么就存为单一的颜色编号,然后利用小学五年级的四则混合运算,反复的试验,很快可以找到这样的规律:

    当读取的当前字节值X>192时,那么下一个字节就代表颜色编号,并且连续显示(X-192)次;如果当前字节值X<=192时,那么当前字节值本身就是颜色编号。接着我们试着编码去正常的显示这些颜色编号。

    (代码在Turbo C++ 3.0中调试通过,详见附件\2.2.3\code\简单读取_没有读取色盘.CPP

    …………    …………        /*open .anp file*/        if ((fp = fopen("csr_push.anp", "rb+"))== NULL)            printf("Cannot open anp file.\n");         /*Load width*/        fseek(fp,16+2,0);        nWidth=fgetc(fp);        /*printf("%d",width);*/         /*Load height*/        fseek(fp,16+6,0);        nHeight=fgetc(fp);        /*printf("%d",nHeight);*/         /*mem set*/        img=(char *)malloc(nWidth*nHeight);        imgBufPtr=img;         unsigned char ch1,ch2;        unsigned char temp1,temp2,temp3,temp4;        fseek(fp,2*16+12,0);/* csr_push.anp 's start byte */         do{        /* if [F0 0F AA 77], then break */        temp1=fgetc(fp);        temp2=fgetc(fp);        temp3=fgetc(fp);        temp4=fgetc(fp);        if(temp1==240 & temp2==15 & temp3==170 & temp4==119) break;        fseek(fp,-4,1); /* return to current posit */         ch1=fgetc(fp);        if(ch1>192) /* &HC0=192 */            {                ch2=fgetc(fp);                for(int n=192+1;n<=ch1;n++)                    {                        *imgBufPtr=ch2;                        imgBufPtr++;                    }            }        else            {                *imgBufPtr=ch1;                imgBufPtr++;            }         }while(1);         fclose(fp);         imgBufPtr=img;        for(j=0;j<nHeight;j++)            for(i=0;i<nWidth;i++)            {                fos_putpixel(i,j,*imgBufPtr);                imgBufPtr++;            }    …………    …………


   

    这样我们就成功的读取了ANP包中的第一幅图片,不过现在还不能高兴的太早,因为这个图案的颜色还是和我们实际想要的效果相差甚远,我们在读取图片的时候并没有考虑颜色编号对应颜色的问题。

    接下来要做的就是找到这些颜色编号的对应色盘。色盘文件肯定会被保存在某一个程序档案中的,不过我们想要找到它可并非容易之事。我们可以这样分析,它可能被保存在某一个极其不显眼的位置,甚至保存在游戏的主程序中,但是在游戏运行的时候,我们看到游戏画面的时候,它必然已经被读取到了内存中。这样想就好办了,直接用截图软件截取一张游戏图片吧,只要截图软件支持截取256色图,它在截取图像颜色编号的同时,必然也截取了色盘。

    该游戏的截图大小是640*480(像素点),用十六进制编辑器将截取的图片打开,从0036H到0430H这段数据就是色盘对应的颜色值。从数据排列可以得知,每种颜色对应四个颜色分量,从左到右分别是蓝色,绿色,红色,00,我们可以直接把这些数据拷贝出来备用。不过我是使用某绘图软件将色盘文件直接导出的,最终生成的色盘文件是按每颜色三个字节存储的,并且排列顺序是按从左到右红色,绿色,蓝色,见附件\2.2.3\pal.act,您可以直接使用(是的,我承认我是用PhotoShop将它导出来的,但是我在影楼里面打工,我用的应该不是盗版的PS!……至少不是我自己安装的。)。

    在早期256色的游戏中,色盘一般被分为两部分,一部分颜色用来显示地图场景,另一部分颜色用来显示角色和精灵,因为不同的场景对应的颜色表可能会发生不同的变化,而精灵的颜色一般是不变的。在本游戏中,色盘的前半部分就是用来显示场景的,它会随着场景的不同而变化,而在附件中存放的pal.act文件,我将所有的场景色盘统一改成了亮绿色(FF00FF)。

    有了调色板,我们只需对现有的读图代码稍作改动即可显示出颜色显示正确的图片。

    (代码在Turbo C++ 3.0中调试通过,详见附件\2.2.3\code\简单读取_加入色盘.CPP

  

  …………    …………        /*open .act file*/        if ((act = fopen("pal.act", "rb+"))== NULL)            printf("Cannot open image file.\n");         fseek(act,128*3,0);         /*load palette*/        for(index=128;index<256;index++)        {            palette[index].red=( fgetc(act)>>2 );            palette[index].green=( fgetc(act)>>2 );            palette[index].blue=( fgetc(act)>>2 );        }         for(index=128;index<256;index++)            SetPaletteRegister(index,(RGBColorPtr)&palette[index]);         fclose(act);         /*open .anp file*/        if ((fp = fopen("npc_om1r.anp", "rb+"))== NULL)            printf("Cannot open image file.\n");         /*Load width*/        fseek(fp,16+2,0);        nWidth=fgetc(fp);        //printf("%d",w);//debug         /*Load height*/        fseek(fp,16+6,0);        nHeight=fgetc(fp);//tu2 pian4 kuan1        //printf("%d",h);         /*mem set*/        img=(char *)malloc(nWidth*nHeight);        imgBufPtr=img;         unsigned char ch1,ch2;        unsigned char temp1,temp2,temp3,temp4;        fseek(fp,2*16+12,0);         do{        /* if [F0 0F AA 77], then break */        temp1=fgetc(fp);        temp2=fgetc(fp);        temp3=fgetc(fp);        temp4=fgetc(fp);        if(temp1==240 & temp2==15 & temp3==170 & temp4==119) break;        fseek(fp,-4,1); /* return to current posit */         ch1=fgetc(fp);        if(ch1>192)            {                ch2=fgetc(fp);                for(int n=192+1;n<=ch1;n++)                    {                        *imgBufPtr=ch2;                        imgBufPtr++;                    }            }        else            {                *imgBufPtr=ch1;                imgBufPtr++;            }        }while(1);         fclose(fp);         imgBufPtr=img;        for(j=0;j<nHeight;j++)            for(i=0;i<nWidth;i++)            {                fos_putpixel(i,j,*imgBufPtr);                imgBufPtr++;            }    …………    …………


 

   

    图2.2-7 加入了色盘

   

    哎,这时您可能要问了,不是说正确显示颜色吗,我看图2.2-7中的颜色很明显有一些误差啊!嗯,这就对了,因为这个截图是2006年的,当时我一直在纠结我解压的方式错在哪里,为什么总是有那么几个颜色显示的不对,应该是我对这些数据压缩方式理解的有出入,但是后来也并没有深究它是怎么压缩的,而是从其它的地方做了一些手脚,临时性的解决了这一问题。

    对于anp扩展名结尾的文件,只要在读取的字节数据小于192的情况下,也就是读取单个颜色编号的时候这样做,就可以纠正误差:

    颜色编号=(当前值-128)*4

    对于Face\目录下的文件,不需要这个转换。

    不要问我为什么,您就当是原游戏开发小组故意在这里做了点手脚吧,而我的解决方案是纯试出来的。

 

2.2.4 彻底拆解ANP

    WOWer们都懂得,艰难的第一次过后,我们就可以开始肆意的OOXX了,嘿嘿嘿嘿:)

    在这一节,我们的目标是逐个分析ANP文件中的每一个对我们有用的未知量的含义,然后归纳出它的实际存储格式。通过测试,我们现在已经知道ANP文件中至少会包含声音和动画序列的描述。那么我们就先从这里下手。

    通过筛选,我们可以看到bto目录下面有两个可疑的同名文件——shtm02%.pcx和shtm02%.anp,shtm02%.pcx这幅图片很能是游戏开发人员无意中留在发行光盘上面的测试文件,它应该就是shtm02%.anp包中的所有图片序列,数一数一共有58个连续的小图。好的,就从这里入手吧,为了方便调试,我们把这两个文件重命名为jsa01p.anp,拷贝到我们那个面目全非的测试用精简版游戏的anp目录下。

    运行后,如图2.2-8。好巧,这个文件中既有动画又有声音。

图2.2-8 游戏截图

 

    使用16进制编辑器打开shtm02%.anp文件,惊讶的发现第13个字节处值为3AH,3AH十进制就是58,这个位置记录的很有可能就是本包中的图片总数。对于作者这双慧眼,请您不要过分的惊讶,其实我只是这么写系列而已,这个数值是我测试了好久才定性的....

    文件开始的前的8个字节应该是两个长整型的数值,十进制值是700和480,目前来说对我们好像没有什么实际意义,暂且不管它是什么。第9至12字节为01 00 00 00,而当我随机打开其它的很多扩展名是anp的文件后发现,大都这四个字节只有以上1和0两种取值,所以这四个字节很可能是某个什么什么东西的标志量,这样说可能有点不负责任,不过我们都不是神仙,谁也不能就此定性,这四个字节就是什么什么的(对天发誓这不是在凑字儿,这不是在凑字儿)。

    紧跟着的四个字节我们已经知道,代表anp包中的图片数量,再往后跳过两个值为零意义不明的字节,后面12H到19H这8个字节我们也明确了它们的含义,分别是第一幅图片的宽度和高度。关于那两个不明意义的字节,在我查看了很多anp文件后,发现这两字节始终为0,而且文件开始的16个字节貌似都是规定整个图包的全局参数的(16字节,完美存储,不需要凑位数),所以这两个始终为零的字节很有可能是当时开发人员编写存储结构时为了提高计算机的处理速度而使用的2个字节的占位符,当然,这里并不能断言。接下来的10个字节,从现在来说还是不能推测它们的作用,不过F00FAA77这四个字节暂时可以被我们认为是图片数据开始和结束的标记。再往后24H到2BH,前四字节记录了图像的实际像素数,后四个字节记录了压缩后存储的图片所用的字节数。

    从3CH开始到A1结束,这一大块是第一幅图片的数据块,然后是F00FAA77,第一幅图片的数据到此结束。我们可以想到,这个图包的数据肯定是按照某种规约来存储的,那么我猜接下来的两个字节应该是那个两个“占位符”。我们来看看后两个字节到底是什么……好巧- -!

    经过简单的观察,发现大体上ANP包中的图片都是如上方式存储的,那么先不管三七二十一,写个预览程序试试。代码依然不是很复杂,只要在原有的读第一幅图的基础上加一组循环就好了,循环变量的数值就是0CH到0FH这四个字节对应的数值。

    这次的编码环境我选择了VB6,为什么选择VB6呢?我也不知道为什么选择VB6。(源程序在附件\2.2.4\code\VB_试着预览ANP中的图片序列)

    我们要清楚每次编码的目的,在我们尚未了解ANP全格式的时候,现在的代码无论写的多么完美也是白费……所以我写的那些代码超级粗糙,不过,还好能用。编好了这么一个预览程序,我顺便又尝试了一下用同种方式读取游戏face\,map\Day\,等目录下面的一些非ANP格式文件,发现它们的图像存储方式都是一样的,只是打包形式不同,当然完全正常的读出它们的方法尚待研究。

   

图2.2-9 欣赏一下目前的成果~

 

    不能高兴的太早,我们的行动才刚刚开始。虽然感觉读图的程序已经万无一失,但是奇怪的是总是有那么几个文件读取的时候出错,比如anp\dsa01p.anp文件,按照我们以上的推断,这个文件中应该存有10个图片,并且出现20个F00FAA77的标记,可是实际上这个文件中这样的标记只出现了16个(8个图片),难道是这文件本身就是格式错了?废话,当然不是了。其实一开始我们并没有肯定的说F00FAA77就是唯一的图片开始和结束标记。用16进制编辑器打开dsa01p.anp,仔细观察后发现这一段很诡异(图2.2-10):

 

   

图2.2-10 圈中的部分

 

    原来本应该是F00FAA77的位置变成了31000000,而这个标记后面的两组数的值又很大,显然不是这段数据的数据量表示。我们大胆的猜一下,莫非31000000就是记录的后面的图像数据大小?如果是这样,很快我们可以算出,原来这块数据中竟然包含了两幅图,第二幅图的开始标记是51000000。消失的两幅图就这样找回来了,不过它又是怎么存储的呢。我们看第一幅图的宽度是7,高度也是7,而标记中31H正好等于宽度和高度相乘;第二幅图也是。那么同时也说明了,这两幅图的数据应该是直接存放,没有经过压缩的。(游戏开发者的打包工具应该支持两种不同类型的图像混合打包)

    图像部分除了地图还不在我们的考虑范畴之内以外,到目前为之应该可以告一段落了,仍然返回到shtm02%.anp文件的编辑状态,依照前几步的经验,我们将目标定位到最后一幅图的末尾。这时可以发现……后面怎么还有那么长的数据啊!!

    还算幸运,RIFF和WAVE几个字符忽然出现在我们眼前,这不是WAV格式音频的文件头么,看来ANP包内的声音文件存储的并不是索引而是实际的数据,怪不得后面还有这么多数据呢。好了让我们分析一下,按照编码人员的惯性思维,声音文件的存储方式应该和图像类似,文件的3BECEH和3BECFH这两个字节十有八九是本包中也就是接下来的数据中所包含的声音文件数量。

    搜索后发现,果然不出所料,后面连续出现了6个声音文件,而且综合所有的声音数据块发现,存储声音总数的就只有这两个字节,后面的5个字节(0100010000)意义不明,而这5个字节之后的四个字节,记录了当前声音数据块的总大小。有兴趣的话可以把这个数据块拷贝成单独的文件,扩展名写为.wav就可以播放了(当然,别告诉我您机子上没有任何播放器软件并且没有发声装置)。

    声音的问题就这么华丽丽的解决掉了。

    到了文件最后的部分,数据排列的异常整齐(图2.2-11),我们就喜欢这样的排列,估计分析

图2.2-11 划线部分之上是最后的声音文件末尾

 

的难度会降低不少。

    既然这个包中包含了动画,那么在开始的地方,它很有必要记录一下动画的总帧数,而每一帧的开头必须要记录当前帧包含的图片数,并且在帧的每一层,必须要有可以定位某一图像的编号。不难发现,6D23CH和6D23DH又是两个恒久为零的占位符,在后面的两个字节应该就是记录的总帧数,6D240H到6D243H记录的是第一帧的图片数量,或者说层数,我们使用游戏变速齿轮将游戏的帧速放慢8倍后,会看到真相,这里应该是影子层和人物层。

    那么之后都会是些什么样的数据呢?我猜开头的32个字节肯定是这一帧的攻击判定框,那有人要问了,为什么你会猜到而我就猜不到呢?原因很简单,如果那人高中时因为打电玩被万恶的学校开除,大学又因为电玩而挂掉一半以上的科目的话,那么他会猜得更快。

但是我猜错了……

那么只好通过截图分析一下,随便截取一帧(哎,不要震精,我并没有说 我截取的是shtm02%.anp文件中的图,毕竟分析乌乌公主的帧时心情会比较好嘛),图2.2-12,按照我们的猜想,如果这一帧是由三幅图片构成的话,那么这段整齐排列的数值里面,必然至少有三组坐标。

   

    图2.2-12 帧的图层猜测

将数据稍作改动后运行游戏,发现原来0F010000和E4000000是影子的坐标。是的,最终我选择的办法就是这么反复的改动,运行游戏,看发生了什么变化,然后记录下来。

    经过反复的试验,最终我们获得了如下信息:

    6D254H-6D257H 图片编号(从0到图片总数-1)

    6D260H-6D263H 图层特殊效果

                  00 正常

                  01 镜像

                  02 半透明显示1

                  03 镜像+半透明显示1

                  04 半透明显示2

                  05 镜像+半透明显示2

                  06 半透明显示3

                  07 镜像+半透明显示3

                  08 白色闪光

                  09 镜像+白色闪光

                  XX 其它的不是很确定

                  透明度:半透明显示3>半透明显示2>半透明显示1

    6D289H-6D28BH 帧延时,应该以是每一帧的正常播放时间为单位

    6D28EH-6D28FH 声音编号(从0到声音总数-1)

 

    当然,我们只是要获得所需的一些信息,而并不需要完全弄懂包中每一个字节都代表什么意义,如果这样会花费相当多的时间。

    现在我们所掌握的信息,对于拆解ANP包应该暂时还够用,那么接下来我们要整理一下思路,看看如何读取它。(详细的格式图解说明见附件2.2.4\ANP图像格式-shtm02%.anp.bmp)

 

 

 

2.2.5 制作ANP拆解工具

    了解了ANP的存储形式,我们就可以编写软件来处理这些文件了,在这里首先我们简单的回顾一下计算机的数据类型。

    我们将会用到的无非是这三种:8位字节型,16位整型,32位长整型,在大多数汇编语言中,我们往往无需考虑某个机型对应的整型是多少位的,而在高级开发语言中,这里还是要注意一下,马上要用到的开发工具是VB6,在VB6中,Byte类型估计是8位,Integer类型可能是16位,而Long类型大概是32位吧,至于有无符号问题,也都拿不准是吧,所以使用的时候要小心才对。

    我们试着将ANP的格式结构化,大体上将它分为总文件头,图像文件头,图像数据块,总声音文件头,声音文件头,声音数据块,总动画文件头,帧文件头,帧数据块几小部分,根据之前的归纳,编写数据存储结构(VB6)。

   

   'ANP格式        Type ANP_HEAD        PkgWidth As Long    '(仅猜测)包整体图片宽度        PkgHeight As Long   '(仅猜测)包整体图片高度        Unknown As Long     '未知        PicCount As Long    '包中包含的图片数量    End Type        Type ANP_IMGHEAD        NONE(0 To 1) As Byte    '占位符        ImgWidth As Long        '图像宽度        ImgHeight As Long       '图像高度        UnknownA As Long         '未知        UnknownB(0 To 1) As Byte '未知        Flag As Long            '图像开始标记    End Type        Type ANP_IMGDATAHEAD        PixelCount As Long  '实际像素数        DataSize As Long    '压缩后的图像字节数    End Type        'Dim ANP_IMGDATA() As Byte        '压缩的图像数据    'Dim ANP_ENDFLAG As Long          '图像数据结束标记    'Dim ANP_SOUNDCOUNT As Integer    '包内声音文件总数        Type ANP_SOUNDHEAD        Unknown(0 To 4) As Byte '未知        SoundFileSize As Long   '声音文件大小    End Type        Type ANP_FLASHHEAD        NONE(0 To 1) As Byte    '占位符        FrameCount As Integer   '总帧数    End Type        'Dim ANP_LAYERCOUNT As Long  '每帧的层数        Type ANP_LAYER        X As Long           '图片X坐标        Y As Long           '图片Y坐标        UnknownA As Long     '未知        UnknownB As Long     '未知        ImgId As Long       '图片编号        UnknownC As Long     '未知        UnknownD As Long     '未知        Spe As Long         '特殊效果    End Type        Type ANP_FLASHFRAMEEND        UnknownA As Long             '未知        Delay As Long               '帧延迟        UnknownB(0 To 1) As Byte     '未知        SoundId As Integer          '声音编号    End Type        Type ANP_FLASHFRAME        LayerData() As ANP_LAYER        '层信息        FrameEnd As ANP_FLASHFRAMEEND   '帧数据结尾信息    End Type        '读取时还会用到的一些类型        Type ID_RLE_DATA        isRle As Boolean    '数据是否压缩        Width As Long       '图像宽度        Height As Long      '图像高度        DataSize As Long    '压缩后数据大小        PixelCount As Long  '实际像素数        Data() As Byte      '图像数据    End Type     Type ID_SND_DATA        Data() As Byte      '声音数据    End Type     Type ID_BMP        Width As Long       '图像宽度        Height As Long      '图像高度        Data() As Long      '图像数据    End Type        Public Rle() As ID_RLE_DATA     '读出的图像数据    Public Sound() As ID_SND_DATA   '读出的声音数据    Public Bmp() As ID_BMP          '图像数据


    

    ANP_HEAD结构用来读取包中的图片数量,我们需要利用这个结构中的PicCount变量作为循环变量,配合ANP_IMGHEAD结构来遍历包中的图片数据。ANP_IMGHEAD结构记录了图像的宽度,高度还有开始标记,最后这个Flag标记当值为2007633904(F00FAA77标记)的时候,表示之后的图像数据是压缩过的,这时我们还需要再引入一个存储结构ANP_IMGDATAHEAD,用它读取图像的实际像素数和压缩后的字节数,然后才可以进行数据读取,而读取图像数据之后,还要读一个结束标志;当Flag标记值为其它时,表示之后的图像数据是按点存储,没有压缩过的,Flag本身的值,代表之后图像数据所占的字节数。

    声音数据是紧跟着图像数据末尾的,我们先要使用一个ANP_SOUNDCOUNT变量获得本包中所含的声音文件总数,ANP文件中,记录声音个数使用了2个字节,所以这个ANP_SOUNDCOUNT的类型是16位整型。我们还是要像读图片数据的方法一样,利用ANP_SOUNDCOUNT变量,配合ANP_SOUNDHEAD结构遍历所有的声音文件。

    帧数据是紧跟着声音数据末尾的,我们使用ANP_FLASHHEAD读取包中总帧数,然后配合ANP_FLASHFRAME结构读取包中所有的动画参数。帧数据的读取方式和图像,声音数据大同小异,这里不再详述。

    除了这些完全对应ANP文件存储格式的结构外,我们还另外定义了三种存储结构,用以存储每次读取出来的图像和声音数据,方便以后的进一步处理。

    ID_RLE_DATA中除了记录图像字节数据的data变量外,还增加了一个布尔型的isRle变量,用来标记当前存储的图像数据是否压缩。而ID_SND_DATA和ID_BMP准备用来存储声音数据和最终转化成RGB颜色模式后的图像数据。

    数据结构整理完之后,就可以开始尝试编写工具读取ANP文件了(还是继续拿shtm02%.anp文件开刀)。

    打开来历不明的VB6.0,新建一个标准EXE工程,在工程->部件的控件选项卡中选取Microsoft Common Dialog Control 6.0(sp3),并将这个控件拖入窗体Form1内。然后在这个窗体上绘制按钮1(Command1),Caption设为打开调色板文件和按钮2(Command2),Caption设为读入文件并开始转换。

图2.2-13 界面

   

    我看过很多计算机相关的书籍,很多作者都会在即将插入主题的那一时刻,很邪恶的开始断楼(自断?!)讲述一些很是相关但是绝非重点的东西,当然,如果你以为我会很伟大的直插主题的话,那么你错了,因为我也很邪恶。

    双击Command1,进入代码编辑页面,在这里加入获取色盘文件名以及导入色盘文件到内存的代码。

 

 

   '获取文件名    With CommonDialog1        '只显示ANP文件        .Filter = "ACT Files|*.act"        ' FileName属性的最大长度,用来存储文件名        .MaxFileSize = FILE_LOAD_MAX_SIZE        '显示"打开文件"对话框        .ShowOpen        '如选择了"取消"按钮,则退出本次处理        If .FileName = "" Then            Exit Sub        End If        '记录打开的文件路径        FileName = .FileName    End With    '载入色盘文件    Open FileName For Binary As #1    Get #1, , PalIndex    Close #1


 

    通过设置CommonDialog1的属性,就可以得到被选择文件的路径数据,FileName字符串中记录了要打开的文件的完整路径和文件名。在本程序中,我们使用一个共有256个元素的PalIndex数组来存储读取的色盘数据,元素的类型是ColorIndex,这种类型由红,绿,蓝,三个颜色分量构成。

     '调色板数据块    Type ColorIndex        Red As Byte        Green As Byte        Blue As Byte    End Type        Public PalIndex(255) As ColorIndex 


 

    然后双击Command2,进入按钮2的代码编辑页面。我们将在这里加入获取ANP类型文件名以及转换ANP文件的代码。

 

  

  '获取文件名    With CommonDialog1        '只显示ANP文件        .Filter = "ANP Files|*.anp"        ' FileName属性的最大长度,用来存储文件名        .MaxFileSize = FILE_LOAD_MAX_SIZE        '显示"打开文件"对话框        .ShowOpen        '如选择了"取消"按钮,则退出本次处理        If .FileName = "" Then            Exit Sub        End If        '记录打开的文件路径        FileName = .FileName    End With        '读取并转换ANP文件    LoadAnpFile FileName    '转换ANP文件    TransformAnpFile    MsgBox "ANP转换完成!"


 

    新建一个模块(Module1.bas),将之前整理的ANP文件头数据格式声明放入模块里,并对将要贯穿整个转换过程的相关变量做全局声明。

 

  

  Type ANP_INFO        PkgWidth As Long    '(仅猜测)包整体图片宽度        PkgHeight As Long   '(仅猜测)包整体图片高度        Unknown As Long     '未知        PicCount As Long    '包中包含的图片数量        SndCount As Integer '包中包含的声音数量        FrmCount As Integer '包中包含的动画帧数量    End Type     Public AnpInfo As ANP_INFO    Public anpFlashFrame() As ANP_FLASHFRAME    '读出的层数据    Public Rle() As ID_RLE_DATA     '读出的图像数据    Public Sound() As ID_SND_DATA   '读出的声音数据    Public Bmp() As ID_BMP          '图像数据


 

    其中ANP_INFO是我们自定义的用来记录一些贯穿性参数的结构。

    一些教科书或是专业的计算机书籍中不提倡使用过多的全局变量……但是WOWer可以用。这些全局变量可以方便我们在任意过程中调用,从而省去了一些过程中的参数传递过程。下面我们要实现LoadAnpFile过程。

    Private Sub LoadAnpFile(strFileName As String)    Dim anpHead As ANP_HEAD    Dim anpImgHead As ANP_IMGHEAD    Dim anpImgDataHead As ANP_IMGDATAHEAD    Dim anpEndFlag As Long          '图像数据结束标记    Dim anpSoundCount As Integer    '包内声音文件总数    Dim anpSoundHead As ANP_SOUNDHEAD    Dim anpFlashHead As ANP_FLASHHEAD    Dim anpLayerCount As Long            '打开文件        Open strFileName For Binary As #1                '读取ANP_HEAD文件头,并将信息计入全局变量        Get #1, , anpHead        With AnpInfo            .PkgWidth = anpHead.PkgWidth            .PkgHeight = anpHead.PkgHeight            .Unknown = anpHead.Unknown            .PicCount = anpHead.PicCount        End With                        '读取包内所有的图像,编号从0开始        Dim i As Long        For i = 0 To anpHead.PicCount - 1            '循环读取所有的ANP_IMGHEAD            Get #1, , anpImgHead            If anpImgHead.Flag = 2007633904 Then    '"F00FAA77"                '标记是压缩形式存储的图像数据                                '读取图像文件头                Get #1, , anpImgDataHead                                '读取图像数据                ReDim Preserve Rle(0 To i)                ReDim Rle(i).Data(0 To anpImgDataHead.DataSize - 1)                Get #1, , Rle(i).Data                                '将相关信息计入全局变量                With Rle(i)                    .isRle = True '压缩标记量isRle记为True                    .Width = anpImgHead.ImgWidth                    .Height = anpImgHead.ImgHeight                    .PixelCount = anpImgDataHead.PixelCount                    .DataSize = anpImgDataHead.DataSize                End With                                '读取完数据后,还要读取数据结束标记                Get #1, , anpEndFlag                            Else                '标记是压缩形式存储的图像数据                                            '读取图像数据                '在没有压缩的情况下,flag即为数据大小                ReDim Preserve Rle(0 To i)                ReDim Rle(i).Data(0 To anpImgHead.Flag - 1)                Get #1, , Rle(i).Data                                '将相关信息计入全局变量                With Rle(i)                    .isRle = False '压缩标记量isRle记为False                    .Width = anpImgHead.ImgWidth                    .Height = anpImgHead.ImgHeight                    .PixelCount = anpImgHead.Flag                    .DataSize = anpImgHead.Flag                End With                            End If                    Next i                '读取声音文件数量,并将其存入全局变量        Get #1, , anpSoundCount        AnpInfo.SndCount = anpSoundCount                '读取包内所有的声音,编号从0开始        If anpSoundCount <> 0 Then            For i = 0 To anpSoundCount - 1                                '读取声音文件头                Get #1, , anpSoundHead                                '读取声音数据                ReDim Preserve Sound(0 To i)                ReDim Preserve Sound(i).Data(0 To anpSoundHead.SoundFileSize - 1)                Get #1, , Sound(i).Data            Next i        End If                '读取动画文件头(动画文件数量),并将其存入全局变量        Get #1, , anpFlashHead        AnpInfo.FrmCount = anpFlashHead.FrameCount                '读取包内所有动画帧,编号从0开始        For i = 0 To anpFlashHead.FrameCount - 1                        '读取该帧的层数            Get #1, , anpLayerCount                        '读取动画帧            ReDim Preserve anpFlashFrame(0 To i)            ReDim anpFlashFrame(i).LayerData(0 To anpLayerCount - 1)            Get #1, , anpFlashFrame(i).LayerData            Get #1, , anpFlashFrame(i).FrameEnd            anpFlashFrame(i).LayerCount = anpLayerCount                    Next i                '关闭文件        Close #1        End Sub


 

    可以看到,通过这个过程,所有的图像数据都被保存到了Rle数组中,并且还做了是否压缩的标记;所有声音数据都被保存到了Sound数组中;其它相关的贯穿性参数都被存储到了AnpInfo结构中。这些数组和结构变量的声明都是全局的,可以被之后的转换过程所调用。

    所谓的格式转换,实际上是在为我们日后开发同人游戏时的偷懒而做准备。我们已经基本了解了ANP的存储格式,而这时如果直接对其加以利用,也未尝不可。但是,如果我们想定义新的动画或者做一些新的修改,直接使用ANP格式会很不方便,另外在科技飞速发展的今天,家用计算机的处理速度已经允许我们这些非专业开发员可以不用过分的去考虑游戏程序的“瘦身”问题,我们没有太大的必要为了省出1000个MB的存储空间而去把算法复杂度提高N倍,然后再为了将算法优化30%而吐血身亡,最终沦为烧饼。所以,我们一定要将图像转换为操作简便的标准24位或是32位RGB色。

    下面就让我们来实现这个由繁至简的转换吧!

    要想把这些压缩的图像数据转成RGB色位图文件,就要在解压图片数据的同时,用实际颜色值来替换现有的颜色编号。

   

  

   Private Sub TransImgBitmap(fileType As Integer)    Dim i As Long    Dim m, n, k As Long    Dim ch1, ch2 As Byte            '遍历所有图像数据        For i = 0 To AnpInfo.PicCount - 1                        '申请一个Bmp元素            ReDim Preserve Bmp(0 To i)                        '处理压缩的数据            If Rle(i).isRle = True Then                                '申请Bmp元素的存储空间                ReDim Bmp(i).Data(0 To Rle(i).PixelCount - 1)                                '记录转换后的图像宽度和高度                Bmp(i).Width = Rle(i).Width                Bmp(i).Height = Rle(i).Height                                '开始转换                k = 0: n = 0 '初始化                Do                    '得到第一字节                    ch1 = Rle(i).Data(k)                                        '如果第一字节值>&HC0 则读入第二字节                    If ch1 > 192 Then                                                '读入第二字节                        k = k + 1                        ch2 = Rle(i).Data(k)                                                For m = (192 + 1) To ch1                                                        '转换颜色                            Bmp(i).Data(n) = RGB(PalIndex(ch2).Red, _                                        PalIndex(ch2).Green, _                                        PalIndex(ch2).Blue)                            n = n + 1                                                Next m                                        '如果第一字节值<&HC0 则这一字节即为颜色值                    Else                                                'Anp 文件需要做色盘转换                        If fileType = ANP_FILE_TYPE Then                            If ch1 > 128 Then ch1 = (ch1 - 128) * 4                        End If                                                '转换颜色                        Bmp(i).Data(n) = RGB(PalIndex(ch1).Red, _                                        PalIndex(ch1).Green, _                                        PalIndex(ch1).Blue)                        n = n + 1                                        End If                                        k = k + 1                                    '所有数据转换完成,则退出循环                    If k = Rle(i).DataSize Then Exit Do                Loop                            '处理未压缩的数据            Else                                '申请Bmp元素的存储空间                ReDim Bmp(i).Data(0 To Rle(i).PixelCount - 1)                                '记录转换后的图像宽度和高度                Bmp(i).Width = Rle(i).Width                Bmp(i).Height = Rle(i).Height                                '开始转换                For n = 0 To Rle(i).PixelCount - 1                                        '获得当前字节数据                    ch1 = Rle(i).Data(n)                                        '转换颜色                    Bmp(i).Data(n) = RGB(PalIndex(ch1).Red, _                                        PalIndex(ch1).Green, _                                        PalIndex(ch1).Blue)                Next n                        End If                    Next i                '保存Bitmap图像文件        SaveBitmap 24, AnpInfo.PicCount, CurrentAnpFileName & "_Pic", Bmp()        End Sub


 

    可以看到,TransImgBitmap()过程的实现非常简单,只是单纯的将按字节存储的颜色编号数组转存到一个32位长整型数组,同时将颜色编号替换成编号对应的实际的颜色值。至此所有的图像都转换成了按RGB色彩存储的形式,而所有的图像数据,都被存储到了Bmp()全局数组中。在过程的最后,我们要利用SaveBitmap这个自定义的过程将Bmp()中的图像数据保存为24位或32位的位图文件。

    由于将来我们可能还需要将其它的图像数组保存到位图文件,所以这里设计了一个通用的实现过程,所有使用ID_BMP格式定义的数组,都可以使用这个过程将数据保存成任意名称的24位或32位位图文件。

    这个过程还依赖于其它的三个函数,分别是:CountBmpFileSize(),这个函数用来计算并返回当前的位图文件大小,用来填写位图文件头中的一个记录参数;Get24BitScanLineZeroCount(),这个函数用来返回存储24位图像时,每个扫描行后面需要补齐的0值字节个数;Get3BQCount(),这个函数的存在主要是为了日后看图方便,因为生成序列文件时,如果不将序号的位数对其,在Windows下按文件名排列文件时,bmp10会在bmp8之前,导致顺序紊乱。

 

   

 Private Sub SaveBitmap(ByVal BitSet As Long, ByVal PicCount As Long, ByVal SaveFileName As String, ByRef CurrBmp() As ID_BMP)    Dim bmpFileHeader As BITMAPFILEHEADER    Dim bmpInfoHeader As BITMAPINFOHEADER    Dim n, m As Long    Dim i, j As Long    Dim CurrWidth, CurrHeight As Long    '以下声明如果写在一排就出错,如下    'Dim Color24(0 To 2), Color32(0 To 3), chr As Byte    Dim Color24(0 To 2) As Byte    Dim Color32(0 To 3) As Byte    Dim chr As Byte: chr = &H0    Dim tmpC As Long            '开始存储图像序列        For n = 0 To PicCount - 1                        '初始化暂存变量            CurrWidth = CurrBmp(n).Width            CurrHeight = CurrBmp(n).Height                        '创建一个位图文件            Open SaveFileName & "_bmp" & BitSet & "_" & Get3BQCount(n) & ".bmp" For Binary As #1                        '写24位Bitmap文件头            With bmpFileHeader                .bfType(0) = &H42   'B                .bfType(1) = &H4D   'M                .bfSize = CountBmpFileSize(BitSet, CurrWidth, CurrHeight) '位图文件大小(字节数)                .bfOffBits = 54     '文件头到数据区的偏移量(文件头大小)            End With            With bmpInfoHeader                .biBitCount = BitSet                .biWidth = CurrWidth                .biHeight = CurrHeight                .biSize = 40    '版本号[40_3.0][108_4.0][124_5.0]                .biPlanes = 1   '设备的位平面数。现在都是1            End With            Put #1, , bmpFileHeader            Put #1, , bmpInfoHeader                        '默认情况下,Bmp图像数据文件按列倒序存储            For j = CurrHeight - 1 To 0 Step -1                For i = 0 To CurrWidth - 1                                        '转换颜色值                    tmpC = CurrBmp(n).Data(j * CurrWidth + i)                                        If BitSet = 32 Then                        '分解RGB颜色值                        Color32(2) = (tmpC Mod 256)   'R                        Color32(0) = (Int(tmpC \ 65536))   'B                        Color32(1) = ((tmpC - (Color32(0) * 65536) - Color32(2)) \ 256)   'G                        Color32(3) = 0                        Put #1, , Color32                    Else                        '分解RGB颜色值                        Color24(2) = (tmpC Mod 256)   'R                        Color24(0) = (Int(tmpC \ 65536))   'B                        Color24(1) = ((tmpC - (Color24(0) * 65536) - Color24(2)) \ 256)   'G                        Put #1, , Color24                    End If                                    Next i                                If BitSet = 24 Then                    '补齐扫描行为4的倍数                    For m = 1 To Get24BitScanLineZeroCount(CurrWidth)                        Put #1, , chr                    Next m                End If                            Next j                        '关闭文件            Close #1            Next n            End Sub


 

    24位和32位的位图文件都是由54字节的文件头和数据区域两部分组成,其中文件头记录了一些关于对应文件的一些诸如高度、宽度、颜色位数、文件大小等状态描述,数据区记录了图像的颜色描述。在第一章已经提到过,这两种位图文件的数据区描述方式并不相同,但是SaveBitmap()这个函数为了通用性,将两种保存方式结合到了一起,所以在转换颜色值时还有每个扫描行的实际颜色数存储完毕时,都对两种位图描述做了区分处理。

    之所以在这里要转换颜色值,是因为在存储24位图时,我们要存储的颜色值的源值是4个字节,而实际要写入3个字节,在VB6中,由于作者的知识范围限制,未能找到一种24位的数据类型,而不得不使用3字节的字节数组。那有的朋友又要问了,为什么存储32位的位图也要进行数值转换呢?那么,很淡定的告诉您,因为之前颜色值我存的格式不对,正确的按每8位应该是RGB0,而我存成了0RGB,就这么简单。

    当然本代码乃至本系列中出现的所有代码都有诸多可以被改进和优化的地方,如果您有充足的时间,要将程序表达至极致,没有人会阻拦您,但是本系列本着非专业和时间至上的原则,对部分无关重心的代码要求仅仅是能运行就好了,读者请见谅。

    言归正传,这里要注意一个小小的细节,对于Color24和Color32两个字符数组的声明,如果将二者写在同一行,如下:

    Dim Color24(0 To 2), Color32(0 To 3) As Byte

就会出现莫名其妙的数组元素负数值的错误,而写成两行,如下:

    Dim Color24(0 To 2) As Byte

    Dim Color32(0 To 3) As Byte

错误就会避免。为什么会这样呢?……是呀,为什么会这样呢。到此就完成了对ANP包中所有图像的析出工作。(VB的类型声明不同于C,Dim a, b as Byte,表示的是Dim a as Variant 和 b as Byte的意思)

    相对于图像而言,声音文件的析出那就再简单不过了——TransSnd()过程用来保存所有的声音文件,代码应该没有必要注解。

    最有趣的是帧动画的析出部分,其实合成后的动画可能对日后的同人开发用处不大,我本想只是顺便处理一下,忽略声音和帧延时,将它们导出为动画序列,以作日后动画合成时的参考,结果意外的又发现了ANP文件中几个Unknown属性的意义,这真是有心钓马马不从,无心泡姊姊成双啊。

    首先为了导出图片方便,我们定义一个新的ID_BMP类型数组。

   

    Public Flash() As ID_BMP        '动画图像数据

 

    不知观众大人们有没有接触过Flash动画的制作,这个Flash()数组就可以作为我们的“舞台”,我们下一步要做的就是将每一帧中的每一层以不同的方式或特效叠加到舞台上。恰恰就在这时新的问题出现了,我们的“舞台”长度和宽度如何找到呢,很显然,目前ANP包的未知量中,只有ANP_HEAD结构中的PkgWidth和PkgHeight最为合适。我们先不考虑每层的特效处理,粗略的实现转存动画帧的TransFlash()过程。这个TransFlash()过程是完全依赖于类似于Bmp()等全局数组和变量的,所以请读者注意过程调用顺序。(这里的代码并不是要向大家演示某种OOP或者BPEL,AOP,BPPPP之类的开发思想,如果读者实在忍受不下去了,就自行修改吧)

   

    '转换动画到静态的动画序列    Private Sub TransFlash()    Dim n As Long    Dim i, j As Long            '遍历所有帧        For n = 0 To AnpInfo.FrmCount - 1                        '创建一个静态帧图像数据存储区            ReDim Preserve Flash(0 To n)            Flash(n).Width = AnpInfo.PkgWidth            Flash(n).Height = AnpInfo.PkgHeight            ReDim Flash(n).Data(0 To AnpInfo.PkgWidth * AnpInfo.PkgHeight - 1)                        '初始化背景色            For i = 0 To AnpInfo.PkgWidth * AnpInfo.PkgHeight - 1                Flash(n).Data(i) = ALPHA            Next i                                    '遍历每帧所有图层            For i = 0 To anpFlashFrame(n).LayerCount - 1                                '将图层数据绘制到舞台上                DrawNormal Flash(n), anpFlashFrame(n).LayerData(i)                 '帧延时的处理                If anpFlashFrame(n).FrameEnd.Delay <> 0 Then                    'TODO:                    '记入log文件                End If                                '本帧声音播放的处理                If anpFlashFrame(n).FrameEnd.SoundId <> &HFFFF Then                    'TODO:                    '记入log文件                End If                        Next i                    Next n                '保存Bitmap图像文件        SaveBitmap 24, AnpInfo.FrmCount, CurrentAnpFileName & "_Fra", Flash()        End Sub            Private Sub DrawNormal(ByRef scr As ID_BMP, ByRef layer As ANP_LAYER)    Dim i, j As Long    Dim lWidth, lHeight As Long    Dim sWidth, sHeight As Long            '临时变量-层宽度,层高度        lWidth = Bmp(layer.ImgId).Width        lHeight = Bmp(layer.ImgId).Height        '临时变量-背景宽度,背景高度        sWidth = scr.Width        sHeight = scr.Height                For j = 0 To lHeight - 1            For i = 0 To lWidth - 1                            '透明色键不绘                If Bmp(layer.ImgId).Data(j * lWidth + i) <> ALPHA Then                    scr.Data((layer.Y + j) * sWidth + layer.X + i) = Bmp(layer.ImgId).Data(j * lWidth + i)                End If                        Next i        Next j        End Sub


 

图2.2-14 舞台和各层之间的关系

 

    好了,迫不及待的先看一下大体的效果,结果又出现了莫名其妙的问题。影子,人物和剑光三个图层合成时竟然出现了一些错位,难道每一层图片对应的原点坐标是不同的吗。

 

图2.2-15 shtm02%第三帧(程序中编号是2)

 

    图2.2-14中所示的h,经过手工粗测,值大概是74像素到77像素之间。这个不可思议的现象使我不得不又重新关注起原先总结的ANP文件格式中那些被忽略掉的Unknown属性们。还是拿shtm02%.anp下手,很快,数据被锁定在6D350H和6D353H这四个字节上(对应ANP_LAYER结构的UnknownB)。FFFFFFB4十进制值为4294967220,错,先不要盲目的去转换进制,不要忘了计算机课刚开课不久时讲到的计算机存储负值的形式。如果这个是个有符号数,并且是复数的话,那么我们将它转换成二进制,然后按位取反再加1,这样得到的最终结果是-76。哈哈,正是我们想要的,不难推断,它之前的四个字节应该是X方向的偏移量,我们将这两个属性分别重命名为XOffset和YOffset。

    我们只要在绘制图像的时候加上这组偏移量,就可以得到准确的动画帧截图了(注意有些图层的显示是需要剪裁的,叠加的时候还要注意处理数组下标越界的问题)。

    (图解ANP文件存储格式见附件\2.2.5\ANP图像格式-shtm02%.anp.bmp)

    …………    …………    scr.Data((layer.Y + layer.YOffset + j) * sWidth + (layer.X + layer.XOffset + i)) = Bmp(layer.ImgId).Data(j * lWidth + i)    …………    …………


 

    运行后,我们就可以看到一套完整的动画序列了。可是就当我们观看这组动画序列图片时,细心的话可以发现,除了我们已知的特效外,仍然有些地方似乎与游戏实际的显示不同,我们比较一下第38帧,如图2.2-16和图2.2-17。

图2.2-16 第38帧导出

 

图2.2-17 游戏中第38帧

   

    可以发现,这里除了半透明图层外,还用到了拉伸图层的特效,那道半透明光影很明显是经过X方向和Y方向拉伸的。于是我们对应ANP的数据文件,就又确定了两个属性,ANP_LAYER结构中的UnknownC属性和UnknownD属性应该定义为,实际显示的图层宽度和实际显示的图层高度,我们将这两个Unknown属性重新命名为DispWidth和DispHeight。

    和这个图层对应的Spe属性值为44H,我们不能推断它的具体含义,不过可以保守的认为44H代表本图层为半透明拉伸层。这个Spe属性的值很可能有某种取值规范,但是由于时间的因素,我们没有必要过多的去刻意研究它。

    最后,如果有闲心,还可以在转换时把帧的层特效小小实现一下,虽然这是无关紧要的事情。(场景文件,Face目录下各文件格式相对比较简单,有兴趣的读者可以自行研究,没兴趣的话就直接使用下一章提供的转换工具,本系列不再详述。本节程序和源代码保存在附件\2.2.5\code\LoadANPTest\)

 

 

 

2.2.6 风雷系列游戏图像提取工具

    通过前面的步骤不难发现,对于一个业余的游戏开发爱好者来说,分析游戏中的这些莫名奇妙的诡异数据包确实是一件很令人头痛的事情。所以本作者就替代各位读者把这个“罪”义无反顾的承受了下来。

    这一节我们介绍几款针对风雷小组开发的一些游戏的原创图像(声音等)提取工具,供大家学习使用和参考,我就不假装的再说一句“仅供学习研究,请24小时之内删除”了。有的时候有些话不说也罢。

    ……

    我还是强调一下吧,本系列所有涉及原作的不雅行为及由此所产生的程序,描述等仅供学习研究,不得用于商业用途,如有侵权嫌疑,请果断删除。

    GSPkg2Sth V1.0

    (程序和完整源代码在附件\2.2.6\GSPkg2Sth\)

图2.2-18 GSPkg2Sth V1.0

 

    这个小工具就是前面几节我们探讨的图像声音动画提取工具的加强版,它适用于《守护者之剑外传至无尽的宿命》和《守护者之剑》两款游戏,不但可以将anp结尾的文件包分解开,还可以将Face目录下的文件以及地图场景文件中的图片统统转换成24位或者32位的位图文件。

    但是不得不承认,尽管我们在猜测文件存储格式着实费了一些脑筋,但毕竟大家大都不是当时的开发人员,猜测结果和真相有很大出入也是不可避免的。我在附带的源代码中加入了详细的注释,供细心的读者在发现软件的瑕疵或者错误并修改时参考。

 

    另外在这里不得不提起风雷小组的又一神作,那就是《圣女之歌》系列,相信大家当年即使没玩过想必也看过这款游戏的精彩宣传动画吧。

    这款游戏即使到了现在还是有育碧代理的正版(起码包装是正版包装)卖,正版软件也不贵,而且还有良好的售后(我买到的圣女之歌I代中第二张碟片是坏的,寄回公司,马上他们就给换了一张新的,包裹的很精致,让我多赚到一个盘盒,哈哈),所以希望大家为了支持国产游戏,尽可能的还是买几套正版吧。

    值得注意的是,现在市面上有很多大盒装单机游戏都是“伪正版”,这里告诉大家一个辨别的小窍门,那就是用鼻子仔细的闻一闻,如果包装盒里面有一股难闻的废塑料味道,那一般都是“伪正版”的,而正版软件如果是塑料盒包装的,闻起来应该有一股正版的刻录盘味儿。

 

文章中提到的附件可以到我的资源什么什么栏那里下载:

 http://download.csdn.net/detail/fosly/4884212

 

>>虽然ZCloud的<<拜见女皇陛下>>很好看, 但是寒舞依然是神.

原创粉丝点击