启动NES模拟器,再一次打开我们经典的超级马里奥1。
选择工具->查看器->卷轴查看器。这次会出现如下的一个窗口。
响应函数依旧是
WNDCMD CMainFrame::OnViewCommand( WNDCMDPARAM )
这方面的内容上节说过就不说了。
这次进入的是第二个分支
case ID_VIEW_NAMETABLE: if( !m_NameTableView.m_hWnd ) { m_NameTableView.Create( HWND_DESKTOP ); } ::SetWindowPos( m_NameTableView.m_hWnd, HWND_NOTOPMOST, 0, 0, 0, 0, SWP_NOMOVE|SWP_NOSIZE ); break;
直接介绍m_NameTableView所属的类。
CNameTableView
所在文件 Source Files/NameTableView.cpp Header Files/NameTableView.h
可以看到CNameTableView和CPatternView不论是类成员的组成,还是成员函数,都是大同小异的,因此下面我只挑不同的讲,有什么疑问看上节就好了。
位图信息头:
m_BitmapHdr.bih.biSize = sizeof(BITMAPINFOHEADER); m_BitmapHdr.bih.biWidth = 512; m_BitmapHdr.bih.biHeight = -480; m_BitmapHdr.bih.biPlanes = 1; m_BitmapHdr.bih.biBitCount = 8; m_BitmapHdr.bih.biCompression = BI_RGB; m_BitmapHdr.bih.biClrUsed = 16;
可以知道显示在窗口上的位图宽512像素,高480像素。
512*480有什么特殊含义吗?有!
NES中有4个命名表和属性表的组合。1个组合就可以在屏幕上显示出一幅画面了。
试着将512*480除以4,得到256*240。NES游戏画面的分辨率正好是256*240(32*30个tile组成画面,tile是8*8像素的,由此求得)。
这样就可以推断出,显示在卷轴查看器上的是4幅游戏背景。
至于NES中为何有4个命名表和属性表的组合,这暂时还不必深入研究(其实是博主我自己也还不懂)。
接下来深入讲解OnTimer函数。
for( INT i = 0; i < 16; i++ ) { m_BitmapHdr.rgb[i] = m_Palette[BGPAL[i]]; }
读取背景的16个颜色。
for( INT n = 0; n < 4; n++ ) { LPBYTE lpVRAM = PPU_MEM_BANK[8+n]; LPBYTE lpScnv = &m_lpPattern[(n>>1)*512*240+(n&1)*256]; for( INT y = 0; y < 30; y++ ) { for( INT x = 0; x < 32; x++ ) { INT tile = lpVRAM[x+y*32]*16+((PPUREG[0]&PPU_BGTBL_BIT)<<8); BYTE attr = ((lpVRAM[0x03C0+(x/4)+(y>>2)*8]>>((x&2)+(y&2)*2))&3)<<2; LPBYTE lpScn = &lpScnv[x*8+y*8*512]; LPBYTE lpPtn = &PPU_MEM_BANK[tile>>10][tile&0x03FF]; for( INT p = 0; p < 8; p++ ) { BYTE chr_l = lpPtn[p]; BYTE chr_h = lpPtn[p+8]; lpScn[0] = ((chr_h>>6)&2)|((chr_l>>7)&1)|attr; lpScn[4] = ((chr_h>>2)&2)|((chr_l>>3)&1)|attr; lpScn[1] = ((chr_h>>5)&2)|((chr_l>>6)&1)|attr; lpScn[5] = ((chr_h>>1)&2)|((chr_l>>2)&1)|attr; lpScn[2] = ((chr_h>>4)&2)|((chr_l>>5)&1)|attr; lpScn[6] = ((chr_h>>0)&2)|((chr_l>>1)&1)|attr; lpScn[3] = ((chr_h>>3)&2)|((chr_l>>4)&1)|attr; lpScn[7] = ((chr_h<<1)&2)|((chr_l>>0)&1)|attr; // Next line lpScn+=512; } } } }
第1行 从左到右依次画4幅游戏背景。
第2行 上节已经讲过PPU_MEM_BANK的后四个字节指针是指向四个命名表和属性表。因此lpVRAM指向第n个命名表和属 性表。
第3行 第n幅背景的首像素的地址。
第5-6行 30,32两个数字大家不陌生了吧,分别是纵向和横向的tile数。
第7-8行 如果把PPU_MEM_BANK看成大小为12*1K的连续空间(在真机,也就是FC家用机中,这块地址空间就是连续的),那么这个式子求出的结果就是tile的地址。不过由于这里把地址空间给分成了12份,因此求得的值10~13位是PPU_MEM_BANK数组的下标,0~9位是1K空间中的偏移量。
式子每部分的含义,再介绍一下。
lpVRAM[x+y*32]是tile的编号。一个tile在图案表中占16个字节,因此lpVRAM[x+y*32]*16是该tile在图案表中的偏移地址。
PPUREG[0]是一个IO端口,长16位,每一位都有特定含义。具体每一位什么含义可先不管,只需知道第4位表示当前使用的图案表的地址。该位为0,使用0x0000的图案表,该位为1,使用0x1000的图案表。(PPUREG[0]&PPU_BGTBL_BIT)<<8这个式子就是用来求得0x0000或0x1000。
第9-10行 求得的是每一个tile对应的高两位数据。attr第2,3位存放求得的数据,其余位为0(之所以这么保存,是为了后面求算方便)。
第11行 tile的起始屏幕指针。
第12行 从图案表读取1个tile所有像素的低2位,共16个字节。
第13行 正式开始画tile,每次循环画1行。
第14-15行 之前提到过,2个字节,第1个字节是像素的第0位,第2个字节是像素的第1位。
第17-24行 在求每一个像素。有了前面的基础,这些个式子应该很容易看懂。
第26行 指向tile的下一行的起始位置。