前几天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环境变量里,这样方便我们使用rgbasm
、rgblink
和rgbfix
这三个工具。这里就省略介绍PATH变量怎么配了,其实不配置也行,就是用起来麻烦点呗。
但是像我的话,我肯定随手就把PATH配了,配个PATH岂不是洒洒水嘛,配好以后不要忘记终端(我用的Windows Terminal配PowerShell7)里打一下
rgbasm --version
看看正不正常,我这里显示的是
rgbasm v0.6.0
只要不报错那就是OK,编译环境准备完毕!
模拟器
选个趁手的模拟器也很重要,这里我跟着巨佬的教程选择用bgb。
我下载了我目前看到的最新版本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,打开后界面是这样的。
这里补充两个冷知识
- GameBoy只能在它那个绿不拉几的显示屏上显示四种颜色,分别是白色(0)、浅灰色(1)、深灰色(2)和黑色(3),不难发现我们使用2bit就能表示一个颜色,所以GB的颜色又叫做2bpp颜色。
- 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文件,然后还是File
点Export 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种情况,分别是c
、nc
、z
和nz
。c
表示溢出,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
开始写,_VRAM9000
是hardware.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平台研究出来的黑科技等待我们探索。
这里只是浅浅的了解了其中的一部分内容,后续再接着看还有什么有意思的东西待我们挖掘!