试着去开发一个Game Boy游戏

前几天aco姐问我一个问题,她说她想买个游戏机,但是不知道买啥。我毫不犹豫的就告诉她,要买就买Game Boy!

我个人觉得Game Boy是一个对游戏界意义很大的游戏机,有几家媒体叫这个游戏机“不老奇迹”,因为它有空前绝后的十几年的产品周期。这上面诞生过的诸如超级马里奥·世界Pokemon系列的游戏都是家喻户晓的老IP。

对于写Game Boy游戏这件事,不觉得很酷嘛?作为一个理工废宅,我觉得这太酷了,很符合我对未来生活的想象,科技并带着些趣味(逃

这几天我就在琢磨怎么写一款Game Boy游戏。事实上,如果只是单纯写一款Game Boy游戏的话,其实现在就有一个叫做GB Studio的软件,可以一站式开发Game Boy游戏。GameBoy的游戏实际上也可以用C语言开发了。
但是我就打算特立独行,要整活就整的彻底一点,我们直接就从ASM代码开始写GameBoy!

准备工作

真正的勇士应该用什么语言开发Game Boy游戏呢?emmmmm,答案当然是RGBASM啦!RGBASM跟汇编是很贴近的。
在被编译器拖到地上吊打过无数遍后,我觉得我已经稍微了解了一些这方面的知识了!

如果要用RGBASM开发GB游戏的话,可以看看这篇教程,这篇教程深入浅出地讲述了GB游戏应该到底怎么开发。
https://eldred.fr/gb-asm-tutorial/index.html

那么现在,让我们来做做准备工作吧!

操作系统和代码编辑器

对于开发环境的选择,我选择了Windows10系统的环境。这主要是由于开发一个GB游戏最重要的是玄学调试,但是调试用的模拟器是只支持Windows平台的,所以我选择还是用Windows来开发了!

写代码用的文本编辑器我选择使用VS Code,装了RGBDS Z80插件,效果不错。

编译环境(RGBDS)

首先需要准备一下编译环境,也就是RGBDS的编译套件。
官方的GitHub仓库里,在Releases里下载最新版本的RGBDS。这里我选择官方Doc文档里描述的最新版本v0.6.0,所以我在这里下载了rgbds-0.6.0-win64.zip

解压放在一个文件夹里,然后在Windows环境下把这个文件夹加入系统PATH环境变量里,这样方便我们使用rgbasmrgblinkrgbfix这三个工具。这里就省略介绍PATH变量怎么配了,其实不配置也行,就是用起来麻烦点呗

但是像我的话,我肯定随手就把PATH配了,配个PATH岂不是洒洒水嘛,配好以后不要忘记终端(我用的Windows Terminal配PowerShell7)里打一下

rgbasm --version

看看正不正常,我这里显示的是

rgbasm v0.6.0

只要不报错那就是OK,编译环境准备完毕!

模拟器

选个趁手的模拟器也很重要,这里我跟着巨佬的教程选择用bgb

官网是 https://bgb.bircd.org/

我下载了我目前看到的最新版本1.5.10,我下载的是64位的版本。这个模拟器没什么讲究,下载下来解压缩就行了。

方便开发的小工具

为了方便开发,我们还需要准备几个小工具,一个是方便我们绘制Tile的小工具,一个是方便绘制Tilemap的小工具。

这里我选择用Harry Mulder's Gameboy Development里提供的Gameboy Tile Designer(GBTD)和Gameboy Map Builder(GBMB)两款工具。地址:
http://www.devrs.com/gb/hmgd/intro.html

这两款工具也都是下载下来解压就能用的工具。

仔细一看这两个工具初版发版都是1997年的东西了,啧,这工具比我大不少(

至此,我们准备工作就做好了!

试着做个Demo出来

准备完毕后,我们就可以做个Demo出来了。虽然我们还不太懂开发的具体内容,但是我们先体验体验开发流程总没什么大问题。

准备Tile和Tilemap

对于什么是Tile,以及什么是Tilemap的问题,我觉得很难讲明白。但是意思我觉得还是很好理解的。我按照Minecraft举例子,Tile就是方块,Tilemap就是方块怎么摆。

首先先打开GBTD,打开后界面是这样的。

这里补充两个冷知识

  1. GameBoy只能在它那个绿不拉几的显示屏上显示四种颜色,分别是白色(0)、浅灰色(1)、深灰色(2)和黑色(3),不难发现我们使用2bit就能表示一个颜色,所以GB的颜色又叫做2bpp颜色。
  2. GameBoy的一个Tile是8x8像素的,一个GameBoy最多显示宽高为20x16个Tile。

这个软件的操作方式跟Windows老版的画图意思是差不多的。左面是工具,一般就用第一个画笔工具就够了。
在下方有四个颜色可以选,可以通过鼠标左右键点击对应的颜色,给鼠标的左右键绑定不同的颜色,然后在上面的格子里点击鼠标左键和右键,就是在这个格子上画颜色。

这个软件一次可以画多个Tile,我们一般会空着0号位置,因为0号位置一般表示缺省,缺省一般不去动。

所以我们在右面点击1号格,代表我们开始画第1号Tile。我随便画了2个,就像这样。

好了,画好Tile以后,我们就可以先保存一下然后关掉了。
现在我们就可以画Tilemap了。
打开GBMB,界面是这样的。

我们可以点击File,点击Map propertiles,打开地图设置窗口。

这里在Tileset里选刚才我们画好保存的那个Tileset文件,宽和高设置为32。诶?为什么要设置成32呢?
这里有个小知识:虽然GB确实宽高是20x18的,但是GB原生支持Tilemap的位移,也就是你可以加载进去一张32x32大小的Tilemap,然后设置这个Tilemap你想位移多少,他就可以滚动Tilemap了,默认x和y方向都是0,也就是显示Tilemap左上角的20x18这一块地方。

点击OK了以后,我们就能绘制了。

这里画笔工具的操作逻辑是,鼠标左键可以在右面的Tileset里选择一个Tile,然后鼠标右键可以在中间的区域里的某个格子上画东西。
我就在这里绘制了一个这样的Tilemap。

OK,保存一下,Tile和Tilemap就整好了。

其实如果足够的牛逼的话,你也可以选择不用这种工具绘制,依靠纯口算把Tile和Tilemap算出来。其实还真的能算,我看了几个上古时期的教程和杂质照片,人家还真的有人口算的......

下载hardware.inc

可以去gbdev的hardware.inc仓库里下载一份hardware.inc,在这里:

https://github.com/gbdev/hardware.inc/releases

这个玩意儿定义了很多常量之类的内容,方便开发,很有用,先把这个下载下来,放在跟一会儿新建的代码文件所在的同一个文件夹下。

写代码

咱们的HelloWorld也别整得太花里胡哨,就把咱们的Tilemap显示出来就不错了。现在写点代码先。

首先新建个asm文件,这里我就叫他fuck.asm好了,我用我装好RGBDS z80插件的VSCode打开。

然后写下面这么一段代码。

include "hardware.inc"

section "Header", rom0[$100]
    jp EntryPoint
    ds $150 - @, 0

EntryPoint:
    ; Shut down audio circuitry
    ld a, 0
    ld [rNR52], a

WaitVBlank:
    ld a, [rLY]
    cp 144
    jp c, WaitVBlank

    ; Turn off LCD
    ld a, 0
    ld [rLCDC], a

    ; Load tile
    ld de, Tiles
    ld hl, _VRAM9000
    ld bc, TilesEnd - Tiles
LoadTiles:
    ld a, [de]
    ld [hli], a
    inc de
    dec bc
    ld a, b
    or a, c
    jp nz, LoadTiles

    ; Load Tilemap
    ld de, Tilemap
    ld hl, _SCRN0
    ld bc, TilemapEnd - Tilemap
LoadTilemap:
    ld a, [de]
    ld [hli], a
    inc de
    dec bc
    ld a, b
    or a, c
    jp nz, LoadTilemap

    ; Turn on LCD
    ld a, LCDCF_ON | LCDCD_BGON
    ld [rLCDC], a

    ; init display registers in first frame
    ld a, %11100100
    ld [rBGP], a

Done:
    jp Done

section "Tile data", rom0
Tiles:
    ; tile data
TilesEnd:

section "Tilemap data", rom0
Tilemap:
    ; tilemap data
TilemapEnd:

以上代码绝大部分都是我按着上面那个教程的dalao的帖子抄的,我自己想不出来这个代码要这样写

这个代码还缺少一部分内容——我们并没有往里面加入刚才准备好的Tile和Tilemap的数据,所以我们要把这些数据加进去。

导出Tile和Tilemap

我们分别打开GBTD和GBMB,先说说GBTD怎么导出Tile数据吧。

首次打开的话,点击File,点击Export to,打开下面的窗口。

这个窗口里配置一下Type,选择RGBDS Assembly file(*.z80)
由于这里我们的Tile一共是三个(0、1、2),这里窗口里的From..to..要设置为From 0 to 2,记得把这里改掉。
由于我们开发的是GB的游戏,所以这里Format要改成Gameboy 4 color(因为GB就是2pp的嘛,2pp表示四个颜色)。
别的一般就不用改了。

点击OK然后保存好这个文件,以后可以在File里点击Export或者工具栏里第三个导出按钮点一下就能导出了。

导出完了以后会生成对应的.z80文件,这个文件用VSCode打开,把DB开头的那几行复制到我们的代码的section里(我们代码第60行不是有一行; tile data嘛,把这个删了,换成这一堆DB语句)

就像这样:

我这里因为希望它好看点,调了一下缩进,其实调不调无所谓。

然后我们在GBMB里导出一下Tilemap。

GBMB里打开刚才咱们的gbm文件,然后还是FileExport to先配置一下。

这里需要指定一下Filename,还有Label和Section,我这里就直接瞎写了,因为我这里打算跟刚才的操作一样,只是把DB里的内容复制粘贴过来,不用他生成的其他的代码,所以这里写啥都无所谓,只要它能生成这个z80文件就行了。

然后打开Location format选项卡,这里需要配一下。

这里要在左面添加一个Property,指定Tile number,由于我们准备的Tile最后一个编号是2,所以这里我设置为了2。
右侧如图配置,尤其注意Plane count应该是1 plane(8 bit)
配置完毕点OK,跟刚才一样的操作导出一下,导出完了以后用VSCode打开z80文件(inc文件我们不用)。

把这堆DB数据复制到刚才Tilemap data的section我们留好; tilemap data的位置,一样的操作,把这堆DB数据放这里。

好了,代码就写完了!

编译一下

打开终端切到代码目录下,来下面这三行命令,把代码编译并生成最终的gb文件。

rgbasm -L -o fuck.o fuck.asm
rgblink -o fuck.gb fuck.o
rgbfix -v -p 0xFF fuck.gb

这里的fuck只是因为我的代码文件叫fuck,可以改成别的,以及fuck.asm、fuck.o、fuck.gb这仨文件的名字是随便取的,不一定非得保持命名上的一致

然后运行bgb,把生成好的gb拖进去(不支持拖拽就右键一下,选Load ROMs打开这个gb文件)

成功运行!

以上就是我们开发的第一个GB游戏的全过程了!

解析Demo的工作原理

也许你跟我一样,很好奇这个Demo为啥能跑起来,我刚才写的那段ASM代码究竟是什么意思。
别急,下面我们浅浅的解析一下这玩意儿的工作原理。

Header

首先我们要知道一件事,我们的卡带插入Game Boy里头,Game Boy一开机,会有个Nintendo的LOGO从上面划到屏幕中间,然后响一下“棒——丁!——”一声。
实际上,在播放这个动画和音效的时候,GB并没有闲着,GB内部会在开机的时候立即启动一个Boot ROM来检查和加载相关的数据,然后再加载咱们的卡带。
对于我们而言意义比较大的是Boot ROM的开机自检。可以在这里找到对GB开机过程的详细描述,里面就写明了在开机的过程中,Boot ROM会检查咱们的卡带里面是不是包含了正确的任天堂Nintendo的LOGO,并且检查Header的checksums是否正确,如果不正确机器就直接锁死了。

嗯?卡带里是不是包含了Nintendo的LOGO?这是啥意思?实际上,咱们刚才编译的过程的第三步rgbfix,就是往咱们的gb文件里补足Boot ROM校验所需的checksums、Nintendo的LOGO等信息。

我们回到代码里,可以发现我们的代码前几行是这样的:

include "hardware.inc"

section "Header", rom0[$100]
    jp EntryPoint
    ds $150 - @, 0

第一行表示我们要把hardware.inc导入进来,这没啥说的,我们代码里还要用里面定义的常量,肯定要导入进来对吧。

然后在RGBASM里,我们的代码逻辑必须要写在某一个section里,也就是我们认为我们的代码基本组成元素是一个一个的section。我们在第三行定义了咱们代码里的第一个section,起了个名叫Header,实际上起啥名字都行。后面表示我们这个section要放在rom0里面,并且起始位置是$100

在RGBASM里,我们用$表示16进制,用%表示2进制,例如%1010表示十进制的10,$B表示十进制的11。所以这里的$100是十进制的256。

根据上面的文档可以知道,Header的位置是从$104$150(不含$150),我们的Header这个section是从$100开始的,第一行是jp EntryPoint,在RGBASM里,EntryPoint后面会被rgbasm处理成一个具体的地址,这个地址是16位的,所以算上jp这个指令头,一共一条语句占了3字节的空间(分别占了$100$101$102这三个字节的位置),也就是jp语句的后面的地址是$103

在RGBASM里,参数和参数之间用逗号隔开。在下一行,我们用ds语句,他的意思是把从$150-@,到$150(不含$150)这一段内存空间全部清空,置为0。也就是把Header这块区域全部清空掉,方便后续的处理,否则会运行出问题。

所以这块是一个固定代码,是为了处理Boot ROM所需的Header而写的代码。

VBlank

再往下看,可以发现这么一块内容。

EntryPoint:
    ; Shut down audio circuitry
    ld a, 0
    ld [rNR52], a

WaitVBlank:
    ld a, [rLY]
    cp 144
    jp c, WaitVBlank

    ; Turn off LCD
    ld a, 0
    ld [rLCDC], a

跳转语句

看到这一块第一行的EntryPoint的嘛,如果你用过Basic肯定对这玩意儿很熟悉,这相当于打了个位置标记。
上面的jp EntryPoint的意思就是跳转到这个位置上去。我们看看这一块代码干了什么事。

这个打标记的方法要记住,它还有别的妙用,后面会讲。

关闭声音的具体操作

在RGBASM里,分号表示单行注释。首先就像注释说的一样,我们先关掉了音效,因为我们这个游戏没音效。

我们先看看这个关掉音效的设置是怎么操作的。

首先是ld a, 0,这个意思是把寄存器a设置为0。

在GB里有7个我们可以读写控制的寄存器,分别是a、b、c、d、e、h、i,这7个寄存器都是8位的,也就是意味着他们都能存0到255。在实际使用的时候,我们可以把bc、de、hi这三对寄存器合在一起用。
在这里面,a寄存器是特殊的寄存器,他是加法寄存器,同时我们依靠它来设置值。
举个例子,我们想把d寄存器设置为2,怎么操作?答案是:

ld a, $2
ld d, a

也许也就是我们给其他几个寄存器设置值,必须用寄存器a倒手转一下才行,不能直接一行ld d, $2就直接操作。

这里ld [rNR52], a就有点复杂了。这个中括号表示当地址处理,也就是意思是把rNR52表示的那个数字当做内存地址处理。因为这里rNR52是hardware.inc里定义的一个具体的数字,不是内存地址,所以这里要用中括号把数字转成地址用。
这里值得一提的是,GB里内存地址是16位的。
ld语句不仅能写寄存器,还能写具体的某个内存,所以这里的意思是把寄存器a的值写进rNR52这个数字对应的那个内存上去。

所以合起来的意思是,把rNR52对应的内存设置成0。在文档里查一下可以知道,NR52这个位置表示开关声音,0就表示直接把声音芯片(在GB里,声音运算芯片的名字叫做APU)给关掉了。

VBlank是什么

GameBoy的屏幕是一块4级灰度的2.45寸LCD液晶屏,这是我们后面一些神奇操作的前提,硬件决定软件。

对于显示屏而言,他的图像是怎么显示的呢?我们要了解一些扫描线的知识点。显示器显示图像,实际上需要从左上角那个点开始一行行按顺序刷,一直刷到最后一行。
这里我也不太了解显示器的具体工作原理,但是大概是这么个意思。
那么到最后一行右下角的位置上了,它就需要返回到左上角去,这个重返左上角的过程就是VBlank。如果没有VBlank,那很显然,会有一条从右下角连接到左上角的斜线被显示出来。

那么我们根据这个工作原理就可以明白,VBlank的发生的那个时刻,意味着屏幕上所有改画出来的部分都画完了,这意味着VBlank发生的那个时间段没有进行屏幕扫描,关闭屏幕是安全的。假设我们在VBlank之前把屏幕关了,那很有可能会因为绘制下一个将要画出来的像素点的过程,而伤害屏幕,这会对实际的GameBoy游戏机造成不可逆的硬件创伤。

所以关闭屏幕这个操作,必须要在VBlank进行时才能进行。
我们接下来要加载Tile资源,这里因为我自己没看懂因为啥的原因,加载Tile和Tilemap的时候必须要关闭屏幕,否则不好操作。

等待VBlank代码的工作原理

OK,那么我们下面要加载Tile和Tilemap,但是加载Tile需要关闭屏幕,但是关闭屏幕需要等VBlank到来。我们究竟是怎么等的呢?先看看代码。

首先我们把[rLY]写进了寄存器a里。这里我们可以查文档发现[rLY]指向的内存存储了一个从0到153的值,表示当前屏幕扫描到达了第几个像素行,并且是只读的。
如果是从0到143(包含143)的话,表示的是现在屏幕正在进行屏幕扫描,图像还在绘制过程中,这个时候关闭屏幕肯定就寄了。
但是文档里同时指出,如果这个值是144到153的话,就说明此时正在VBlank。很显然,这个值从0到144肯定是以1为单位累加过来的,不可能发生突变,因为我们刚才学习了一点点原理,屏幕扫描是一行行进行的,它不可能biu的一下去其他行了。那么如果这个值刚刚好到了144,显然说明扫描结束,开始VBlank了。

所以我们的目标就是,当这个值在144-153这个区间的那个时候,我们就立刻关闭屏幕,因为这个时候正在VBlank,没进行屏幕扫描,关闭屏幕是安全的。

我们把[rLY]写进寄存器a里面以后,紧接着又做了cp 144,这行代码的意思是把寄存器a里的数值跟十进制的144作比较。

这里需要提一嘴,我们刚才学习了七个寄存器,实际上显然GB里不只有7个寄存器,还有一个寄存器叫做f,它是一个只读的寄存器。寄存器f存储操作标记,我们在做数字运算和比较的时候会用到它。
对于寄存器f里的值,存在4种情况,分别是cncznzc表示溢出,nc表示没溢出,z表示相等,nz表示不相等。

我们这里用到了jp语句的第二种表示形式,也叫作条件跳转。jp语句在表示条件跳转时,可以在寄存器f为某一个状态下再进行跳转。

刚才我不是说了嘛,我们把[rLY]写进了寄存器a,cp 144语句的含义是把144这个数和寄存器a里的值作比较(你会发现寄存器a就是挺特殊的,很多语句的操作对象就是寄存器a),比较的结果其实会修改寄存器f。
下面的语句jp c, WaitVBlank表示的是,如果寄存器f是c的话,那么就跳转到WaitVBlank位置去。

这里需要详细说说cp语句了。其实它的内部工作原理是做了个运算,算了一下a-操作数是多少。
在GB里,溢出分为两种情况,一种是诸如在八位寄存器(最大存255,最小存0)上计算255+1的这种向上溢出,最后会得到结果0;一种是在八位寄存器上计算0-1的这种向下溢出,最后会得到结果255。
在GB里,每一个运算都会影响寄存器f的值。cp语句的本质操作不就是算数嘛,所以这里的这个运算也会影响寄存器f的值。

我们很明显就可以发现,a-操作数这个运算,发生了溢出,你说什么情况会溢出?啊哈,肯定是 a < 操作数 的时候会发生溢出啦!

所以我们得出一个结论,进行cp操作后寄存器f变成状态c,那就意味着 寄存器a存的数值 < 操作数。

所以这里的cp 144意思很明显,就是看看寄存器a里的数字是不是小于144,如果是,那就满足了条件跳转的条件,就会一直在这里做循环,相当于等着VBlank到来,这个循环会因为[rLY]大于等于144而终止。

关闭屏幕

VBlank来了就可以关闭屏幕了,这里逻辑很简单,想必你已经理解了。

    ; Turn off LCD
    ld a, 0
    ld [rLCDC], a

意思就是把[rLCDC]设成0。

加载Tile和Tilemap

加载Tile和加载Tilemap的代码是一模一样的,这里以Tile为例介绍。

    ; Load tile
    ld de, Tiles
    ld hl, _VRAM9000
    ld bc, TilesEnd - Tiles
LoadTiles:
    ld a, [de]
    ld [hli], a
    inc de
    dec bc
    ld a, b
    or a, c
    jp nz, LoadTiles

    ; Load Tilemap
    ld de, Tilemap
    ld hl, _SCRN0
    ld bc, TilemapEnd - Tilemap

内存地址的表示

我们刚才介绍的时候省略了一个细节没说,但是在上面的两处跳转语句里一直在用。

例如对于jp EntryPoint指令,他的意思是跳转到EntryPoint标记处,这没问题。但是我们上面也说了,EntryPoint标记其实就是标记了这个位置。啥叫标记了这个位置,意思就是定义这个内存地址叫做EntryPoint

说白了,打标记操作的本质是给内存地址起别名,而标记的名称这件事其实就表示了一个内存地址。后面在使用rgblink的时候会把这个标记名称擦除掉,改为对应的内存地址。

对于跳转语句,如果我足够有自信,能够知道内存地址比如$C123是我想要跳转到的位置,我可以直接写jp $C123

内存地址其实是一个16位的数字。我们可以把内存地址放在寄存器里备用。对于语句ld de, Tiles,还记得上面我们说的,我们设值需要用寄存器a倒一下才可以嘛?这里是一个特例!
还记得寄存器可以拼起来用嘛?我们这里正是把Tiles这个标记对应的内存地址写进了寄存器d和寄存器e拼起来的寄存器里了。

db语句

一个section可以仅表示数据。
db语句可以单次或批量地开辟一个八位的空间出来。看一眼咱们的Tile data这个section,如下:

section "Tile data", rom0
Tiles:
    DB $00,$00,$00,$00,$00,$00,$00,$00
    DB $00,$00,$00,$00,$00,$00,$00,$00
    DB $AA,$AA,$55,$55,$AA,$AA,$55,$55
    DB $AA,$AA,$55,$55,$AA,$AA,$55,$55
    DB $FF,$FF,$99,$99,$BD,$BD,$E7,$E7
    DB $E7,$E7,$BD,$BD,$99,$99,$FF,$FF
TilesEnd:

这里就是批量用db语句开辟空间的用法。实际上,db语句可以单独开辟一个空间,类似这样:

db

对,就只写一个db就行了。如果我们希望给这个空间赋一个初始值,那就可以在后面写一个常量值。

db $06

这表示我们开了一块空间,里面存好了数字6。

我们开辟了一块空间,肯定是希望使用这块空间。我们可以使用内存地址使用,这里我们也可以用打标记的方法使用。看这里我们再开始标记好了Tiles,结尾标记好了TilesEnd,我们这里其实开辟了8x6=48个8位的空间,每个空间都给了初始值,这一块空间的开头地址可以用Tiles表示,末尾可以用TilesEnd表示。

加载Tile的实现

来看看我们的语句究竟是怎么加载Tile的。

首先很显然,开头三行向de里写了起始地址,向hl里写了_VRAM9000,向bc里写了Tile数据的总长度。注意这里,为啥非要这样设置,我为啥不能把这三个寄存器里存的数据互相换换呢?这是有原因的,往下会说到。

这里根据文档描述,VRAM9000开头的这块$9000$97FF这块空间就是用来存Tile数据的空间。所以我们的数据要从$9000开始写,_VRAM9000hardware.inc里定义好的常量。

然后下面的操作是一个写内存的标准示范,思路很巧妙,下面逐行解析一下。

我们从LoadTiles的位置往下一次看,首先先把de表示的数据写进a里,这里注意一下,ld指令的第二个参数如果是个地址的话,表示把这个地址里的数字写进对应位置上。

所以第一条指令目前的意思是,把Tile的第一个字节的数据写进a。

接着往下看,ld [hli], a,这里要注意的是,寄存器h和寄存器l拼起来有特殊用法,hli的意思是用完hl的值以后把这个位置上的数字自增1。这条语句的意思是把a的值写到hl存储的数字对应的那个位置的内存上去。

很明显,第二条语句的意思就是把寄存器a的值写到$9000上,也就是把Tile的第一个字节写进第一个对应位置上。同时这里hl的值增加了1。

下面的inc de的意思是给de加1,dec bc的意思是给bc键1。给de加1使得de指向了Tile数据的第二个字节,给bc减1表示我们还剩下的字节数少了1。
我们至此可以窥见,这个代码应该一直这样循环,直到bc为0。那么我们怎么实现比较bc是不是0的呢?接着往下看。

ld a, b字面意思,把b的值写进a里。下面的or a, c操作是个位运算的或运算,表示寄存器c和a做一个或运算。再次提醒,GB里每个运算都会更新寄存器f存的状态数据。很显然,按照我们上面的分析逻辑,如果bc不是0的话,那么就应该一直做这个逻辑,为啥这里or运算可以判断bc是不是0呢?
这个道理很简单,如果寄存器f是nz的话,说明这个运算结果不是0,这两个数字必须全是0才能不满足条件跳转。

这就是加载Tile的工作原理了!

调色板

GB里有个调色板机制。他可以规定四个颜色分别对应什么数值。默认的话,0表示白,1表示浅灰,2表示深灰,3表示黑,我们可以利用调色板机制手动修改对应的数值。

修改调色板需要关闭屏幕。下面的代码有这样的代码块:

    ld a, %11100100
    ld [rBGP], a

这里是二进制串,要两两从右往左阅读,这就是规定了0号颜色是0号颜色,1号颜色是01号颜色,2号颜色是%10号颜色,3号颜色是%11号颜色,也就是默认调色板的意思。

也许你已经发现了,如果你想实现一个闪光特效,那你可以直接改调色板就行了,不用改tile和tilemap,你搞个调色板%00000000%11111111重复个四五遍的效果,再调回%11100100,那看起来就会很闪。

最后的代码

最后我们写了个

Done:
    jp Done

对,执行完毕以后搞了个死循环,让程序还活着。

至此,我们就读完了整个的代码!

后续

GB的游戏开发需要大量的底层知识,这可见早期的游戏开发者个个都身怀绝技,有很多针对GB平台研究出来的黑科技等待我们探索。

这里只是浅浅的了解了其中的一部分内容,后续再接着看还有什么有意思的东西待我们挖掘!

上一篇
下一篇