对战坦克大战
转载请注明出处
本节将介绍一个和FC(FamilyComputer)上的经典游戏《坦克大战》类似的游戏——对战坦克
大战。这是一个4 人对战的坦克游戏,4个玩家两两一组,率先攻击到对方鹰巢的一组玩家获胜。
对战坦克大战是一个C/S 结构的网络游戏,它的网络部分是用重叠I/O的Socket实现的。它分成
服务器端和客户端。服务器端用来接受客户端连接,并对游戏作出控制。先来看看服务器部分的实现。
4.10.1 对战坦克大战的服务器程序
服务器程序界面如图4.15 所示。
图4.15对战坦克大战的服务器程序
服务器是一段Win32 程序。程序入口WinMain 和前面游戏中介绍过的入口函数并无二样。WndProc
是WinMain 中定义的消息回调函数,代码如下:
LRESULT CALLBACK WndProc (HWND hwnd, UINTmessage, WPARAM wParam, LPARAM lParam)
{
int cxChar, cyChar ;
switch (message)
{
case WM_CREATE :
cxChar = LOWORD (GetDialogBaseUnits ()) ;
cyChar = HIWORD (GetDialogBaseUnits ()) ;
hwndList = CreateWindow (TEXT("listbox"), NULL,
WS_CHILDWINDOW|WS_VISIBLE | LBS_STANDARD ^LBS_SORT,
cxChar, cyChar,
cxChar * 44 + GetSystemMetrics (SM_CXVSCROLL),
cyChar * 16,
hwnd, (HMENU) ID_LIST,
(HINSTANCE) GetWindowLong (hwnd, GWL_HINSTANCE),
NULL) ;
//初始化服务器
if ( !InitServer() )
第4 章网络游戏开发277
PostQuitMessage (0) ;
//创建socket 监听线程
CreateThread( NULL, 0, AcceptThread, NULL, 0,NULL );
//创建socket 工作线程
CreateThread( NULL, 0, WorkerThread, NULL, 0,NULL );
return 0 ;
case WM_SETFOCUS :
SetFocus (hwndList) ;
return 0 ;
case WM_DESTROY :
TerminateServer();
PostQuitMessage (0) ;
return 0 ;
}
return DefWindowProc (hwnd, message, wParam,lParam) ;
}
WndProc 在创建消息中首先调用了InitServer,以初始化服务器。然后,它开启两个线程,一个是
socket 监听线程AcceptThread,另一个是socket工作线程WorkerThread。
初始化服务器函数InitServer,定义如下:
bool InitServer()
{
WSADATA wsd;
sockaddr_in local;
// socket 初始化
if (WSAStartup(MAKEWORD(2, 2), &wsd) != 0)
return false;
// 创建监听socket
slisten = socket(AF_INET, SOCK_STREAM,IPPROTO_IP);
if (slisten == SOCKET_ERROR) {
WSACleanup();
return false;
}
// 绑定地址和端口
local.sin_addr.s_addr = htonl(INADDR_ANY);
local.sin_family = AF_INET;
local.sin_port = htons(sport);
if(bind(slisten,(struct sockaddr *)&local,
sizeof(local)) == SOCKET_ERROR) {
closesocket( slisten );
WSACleanup();
return false;
}
// 将socket 变成文件使用方式,并在上面监听socket
iocp =CreateIoCompletionPort(INVALID_HANDLE_VALUE,NULL,0,0);
if ( !iocp ) {
278 Visual C++游戏开发技术与实例
closesocket( slisten );
WSACleanup();
return false;
}
// 初始化socket 池和玩家信息池
if ( !olexPool.InitPool(0) ||!playerPool.InitPool(16) ) {
closesocket( slisten );
WSACleanup();
return false;
}
ZeroMemory( &gTable, sizeof(GAMETABLE) );
// 开始监听
if ( listen(slisten,SOMAXCONN) != 0 ) {
closesocket( slisten );
WSACleanup();
return false;
}
Notice(1, "Server startsuccessfully!");
return true;
}
Socket 连接监听线程函数AcceptThread定义如下。它采用轮寻方式监听连接,并将创建的会话
Socket 与文件I/O进行关联。
DWORD WINAPI AcceptThread( LPVOID pParam ) {
sockaddr_in client;
int size;
SOCKET ret;
OVERLAPPEDEX *lpolex;
while(true) {
size = sizeof(sockaddr_in);
ret = accept(slisten,(sockaddr*)&client,&size);
if(ret != INVALID_SOCKET)
{
Notice(2, "Connect:",inet_ntoa(client.sin_addr));
lpolex = olexPool.GetUsable();
if ( lpolex )
{
//成功接受连接
//将会话Socket 和文件关联
CreateIoCompletionPort((HANDLE)ret, iocp, NULL,0);
lpolex->socket = ret;
RecvMsg( lpolex );
}
else {
closesocket( ret );
第4 章网络游戏开发279
}
}
else { // accept error
ret = WSAGetLastError();
WSAErrorTrigger(ret, TEXT("AcceptErr:"));
}
}
return 0;
}
另一个线程函数WorkerThread 用于和客户端进行通信,并对整个游戏进行控制。
DWORD WINAPI WorkerThread(LPVOID pParam) {
ULONG_PTR ckey;
OVERLAPPED *pol;
OVERLAPPEDEX *polex;
DWORD BytesTransferred;
int ret;
int *ibuf;
while(true) {
ret = GetQueuedCompletionStatus(iocp,&BytesTransferred,
&ckey,&pol,INFINITE);
// OVERLAPPEDEX 是自定义结构
polex = CONTAINING_RECORD(pol, OVERLAPPEDEX, ol);
// 远程主机断开连接
if ( ret == 0) {
int size = sizeof(sockaddr_in);
sockaddr_in client;
getpeername(polex->socket,(sockaddr*)&client,&size);
Notice(2, "Discont:",inet_ntoa(client.sin_addr));
// 删除所占的座位
for ( int i=0; i<gTable.current; i++ ) {
if ( gTable.players[i] == polex->ppla ) {
if ( i > 0 )
gTable.players[i-1]->next =polex->ppla->next;
break;
}
}
for ( ; i<gTable.current-1; i++ )
gTable.players[i] = gTable.players[i+1];
gTable.current--;
// 回收资源
playerPool.Recycle( polex->ppla );
olexPool.Recycle( polex );
continue;
}
280 Visual C++游戏开发技术与实例
// 成功收到消息
switch (polex->op) {
case OP_READ:
ibuf = (int *)(polex->wbuf.buf);
switch ( ibuf[0] ) {
// 分配玩家座位表
case NETMSGTK_ASKGROUPINFO:
Notice( "AskGroup: ", ibuf[2] );
polex->ppla = playerPool.GetUsable();
polex->ppla->seat = gTable.current;
gTable.players[gTable.current] = polex->ppla;
gTable.players[gTable.current]->socket =polex->socket;
SendMsg( NETMSGTK_ANSWERSEATINFO,polex->socket,
&gTable.current, sizeof(int) );
Notice( "AnswerSeat: ", gTable.current);
polex->ppla->next = NULL;
if ( gTable.current > 0 ) {
gTable.players[gTable.current-1]->next =polex->ppla;
SendMsgToOther( NETMSGTK_MOREPLAYER, gTable,gTable. current,
&gTable.current, sizeof(int) );
}
if ( ++gTable.current == MAXPLAYER )
SendMsgToTable( NETMSGTK_GAMEREADY, gTable, NULL,0 );
break;
case NETMSGTK_PLAYERREADY:
if ( ++gTable.counter == MAXPLAYER ) {
SendMsgToTable( NETMSGTK_GAMESTART, gTable, NULL,0 );
//初始化奖子
gTable.food.exsit = false;
gTable.food.existnum = DEFFOODEXFRAME;
gTable.food.notexistnum = DEFFOODNOTEXFRAME;
gTable.food.counter = DEFFOODNOTEXFRAME;
gTable.counter = 0;
}
break;
case NETMSGTK_CMDINFO:
if ( gTable.food.counter-- <= 0 ) {
if ( gTable.food.exsit ) { // 删除
gTable.food.counter = gTable.food.notexistnum;
SendMsgToTable( NETMSGTK_CMDFOODDELETE, gTable,NULL, 0 );
} else { // 创建
gTable.food.counter = gTable.food.existnum;
int foodparam[3];
foodparam[0] = rand() % FOOD_MAX;
foodparam[1] = rand() % 608;
foodparam[2] = rand() % 608;
SendMsgToTable(NETMSGTK_CMDFOODCREATE,gTable,foodparam,sizeof(int)*3 );
第4 章网络游戏开发281
}
gTable.food.exsit = !gTable.food.exsit;
}
SendMsgToOther( NETMSGTK_CMDINFO, gTable,polex->ppla->seat, ibuf+2,
ibuf[1] );
break;
case NETMSGTK_TEAMVICTORY:
SendMsgToTable( NETMSGTK_TEAMVICTORY, gTable,ibuf+2, ibuf[1] );
break;
}
RecvMsg( polex );
break;
case OP_WRITE:
olexPool.Recycle( polex );
break;
}
}
return 0;
}
4.10.2 对战坦克大战的客户端程序
对战坦克大战的客户端程序界面如图4.16 所示。
图4.16 坦克大战客户端
注意:如果想测试这个游戏,需要同时运行4个客户端程序。
282 Visual C++游戏开发技术与实例
程序主框架首先调用InitNetwork 函数用于初始化网络通信。InitNetwork 函数定义如下:
bool InitNetwork( const char *serv_addr, unsignedint serv_port)
{
WSADATA wsd;
sockaddr_in local,server;
unsigned long ul = 1;
int ret;
// 初始化socket
if (WSAStartup(MAKEWORD(2, 2), &wsd) != 0)
return false;
// 创建客户端socket 并绑定
c_socket = socket(AF_INET, SOCK_STREAM,IPPROTO_IP);
if (c_socket == SOCKET_ERROR)
return false;
local.sin_addr.s_addr = htonl(INADDR_ANY);
local.sin_family = AF_INET;
c_port = NET_CLIENT_PORT_MIN;
while(c_port < NET_CLIENT_PORT_MAX)
{
local.sin_port = htons(c_port);
if(bind(c_socket,(struct sockaddr *)&local,
sizeof(local)) == SOCKET_ERROR) {
ret = WSAGetLastError();
if(ret == WSAEADDRINUSE)
c_port++;
else break;
}
else break;
}
if(c_port >= NET_CLIENT_PORT_MAX)
return false;
server.sin_addr.s_addr = inet_addr(serv_addr);
server.sin_family = AF_INET;
server.sin_port = htons(serv_port);
// 连接服务器
if( connect( c_socket, (const sockaddr*)&server,sizeof(server) ) == SOCKET_ERROR )
{
ret = WSAGetLastError();
if(ret == WSAENETDOWN || ret == WSAENETUNREACH)
ERRORMSG("Can’t reach server.\nPlease checkyour network connection.");
else if(ret == WSAECONNREFUSED)
ERRORMSG("The server does not work!");
else if(ret == WSAEPROCLIM)
ERRORMSG("Too many users.\nPlease trylater.");
第4 章网络游戏开发283
return false;
}
// 设置socket 为非阻塞
if( ioctlsocket( c_socket, FIONBIO, &ul ) ==SOCKET_ERROR )
return false;
// 初始化消息列表
NetList.CreatMsgList( 8, true ); // networkmessage list
// 创建消息接受线程
HANDLE hThread =CreateThread(NULL,0,MsgReceiver,NULL,0,NULL);
if(!hThread)
return false;
return true;
}
InitNetwork 函数中开启了一个新线程用于接受网络消息,线程函数是MsgReceiver。在MsgReceiver
中,程序采用轮寻方式检测网络数据,函数定义如下:
DWORD WINAPI MsgReceiver( LPVOID param )
{
fd_set fdread;
timeval tval;
int ret, msgsize;
char buf_char[BUFFERSIZE]; // 接受缓冲
char *mark;
CMsgElem elem;
// 向服务器查询组信息
ret = 0;
SendMsg( NETMSGTK_ASKGROUPINFO, &ret,sizeof(int) );
tval.tv_usec = 0;
tval.tv_sec = 1;
//轮寻方式检测是否有网络消息
while(true)
{
FD_ZERO(&fdread);
FD_SET(c_socket,&fdread);
ret = select(0,&fdread,NULL,NULL,&tval);
if ( ret == 0 || ret == SOCKET_ERROR ) {
ret = WSAGetLastError();
continue;
}
// 可能还未初始化
if ( !NetList.GetSize() )
continue;
//接受数据
284 Visual C++游戏开发技术与实例
ret = recv(c_socket,buf_char,BUFFERSIZE,0);
if(ret == SOCKET_ERROR) {
ret = WSAGetLastError();
NetList.Lock();
char *temp = "Connection shutdown!";
elem.CreateMsgElem(MSGNET_RECEIVEERROR, temp,strlen(temp)+1, MSG_NET );
NetList.Push(&elem);
NetList.UnLock();
break;
}
// 把消息弹入列表中
NetList.Lock();
mark = buf_char;
while ( ret > 0 &&
elem.CreateMsgElemFromBuf( mark, msgsize, MSG_NET) ) {
NetList.Push(&elem);
mark += msgsize;
ret -= msgsize;
}
NetList.UnLock();
}
return 0;
}
MsgReceiver 中还调用了SendMsg函数,这是向服务器发送消息的函数。
bool SendMsg(int msg, LPVOID param, int size)
{
int ret = size+sizeof(int)*2;
char *buffer = new char[ret];
if(!buffer)
return false;
*(int *)buffer = msg;
*(int *)(buffer+sizeof(int)) = size;
if(param && size>0)
memcpy( buffer+sizeof(int)*2, param, size );
ret = send(c_socket,buffer,ret,0);
delete[] buffer;
if( ret == SOCKET_ERROR)
return false;
else
return true;
}
在InitNetwork 函数完成后,系统调用GameMain进入游戏控制循环。在GameMain函数中,程序
首先调用MsgProcessor 处理网络消息,接着根据当前的游戏状态作出不同动作。而当游戏处于运行状
态时,程序首先对子弹进行碰撞检测相关计算,接着对坦克运动做计算,然后再对奖子做碰撞检测计
算,最后是向电脑控制的坦克做AI 命令。当这些控制完成后,程序将上面的动作统一发送到服务器
第4 章网络游戏开发285
端。GameMain 的最后部分是绘制这些精灵,绘制的顺序是地图、子弹、坦克、鹰巢和草地(雪地)。
注意:这里实现坦克游戏能够完全模仿FC(Family Computer)上的坦克大战,所以坦克是可以
在草地中隐藏的,这也是为什么将草地最后绘制的原因。
void ConsoleNet::GameMain() {
static int counter = 0;
static DWORD start_time = 0, last_get;
static DWORD frame_start = 0;
DWORD end_time;
if ( m_dwStatus == CONSTAT_ENDGAME )
return ;
// 如果网络消息队列非空,则调用MsgProcessor 函数处理消息列表。
if ( !NetList.IsEmpty() )
MsgProcessor( &NetList );
// 判断当前游戏状态
if ( m_dwStatus < CONSTAT_WAITMORE ) {
return ;
} else if ( m_dwStatus == CONSTAT_WAITBEGIN ) {
last_get = timeGetTime();
return ;
} else if ( m_dwStatus == CONSTAT_WAITPLAYER ) {
if ( bFresh ) {
cmdbuf = uiCurrentCmd;
firebuf = bFired;
bFresh = false;
}
if ( bRecvCmd ) {
last_get = timeGetTime();
m_dwStatus = CONSTAT_RUNNING;
} else {
end_time = timeGetTime();
if( end_time - last_get > WAITTIMEOUT ) {
DebugOutput( 1, "Wait error!" );
m_dwStatus = CONSTAT_WAITERROR;
}
return ;
}
}
// FPS 控制, 理论上1000/x
// x = 20, 30 fps
while ( timeGetTime() - frame_start < 30 );
frame_start = timeGetTime();
// 如果程序处于运行状态
if ( m_dwStatus == CONSTAT_RUNNING ) {
BulletsProc(); // 1——先执行子弹运动和碰撞检测
286 Visual C++游戏开发技术与实例
#ifdef _DEBUG_CMDOUTPUT
char temp[4];
DebugOutput( 1, "*****Last Get:*****");
for ( int i=0; i<DEFTANKNUM; i++ ) {
itoa(m_pCmd[i].cmd,temp,10);
DebugOutput( 2, temp, "\t" );
}
DebugOutput( 1, "\r\n" );
#endif
TanksProc(); // 2——执行坦克运动
FoodProc(); // 3——奖子碰撞检测和信息更新
// 交换命令信息
m_pCmd[m_nLocal].cmd= cmdbuf;
m_pCmd[m_nLocal].fire= firebuf;
bFresh = true;
CreateAICmd(); // 向电脑控制的坦克发出AI 命令
#ifdef_DEBUG_CMDOUTPUT
DebugOutput( 1,"*****Generate:*****" );
for ( intk=m_nLocal; k<m_nLocal+DEFAINUM+1; k++ ) {
itoa(m_pCmd[k].cmd,temp,10);
DebugOutput( 2,temp, "\t" );
}
#endif
//向服务器发送消息
SendCmdMsg(&m_pCmd[m_nLocal],DEFAINUM+1,m_nLocal);//home-0,enemy-5
// 等待下一条消息
m_dwStatus =CONSTAT_WAITPLAYER;
m_nMsgCounter = 0;
ZeroMemory(m_pMsgFlag,sizeof(bool)*m_nPlayers);
bRecvCmd = false;
}
// 如果应用窗口没有被激活,则跳过刷新屏幕
if ( !g_bActive )
return ;
// 动画,帧卷屏
RollBlockObjects(m_pRiver, m_nNumRiver );
// 重绘地图层
Display.Clear(0); //level ground
Display.Blt(0,0,pMapSolid->GetDDrawSurface(),NULL,DDBLTFAST_SRCCOLORKEY);
// 重绘精灵层
BlitBullets(m_ppBullets, DEFTANKNUM );
BlitTanks(m_ppTanks, DEFTANKNUM );
BlitBases(m_ppBases, 2 );
第4 章网络游戏开发287
// 重绘草地层,树层
Display.Blt(0,0,pMapGrass->GetDDrawSurface(),NULL,DDBLTFAST_SRCCOLORKEY);
BlitFood();
// 显示当前FPS
counter++;
end_time =timeGetTime();
if ( end_time -start_time >= 1000 ) {
start_time =end_time;
itoa( counter,fps+5, 10 );
counter = 0;
}
pText->DrawText(NULL, " ", 5, 5, RGB(0,0,0), RGB(0,0,0) );
pText->DrawText(NULL, fps, 5, 5, RGB(0,0,0), RGB(255,0,0) );
Display.Blt(0,0,pText->GetDDrawSurface(),NULL, 0);
// 交换前后缓冲区
Display.Present();
}__