让网游运行在主流平台——步入Windows编程
技术与开发
编者按:前两期,我们教大家学习了网络游戏的主流开发语言——C++以及程序的灵魂——算法和数据结构。有了这些知识,你就可以开发一些简单的游戏了。但是,要想自己开发的网络游戏在Windows平台下兼容性良好且高效运行,这些知识还不够。本期,我们就将你带进Windows编程的殿堂。
Windows是一个很大的舞台。目前为止,一些为我们所熟悉的网络游戏都是运行在Windows平台上的,如《魔兽世界》、《梦幻西游》、《跑跑卡丁车》等。如果我们要在Windows平台下开发游戏,那么了解Windows系统就尤其重要了。
网络游戏的进程与线程
首先,我们简单介绍一下进程与线程。Windows启动一个应用程序(如《魔兽世界》),实际上就是创建了一个进程。进程在创建的时候自动启动了一个主线程,一个进程可以有多个线程。
多线程编程是一种高级的编程技术,在网络游戏开发中起到很大的作用。如一个网络游戏最少要有两个线程,一个是主线程,一个是网络线程。Windows是一个多任务的操作系统,可以同时运行多个应用程序。其实实际上在同一时刻只会有一个应用程序占用CPU,Windows为启动的所有应用程序分配一些时间片,让程序看起来是同时运行的。
当然,并不是简单地为启动的应用程序进行轮循,Windows还要根据优先级进行分配。优先级高的将得到更多的CPU占用时间,如Windows任务管理器就是一个优先级高的应用程序。
编写自己的第一个窗口
编写Windows程序实际上就是编写窗口程序。一个对话框是一个窗口,一个按钮也是一个窗口,就算你的游戏是全屏的,实际上它也是一个窗口。
Windows程序都是消息驱动的,理解这个很重要,消息可以说是Windows程序的核心。每一个窗口都有一个窗口函数用于处理Windows消息。如在窗口内点击一下鼠标,Windows便向该窗口的消息队列发送一个点击鼠标的消息,该窗口检测自己的消息队列有消息,然后便会取出消息交给窗口函数处理。
还是让我们先看一段简单的Windows程序的代码。
#include <Windows.h>
LRESULT CALLBACK WndProc( HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam );
int WINAPI WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance,
PSTR szCmdLine, int iCmdShow )
{ // 注册窗口类
// 创建窗口
// 显示更新窗口
// 消息循环}
WinMain函数是程序的入口,函数里主要执行以下几个步骤:注册窗口类、创建窗口、显示更新窗口、执行消息循环。WndProc就是上面所说的窗口函数,用于处理该窗口的消息,它是一个回调函数。
1.注册窗口类
要创建一个自己的窗口,就要先注册一个自己的窗口类。注册一个窗口类需要给出该窗口的一些信息,如样式、背景色等等,最重要的是要设置该窗口的窗口函数。
WNDCLASS wndclass;
wndclass.style =CS_HREDRAW|CS_V REDRAW;
wndclass.lpfnWndProc = WndProc;
wndclass.cbClsExtra = 0;
wndclass.cbWndExtra = 0;
wndclass.hInstance = hInstance;
wndclass.hIcon = LoadIcon(NULL, IDI_APPLICATION);
wndclass.hCursor = LoadCursor(NULL, IDC_ARROW);
wndclass.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
wndclass.lpszMenuName = NULL;
wndclass.lpszClassName = TEXT("My Window");
RegisterClass( &wndclass );
注册一个窗口类使用的是RegisterClass函数,调用这个函数之前必须先填一个WNDCLASS结构。WNDCLASS结构的成员我就不详细介绍了。
注意:lpfnWndProc是该窗口的窗口函数指针。lpszClassName是该窗口的名字,在创建窗口的时候必须使用该名字。
2.创建窗口
使用RegisterClass函数成功注册了一个窗口类后,便可以使用CreateWindow函数创建一个窗口了,若窗口创建成功,该函数返回新创建窗口的窗口句柄。
HWND hwnd = CreateWindow( TEXT("MyWindow"),
TEXT("Get System Metrics No.1"),
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT,
CW_USEDEFAULT,
CW_USEDEFAULT,
CW_USEDEFAULT,
NULL,
NULL,
hInstance,
NULL );
CreateWindow的第一个参数就是窗口类的名字,后面的参数分别是窗口的标题、窗口风格、坐标、大小等等,详细信息可以去查阅MSDN。
3.显示更新窗口
使用CreateWindow函数成功创建窗口后,实际Windows只是给窗口分配了一块内存,但我们的窗口仍然还不肯出来。这时我们需要使用下面的函数客气地把它“请”出来。
ShowWindow(hwnd, iCmdShow);
UpdateWindow(hwnd);
ShowWindow这个函数实际有点名不符实,它实际只是根据它的第二个参数设置了窗口的显示状态(如最大化显示、最小化显示)而已。UpdateWindow才会真正把窗口“请”出来。但它还是没有立即让窗口显示出来,它只是给该窗口发了一个WM_PAINT消息,当该窗口处理WM_PAINT的时候,它才会真正地显示出来。
4.执行消息循环
之前已经说了,Windows程序是以消息驱动的,每一个Windows程序都有一个自己的消息队列。在程序里,我们需要有一个消息循环,去不断从自己的消息队列里取出消息,并交给窗口函数进行处理。
MSG msg;
while(GetMessage(&msg,NULL,0,0))
{ TranslateMessage(&msg);
DispatchMessage(&msg); }
MSG结构记录了一个Windows消息。消息循环以GetMessage函数开始,它从消息队列中取出一个消息,如果取出的消息是WM_QUIT便退出消息循环。否则将消息交给TranslateMessage函数进行一些键盘转换。DispatchMessage函数将消息回传给Windows,然后Windows将该消息发送给该窗口的窗口函数进行处理。
5.设置窗口函数
窗口函数是处理该窗口消息的地方。
LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{ switch(message)
{ case WM_DESTROY:
PostQuitMessage(0);
return 0; }
return DefWindowProc(hwnd, message, wParam, lParam);}
窗口函数的第一个参数是窗口句柄,第二个参数是消息的ID号。wParam和lParam是消息的参数。在窗口函数里我们是用一个switch/case结构来确定窗口函数接收的是什么消息。如果窗口函数不处理的消息应该交给DefWindowProc函数进行处理。DefWindowProc函数为窗口不处理的消息进行默认处理,这是很重要的。
改进消息循环
网络游戏和其他大多数应用程序不同的是,游戏会在后台不断地做一些事情,如《魔兽世界》每秒要进行几十次的屏幕画面刷新。这样的话上面所说的消息循环就不适合在游戏里使用了,因为GetMessage是只有获取到消息才返回参数,没有消息的话会一直等待。我们必须要对消息循环做一些改进。
MSG msg;
while( msg.message != WM_QUIT )
{ if( PeekMessage( &msg, NULL, 0, 0, PM_REMOVE ) )
{ TranslateMessage(&msg);
DispatchMessage(&msg); }
else{
GameRender(); // 刷新游戏
}}
在新的消息循环里,我们用的是PeekMessage函数而不是GetMessage函数。PeekMessage也是从消息队列里取出消息,但跟GetMessage不同,PeekMessage对消息队列进行查看,如果有消息便取出消息,如果没有也会立即返回,不会等待消息。上面的消息循环的每一次循环做以下事情:对消息队列进行查看,如果有消息便处理,如果没有便调用GameRender刷新游戏。
3D网游之必需——FPS控制
游戏在一秒内进行画面刷新的次数称为游戏的刷新率。3D网络游戏(如《魔兽世界》)一般要把刷新率控制在30次(即30FPS)左右,才会得到平滑的动画效果。除非你想让你的游戏占用全部的CPU时间。否则便要对游戏刷新次数进行控制了。
DWORD dwPerFrame = 1000 / 30; // 控制在30FPS以内
DWORD dwCurTime = 当前时间;
void GameRender( void )
{ DWORD dwTick = 当前时间 - dwCurTime;
if( dwTick >= dwPerFrame )
{ dwCurTime = 当前时间 - ( dwTick - dwPerFreme );
RunGame(); // 处理游戏逻辑与刷新游戏画面 }
else{ Sleep( 1); }
}
宝刀未老的MFC
许多人用VC++进行Windows编程都是从MFC开始的,所以都把VC++/MFC/Windows编程搞混了。其实MFC只是一个类库而已。很多做游戏开发的人都会对MFC有成见,认为MFC很“落后”,对游戏开发来说,它的效率不高。不能说他们说得不对,的确大多数游戏都不使用MFC,但我们不能忽视它的优点,它是一个很成功的框架类库。使用MFC可以很方便快捷地开发应用程序,如使用MFC开发一个地图编辑器就是很不错的选择。
正确的学习途径
学习Windows编程一定要弄明白Windows的消息机制,接着是窗口的各种处理、GDI、键盘和鼠标、多线程等等。《Windows程序设计(第五版)》是一本非常好的书,它包罗了这些内容,花三个月时间好好看看吧。要在Windows平台下编程,有许多Win32 APIs可以帮到我们,但数千个APIs,要全都掌握、全都记住是不太现实的,也没这个必要。只要掌握一些常用的,遇到问题的话就去查MSDN。MSDN是开发人员的一大得力助手,经常查阅是一个好习惯。
对Windows系统了解越是深入,在Windows平台做开发便越能得心应手。否则无论你的编程功底如何深厚,面向Windows编程也无从下手。所以“操作系统”是一门必修课,但许多想成为程序员的人甚至某些已成为程序员的人都对此不以为然,这其实是一个误区。
附:学习书籍推荐
《Windows程序设计》
本书已成为经典之作,被称为学习Windows编程的必修之书。它包括了Windows编程的方方面面,提供了许多例子,由浅入深。
《Windows核心编程》
本书从操作系统内部机制出发,全面系统地介绍了Windows的各种基本构件,如进程、线程、DLL和内存管理等,并列举了大量应用程序。是一本提高Windows编程水平的好书。