口袋妖怪绿宝石——数据提取与代码分析(A-穿墙金手指的原理)
说在前面:
这个系列的专栏停止更新了大概4个月的时间。在上期的结尾。作者说可能会拿一个真正的改版“开刀”,不过在反复审视专栏的这些内容之后,作者发现有一类金手指一直没有被提及,就是穿墙金手指,这个金手指算是比较出名的一类。因此,拿改版“开刀”的事还是再缓缓,先用本期内容把穿墙金手指的原理讲清楚。
为什么作者在之前的专栏中一直没有提到穿墙金手指呢?尤其是作者的另一个专门讨论金手指的专栏系列(究极绿宝石5-金手指原理介绍),也没有提,这是因为当时那个专栏提供的知识不足以找到或者解释穿墙金手指的原理,要说也只能提供一个代码,这样一来就和网络上那些只给代码不给解释的网站没区别了。作者不希望对读者说“拿去用就好,别问为什么”这么不负责任的话。现在加上本系列专栏的知识,穿墙金手指的原理就可以解释了,所以放到本期内容来说。

穿墙与碰撞检测
先来说一下穿墙金手指的效果:开启穿墙金手指之后,所有原先因地形或者人物阻挡而无法到达的位置,现在都可以到达了。比如说因为墙壁、石块、树木、栏杆或者NPC阻挡的位置,开启了穿墙金手指之后就可以“如入无人之境”,想走到哪就走到哪,“穿墙”二字也是由此而来,下图就是一个开了穿墙金手指的效果:

这张图中,主角竟然站在了石头上,这在正常情况下是不可能的。二周目的天空之柱布满了让人掉落的陷阱,只有骑着音速自行车快速通过才能避免掉落,比较考验玩家的自行车水平。和原版相比,有的绿宝石改版加大了这里的难度,掉落的格子变得更多、自行车轨迹变得更曲折,让许多想要攀登到塔顶抓烈空坐的玩家经受了不小的考验。
于是就有玩家求助于穿墙金手指,原本远在天边、被各种陷阱隔开的两道门,一个穿墙直接穿过去就万事大吉了。还有另一种解决方案是瞬移金手指,这在究极绿宝石5.3——科普向,什么是金手指(八)里有详细说明。
这里可以说一下穿墙金手指的原理了。首先介绍一个概念:碰撞检测。这是一种游戏机制,字面意思不难理解,就是检测两个物体是否碰撞,或者用数学上的概念来说,两个点集在空间中是否有交集。碰撞检测在游戏领域的存在极其广泛,一些相当古老的游戏,例如吃豆人、合金弹头、街头霸王等等都存在碰撞检测机制。
举个例子就能说明碰撞检测机制有多么普遍:在射击类游戏中,玩家打出的子弹有没有命中敌人就需要碰撞检测。这个机制是如此基础、如此底层,以至于许多玩家将其当做理所应当的事。碰撞检测机制也有做得不好的时候,3D游戏中的“穿模”就是碰撞检测实现不好的后果。
在绿宝石中,玩家可以用上下左右四个方向键控制游戏主角进行移动,被障碍物阻挡时,主角便无法前进,这就是说此时碰撞检测的结果是发生了碰撞,因此游戏机制认定主角无法按照原有的方向继续移动,只能停在原地。而穿墙金手指就是要破坏碰撞检测机制、让碰撞检测机制失效,从而达到“穿墙”的目的。

原版绿宝石的碰撞检测机制
下面介绍的是游戏中的主角在行走状态下的碰撞检测。除了行走状态,主角还可以骑自行车,此时的碰撞检测和行走状态是有区别的(比如说一些只有自行车才能去的地方)。在源代码项目中,这个碰撞检测机制的关键函数是CheckForPlayerAvatarCollision(检查游戏主角碰撞),先来看一下它被调用的地方,一处是在PlayerNotOnBikeMoving内,位于src/field_player_avatar.c中:

PlayerNotOnBikeMoving函数名的字面意思是主角没骑自行车时的运动,函数的第一行(607行)就在执行碰撞检测:
从这个函数的内容可以看出,主角在走路状态下的运动情况,由函数第一行执行的碰撞检测结果collision变量决定,这个变量表示的就是碰撞类型,例如611行的COLLISION_LEDGE_JUMP,指的就是主角从坡上跳到坡下(碰撞到了坡的分界线),如下图:

因此,从CheckForPlayerAvatarCollision函数的外部可以得知它的返回值类型,表示的就是不同的碰撞类型,这些碰撞类型是以COLLISION_开头的常量,它的定义如下:

碰撞类型是由enum定义的枚举类型常量,其中COLLISION_NONE(无碰撞)的取值是0,COLLISION_OUTSIDE_RANGE(超出边界)的取值是1,COLLISION_IMPASSABLE(不可通过)的取值是2,以此类推。
碰撞类型中还涉及了怪力术推石、飞行道馆中的旋转门等等碰撞检测,有兴趣的读者可以在这些碰撞类型上做做文章。
这样一来,穿墙金手指的思路就很明确了,如果能让CheckForPlayerAvatarCollision函数的返回值总为0,也就是总为COLLISION_NONE,那么主角在走路时就不会碰到任何障碍,碰撞检测机制失效了、被破坏了,穿墙的目的也就达到了。具体的代码是什么,读者可以仿照本系列专栏介绍反作弊机制的6、7、8三期专栏,自行探索这种修改ROM的金手指。

穿墙金手指的另一种思路
上面那种直接破坏碰撞检测机制的思路,得到的穿墙金手指非常“暴力”,无论是石头,是树,还是NPC,都是直接对穿而过。还有另一种思路,改的是地图,这种穿墙金手指穿越石头、树这样的障碍没问题,但是碰到NPC还是会被阻挡住。
在本系列专栏第3、4期的时候提到了地图信息提取,在AdvaceMap这个工具中可以看到地图是由一个个小正方形拼起来的:

在“设置”菜单中,勾选了第一个选项“显示网格”就可以看到地图被分割成了小正方形,这就是组成地图的最基本单元,在源代码项目中,这个基本单元被称作metatile(元地图块)。
地图上方的五个标签栏中,第二个是“运动许可”,打开看一下:

可以看到,每个元地图块都有一个数字标号,和地图进行比对,可以发现所有标1的元地图块对应到了树木、建筑、围墙等等,都是运动障碍。因此,如果能够修改地图,改变这些元地图块的“运动许可”,就可以做到穿墙金手指的效果了。
目前为止还有一个很大的问题,改地图的话工作量太大了。前一个思路,一条代码就可以破坏碰撞检测机制,可以做到在任何位置都能穿墙;而这个思路打算改多少个元地图块呢?改哪些元地图块呢?会不会让金手指的行数变得特别庞大呢?
碰巧的是,作者最近在网上找到了一条符合这个思路的穿墙金手指,接下来就从分析这个金手指的角度来反推一下它能够实现穿墙的原理。

一条可以引起思考的穿墙金手指
这条穿墙金手指是这样的:
格式是V1/V2格式码,这个格式的代码之前没有介绍过,但是它的“近亲”V3格式码的信息可以参考另一个系列专栏的究极绿宝石5.3——科普向,什么是金手指(九)。V3格式码可以看做是V1/V2格式码的进化版本,也就是说V1/V2格式码相对V3格式码要落后一些,能够实现的功能比V3格式码少一些,但是代码类型、中间代码、加密这些概念还是通用的。
在VBA模拟器中,V1/V2格式码和V3格式码的输入方式是相同的,都是在“金手指列表”的GameShark按键弹出的输入框内输入。
关于这条中间没有空格的代码,VBA模拟器也有一些误导之处:在输入代码时,无论中间有没有空格,最后显示在列表里的都是没有空格的样子。但是,上面这条穿墙金手指中间一旦加了空格,含义就发生了变化,也就不能起到穿墙的作用了,具体来说看下图:

第一条金手指,输入的时候在中间加了一个空格;第二条保持原样。可是在最后在金手指列表中,显示的都是没有空格的样子。虽然看上去一样,但只有第二条是能够穿墙的金手指,第一条不会起作用。
虽说格式有了变化,不过将V1/V2格式码转换为原始代码的流程还是可以类比到V3格式码的。在这里,作者把这个过程再重复一遍(就像在究极绿宝石5.3——科普向,什么是金手指(九)里一样),就当是复习了。
首先是AR Crypt解密,注意代码类型左边选择AR v1/2,右边选择RAW:

得到的中间代码没有空格,但是,对于中间代码来说,有没有中间的空格已经无关紧要,它是不会影响内容的,完全可以看做:
接下来是去查文档,这次要到GameShark V1/V2的区域去找:

这个中间代码以6开头,右半边的8位十六进制数以4个0开头,符合16-bit ROM Patch这个代码类型的定义,含义是在ROM的某个地址处写入半字xxxx(两个字节)。这里插句话,对于ROM(只读存储器),说“写入”有些不太合适(只读的东西怎么能写入呢),这只是为了方便理解,英文用的是Patch而不是Write就是进行了严谨的区分。
下一步是计算在ROM内写入的地址,按照规则的说明,需要把6后面的aaaaaaa这个7位2进制数乘2,才能得到真实地址
这样就最终得到了原始代码:
然后可以去符号表查一下这条金手指究竟改的是哪个函数:

MapGridGetCollissionAt这个函数的范围位于[080881b0,08088224),正好是0808820C所在的范围,这就是穿墙金手指修改的函数。这个函数其实就是根据元地图块的“运动许可”信息来计算碰撞类型的函数,修改它也就相当于同时修改了所有元地图块,这也就解决了修改地图是否会工作量过大的问题。

为什么要把金手指写得这么晦涩
通过上一节的讨论,我们知道下面这两条金手指是等价的:
如果不借助工具,或者对V1/V2格式码了解不多,从第一行金手指中完全无从得知它能够实现穿墙的原理;相比之下,第二行的原始代码则是简单直接地告诉人们它修改了什么地方,研究它的原理要简便得多。
作者在另一个系列的专栏里提到过,V3格式码可以实现原始代码实现不了的功能,比如条件判断之类,在那种情况下,使用V3格式码,或者它的前身V1/V2格式码有充足的理由。但是,对于这条穿墙金手指来说,明明可以用原始代码写得这么简单明了,为什么还要用晦涩的V1/V2格式码来写?它们明明在功能上是等价的。
下面来说一下作者自己的看法。
本系列专栏提到过绿宝石的反作弊机制,让金手指的使用变得没有那么容易。但是反作弊机制再强大,在原版绿宝石不再更新的背景下,这就是个死东西,任何被确认有效的金手指以后将一直有效下去。而基于绿宝石改版的游戏则不同,如果它会不断更新下去,制作组可能就会在新的一次更新中考虑让一些能够在旧版里生效的金手指失效。
此时去看那些原始代码写成的金手指,一眼就能看到它修改了哪里,到时候在游戏更新时稍微改改函数的语句、数据的位置,就能废掉相当一部分金手指。如何应对这一点呢?制作金手指的人也有自己的“反反作弊”机制,那就是用V3格式码(或者V1/V2格式码)加密,让制作组不那么轻易地看出自己制作的这条金手指究竟改的是哪里,也就无从对症下药让它不再生效。
又说到“魔和道究竟谁高一尺,谁高一丈”的问题了。以上仅代表个人观点~

虽说作者通过写个这系列的专栏想让读者了解到一些知识,但作者其实也学到了不少东西。这次写穿墙金手指给作者一个提示,那就是专栏的内容远称不上完善,那就随缘继续更新,看看下次还能补充进来什么内容。继续感谢众位读者的支持!