逃逸的小球(BALL Escape)

    由于最近要找工作,要将自己浸泡在编码的环境中热身,也抱着认真学习MFC精神的意图编写了这个小程序;本文章重在锻炼MFC中绘图、按键控制,编写过程中也遇到了一些困难,尤其是第四节提到的关键技术攻克,还望各位大侠指点。

一. 功能介绍

此应用程序具有一个红球和30个蓝球,用户按F2开始游戏,通过键盘方向键控制红球走向避免和篮球进行碰撞,记录红球逃逸时间,测试用户的应变能力;背景音乐采用经典魂斗罗。

程序外观如下图所示(比较丑陋):


二. 采用的MFC技术

2.1 控件背景重绘

在程序中将对话框的背景色设置为白色,则需要再对话框绘制前将其画刷设置为白色;

生成一个白色画刷:

#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;
}

2.2 按键响应

针对按回车会关闭窗口问题,并且程序中采用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);
}

2.3 播放音乐

在应用程序中添加音乐,自然也添加了激情。有些应用程序发布的时候并没有带音频文件,其将音频作为资源集成到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;
}


四. 关键技术攻克

4.1 问题研究

通过方向键控制小球,那如何捕获按键呢?在2.2节中介绍了采用隐藏消息预处理函数PreTranslateMessage中添加对按键消息的处理,顺利获取方向键虚拟键盘码,每捕获取一次按键消息就将红色控制球移动给定的距离值。

当方向键按下不松开的情况下,操作系统也会不断产生按键消息,但有一个很严重的问题,当按下一个方向键不松开的情况下,一开始移动总有一小会儿的延迟。

经过细致的研究发现:以按下左方向键不放为例,会连续不断地产生按左方向键的消息,如下图所示,但是LEFT1消息与LEFT2消息到来的时间间隔,比后面的任意按键消息的间隔要长,而且后面的消息时间间隔基本相同;如果你打开一个文本编辑器去控制光标的移动,同样会发现相同的问题,如果按下方向键不松开,按下方向键的开始时候光标移动会有短暂的延迟。


为什么会产生这种现象?因为当你按下某一个按键的时候,操作系统底层会先产生一个按键消息,然后进行一个时间判定,如果在这段时间内按键一直处于按下状态,那么接下来会按照某一极小的时间间隔产生按键消息 直到按键松开;由此可得,刚按下按键操作的延迟主要来源于底层对"是否一直按下此键"做出的判断时间。

这个情况在编辑器中允许出现,可在APM高操作的游戏中可不能出现,大大降低了用户操作的快感,那如何解决这个问题呢?

4.2 解决方法

思考了不少时间,尝试了不少方法,就在本人准备放弃了一刻,尝试了最后一个思路,而且一次性成功了,与大家分享一下。

当根据系统发送的消息不能很好的完成操作处理的时,可以自己获取系统的状态并且进行相应的处理操作。程序中游戏开始后创建一个线程,获取按键的状态(通过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;
}

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值