代码改变世界

WindowsPhone-GameBoy模拟器开发五--使用XNA初略实现Gameboy显示系统

2013-09-02 20:10  Hundre  阅读(2070)  评论(0编辑  收藏  举报

开篇前,最近弄了个空间,大家不嫌弃的话可以上去讨论讨论J

http://www.lihengzhe.cn

这一次,就来简单地实现gameboy的实现机制。先说一下本次内容涉及到的技术,其实也就一项—XNA,用XNA来完成我们最后的显示(windows phone的开发嘛,也只能用XNA了)。

思路大概是这样的:首先通过gameboy的内存取出图像的图块映射数据,通过映射来获取像素的数据,由于像素的数据实际上又是一个颜色值在调色板寄存器上的索引,所以再获取到调色板寄存器的数据,结合该像素的颜色索引值,最终确定该像素的颜色数据。

思路清楚了之后就要说一下这次要用到的技术了,首先决定使用一个2D贴图来当作Gb的背景来显示,2D贴图的数据就是从GB中获取的颜色数据。在XNA中,颜色是用32位数来表示的,看下图

image

通过上一篇文章知道,GB中模拟的颜色总共有4种

模拟的颜色

0

[255, 255, 255]

1

[192, 192, 192]

2

[96, 96, 96]

3

[0, 0, 0]

用对应的代码表示成

Dictionary<int,UInt32> ColorMap =new Dictionary<int,uint>(){{0, 0xFFFFFFFF},{1, 0xFFC0C0C0},{2, 0xFF606060},{3, 0xFF000000}};

这里最开始的8位永远是FF,因为在Gb的显示系统中没有实现alpha通道(就是透明度),好,接下来先看看运行的结果:

image

可以看到“内存”上方的框内有些黑黑白白的东西,其实这整个一个框是一个picturebox控件,里面这些黑黑白白的东西就是从Gb内存中显示出来的图像数据,当然,现在里面都是些测试数据,而且Gb的Cpu指令也都还没有实现,不过这不是重点了,重点是终于实现了从GameBoy内存中读取出图像数据并显示了。

下面进一步说明下是怎么实现的,供大家一起交流指点:

public Form1()
{
InitializeComponent();
PresentationParameters mPP = new PresentationParameters();
mPP.BackBufferHeight = pcbBackground.Height;
mPP.BackBufferWidth = pcbBackground.Width;
mPP.DeviceWindowHandle = pcbBackground.Handle;
mPP.DepthStencilFormat = DepthFormat.Depth24;
mPP.PresentationInterval = PresentInterval.Immediate;
mPP.IsFullScreen = false;
mGraphicsDevice = new GraphicsDevice(GraphicsAdapter.DefaultAdapter, GraphicsProfile.Reach, mPP);
mTexture = new Texture2D(mGraphicsDevice, 256, 256);
mSpriteBatch = new SpriteBatch(mGraphicsDevice);
mGBConsole.GPU.SetTheMapData(mLogo);
}

因为这次是在窗体程序中用XNA来显示,所以在初始化XNA中使用的图像设备(GraphicsDevice)对象时要告诉该对象在什么地方上进行画图,把picturebox变量的对象指针传给它的初始化参数中mPP.DeviceWindowHandle = pcbBackground.Handle;其中,这一句 mGBConsole.GPU.SetTheMapData(mLogo);是用来初始化映射数据的,当然这只是测试数据,以后会删去。

然后在窗体中放置一个timer控件,用来定时刷新我们的GB“屏幕”。

private void timer1_Tick(object sender, EventArgs e)
{
mGraphicsDevice.Clear(Framework.Color.White);
mGraphicsDevice.Textures[0] = null;
mFrameData = mGBConsole.OutputFrame();
mTexture.SetData(mFrameData);
mSpriteBatch.Begin();
mSpriteBatch.Draw(mTexture, new Framework.Vector2(0, 0), Framework.Color.White);
mSpriteBatch.End();
mGraphicsDevice.Present();
}

 

这都是很常规的XNA的绘图代码了,其中mFrameData = mGBConsole.OutputFrame();这一句是从GB的内存中获取到视屏图像的数据。跟进到代码中,最后可以去到一个GameboyGPU的类,这个就是用来处理视频数据的类了,详细的请看代码中的注释:

public class GameboyGPU
{
readonly int TileSize = 8 * 2;//16个地址,每个地址存8位数据,共16KB
byte[] mVRAM = new byte[8 * 1024];//8KB video ram
byte PalleteRegister = 0;

//for test,给内存中的图像块的数据区随机的放上一些数据,该方法会在以后删除 public void InitialData() { Random mRandom = new Random(); for (int i = 0; i < 0x17FF;i++ ) { mVRAM[i] = (byte)(mRandom.Next() % 0xFF); } for (int i = 0x1800; i < 0x1FFF; i++) { mVRAM[i] = (byte)(mRandom.Next() % 0xFF); } } public byte[] Tiles1Data { get { return mVRAM.Take(0xFFF).ToArray(); }} public byte[] Tiles1Map { get { return mVRAM.Skip(0x1800).Take(0x3FF).ToArray(); } } public byte[] Tiles2Map { get { return mVRAM.Skip(0x1C00).Take(0x3FF).ToArray(); } } //设置图像映射数据(总感觉这个方法不是很好,以后要改改) public void SetTheMapData(byte[] aMapData) { Buffer.BlockCopy(aMapData, 0, mVRAM, 0x1800, aMapData.Length); } public byte[] GetSingleTileData(int aTileIndex) { return Tiles1Data.Skip(aTileIndex * TileSize).Take(TileSize).ToArray(); } //从这个方法开始处理图像数据,传入内存中视频缓存部分的数据和调色板的数据 public UInt32[] OuputFrameData(byte[] aMemoryData, byte aPalleteRegister) { mVRAM = aMemoryData; PalleteRegister = aPalleteRegister; InitialData();//为测试初始化一些数据 UInt32[] mResult = new UInt32[256 * 256];//每帧图像的大小为256*256个像素 int mResultCount = 0; foreach (byte tileIndex in Tiles1Map) { //根据映射获取到图块数据,再根据图块数据获取到每个像素的颜色值 uint[] mTileColorData = GetColorData( GetSingleTileData(tileIndex)); Buffer.BlockCopy(mTileColorData, 0, mResult, mResultCount, mTileColorData.Length); mResultCount += mTileColorData.Length; } return mResult; } //获取每个图块的颜色数据 public UInt32[] GetColorData(byte[] aTileData) { if (aTileData.Length % 2 != 0) throw new Exception("VRAM data error"); UInt32[] mResultData = new UInt32[8*8]; int mColorCounter=0; for (int i = 0; i < aTileData.Length - 1; i++) { //由上一篇文章分析得,图块的每行有8个像素,使用两个字节来表示,一个字节表示每个像素的高位,另一个字节用来表示每个像素的低位,两个位能表示的数据范围为0—3共4个数,这个数是一个调色板的索引代码,用这个代码可以去到调色板中查出实际的颜色值。 uint mHigh = aTileData[i]; uint mLow = aTileData[++i]; //caculate the color index for (int j = 7; j >=0; j--) { int mHighValue = (mHigh & (16*(j+1)))==(16*(j+1))?1:0; int mLowValue = (mLow & (16 * (j + 1))) == (16 * (j + 1)) ? 1 : 0; mResultData[mColorCounter] = GetColorValue( (uint)mHighValue*2+ (uint)mLowValue);//表示高位的数据要乘以2相当于左移一位,从个位变成十位 if (mColorCounter > 8*8) throw new Exception("Color data error"); mColorCounter++; } } return mResultData; } //查询调色板中的颜色获取实际的颜色值 public UInt32 GetColorValue(uint aColorIndex) { try { //调色板共八位,由右到左每两位划分为一组,共4组,传进来的索引值就是用来索引取哪一组的数据。但是没个组中存放的其实还是一个索引值,用来到颜色表中查找实际的颜色数据,因为每2位划分为一组,索引每组能表示4个索引值,正好对应着Gb能表示的4种颜色 switch (aColorIndex) { case 0://获取第1组的颜色索引 return ColorConfig.ColorMap[PalleteRegister&3]; case 1://第2组 return ColorConfig.ColorMap[(PalleteRegister&12)>>2]; case 2: /第3组 return ColorConfig.ColorMap[(PalleteRegister&48)>>4]; case 3: /第4组 return ColorConfig.ColorMap[(PalleteRegister&192) >> 6]; default: return 0; } } catch { return 0; } } }

 

这里说一下,为什么从图块中映射过来的数据不直接映射到颜色表而是映射到调色板上

通过一幅图来表示一下这个家伙的映射关系:

个人觉得,其实在硬件中,3中的数据应该是直接固化在硬件上的,因为gameboy中模拟的颜色都是固定了的,所以这部分数据是无法修改的。在这个前提下,如果不使用调色板,在要对画面进行修改的时候,就需要修改1中的数据,但是,有些游戏特效,比如画面的反色,只是修改像素的颜色,也需要重新刷新内存中的每个像素点的数据,而如果使用了调色板的话,只需要修改调色板中的数据即可。对于gameboy而言,整版数据有256*256个点的数据需要修改,最坏情况下整个图块区域的数据都要修改,共4KB的数据,而使用了调色板的话,最多只需要修改1Byte的数据,差了4000倍,在速度上快了很多。

好了,这就是比较粗鲁的Gb显示系统的代码了,这次的代码还有非常多的,如显示的画面会不停地闪烁,但是对于观察指令的运行情况应该已经足够了,接下来就是实现cpu的指令了。

代码已上传到codeplex,不嫌弃的话,欢迎大家指点:https://emulatorwp.codeplex.com/SourceControl/list/changesets