启动NES模拟器,再一次打开我们经典的超级马里奥1。

选择工具->查看器->卷轴查看器。这次会出现如下的一个窗口。

141725196.png


响应函数依旧是

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的下一行的起始位置。