此应用程序具有一个红球和30个蓝球,用户按F2开始游戏,通过键盘方向键控制红球走向避免和篮球进行碰撞,记录红球逃逸时间,测试用户的应变能力;背景音乐采用经典魂斗罗。
程序外观如下图所示(比较丑陋):
在程序中将对话框的背景色设置为白色,则需要再对话框绘制前将其画刷设置为白色;
生成一个白色画刷:
#define WRITE RGB(255, 255, 255) // 设置对话框画刷颜色 m_Brush.CreateSolidBrush(WRITE);
重新绑定颜色控制消息处理函数,并且隐藏OnCtlColor函数(通常人都称为重载,这个是不严格的,应该称其为隐藏),返回绘制对话框的画刷:
//设置背景色画刷 HBRUSH CEscapeBallDlg::OnCtlColor(CDC* pDC, CWnd* pWnd, UINT nCtlColor) { HBRUSH hbr = CDialog::OnCtlColor(pDC, pWnd, nCtlColor); //判断是否为对话框 if(nCtlColor == CTLCOLOR_DLG) return m_Brush; return hbr; }
针对按回车会关闭窗口问题,并且程序中采用F2开始游戏,采用隐藏消息预处理函数PreTranslateMessage:
//用于处理按键控制:F2,方向键 BOOL CEscapeBallDlg::PreTranslateMessage(MSG* pMsg) { if (pMsg->wParam == VK_RETURN) //回车键不进行处理 return TRUE; if (pMsg->wParam == VK_F2 && !m_bEscaping) //按键F2开始游戏 BeginGame(); return CDialog::PreTranslateMessage(pMsg); }
在应用程序中添加音乐,自然也添加了激情。有些应用程序发布的时候并没有带音频文件,其将音频作为资源集成到MFC中;
先将WAV格式文件导入到MFC工程资源中,对音频采用异步、循环播放的模式:
PlaySound(MAKEINTRESOURCE(IDR_WAVE_HUNDOULUO), NULL, SND_RESOURCE|SND_ASYNC|SND_LOOP);
整个程序挖掘不出高端算法,本人也就介绍下程序中采用的简单的篮球运动方向算法;蓝球的运动方向是在 游戏开始 和 碰撞边界反弹的时候进行确定,采用半随机的方式。所谓半随机:在确定某个篮球运动方向前,先抛硬币正反面决定是否随机产生方向;如果不随机方向,则根据红球当前的位置作为走向,如果随机,这产生一个随机的方向。
/*********************************************************************** *函数名称:ComputeForward *函数参数:BallInfo *bBall: 当前篮球的信息,包括位置、大小、颜色等 * BallInfo *rBall: 当前红球的信息,包括位置、大小、颜色等 * BOOL is_rand : 是否采用半随机方法; * TRUE表示采用随机算法 * FALSE表示以红球作为运动方向 *返回值 : 无 *功能 : 计算蓝球运动偏移方向和大小 *************************************************************************/ void CEscapeBallDlg::ComputeForward(BallInfo *bBall, BallInfo* rBall, BOOL is_rand = TRUE) { int x = bBall->pos_x; int y = bBall->pos_y; int x1 = rBall->pos_x; int y1 = rBall->pos_y; //随机硬币 BOOL is_randDirect = (rand() % 2) ? TRUE:FALSE; if (is_rand && is_randDirect) { int rand_x = rand() % m_MaxX; int rand_y = rand() % m_MaxY; x1 = rand_x; y1 = rand_y; } //非特殊情况,根据斜率比求出移动偏移量 int offset_x = fabs((x - x1) * 1.0) / sqrt((pow((x - x1) * 1.0, 2.0) + pow((y - y1) * 1.0, 2.0))) * MOVEOFFSET; int offset_y = fabs((y - y1) * 1.0) / sqrt((pow((x - x1) * 1.0, 2.0) + pow((y - y1) * 1.0, 2.0))) * MOVEOFFSET; //计算球的走向 bBall->offset_x = (x1 - x) < 0 ? -1 * offset_x : offset_x; bBall->offset_y = (y1 - y) < 0 ? -1 * offset_y : offset_y; }
通过方向键控制小球,那如何捕获按键呢?在2.2节中介绍了采用隐藏消息预处理函数PreTranslateMessage中添加对按键消息的处理,顺利获取方向键虚拟键盘码,每捕获取一次按键消息就将红色控制球移动给定的距离值。
当方向键按下不松开的情况下,操作系统也会不断产生按键消息,但有一个很严重的问题,当按下一个方向键不松开的情况下,一开始移动总有一小会儿的延迟。
经过细致的研究发现:以按下左方向键不放为例,会连续不断地产生按左方向键的消息,如下图所示,但是LEFT1消息与LEFT2消息到来的时间间隔,比后面的任意按键消息的间隔要长,而且后面的消息时间间隔基本相同;如果你打开一个文本编辑器去控制光标的移动,同样会发现相同的问题,如果按下方向键不松开,按下方向键的开始时候光标移动会有短暂的延迟。
为什么会产生这种现象?因为当你按下某一个按键的时候,操作系统底层会先产生一个按键消息,然后进行一个时间判定,如果在这段时间内按键一直处于按下状态,那么接下来会按照某一极小的时间间隔产生按键消息 直到按键松开;由此可得,刚按下按键操作的延迟主要来源于底层对"是否一直按下此键"做出的判断时间。
这个情况在编辑器中允许出现,可在APM高操作的游戏中可不能出现,大大降低了用户操作的快感,那如何解决这个问题呢?
思考了不少时间,尝试了不少方法,就在本人准备放弃了一刻,尝试了最后一个思路,而且一次性成功了,与大家分享一下。
当根据系统发送的消息不能很好的完成操作处理的时,可以自己获取系统的状态并且进行相应的处理操作。程序中游戏开始后创建一个线程,获取按键的状态(通过GetKeyStae函数),并根据按键状态信息,得到红球的运动方向(总共八个方向),进行相应的移动操作。
//红球控制线程 UINT CEscapeBallDlg::RedBallMoveFunc(LPVOID lParam) { //获得对话框的指针 CEscapeBallDlg* pDlg = (CEscapeBallDlg *)lParam; while (pDlg->m_bEscaping) //正在游戏中 { //仅仅采用低四位作为上下左右的标示 //1表示按下,反正则没有按下 //从高位到低位分别为上、下、左、右按键 BYTE keyState = 0; keyState = (GetKeyState(VK_UP) & 0x8000) ? (keyState | 0x08) : keyState; keyState = (GetKeyState(VK_DOWN) & 0x8000) ? (keyState | 0x04) : keyState; keyState = (GetKeyState(VK_LEFT) & 0x8000) ? (keyState | 0x02) : keyState; keyState = (GetKeyState(VK_RIGHT) & 0x8000) ? (keyState | 0x01) : keyState; //抵消两个相反方向的按键 if ((keyState & 0x01) && (keyState & 0x02)) { keyState = keyState & 0x0C; } if ((keyState & 0x04) && (keyState & 0x08)) { keyState = keyState & 0x03; } int offset_x = 0; //x偏移大小 int offset_y = 0; //y偏移大小 //八个方向判断 switch(keyState) { case UP: offset_x = 0; offset_y = -MOVEOFFSET; break; case DOWN: offset_x = 0; offset_y = MOVEOFFSET; break; case LEFT: offset_x = -MOVEOFFSET; offset_y = 0; break; case RIGHT: offset_x = MOVEOFFSET; offset_y = 0; break; case LEFTUP: offset_x = -SQRTMOVEOFFSET; offset_y = -SQRTMOVEOFFSET; break; case LEFTDOWN: offset_x = -SQRTMOVEOFFSET; offset_y = SQRTMOVEOFFSET; break; case RIGHTUP: offset_x = SQRTMOVEOFFSET; offset_y = -SQRTMOVEOFFSET; break; case RIGHTDOWN: offset_x = SQRTMOVEOFFSET; offset_y = SQRTMOVEOFFSET; break; default: break; } if (offset_x != 0 || offset_y != 0) //计算如果发生按键,红球根据偏移进行移动 pDlg->ReDrawRedBall(offset_x, offset_y); if (!pDlg->CheckEscape()) //发生碰撞则结束游戏 { pDlg->EndGame(); //结束本轮游戏 } Sleep(30); //适当的延迟,不然球速太快了 } return 0; }