二维元胞自动机扩展

来源:互联网 发布:如何购买国外域名 编辑:程序博客网 时间:2024/04/27 21:26

之前做了一个生命游戏,MFC写的,但是觉得界面太丑,所以想用C#换个马甲

于是便动手将主要内容都封装进DLL里面了

之前DLL中只有自动机的类 Automation,用于计算机每一代矩阵的下一步,然后将首指针返回,让界面程序根据数据自己画


后来想到要用C#做马甲的话,要用C#建立子线程的消息循环,发消息,定义消息数值等等等等一系列麻烦的要死,又是DLLIMPORT,又是定义类型,常量什么的

后来想到将工作线程类 CellularThread 类装进DLL中,在DLL里面建立一个工作线程,线程函数如下:

DWORD CellularThread::ThreadMain(void){MSG msg;m_isThreadStarted = true;while (::GetMessage(&msg, nullptr, 0, 0)){this->MessageProc(static_cast<SelfMsg>(msg.message), msg.wParam, msg.lParam);if (this->IsRun){if (!this->OnRun()) break;}}return 0;}

这个是用CreateThread函数创建的线程,里面有this指针是因为这个不是static函数,一般来说要给CreateThread提供的函数指针必须是static 类型的,然后可以提供一个参数

但其实,0个参数的成员函数隐含了一个(this)的指针参数,和1个参数的普通函数没有区别

所以创建线程的时候,只需要做一下转换,一样可以用普通的成员函数做入口函数,具体如下,先写一个 union

union MemberToThreadProc{DWORD(WINAPI *ThreadProc)(LPVOID);DWORD(WINAPI CellularThread::*MemberThreadProc)();} StdCallProc;
然后创建线程时:

Instance->StdCallProc.MemberThreadProc = &CellularThread::ThreadMain;Instance->m_hThread = CreateThread(nullptr, 0, Instance->StdCallProc.ThreadProc,Instance, 0, reinterpret_cast<LPDWORD>(&CellularThread::ThreadId));

其中Instance是这个类的静态唯一对象指针,用来防止多次初始化

因为union成员之间共用内存,且该union的两个成员大小相同,指针都是4字节的,所以可以在不同格式之间转换,免得编译器报错

我试过一些其他方法,还是只有这种可行,不知道是不是vc编译器太坑了。。


进入ThreadMain之后设置号标识,然后建立消息循环(一般来说子线程没有自己的消息循环,但是一个线程第一次调用PeekMessage或GetMessage的时候系统会建立一个)

等待消息到来,之前用的PeekMessage不停的循环,导致空闲时也占满了整整一个核的CPU,所以换成了GetMessage,空闲时几乎不占CPU

若有消息,则进入 MessageProc消息过程函数处理,然后是运行时处理函数OnRun(接收到启动消息this->IsRun设为true)

bool CellularThread::OnRun(void){static MSG msg = { 0 };static uint fps = 0;while (this->IsRun){if (Automation::Instance->Radius == 1)Automation::Instance->ToNext(3);elseAutomation::Instance->ToNext();this->DrawCurCells(true);if (this->m_hParentWnd){if (++this->m_runTimes == MaxRunTimes){QueryPerformanceCounter(&m_liPerf);fps = static_cast<uint>(this->m_TickCountPerSec / (this->m_liPerf.QuadPart - this->m_lastRunTime));::PostMessage(this->m_hParentWnd, SelfNotifyMsg::wmNotifyPref,fps, NULL);}}if (::PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE)){if (msg.message == WM_QUIT)return false;this->MessageProc(static_cast<SelfMsg>(msg.message), msg.wParam, msg.lParam);}}}

主要就是两个 ToNext 和 DrawCurCell  计算下一代然后显示,然后有父窗口句柄(界面窗口)的话就返回一些帧数FPS 的信息, 最后PeekMessage看看有没有消息过来


这个ToNext步骤也改了一下,主要是没有自建数组了,在说明之前要说一下另外一个类, CellularImage 图像类,以下是初始化函数:

uint* CellularImage::InitImage(int width, int height){if (Instance)Instance->UnInitImage();Instance = new CellularImage(width, height);if (!Instance->m_image.Create(width, height, 32)){Instance->UnInitImage();return nullptr;}Instance->m_hImgDC = Instance->m_image.GetDC();BITMAP bm = { 0 };::GetObject((HBITMAP)(Instance->m_image), sizeof(BITMAP), &bm);Instance->m_pBmData = static_cast<uint*>(bm.bmBits);Instance->CellColor = RGB(40, 90, 160);Instance->BackgroundColor = RGB(0, 0, 0);Instance->CellLength = 1;return Instance->m_pBmData;}

主要是创建一个 Cimage 对象,大小是固定的, 1024*512  代表 横向有1024个细胞,纵向512个,这比之前的机会大了4倍,而性能不降反升

创建好CImage后取得 HBITMAP句柄,然后通过GetObject 获取 位图的数据结构,BITMAP结构如下:

typedef struct tagBITMAP {  LONG   bmType;  LONG   bmWidth;  LONG   bmHeight;  LONG   bmWidthBytes;  WORD   bmPlanes;  WORD   bmBitsPixel;  LPVOID bmBits;} BITMAP, *PBITMAP;

里面有图片的宽高,位数等,这里主要用到的是最后一个参数, bmBits,指向位图数据的第一个指针

其实位图中内存中的数据也就相当于一个 W * H 的二维数组,数据是连续的,因为我创建的是32位的位图

就刚好是一个uint 类型的二维数组,每个uint 保存了位图该坐标的像素信息(BGR, 和RGB 是倒过来的,红色在低位,蓝色在高位,最高的8位应该是Alpha通道的值,关于透明度的,不过没用到,都是0)

这样的话,就不用再通过那些低效的GDI函数绘图了,直接在位图的内存上操作,快了不是一点两点

返回bmBits 指针给Automation类,不过要注意的是,这个指向的不是位图的左上角,而是左下角,它是一行一行往上走的,最后一行数据对应的位图的第一行

一开始不知道画出来的都是个倒着的图片,然后是ToNext

void Automation::ToNext(uint times){while (times--){this->CalcNextInTemp();for (int y = 0; y < VerticalCellCount; ++y){for (int x = 0; x < HorizontalCellCount; ++x){if (m_ctmpCells[y][x] & m_ConditionOfLiving)BitmapData[y][x] = CellularImage::Instance->CellColor;else if (m_ctmpCells[y][x] & m_ConditionOfDeath)BitmapData[y][x] = CellularImage::Instance->BackgroundColor;m_ctmpCells[y][x] = 1;}}}}

这个BitmapData 是一个属性,用VC的扩展特性写的:

__declspec(property(get = GetBitMapData, put = SetBitMapData)) uint BitmapData[][];uint GetBitMapData(int row, int col){return m_pBmp[((VerticalCellCount - 1 - row) << X_EXPONENT) + col];}void SetBitMapData(int row, int col, uint value){m_pBmp[((VerticalCellCount - 1 - row) << X_EXPONENT) + col] = value;}
其实就是方便根据坐标返回位图数据,因为宽度固定是1024,所以X_EXPONENT 是10

inline void Automation::CalcNextInTemp(void){for (int y = 0, top = VerticalCellCount - this->Radius; y < VerticalCellCount; ++y, ++top){if (top == VerticalCellCount) top = 0;for (int x = 0, left = HorizontalCellCount - this->Radius; x < HorizontalCellCount; ++x, ++left){if (left == HorizontalCellCount) left = 0;if (BitmapData[y][x] == CellularImage::Instance->CellColor){for (int row = top, i = 0; i < (this->Radius << 1) + 1; ++i, ++row){if (row == VerticalCellCount) row = 0;for (int col = left, j = 0; j < (this->Radius << 1) + 1; ++j, ++col){if (col == HorizontalCellCount) col = 0;m_ctmpCells[row][col] <<= 1;}}m_ctmpCells[y][x] >>= 1;}}}}

在临时矩阵中计算下一代,先是遍历Bitmap,如果该坐标的颜色等于细胞颜色而不是背景色的话表示该坐标的细胞是活的,然后将该坐标周围的细胞的 周围存活细胞数右移一位,原因在上一篇写了,就不多说了

一般的生命游戏的话计算周围8个细胞,不过也可以扩展计算范围,比如周围24个,这个看自己设置

计算完后根据结果,符合存活条件的话,就将该坐标的颜色设置为细胞的颜色,死亡的话就是背景的颜色,其余的话不变


最后就简单了,位图都已经画好了,最后DrawCurCell 的时候调用回调指针将 CImage对象的HDC 句柄返回给界面程序,然后用StretchBlt 缩放即可

void MatrixWnd::DisplayCells(HDC hSrcDC, int xOffset, int yOffset, int wdith, int height){::StretchBlt(Instance->m_hClientDC, 0, 0,Instance->m_Widht, Instance->m_Height,hSrcDC, xOffset, yOffset, wdith, height, SRCCOPY);}

DLL的接口的话主要是用PostThreadMessage 向内置线程发送消息,消息类型 UI程序不用管,只要传递一下参数值就可以了,而且接口函数也精简分类了


另外还有图像缩放功能,比如默认的细胞大小只有1,这显然反人类,如果要修改的话眼睛都要看瞎,所以添加了一个功能,可以用过鼠标滚轮缩放图像,并且位置不变

其实主要就是计算上面StrethBlt 中的 xOffset , yOffset , width , height 等参数,具体代码都在源码中可以看到,下面上一张图:



如上图,设置规则的话只需要点上面蓝个蓝红相间的窗口就可以哦了,上面一排蓝色表示该细胞周围有这些数目的活细胞时该细胞存活

红色也一样,只不过是该细胞死亡,其余的没有颜色的表示,周围存活这些数量的细胞时该细胞不变,然后点设置,即可生效

当然,如果你想设置后面9-25 的规则的话要勾选宽半径才行


以下是下载链接:生成时修改了一些,XP应该也能运行,然后是 运行时库DLL也在里面了

生命游戏扩展版下载

0 0
原创粉丝点击