游戏编程指南 — 容纳游戏的空间(windows编程基础)
转自:《游戏编程指南》 作者: 彭博
第三章 容纳游戏的空间
因为我们编好的游戏将在Windows下运行,所以学习一点Windows编程知识是必需的。Microsoft为了方便Windows编程制作了一个庞大的类库MFC,把Windows的方方面面都封装了起来。但此类库只是比较适合编写字板之类的标准Windows程序,对于游戏来说它实在是过于烦琐和累赘,所以我们一般都不使用它,自己从头用Windows API(Application Programming Interface 应用编程接口,其实就是一堆Windows为开发者提供的函数)写Windows程序。
3.1 基本Windows程序
最基本的Windows程序看起来都有点长,它的流程图是这样的:
图3.1
但你不必担心Windows编程过于复杂。在所有的Windows程序中,都需要一个初始化的过程,而这个过程对于任何Windows程序而言,都是大同小异的。你也许会想到使用VB做一个最简单的程序不用敲一行代码,其实这是因为VB已经暗地里帮你敲好了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 |
#include <windows.h> //函数声明 BOOL InitWindow( HINSTANCE hInstance, int nCmdShow ); LRESULT CALLBACK WinProc( HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam ); //变量说明 HWND hWnd; //窗口句柄 //************************************************************ //函数:WinMain( ) //功能:Windows程序入口函数。创建主窗口,处理消息循环 //************************************************************ int PASCAL WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) { if ( !InitWindow( hInstance, nCmdShow ) ) return FALSE; //创建主窗口 //如果创建不成功则返回FALSE并同时退出程序 MSG msg; //进入消息循环: for(;;) { if(PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) { if ( msg.message==WM_QUIT) break; TranslateMessage(&msg); DispatchMessage(&msg); } } return msg.wParam; } //************************************************************ //函数:InitWindow( ) //功能:创建窗口 //************************************************************ static BOOL InitWindow( HINSTANCE hInstance, int nCmdShow ) { //定义窗口风格: WNDCLASS wc; wc.style = NULL; wc.lpfnWndProc = (WNDPROC)WinProc; wc.cbClsExtra = 0; wc.cbWndExtra = 0; wc.hInstance = hInstance; wc.hIcon = NULL; wc.hCursor = NULL; wc.hbrBackground = CreateSolidBrush (RGB(100, 0, 0)); //暗红色的背景 wc.lpszMenuName = NULL; wc.lpszClassName = "My_Test"; RegisterClass(&wc);//注册窗口 //按所给参数创造窗口 hWnd = CreateWindow("My_Test", "My first program", WS_POPUP|WS_MAXIMIZE,0,0, GetSystemMetrics( SM_CXSCREEN ), //此函数返回屏幕宽度 GetSystemMetrics( SM_CYSCREEN ), //此函数返回屏幕高度 NULL,NULL,hInstance,NULL); if( !hWnd ) return FALSE; ShowWindow(hWnd,nCmdShow);//显示窗口 UpdateWindow(hWnd);//刷新窗口 return TRUE; } //************************************************************ //函数:WinProc( ) //功能:处理窗口消息 //************************************************************ LRESULT CALLBACK WinProc( HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam ) { switch( message ) { case WM_KEYDOWN://击键消息 switch( wParam ) { case VK_ESCAPE: MessageBox(hWnd,"ESC键按下了! 确定后退出!","Keyboard",MB_OK); PostMessage(hWnd, WM_CLOSE, 0, 0);//给窗口发送WM_CLOSE消息 break; } return 0; //处理完一个消息后返回0 case WM_CLOSE: //准备退出 DestroyWindow( hWnd ); //释放窗口 return 0; case WM_RBUTTONDOWN: MessageBox(hWnd,"鼠标右键按下了!","Mouse",MB_OK); return 0; case WM_DESTROY: //如果窗口被人释放… PostQuitMessage( 0 ); //给窗口发送WM_QUIT消息 return 0; } //调用缺省消息处理过程 return DefWindowProc(hWnd, message, wParam, lParam); } |
按1.1节的方法建立一个工程后,输入程序,按Ctrl+F5执行一下,就会出现一个暗红色的"窗口"。然后你可以试试按按鼠标右键或Esc键看看效果,就像图3. 2。怎么样?VB要做到同样的效果恐怕有点麻烦,这也算是从头写代码的一点好处吧。
图3.2
3.2 WinMain函数
3.2.1 简介
WinMain( )函数与DOS程序的main ( )函数基本起同样的作用,但有一点不同的是WinMain( )函数必须带有四个系统传递给它的参数。WinMain( )函数的原型如下:
1 2 |
int PASCAL WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) |
第一个参数hInstance是标识该应用程序的句柄。不过句柄又是什么呢?其实就是一个指向该程序所占据的内存区域的指针,它唯一地代表了该应用程序,Windows使用它管理内存中的各种对象。当然,它十分重要。在后面的初始化程序主窗口的过程中就需要使用它作为参数。
第二个参数是hPrevInstance,给它NULL吧,这个参数只是为了保持与16位Windows的应用程序的兼容性。
第三个参数是lpCmdLine,是指向应用程序命令行参数字符串的指针。比如说我们运行"test hello",则此参数指向的字符串为"hello"。
最后一个参数是nCmdShow,是一个用来指定窗口显示方式的整数。关于窗口显示方式的种类,将在下面说明。
3.2.2 注册窗口类
一个程序可以有许多窗口,但只有一个是主窗口,它是与该应用程序唯一对应的。
创建窗口前通常要填充一个窗口类WNDCLASS,并调用RegisterClass( )对该窗口类进行注册。每个窗口都有一些基本的属性,如窗口标题栏文字、窗口大小和位置、鼠标、背景色,窗口消息处理函数(后面会讲这个函数)的名称等等。注册的过程就是将这些属性告诉系统,然后再调用CreateWindow( )函数创建出窗口。
下面列出了WNDCLASS的成员:
1 2 3 4 5 6 7 8 9 10 |
UINT style; //窗口的风格 WNDPROC lpfnWndProc; //窗口消息处理函数的指针 int cbClsExtra; //分配给窗口类结构之后的额外字节数 int cbWndExtra; //分配给窗口实例之后的额外字节数 HANDLE hInstance; //窗口所对应的应用程序的句柄 HICON hIcon; //窗口的图标 HCURSOR hCursor; //窗口的鼠标 HBRUSH hbrBackground; //窗口的背景 LPCTSTR lpszMenuName; //窗口的菜单资源名称 LPCTSTR lpszClassName; //窗口类的名称 |
WNDCLASS的第一个成员style表示窗口类的风格,它往往是由一些基本的风格通过位的"或"操作(操作符"|")组合而成。下表列出了一些常用的基本窗口风格:
表3.1
风格 |
含义 |
CS_HREDRAW |
如果窗口宽度发生改变,重绘整个窗口 |
CS_VREDRAW |
如果窗口高度发生改变,重绘整个窗口 |
CS_DBLCLKS |
能感受用户在窗口中的双击消息 |
CS_NOCLOSE |
禁用系统菜单中的 " 关闭 " 命令 |
CS_SAVEBITS |
把被窗口遮掩的屏幕图像部分作为位图保存起来。当该窗口被移动时, Windows 使用被保存的位图来重建屏幕图像 |
第二个成员是lpfnWndProc,给它消息处理函数的函数名称即可,必要时应该进行强制类型转换,将其转换成WNDPROC型。
接下来的cbClsExtra和wc.cbWndExtra一般都可以设为0。
然后的hInstance成员,给它的值是窗口所对应的应用程序的句柄,表明该窗口与此应用程序是相关联的。
下面的hIcon是让我们给这个窗口指定一个图标,这个程序没有设置。
鼠标也没有设置,因为编游戏时的鼠标都是在刷新屏幕时自己画上去的。
hbrBackground成员用来定义窗口的背景色。这里设为CreateSolidBrush (RGB(100, 0, 0)),即暗红色。关于CreateSolidBrush函数,请参阅4.10节。
lpszMenuName成员的值我们给它NULL,表示该窗口没有菜单。
WNDCLASS的最后一个成员lpszClassName是让我们给这个窗口类起一个独一无二的名称,因为Windows操作系统中有许许多多的窗口类。通常,我们可以用程序名来命名这个窗口类的名称。在调用CreateWindow( )函数时将要用到这个名称。
填充完WNDCLASS后,我们需要调用RegisterClass( )函数进行注册;该函数如调用成功,则返回一个非0值,表明系统中已经注册了这个窗口类。如果失败,则返回0。
3.2.3 创建窗口
当窗口类注册完毕之后,我们就可以创建一个窗口,这是通过调用CreateWindow( )函数完成的。窗口类中已经预先定义了窗口的一般属性,而在CreateWindow( )中的参数中可以进一步指定窗口更具体的属性。下面举一个例子来说明CreatWindow( )的用法:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
hwnd = CreateWindow( "Simple_Program", //创建窗口所用的窗口类的名称 "A Simple Windows Program", //窗口标题 WS_OVERLAPPEDWINDOW, //窗口风格,定义为普通型 100, //窗口位置的x坐标 100, //窗口位置的y坐标 400, //窗口的宽度 300, //窗口的高度 NULL, //父窗口句柄 NULL, //菜单句柄 hInstance, //应用程序句柄 NULL ); //一般都为NULL |
第一个参数是创建该窗口所使用的窗口类的名称,注意这个名称应与前面所注册的窗口类的名称一致。
第三个参数为创建的窗口的风格,下表列出了常用的窗口风格:
表3.2
风格 |
含义 |
WS_OVERLAPPEDWINDOW |
创建一个层叠式窗口,有边框、标题栏、系统菜单、最大最小化按钮,是以下几种风格的集合: WS_OVERLAPPED, WS_CAPTION, WS_SYSMENU, WS_THICKFRAME, WS_MINIMIZEBOX, WS_MAXIMIZEBOX |
WS_POPUPWINDOW |
创建一个弹出式窗口,是以下几种风格的集合: WS_BORDER, WS_POPUP, WS_SYSMENU 。必须再加上 WS_CAPTION 与才能使窗口菜单可见。 |
WS_OVERLAPPED & WS_TILED |
创建一个层叠式窗口,它有标题栏和边框。 |
WS_POPUP |
该窗口为弹出式窗口,不能与 WS_CHILD 同时使用。 |
WS_BORDER |
窗口有单线边框。 |
WS_CAPTION |
窗口有标题栏。 |
WS_CHILD |
该窗口为子窗口,不能与 WS_POPUP 同时使用。 |
WS_DISABLED |
该窗口为无效,即对用户操作不产生任何反应。 |
WS_HSCROLL / WS_VSCROLL |
窗口有水平滚动条 / 垂直滚动条。 |
WS_MAXIMIZE / WS_MINIMIZE |
窗口初始化为最大化 / 最小化。 |
WS_MAXIMIZEBOX / WS_MINIMIZEBOX |
窗口有最大化按钮 / 最小化按钮 |
WS_SIZEBOX & WS_THICKFRAME |
边框可进行大小控制的窗口 |
WS_SYSMENU |
创建一个有系统菜单的窗口,必须与 WS_CAPTION 风格同时使用 |
WS_TILED |
创建一个层叠式窗口,有标题栏 |
WS_VISIBLE |
窗口为可见 |
在DirectX编程中,我们一般使用的是WS_POPUP | WS_MAXIMIZE,用这个标志创建的窗口没有标题栏和系统菜单且窗口为最大化,可以充分满足DirectX编程的需要。
如果窗口创建成功,CreateWindow( )返回新窗口的句柄,否则返回NULL。
3.2.4 显示和更新窗口
窗口创建后,并不会在屏幕上显示出来,要真正把窗口显示在屏幕上,还得使用ShowWindow( )函数,其原型如下:
BOOL ShowWindow( HWND hWnd, int nCmdShow );
参数hWnd就是要显示的窗口的句柄。
nCmdShow是窗口的显示方式,一般给它WinMain( )函数得到的nCmdShow的值就可以了。
常用的窗口显示方式有:
表3.3
方式 |
含义 |
SW_HIDE |
隐藏窗口 |
SW_MINIMIZE |
最小化窗口 |
SW_RESTORE |
恢复并激活窗口 |
SW_SHOW |
显示并激活窗口 |
SW_SHOWMAXIMIZED |
最大化并激活窗口 |
SW_SHOWMINIMIZED |
最小化并激活窗口 |
ShowWindow( )函数的执行优先级不高,当系统正忙着执行其它的任务时窗口不会立即显示出来。所以我们使用ShowWindow( )函数后还要再调用UpdateWindow(HWND hWnd); 函数以保证立即显示窗口。
3.2.5 消息循环
在WinMain( )函数中,调用InitWindow( )函数成功地创建了应用程序主窗口之后,就要启动消息循环,其代码如下:
1 2 3 4 5 6 7 8 9 |
for(;;) { if(PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) { if ( msg.message==WM_QUIT) break; TranslateMessage(&msg); DispatchMessage(&msg); } } |
Windows应用程序可以接收各种形式的信息,这包括键盘和鼠标的动作、记时器消息,其它应用程序发来的消息等等。Windows系统会自动将这些消息放入应用程序的消息队列中。
PeekMessage( )函数就是用来从应用程序的消息队列中按照先进先出的原则将这些消息一个个的取出来,放进一个MSG结构中去。如果队列中没有任何消息,PeekMessage( )函数将立即返回。如果队列中有消息,它将取出一个后返回。
MSG结构包含了一条Windows消息的完整信息,它由下面的几部分组成:
1 2 3 4 5 6 |
HWND hwnd; //接收消息的窗口句柄 UINT message; //主消息值 WPARAM wParam; //副消息值1,其具体含义依赖于主消息值 LPARAM lParam; //副消息值2,其具体含义依赖于主消息值 DWORD time; //消息被投递的时间 POINT pt; //鼠标的位置 |
该结构中的主消息表明了消息的类型,例如是键盘消息还是鼠标消息等。副消息的含义则依赖于主消息值,比如说如果主消息是键盘消息,那么wParam中存储了是键盘的哪个具体键;如果主消息是鼠标消息,那么LOWORD(lParam)和HIWORD(lParam)分别为鼠标位置的x和y坐标;如果主消息是WM_ACTIVATE,wParam就表示了程序是否处于激活状态。这里顺便说一下,定义一个POINT类型的变量curpos后,在程序的任意位置使用GetCursorPos(&curpos)都可以将鼠标坐标存储在curpos.x和curpos.y中。
PeekMessage( )函数的原型如下:
1 2 3 4 5 6 7 |
BOOL PeekMessage ( LPMSG lpMsg, //指向一个MSG结构的指针,用来保存消息 HWND hWnd, //指定哪个窗口的消息将被获取 UINT wMsgFilterMin, //指定获取的主消息值的最小值 UINT wMsgFilterMax, //指定获取的主消息值的最大值 UINT wRemoveMsg //得到消息后是否移除消息 ); |
PeekMessage( )的第一个参数的意义上面已解释。
第二个参数是用来指定从哪个窗口的消息队列中获取消息,其它窗口的消息将被过滤掉。如果该参数为NULL,则PeekMessage( )从该应用程序所有窗口的消息队列中获取消息。
第三个和第四个参数是用来过滤MSG结构中主消息值的,主消息值在wMsgFilterMin和wMsgFilterMax之外的消息将被过滤掉。如果这两个参数均为0,表示接收所有消息。
第五个参数用来设置分发完消息后是否将消息从队列中移除,一般设为PM_REMOVE即移除。
TranslateMessage( )函数的作用是把虚拟键消息转换到字符消息,以满足键盘输入的需要。DispatchMessage( )函数所完成的工作是把当前的消息发送到对应的窗口过程中去。
开启消息循环其实是很简单的一个步骤,几乎所有的程序都是按照Test的这个方法。我们完全不必去深究这些函数的作用,只是简单的照抄就可以了。
另外,这里介绍的消息循环开启方法比某些书上所介绍的用GetMessage( )的方法要好一些,因为GetMessage( )如果得不到消息会一直等待,结果就耗费了许多宝贵的时间,使游戏不能及时刷新。
3.3 消息处理函数
消息处理函数又叫窗口过程,在这个函数中,不同的消息将被switch语句分配到不同的处理程序中去。Windows的消息处理函数的原型是这样定义的:
1 2 3 4 5 6 |
LRESULT CALLBACK WindowProc( HWND hwnd, //接收消息窗口的句柄 UINT uMsg, //主消息值 WPARAM wParam, //副消息值1 LPARAM lParam //副消息值2 ); |
消息处理函数必须按照上面的这个样式来定义,当然函数名称可以随便取。
Test中的WinProc( )函数就是一个典型的消息处理函数。在这个函数中明确的处理了3个消息,分别是WM_KEYDOWN(击键)、WM_RBUTTONDOWN(鼠标右键按下)、WM_CLOSE(关闭窗口)、WM_DESTROY(销毁窗口)。值得注意的是,应用程序发送到窗口的消息远远不止以上这几条,象WM_SIZE、WM_MINIMIZE、WM_CREATE、WM_MOVE等频繁使用的消息就有几十条。在附录中可以查到Windows常见消息列表。
为了减轻编程的负担,Windows提供了DefWindowProc( )函数来处理这些最常用的消息,调用了这个函数后,这些消息将按照系统默认的方式得到处理。因此,在消息处理函数中,只须处理那些有必要进行特别响应的消息,其余的消息都可交给DefWindowProc( )函数来处理。
3.4 常用Windows函数
3.4.1 显示对话框
MessageBox函数可以用来显示对话框,它的原形是:
1 |
int MessageBox(HWND hwndParent, LPCSTR lpszText, LPCSTR lpszTitle, UINT fuStyle); |
其中的四个参数依次为:窗口句柄,文字内容,标题,风格。常用风格有:MB_OK、MB_OKCANCEL、MB_RETRYCANCEL、MB_YESNO、MB_YESNOCANCEL,代表对话框有哪些按钮。常用返回值有IDCANCEL、IDNO、IDOK、IDRETRY、IDYES,代表哪个按钮被按下。
3.4.2 定时器
定时器可以使程序每隔一段时间执行一个函数。用法如下:
1 |
SetTimer(HWND hwnd, UINT ID, UINT Elapse, TIMERPROC TimerFunc); |
四个参数依次为窗口句柄、定时器标识(同一程序内各个定时器的标识应不相同,一般从1、2、3…一直排下去)、每隔多少毫秒(千分之一秒)执行一次程序,要执行的过程。
这个要执行的过程应这样定义:
1 |
void CALLBACK MyTimer(HWND hwnd,UINT uMsg,UINT idEvent,DWORD dwTime); |
这几个规定的参数都没什么用,我们在过程里作自己的事就行了,不用理这几个给我们的参数。
注意:定时器的优先级不高,当处理器很忙时我们需要定时执行的程序常常不能按时地执行;无论你把定时器的Elapse设得多小,它实际上最小只能是55ms;有的Windows函数在TimerFunc中用不了,而且在TimerFunc里不要做一些费时间的东西。
3.4.3 得到时间
我们经常需要在程序中得到当前的准确时间来完成测试速度等工作。这时我们可以使用GetTickCount( ),因为该函数可以返回Windows已经运行了多少毫秒。然而有时我们需要得到更准确的时间,这时可使用这种方法:
1 2 3 4 5 |
__int64 time2, freq; //时间,计时器频率 double time; //以秒为单位的时间 QueryPerformanceCounter((LARGE_INTEGER*)&time2); //得到计时开始的时间 QueryPerformanceFrequency((LARGE_INTEGER*)&freq); //得到计时器频率 time = (double)(time2) / (double)freq; //将时间转为以秒为单位 |
3.4.4 播放声音
我们可以使用MCI来简易地实现在程序中播放MIDI和WAV等声音。使用它需要预先声明,我们需要在文件头#include <mmsystem.h>,并在工程中加入"winmm.lib"
下面先让我们看看播放MIDI的过程。首先我们要打开设备:
1 2 3 4 5 6 7 8 9 |
MCI_OPEN_PARMS OpenParms; OpenParms.lpstrDeviceType = (LPCSTR) MCI_DEVTYPE_SEQUENCER; //是MIDI类型文件 OpenParms.lpstrElementName = (LPCSTR) filename; //文件名 OpenParms.wDeviceID = 0; //打开的设备的标识,后面需要使用 mciSendCommand (NULL, MCI_OPEN, MCI_WAIT | MCI_OPEN_TYPE | MCI_OPEN_TYPE_ID | MCI_OPEN_ELEMENT, (DWORD)(LPVOID) &OpenParms); //打开设备 |
接着就可以播放MIDI了:
1 2 3 4 |
MCI_PLAY_PARMS PlayParms; PlayParms.dwFrom = 0; //从什么时间位置播放,单位为毫秒 mciSendCommand (DeviceID, MCI_PLAY, //DeviceID需等于上面的设备标识 MCI_FROM, (DWORD)(LPVOID)&PlayParms); //播放MIDI |
停止播放:
1 |
mciSendCommand (DeviceID, MCI_STOP, NULL, NULL); |
最后要关闭设备:
1 |
mciSendCommand (DeviceID, MCI_CLOSE, NULL, NULL); |
打开WAV文件与打开MIDI文件的方法几乎完全相同,只是需要将MCI_DEVTYPE_SEQUENCER 改为MCI_DEVTYPE_WAVEFORM_AUDIO。
Trackback from your site.
(1)条评论
Louis
| #
了解win32就可以了,应该不用达到掌握MFC的高度。
回复