从零开始写NES模拟器

 

之前写了如何写一个nes模拟器,感觉有些语焉不详,现补充一个小白文章。

FC游戏模拟器是如何工作的

我们小时候很多人玩过任天堂的红白机游戏。但是它是如何工作的,却很少有人提及。

今天我们来讨论任天堂的游戏机工作机制。首先我们看到的是游戏画面,实际上老式的电视的分辨率是240*256的。

它是如何工作的呢?

FC游戏机中包含CPU,PPU,卡带,内存。当我们开机时,CPU加载卡带的游戏程序到内存中,将游戏中的画面通过端口写入到PPU(图片处理器)中。CPU只需要对PPU进行简单的端口操作,PPU就能将需要显示的内容显示在屏幕上。

 

PPU负责绘图图像,请注意PPU绘制了每一个点,即240*256=61440个点。并且绘制这么多点只需要20ms。在1秒的时间内要绘制50幅这样的图像。我们的游戏才会如此的流畅。所以要做这个模拟器。我们需要在窗口中绘制连续的图像。在Windows中我们需要寻找一个能快速绘制图像的函数。它要在1秒钟能绘制50幅连续的图像。

 

第一篇 CreateDIBSection函数的使用

 

在想写模拟器之前,我们寻找这样一个函数。我们把图像的(颜色)RGB值写入到一个内存中,然后把它画到窗口中来。

它就是CreateDIBSection。其实有个知名度比较高的函数SetPixel(),但是它太慢了。SetPixel函数是把一个点直接画到屏幕上来,6万个点,太耗时了。而CreateDIBSection不一样,它帮我们分配了一段内存,我们把一屏幕的颜色值都拷贝进去,一次输出到屏幕,所以速度快。

当然我们还可以用DirectX中的函数,速度更快,先不讨论它。

函数的声明如下:

HBITMAP CreateDIBSection(

HDC hdc, // handle to DC 指向DC的句柄,就是我们窗口的DC,

CONST BITMAPINFO *pbmi, // bitmap data 位图信息

UINT iUsage, // data type indicator 数据类型,我们需要的是RGB格式

VOID **ppvBits, // bit values 指向内存地址的指针,注意这个函数给我们分配的一段内存,我们只需要提供指针给它就好。

HANDLE hSection, // h 不管它

DWORD dwOffset // offset to bitmap bit values 不管它

);

下面我分别用SetPixel和CreateDIBSection函数实现显示点阵图形A。

 

绘制图形的最基本的是绘制点,我们来实现一下。

/************************************************************************/

/* 注释: 屏幕画点函数 - 绘制到内存中

函数名: draw_window_point

参数: x,y 要画点的坐标,color 颜色值,video 对应内存缓冲地址

width 窗口的宽度 */

/************************************************************************/

void draw_window_point(int x,int y,UINT color,UINT * video,int width)

{

//点的偏移量

int p_offset = 0;

//坐标的值超出了限制,就直接返回

if(y > NES_DISP_WIDTH -1 || x > NES_DISP_WIDTH -1)

return;

//计算点位置,

p_offset = y * width + x;

//画点,把要绘制的颜色值放入到对应的内存。

*(video + p_offset) = (UINT)color;

//返回1

return;

}

 

由于所有的图形我们都需要现绘制在内存中,然后一次画到屏幕上,这样才能避免速度慢引起的闪烁。

这个函数就是把点绘制到指定的内存中,x,y指明了绘制的坐标,color是颜色值,video是绘制的起始地址,

width是绘制图形的宽度。现在假如我们要把NES游戏的画面从PPU中取出并绘制到内存中,那么游戏画面是

240高*256宽的。假设我们取第3行,第2列的颜色值,放入到 video中,那么p_offset = 3 * 256 + 2。

如果你理解了这些,我们就可以绘制图形了。在NES的基本块是被称为Tile(砖块),我们先绘制砖块。

 

第一步,绘制Tile砖块

Tile在游戏中被定义为8X8像素的,

00010000 一个字节0x10

00101000 一个字节0x28

01000100 一个字节0x44

10000010 一个字节0x82

11111110 一个字节0xfe

10000010 一个字节0x82

10000010 一个字节0x82

10000010 一个字节0x82

例如上图是以字母A的表示方法,用每一位表示一个点。因为是1位只能表示0或1,上图只能表示两种颜色(例如黑和白)。00010000是二进制,而后面的0x10是16进制,二者等价。我们先来绘制它(一个“A”)。

1.使用SetPixel来绘制

首先定义一个数组,就是上面的内容。

//字模数组

unsigned char buffer[]={0x10,0x28,0x44,0x82,0xfe,0x82,0x82,0x82};

然后写出绘制它的程序:

void Draw_Char88(HDC hdc,int x,int y,unsigned char * p)

{

int i,j;

 

//绘制图案

for (j =0;j < 8;j++)

{

for (i = 0;i < 8; i++)

{

//从数组中取出1位,如果为1,绘制红色点。

if(1 == (p[j] >> (7 - i) & 1))

 

SetPixel(hdc,x + i,y + j,RGB(255,0,0));

 

}

}

}

如果没有意外,你能在坐标(100,100)处看到一个红色的“A”。在这个函数中怎么从一个字节中取出1位是你需要思考一下的,剩下的都很简单。

2.使用CreateDIBSection

如你所见,使用 SetPixel是简单的,由于性能的考量,我们不得考虑更快的实现方式。

这比第一种要复杂的多,你要做好心理准备。

Draw_Char88(hdc,100,100,(unsigned char*)buffer);

hmap = CreateScreen(hWnd);

LoadFrame(g_WorkFrame,hdc,g_pScreenMem,hmap);

这种方式,你需要把图形写入到内存中,然后复制到函数为你分配的位图缓冲中,最后才能显示到屏幕。我们继续用上一个函数Draw_Char88,但是把SetPixel替换成了draw_window_point,因为我们不直接输出到屏幕,而是内存。接下来我们使用 CreateScreen创建了一个位图段,它为我们分配了一段显示内存,我们需要把写入内存的数据拷贝过去。在 LoadFrame中执行了拷贝过程(memcpy),并把图形绘制到屏幕中。下图是demo程序的显示结果。

从零开始写NES模拟器_第1张图片

完成了这2个程序,你应该已经了解点阵图形的显示。当然,上面用1位表示颜色,只能有2种颜色肯定是不够的。如果要表示多种颜色,我们当然期望每个点都能直接用颜色值表示(假如是32位,32*8=256字节,一个Tile图块需要256字节表示,显然占有内存太大了)。但NES内存过小,不可能采用直接颜色值的方式。什么样的方式能减小使用内存呢?

1、使用多级索引,NES的Tile中的值不是颜色值,而是索引到色盘中;

2,减少颜色数,NES在一共只定义了64种颜色。

NES中使用了4位的颜色索引,在属性表中存储了高2位;在图案表中存储了低2位,其中前一个图形块(8字节)表示颜色的bit0位,后一个图形块(8字节)表示颜色的bit1位。

下图显示了低2位是如何表示的,注意圈中的数字,Address中-$000E 的最高为1,$0006中最高位是1,那么图形的第7行第1个颜色的索引就是3(Result中圈中数字).

从零开始写NES模拟器_第2张图片

我们就来利用低2位绘制图案表。

你可能感兴趣的:(编程)