第7章 做游戏的主人——Windows游戏输入消息处理

目前游戏开发中有两套输出消息的体系。一套就是我们本章要讲的Windows 消息处理,另一套就是在本书后面讲解DirectX时讲到的为游戏而生的Directlnput 消息处理API 。很多时候Directlnput 解决不了的问题,还得反过来找Windows消息处理帮忙呢。

7.1 Windows 键盘消息处理

首先我们对Windows 系统下键盘的基本概念及键盘消息的处理方式来一个简单的介绍。

7.1.1 虚拟键码与键盘消息

在Windows 中,所有键盘的按键都被定义为一组通用的“虚拟键码”,也就是说在Windows系统下所有按键都会被视为虚拟键(包含鼠标键在内),而每一个虚拟键都有其对应的一个虚拟键码。
Windows 系统是一个消息驱动的环境, 一旦我们在键盘上进行输入操作,那么系统便会接收到对应的键盘消息,下面我们再列出最常见的5 种键盘息。


当某一按键被接下时,伴随着这个操作所产生的是以虚拟键码类型传送的WM _KEYDOWN 与WM_KEYUP 消息。当程序接收到这些消息时。便可由虚拟键码的信息来得知是哪个按键被按下。
此外, WM_CHAR 则是当按下的按键为定义于ASCII 中的可打印字符时, 便发出此字符消息。

7.1.2 键盘消息处理

在Windows 中,我们把键盘消息和其他消息(比如窗口重绘消息WM_PAINT ) 一视同仁,同样是在消息处理函数中间来处理的。而按下按键事件一定会紧随着一个松开按键的事件, 因此WM_KEYDOWN 与WM_KEYUP 两种消息必须是成对发生的。但我们往往在程序中对WM_KEYDOWN 消息进行处理,而无视WM KEYUP 消息。

我们在之前讲窗口过程函数的时候提到过,窗口过程函数有两个参数与消息输出有关,它们就是——wParam 和lParam :

LRESULT CALLBACK WindowProc(
  __in  HWND hwnd,
  __in  UINT uMsg,
  __in  WPARAM wParam,
  __in  LPARAM lParam
);

当键盘消息触发时, wParam 的值为按下按键的虚拟键码, Windows 中所定义的虚拟键码是以“VK”开头的, lParam 则储存按键的相关状态信息, 因此,如果我们的程序要对键盘输入操作进行处理, 就可以用一个switch 语句来判断wParam 中的内容井进行理。那么消息处理函数的内容可以定义如下:

//-----------------------------------【WndProc( )函数】--------------------------------------
//	描述:窗口过程函数WndProc,对窗口消息进行处理
//------------------------------------------------------------------------------------------------
LRESULT CALLBACK WndProc( HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam )      
{

	switch( message )						//switch语句开始
	{

	case WM_KEYDOWN:	     //按下键盘消息
		//判断按键的虚拟键码
		switch (wParam) 
		{
		case VK_ESCAPE:           //按下【Esc】键
			DestroyWindow(hwnd);    // 销毁窗口, 并发送一条WM_DESTROY消息
			PostQuitMessage( 0 );  //结束程序
			break;
		case VK_UP:				  //按下【↑】键
			break;
		case VK_DOWN:			  //按下【↓】键
			break;
		case VK_LEFT:			  //按下【←】键				
			break;
		case VK_RIGHT:			   //按下【→】键
			break;
		}			
		break;								//跳出该switch语句

	case WM_DESTROY:					//若是窗口销毁消息
		Game_CleanUp(hwnd);			//调用自定义的资源清理函数Game_CleanUp()进行退出前的资源清理
		PostQuitMessage( 0 );			//向系统表明有个线程有终止请求。用来响应WM_DESTROY消息
		break;									//跳出该switch语句

	default:										//若上述case条件都不符合,则执行该default语句
		return DefWindowProc( hwnd, message, wParam, lParam );		//调用缺省的窗口过程
	}

	return 0;									//正常退出
}

7.1.3 示例程序GDldemo10

这个示例让玩家以【↑ 】【↓ 】【← 】【→ 】键, 控制画面中李逍遥的上下左右的移动, 颇有些在玩仙剑奇侠传的感觉。
这里使用了李逍遥在4 个不同方向上走动的连续图案:


我们来看看代码。
程序代码片段一,全局变量声明:

//-----------------------------------【全局变量声明部分】-------------------------------------
//	描述:全局变量的声明
//------------------------------------------------------------------------------------------------
HDC				g_hdc=NULL,g_mdc=NULL,g_bufdc=NULL;      //全局设备环境句柄与两个全局内存DC句柄
HBITMAP		g_hSprite[4]={NULL},g_hBackGround=NULL;								//定义位图句柄数组用于存储四张方向图,以及定义存储背景图的句柄
DWORD		g_tPre=0,g_tNow=0;					//声明l两个函数来记录时间,g_tPre记录上一次绘图的时间,g_tNow记录此次准备绘图的时间
int					g_iNum=0,g_iX=0,g_iY=0;                //g_iNum用来记录图号,g_iX,g_iY分别表示贴图的横纵坐标
int					g_iDirection=0;//g_iDirection为人物移动方向,这里我们中以0,1,2,3代表人物上,下,左,右方向上的移动

程序代码片段二, 窗口过程函数WndProc:

//-----------------------------------【WndProc( )函数】--------------------------------------
//	描述:窗口过程函数WndProc,对窗口消息进行处理
//------------------------------------------------------------------------------------------------
LRESULT CALLBACK WndProc( HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam )      
{

	switch( message )						//switch语句开始
	{

	case WM_KEYDOWN:	     //按下键盘消息
		//判断按键的虚拟键码
		switch (wParam) 
		{
		case VK_ESCAPE:           //按下【Esc】键
			DestroyWindow(hwnd);    // 销毁窗口, 并发送一条WM_DESTROY消息
			PostQuitMessage( 0 );  //结束程序
			break;
		case VK_UP:				  //按下【↑】键
			//根据按键加入人物移动的量(每次按下一次按键移动10个单位),来决定人物贴图坐标的X与Y值,接着判断坐标是否超出窗口区域,若有则进行修正
			g_iY -= 10;
			g_iDirection = 0;
			if(g_iY < 0)
				g_iY = 0;
			break;
		case VK_DOWN:			  //按下【↓】键
			g_iY += 10;
			g_iDirection = 1;
			if(g_iY > WINDOW_HEIGHT-135)
				g_iY = WINDOW_HEIGHT-135;	
			break;
		case VK_LEFT:			  //按下【←】键			
			g_iX -= 10;
			g_iDirection = 2;
			if(g_iX < 0)
				g_iX = 0;		
			break;
		case VK_RIGHT:			   //按下【→】键
			g_iX += 10;
			g_iDirection = 3;
			if(g_iX > WINDOW_WIDTH-75)
				g_iX = WINDOW_WIDTH-75;
			break;
		}			
		break;								//跳出该switch语句

	case WM_DESTROY:					//若是窗口销毁消息
		Game_CleanUp(hwnd);			//调用自定义的资源清理函数Game_CleanUp()进行退出前的资源清理
		PostQuitMessage( 0 );			//向系统表明有个线程有终止请求。用来响应WM_DESTROY消息
		break;									//跳出该switch语句

	default:										//若上述case条件都不符合,则执行该default语句
		return DefWindowProc( hwnd, message, wParam, lParam );		//调用缺省的窗口过程
	}

	return 0;									//正常退出
}
程序代码片段三, Game_Init()函数:

//-----------------------------------【Game_Init( )函数】--------------------------------------
//	描述:初始化函数,进行一些简单的初始化
//------------------------------------------------------------------------------------------------
BOOL Game_Init( HWND hwnd )
{
	HBITMAP bmp;

	g_hdc = GetDC(hwnd);  
	g_mdc = CreateCompatibleDC(g_hdc);  //创建一个和hdc兼容的内存DC
	g_bufdc = CreateCompatibleDC(g_hdc);//再创建一个和hdc兼容的缓冲DC
	bmp = CreateCompatibleBitmap(g_hdc,WINDOW_WIDTH,WINDOW_HEIGHT);

	//设定人物贴图初始位置和移动方向
	g_iX = 150;
	g_iY = 350;
	g_iDirection = 3;
	g_iNum = 0;

	SelectObject(g_mdc,bmp);
	//加载各张跑动图及背景图,这里以0,1,2,3来代表人物的上,下,左,右移动
	g_hSprite[0] = (HBITMAP)LoadImage(NULL,L"go1.bmp",IMAGE_BITMAP,480,216,LR_LOADFROMFILE);
	g_hSprite[1] = (HBITMAP)LoadImage(NULL,L"go2.bmp",IMAGE_BITMAP,480,216,LR_LOADFROMFILE);
	g_hSprite[2] = (HBITMAP)LoadImage(NULL,L"go3.bmp",IMAGE_BITMAP,480,216,LR_LOADFROMFILE);
	g_hSprite[3] = (HBITMAP)LoadImage(NULL,L"go4.bmp",IMAGE_BITMAP,480,216,LR_LOADFROMFILE);
	g_hBackGround = (HBITMAP)LoadImage(NULL,L"bg.bmp",IMAGE_BITMAP,WINDOW_WIDTH,WINDOW_HEIGHT,LR_LOADFROMFILE);

	Game_Paint(hwnd);
	return TRUE;
}

上面这段代码和之前讲解的基本类似,就是初始化三缓冲环境,载入位图以及设定全局参数的初始值。
程序代码片段四, Game_ Paint()函数:
//-----------------------------------【Game_Paint( )函数】--------------------------------------
//	描述:绘制函数,在此函数中进行绘制操作
//--------------------------------------------------------------------------------------------------
VOID Game_Paint( HWND hwnd )
{
	//先在mdc中贴上背景图
	SelectObject(g_bufdc,g_hBackGround);
	BitBlt(g_mdc,0,0,WINDOW_WIDTH,WINDOW_HEIGHT,g_bufdc,0,0,SRCCOPY);

	//按照目前的移动方向取出对应人物的连续走动图,并确定截取人物图的宽度与高度
	SelectObject(g_bufdc,g_hSprite[g_iDirection]);
	BitBlt(g_mdc,g_iX,g_iY,60,108,g_bufdc,g_iNum*60,108,SRCAND);
	BitBlt(g_mdc,g_iX,g_iY,60,108,g_bufdc,g_iNum*60,0,SRCPAINT);
	//将最后的画面显示在窗口中
	BitBlt(g_hdc,0,0,WINDOW_WIDTH,WINDOW_HEIGHT,g_mdc,0,0,SRCCOPY);

	g_tPre = GetTickCount();     //记录此次绘图时间
	g_iNum++;
	if(g_iNum == 8)
		g_iNum = 0;

}

以上这段代码先贴背景图到g_mdc 中,然后根据目前的移动方向取出对应人物的连续走动图,并根据当前图号、宽度以及高度截取人物图中的单张人物一部分贴到g_mdc 中, 最后再将处理完成的g_mdc 中的图贴到g_hdc 中。

来看一下运行截图:



7 .2 Windows 鼠标消息处理

7.2.1 鼠标消息的处理方式

大家都知道, 目前市场上主流鼠标规格为两个按键加上一个滚轮。那么,我们先列出Windows中这种鼠标设备输入时的消息类型:


处理鼠标消息的方法与处理键盘消息的方法类似,同样是在消息处理函数中加入要处理的鼠标消息类型, 当鼠标消息发生时,输入的参数wParam 与lParam 就储存了鼠标状态的相关信息。
下面我们分别来展开讲解一下wParam 与lParam 参数以及滚轮消息。
1. IParam 参数
IParam 参数的值可分为高位字节与低位字节两个部分,其中高节部分储存的是鼠标光标所在的X 坐标值,低位字节部分存储的则是鼠标光标所在的Y 坐标值。
我们可以用下面两个宏来取得鼠标的坐标值:
WORD LOWORD(
    DWORD dwValue   //返回鼠标光标所在的X坐标值
);

WORD HIWORD(
    DWORD dwValue  // 返回鼠标光标所在的Y坐标值
);
2. wParam 参数
wParam 参数的值记录着鼠标按键及键盘CtrI 键与Shift键的状态信息,我们一般通过下面的这些定义在“ WINUSER . H ”中的测试标志与wParam 参数来检查上述按键的按下状态。

比如某个鼠标消息发生时, 要测试鼠标左键是否也被按下, 就把wPararn 拿着和某种消息& (逻辑与) 一下, 就像这样:
if (wParam & MK_LBUTTON) //单击了鼠标左键
{
   //鼠标左键被按下的消息处理代码
}

我们就是这样利用wParam 参数和测试标志来测试鼠标键是否被按下的。当按键被按下时, 条件式“ wParam &MK_LBUTTON ”所传回的结果就会为true 。当然, 若消息函数接收到“WM_LBUTTONDOWN ”消息,同样也可以知道鼠标键被按下而不必再去额外做这样的测试,这点大家要注意。
比如要测试鼠标左键与Shift 键的按下状态,那么程序我们就这样来写:
if (wParam & MK_LBUTTON) //单击了鼠标左键
{
   if (wParam & MK_CONTROL) //单击了鼠标左键,也按下了下Ctrl键
   {
     //单击了鼠标左键并按下Ctrl键时的处理代码
   }
   else  //单击了鼠标左键,但没按下Ctrl键
   {
     // 单击鼠标左键但未按下Ctrl键时的处理代码
   }
}
else  // 没有单击鼠标左键
{
   if (wParam & MK_CONTROL) //没有单击鼠标左键但按下了Shift 键
   {
     // 没有单击鼠标左键但按下Shift 键时的处理代码
   }
   else //单击鼠标左键也没按下Shift 键时的处理代码
   {
     //没有单击鼠标左键也没按下Shift 键时的处理代码
   }
)
通过上面这段代码可以清楚地看到,就是这样的if···el se 组合,加上wParam 参数与测试标志,可以测试鼠标键、Shift键和CtrI 键是否被按下。
3 . 滚轮消息
我们来看一下鼠标滚轮转动消息WM_MOUSEWHEEL。当鼠标滚轮转动消息发生时,lParam参数中的值同样是记录光标所在的位置的,而wParam 参数则分为高位字节与低位字节两部分,低位字节部分跟前面一样是储存鼠标键与Shift、CtrI 键的状态信息的,而高位字节部分的值会是“ 120”或"-120” 。“120 ” 表示鼠标滚轮向前转动,而“-120”则表示向后转动。
这里wParam 高位组值与低位组值所在的函数同样是HIWORD()与LOWORD() 。
HIWORD (wParam) ;  // 高位组值,值为“ 120 ”或“-120”
LOWORD (wParam);  //低位组值,鼠标键以及Shift 和Ctrl 键的状态信息

7.2.2 鼠标相关常用函数讲解

对各种鼠标输入消息及鼠标状态信息的获取方法有了基本认识之后,下面我们来介绍一些游戏程序中用鼠标来做输出设备时常用到的、比较好用的函数,让大家在编写处理鼠标消息相关游戏或者程序时更加得心应手。
1 . 设定鼠标光标位置的函数
我们可以用SetCursorPos 函数来设定光标的位置。在MSDN 中我们查到SetCursorPos 函数的定义如下:
BOOL SetCursorPos(
  __in  int X,  // X 坐标
  __in  int Y   // Y 坐标
);
我们设定的坐标是相对于屏幕左上角的屏幕坐标而言。实际上,我们经常需要将这个屏幕坐标转换为游戏窗口中的游戏窗口坐标。因此需要用到API 中的一个将窗口坐标转换到屏幕坐标的函数,即
ClientToScreen() 。我们来看一下这个函数:
BOOL ClientToScreen(
  __in     HWND hWnd,      //要转换到屏幕坐标的窗口的句柄
  __inout  LPPOINT lpPoint //指向一个含有要转换的用户坐标的结构的指针
);

既然有将窗口坐标转换为屏幕坐标的函数ClientToScreen ,当然也有它的逆向转换函数一一将屏幕坐标转换为窗口坐标的ScreenToClient 函数。我们在MSDN 中查一下这个函数的原型和用法:
BOOL ScreenToClient(
  __in  HWND hWnd,        //要转换到的客户区窗口的句柄
        LPPOINT lpPoint  //指向POINT 结构体的指针,这个结构体中含有要转换的屏幕坐标
);
2 . 显示与隐醺鼠标光标的函数
显示和隐藏鼠标光标就一个函数,这个函数也就一个参数,这个参数取true 的话就是显示光标,取false 的话就是隐藏光标。它就是ShowCursor 函数:
int ShowCursor(
  __in  BOOL bShow  //取true 显示光标,取false 隐藏光标
);

3 . 获取窗口外鼠标消息的函数
为了确保程序可以正确地取得鼠标的输入消息,需要在必要的时候使用SetCapture 函数来指定一下窗口,以取得鼠标在窗口外所发出的消息到这个窗口中。SetCapture 函数在MSDN 中的解释翻译成中文如下:
该函数在属于当前线程的指定窗口里设置鼠标捕获。一旦窗口捕获了鼠标,所有鼠标输入都针对该窗口,无论鼠标是否在窗口的边界内。同一时刻只能有一个窗口捕获鼠标。如果鼠标光标在另一个线程创建的窗口上,只有当鼠标键按下时系统才将鼠标输入指向指定的窗口。
SetCapture 函数的定义是这样的:

HWND SetCapture(
  __in  HWND hWnd  //当前线程里要捕获鼠标的窗口句柄
);
如果调用了上面的SetCapture()函数并输入要取得鼠标消息的窗口代号,那么便可取得鼠标在窗口外所发出的消息。这种方法也适用于多窗口的程序。与SetCapture() 函数相对应的函数为ReleaseCapture()函数,用于释放窗口取得窗口外鼠标消息的函数, 它的定义非常简单:

BOOL ReleaseCapture(void);

这个函数可以这么理解: ReleaseCapture 函数从当前线程中的窗口释放鼠标捕获,并恢复通常的鼠标输入处理。捕获鼠标的窗口接收所有的鼠标输入(无论光标的位置在哪里〉,除非单击鼠标键时,光标热点在另一个线程的窗口中。
4. 限制鼠标光标移动区域的函数
Windows API 中提供的ClipCursor()函数可以用来设置限制鼠标光标的移动区域和解除鼠标光标移动区域的限制。

BOOL ClipCursor(
  __in  const RECT *lpRect
);
唯一的一个参数, const RECT 类型的* lpRect , 指向阳CT 结构的指针,该结构包含限制矩形区域左上角和右下角的屏幕坐标,如果该指针为NULL (空), 则鼠标可以在屏幕的任何区域移动。
这里有一个RECT 移动区域矩形,我们在MSDN 中找出它的声明如下:

typedef struct tagRECT { 
   LONG left;   //矩形区域左上角X坐标
   LONG top;    //矩形区域左上角Y坐标
   LONG right;  //矩形区域右下角X坐标
   LONG bottom; //矩形区域右下角Y坐标
} RECT;
5 . 取得窗口外部区域及内部区域的函数
最后,我们再讲一下取得窗口外部区域及内部区域的API 函数。他们分别是GetWindowRect、和GetClientRect 。在MSDN 中他们的定义如下:

BOOL GetWindowRect(   //取得窗口外部的矩形区域
  __in   HWND hWnd,
  __out  LPRECT lpRect
);
BOOL GetClientRect(  //取得窗口内部的矩形区域
  __in   HWND hWnd,
  __out  LPRECT lpRect
);
这里需要注意的是, GetWindowRect()返回的坐标类型是屏幕坐标。GetClientRect()返回的坐标类型是窗口坐标。

由于限制鼠标光标移动区域的ClipCursor()函数中输入的矩形区域必须是屏幕坐标,因此如果取得的是窗口内部区域,那么还必须将窗口坐标转换为屏幕坐标的操作。下面我们用一段程序代码来说明将鼠标光标限制在窗口内部区域移动的过程:
    //限制鼠标光标移动区域
    POINT lt,rb;
	RECT rect;
	GetClientRect(hwnd,&rect);  //取得窗口内部矩形
	//将矩形左上点坐标存入lt中
	lt.x = rect.left;
	lt.y = rect.top;
	//将矩形右下坐标存入rb中
	rb.x = rect.right;
	rb.y = rect.bottom;
	//将lt和rb的窗口坐标转换为屏幕坐标
	ClientToScreen(hwnd,<);
	ClientToScreen(hwnd,&rb);
	//以屏幕坐标重新设定矩形区域
	rect.left = lt.x;
	rect.top = lt.y;
	rect.right = rb.x;
	rect.bottom = rb.y;
	//限制鼠标光标移动区域
	ClipCursor(&rect);

上面这段代码中,我们先用GetClientRect 函数取得了窗口内部矩形区域到一个矩形结构体rect中,然后将取得的窗口内部矩形区域的左上角坐标和右下角坐标分别存到It 和rb 这两个POINT 类型的结构体中, 接着将It 和rb 的窗口坐标转换为屏幕坐标,最后用经过转换的这两个点重新赋给这个rect 矩形结构体, 这样rect 结构体涅架重生了,成为了表示屏幕坐标的矩形区域,这样再用
ClipCursor 函数以涅架后的矩形区域rect 为参数, 就把鼠标关闭的移动区域限制在窗口中了。


讲了这么多的Windows API 函数,这次给大家的游戏示例程序是“半个”卷轴式飞行射击类游戏。
在这个示例程序中,我们处理了鼠标移动消息WM_MOUSEMOVE ,使剑侠可以根据鼠标的移动方向跟着在窗口中移动,我们还处理了单击鼠标左键消息WM_LBUTTONDOWN 来让剑侠发射出类似子弹的“光剑”,并且我们设定了鼠标光标的初始位置,隐藏了鼠标光标,以及限制了鼠标光标移动的区域(背景贴图采用循环背景滚动,其实很简单,就是每次都把窗口右边多余的部分再贴到窗口坐标来〉。

好了,我们先看一下素材图:



程序代码片段一,全局变量声明:

//-----------------------------------【全局结构体定义部分】-------------------------------------
//	描述:全局结构体定义
//------------------------------------------------------------------------------------------------
struct SwordBullets       //SwordBullets结构体代表剑气(子弹)
{
	int x,y;        //剑气(子弹)坐标
	bool exist;     //剑气(子弹)是否存在
};

//-----------------------------------【全局变量声明部分】-------------------------------------
//	描述:全局变量的声明
//------------------------------------------------------------------------------------------------
HDC				g_hdc=NULL,g_mdc=NULL,g_bufdc=NULL;      //全局设备环境句柄与全局内存DC句柄
HBITMAP		g_hSwordMan=NULL,g_hSwordBlade=NULL,g_hBackGround=NULL;		//定义位图句柄用于存储位图资源
DWORD		g_tPre=0,g_tNow=0;	  //声明l两个函数来记录时间,g_tPre记录上一次绘图的时间,g_tNow记录此次准备绘图的时间
int					g_iX=0,g_iY=0,g_iXnow=0,g_iYnow=0;    //g_iX,g_iY代表鼠标光标所在位置,g_iXnow,g_iYnow代表当前人物坐标,也就是贴图的位置
int					g_iBGOffset=0,g_iBulletNum=0;       //g_iBGOffset为滚动背景所要裁剪的区域宽度,g_iBulletNum记录剑侠现有剑气(子弹)数目
SwordBullets Bullet[30];           //声明一个“SwordBullets”类型的数组,用来存储剑侠发出的剑气(子弹)

以上代码就是做了一些全局结构体和变量的定义。
程序代码片段二, 窗口过程函数WndProc:

//-----------------------------------【WndProc( )函数】--------------------------------------
//	描述:窗口过程函数WndProc,对窗口消息进行处理
//------------------------------------------------------------------------------------------------
LRESULT CALLBACK WndProc( HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam )      
{
	switch( message )						//switch语句开始
	{

	case WM_KEYDOWN:	     //按下键盘消息
		//判断按键的虚拟键码
		switch (wParam) 
		{
		case VK_ESCAPE:           //按下【Esc】键
			DestroyWindow(hwnd);    // 销毁窗口, 并发送一条WM_DESTROY消息
			PostQuitMessage( 0 );  //结束程序
			break;
		}

		break;

	case WM_LBUTTONDOWN:			//单击鼠标左键消息
		for(int i=0;i<30;i++)
		{
			if(!Bullet[i].exist)
			{
				Bullet[i].x = g_iXnow;		//剑气(子弹)x坐标
				Bullet[i].y = g_iYnow + 30; //剑气(子弹)y坐标
				Bullet[i].exist = true;
				g_iBulletNum++;			//累加剑气(子弹)数目
				break;
			}
		}

	case WM_MOUSEMOVE:   //鼠标移动消息
		//对X坐标的处理
		g_iX = LOWORD(lParam);			//取得鼠标X坐标
		if(g_iX > WINDOW_WIDTH-317)	//设置临界坐标
			g_iX = WINDOW_WIDTH-317;
		else if(g_iX < 0)
			g_iX = 0;
		//对Y坐标的处理
		g_iY = HIWORD(lParam);			//取得鼠标Y坐标
		if(g_iY > WINDOW_HEIGHT-283)
			g_iY = WINDOW_HEIGHT-283;
		else if(g_iY < -200)
			g_iY = -200;
		break;

	case WM_DESTROY:					//若是窗口销毁消息
		Game_CleanUp(hwnd);			//调用自定义的资源清理函数Game_CleanUp()进行退出前的资源清理
		PostQuitMessage( 0 );			//向系统表明有个线程有终止请求。用来响应WM_DESTROY消息
		break;									//跳出该switch语句

	default:										//若上述case条件都不符合,则执行该default语句
		return DefWindowProc( hwnd, message, wParam, lParam );		//调用缺省的窗口过程
	}
	return 0;									//正常退出
}

程序代码片段三, Game_Init()函数:

//-----------------------------------【Game_Init( )函数】--------------------------------------
//	描述:初始化函数,进行一些简单的初始化
//------------------------------------------------------------------------------------------------
BOOL Game_Init( HWND hwnd )
{
	HBITMAP bmp;

	g_hdc = GetDC(hwnd);  
	g_mdc = CreateCompatibleDC(g_hdc);  //创建一个和hdc兼容的内存DC
	g_bufdc = CreateCompatibleDC(g_hdc);//再创建一个和hdc兼容的缓冲DC
	bmp = CreateCompatibleBitmap(g_hdc,WINDOW_WIDTH,WINDOW_HEIGHT);

	//设定人物贴图初始值,鼠标位置初始值
	g_iX = 300;
	g_iY = 100;
	g_iXnow = 300;
	g_iYnow = 100;

	SelectObject(g_mdc,bmp);
	//加载各张跑动图及背景图
	g_hSwordMan = (HBITMAP)LoadImage(NULL,L"swordman.bmp",IMAGE_BITMAP,317,283,LR_LOADFROMFILE);
	g_hSwordBlade = (HBITMAP)LoadImage(NULL,L"swordblade.bmp",IMAGE_BITMAP,100,26,LR_LOADFROMFILE);
	g_hBackGround = (HBITMAP)LoadImage(NULL,L"bg.bmp",IMAGE_BITMAP,WINDOW_WIDTH,WINDOW_HEIGHT,LR_LOADFROMFILE);


	POINT pt,lt,rb;
	RECT rect;
	//设定光标位置
	pt.x = 300;
	pt.y = 100;
	ClientToScreen(hwnd,&pt);
	SetCursorPos(pt.x,pt.y);

	ShowCursor(false);		//隐藏鼠标光标

	//限制鼠标光标移动区域
	GetClientRect(hwnd,&rect);  //取得窗口内部矩形
	//将矩形左上点坐标存入lt中
	lt.x = rect.left;
	lt.y = rect.top;
	//将矩形右下坐标存入rb中
	rb.x = rect.right;
	rb.y = rect.bottom;
	//将lt和rb的窗口坐标转换为屏幕坐标
	ClientToScreen(hwnd,<);
	ClientToScreen(hwnd,&rb);
	//以屏幕坐标重新设定矩形区域
	rect.left = lt.x;
	rect.top = lt.y;
	rect.right = rb.x;
	rect.bottom = rb.y;
	//限制鼠标光标移动区域
	ClipCursor(&rect);

	Game_Paint(hwnd);
	return TRUE;
}

其中的36~53 行就用了之前讲到的限定鼠标区域的一套组合代码,以后也一样,如果想在哪个程序中用到限定鼠标区域的功能,把这段代码拷过去就行了。
程序代码片段四, Game_ Paint()函数:

//-----------------------------------【Game_Paint( )函数】--------------------------------------
//	描述:绘制函数,在此函数中进行绘制操作
//--------------------------------------------------------------------------------------------------
VOID Game_Paint( HWND hwnd )
{
	//先在mdc中贴上背景图
	SelectObject(g_bufdc,g_hBackGround);
	BitBlt(g_mdc,0,0,g_iBGOffset,WINDOW_HEIGHT,g_bufdc,WINDOW_WIDTH-g_iBGOffset,0,SRCCOPY);
	BitBlt(g_mdc,g_iBGOffset,0,WINDOW_WIDTH-g_iBGOffset,WINDOW_HEIGHT,g_bufdc,0,0,SRCCOPY);

	wchar_t str[20] = {};

	//计算剑侠的贴图坐标,设定每次进行剑侠贴图时,其贴图坐标(g_iXnow,g_iYnow)会以10个单位慢慢向鼠标光标所在的目的点(x,y)接近,直到两个坐标相同为止
	if(g_iXnow < g_iX)//若当前贴图X坐标小于鼠标光标的X坐标
	{
		g_iXnow += 10;
		if(g_iXnow > g_iX)
			g_iXnow = g_iX;
	}
	else   //若当前贴图X坐标大于鼠标光标的X坐标
	{
		g_iXnow -=10;
		if(g_iXnow < g_iX)
			g_iXnow = g_iX;
	}

	if(g_iYnow < g_iY)  //若当前贴图Y坐标小于鼠标光标的Y坐标
	{
		g_iYnow += 10;
		if(g_iYnow > g_iY)
			g_iYnow = g_iY;
	}
	else  //若当前贴图Y坐标大于于鼠标光标的Y坐标
	{
		g_iYnow -= 10;  
		if(g_iYnow < g_iY)
			g_iYnow = g_iY;
	}

	//贴上剑侠图
	SelectObject(g_bufdc,g_hSwordMan);
	TransparentBlt(g_mdc,g_iXnow,g_iYnow,317,283,g_bufdc,0,0,317,283,RGB(0,0,0));

	//剑气(子弹)的贴图,先判断剑气(子弹)数目“g_iBulletNum”的值是否为“0”。若不为0,则对剑气(子弹)数组中各个还存在的剑气(子弹)按照其所在的坐标(b[i].x,b[i].y)循环进行贴图操作
	SelectObject(g_bufdc,g_hSwordBlade);
	if(g_iBulletNum!=0)
		for(int i=0;i<30;i++)
			if(Bullet[i].exist)
			{
				//贴上剑气(子弹)图
				TransparentBlt(g_mdc,Bullet[i].x-70,Bullet[i].y+100,100,33,g_bufdc,0,0,100,26,RGB(0,0,0));

				//设置下一个剑气(子弹)的坐标。剑气(子弹)是从右向左发射的,因此,每次其X轴上的坐标值递减10个单位,这样贴图会产生往左移动的效果。而如果剑气(子弹)下次的坐标已超出窗口的可见范围(h[i].x<0),那么剑气(子弹)设为不存在,并将剑气(子弹)总数g_iBulletNum变量值减1.
				Bullet[i].x -= 10;
				if(Bullet[i].x < 0)
				{
					g_iBulletNum--;
					Bullet[i].exist = false;
				}
			}

			HFONT hFont;  
			hFont=CreateFont(20,0,0,0,0,0,0,0,GB2312_CHARSET,0,0,0,0,TEXT("微软雅黑"));  //创建字体
			SelectObject(g_mdc,hFont);  //选入字体到g_mdc中
			SetBkMode(g_mdc, TRANSPARENT);    //设置文字背景透明
			SetTextColor(g_mdc,RGB(255,255,0));  //设置文字颜色

			//在左上角进行文字输出
			swprintf_s(str,L"鼠标X坐标为%d    ",g_iX);
			TextOut(g_mdc,0,0,str,wcslen(str));
			swprintf_s(str,L"鼠标Y坐标为%d    ",g_iY);
			TextOut(g_mdc,0,20,str,wcslen(str));

			//贴上背景图
			BitBlt(g_hdc,0,0,WINDOW_WIDTH,WINDOW_HEIGHT,g_mdc,0,0,SRCCOPY);

			g_tPre = GetTickCount();

			g_iBGOffset += 5; //让背景滚动量+5
			if(g_iBGOffset==WINDOW_WIDTH)//如果背景滚动量达到了背景宽度值,就置零
				g_iBGOffset = 0;  
}

最后看一下运行截图吧:



我们单击鼠标,移动鼠标,就可以发出光剑子弹并控制剑侠在空中飞行。


7.3 章节小憩

本章我们一起探讨了Windows 游戏编程中的鼠标和键盘消息处理相关的知识,并带领大家一起学习了两个比较好玩而且有代表性的游戏小demo 。






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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值