一套跨平臺五子棋網遊的開發經歷

wchrt的部落格發表於2015-06-26

閒來無事,因自己想要在伺服器開發方面進行更深入的學習,積累更豐富的經驗。決定寫一套網路遊戲的c/s。

因為主要目的是伺服器的開發,因此遊戲我選用規則較為簡單、畫面特效沒有要求的回合制遊戲:五子棋。我曾經在剛接觸程式設計的時候自己在控制檯下做過這個遊戲,當時寫的ai特nb我自己根本下不贏他。確定是製作五子棋了, 但是還要滿足跨平臺的特性,畢竟移動互聯時代,得終端者得天下。遊戲做成全平臺才能更好的將各種玩家聚集在一起。跨平臺?b/s是人們通常會第一個想到的跨平臺方式,的確現在市面上有很多基於b/s的頁遊,大部分使用的是flash作為遊戲引擎。但手機上很少有人使用瀏覽器玩遊戲。(其實根本不會flash,html也爛得很,曾經給別人用php做的資料管理網站根本就沒有像樣的介面)於是選擇了c++的跨平臺遊戲引擎cocos2dx,這引擎簡單好用,而且因為是c++作為遊戲邏輯,移植特方便,以前也用過這個引擎(某比賽)。最終選用的版本是cocos2d-x 3.4。

既然是網路遊戲的伺服器,那麼就得高效,而且是在linux下,因此我選epoll模型進行服務端的開發,epoll的部分寫在這篇文章裡:epoll模型的理解與封裝實現,使用的linux系統為CENT OS 6.4,核心為linux2.6。

關於遊戲開發步驟的思考:

按照自己以前習慣的套路來說,通訊方式與協議的設計應該是放在首位的,然後是伺服器、再到客戶端(沒有美工)。

而自己以前曾經玩到很多的單機遊戲,更新版本後,遊戲便增加了網路遊戲功能。這似乎說明了很多遊戲與網路協議之間是相互獨立的。甚至網路協議是根據實際的遊戲邏輯設計的,而不是遊戲根據協議來設計自身的邏輯。

最終決定先把單機的版本做出來。於是制定瞭如下的開發流程:

1、遊戲的演算法與資料結構設計與實現

2、遊戲互動設計與實現

3、單機遊戲的實現

4、遊戲通訊協議設計

5、伺服器實現(不可忽略掉的重點,自己寫遊戲的目的)

6、網路遊戲功能實現

7、平臺移植

1、遊戲的演算法與資料結構設計與實現:

五子棋這個遊戲是一個二維平面上的遊戲,我們將棋盤看做一個陣列,每一個格子的狀態分為兩種:沒棋和有棋,有棋因不同玩家而區別(數量不限,可直接作為多人多子棋的遊戲基類)

程式碼:

//Chess.h
#ifndef _CHESS_H_
#define _CHESS_H_
#include "cocos2d.h"
USING_NS_CC;

//下棋座標狀態結構體
struct Chesspos
{
int x,y;
int player;//該步數所屬玩家
Chesspos(){};
Chesspos(int px,int py,int pp)
{
x=px;
y=py;
player=pp;
}
};

class Chessway
{
Chesspos *way;//路徑陣列
int totallen;//總長度
int len;//當前步數

public:
Chessway(int totalnum);
~Chessway(void);
void setempty();
bool addway(int x,int y,int player);//新增步數
int getstep();
Chesspos getnow();
};

class Chess
{
public:
Chess(int width,int heigh,int winlen=5,int playernum=2);
~Chess(void);

int **board;
int w,h;
int pnum; //palyer num
int wlen; //how number can win
Chessway *way;
int playercnt;//player start at 1
bool isgameend;

bool init(int width,int heigh,int winlen=5,int playernum=2);
void exit();

void restart();
bool nextstep(Chesspos np);//下棋,自動判斷玩家
bool nextstep(int x,int y);

int getstatus(int x,int y);//獲取遊戲狀態

bool checklen(int x,int y);
int checkwin();//判斷遊戲是否結束並返回勝利玩家
};

#endif //_CHESS_H_

檢測勝利的邏輯很簡單:找到一個下有棋的位置,檢查這個位置下、右、左下、右下是否有連續相等的5個棋,即為遊戲勝利。遊戲一旦勝利是不可以繼續下棋的,所以只會有一個玩家勝利。下面給出判斷程式碼:

//Chess.cpp
//勝利檢測程式碼
bool Chess::checklen(int x,int y)
{
for(int i=1;i<wlen;i++)
{
if(x+i>=w)
{
break;
}
if(board[x+i][y]!=board[x][y])
{
break;
}
if(i==wlen-1)
{
return true;
}
}

for(int i=1;i<wlen;i++)
{
if(y+i>=h)
{
break;
}
if(board[x][y+i]!=board[x][y])
{
break;
}
if(i==wlen-1)
{
return true;
}
}

for(int i=1;i<wlen;i++)
{
if(x+i>=w||y+i>=h)
{
break;
}
if(board[x+i][y+i]!=board[x][y])
{
break;
}
if(i==wlen-1)
{
return true;
}
}

for(int i=1;i<wlen;i++)
{
if(x-i<0||y+i>=h)
{
break;
}
if(board[x-i][y+i]!=board[x][y])
{
break;
}
if(i==wlen-1)
{
return true;
}
}
return false;
}
int Chess::checkwin()
{
for(int i=0;i<w;i++)
{
for(int j=0;j<h;j++)
{
if(board[i][j])
{
if(checklen(i,j))
{
isgameend=true;
return board[i][j];
}
}
}
}
return 0;
}

2、遊戲互動設計與實現

涉及到遊戲互動,這裡就要使用到遊戲引擎了。首先需要把遊戲的一些圖片資源大致搞定,這裡用畫圖這畫了幾個不堪入目的圖片資源:

別看這畫的醜,我可是用滑鼠和window自帶的畫圖畫出來的,到時候在遊戲中看起來是毫無違和感的(筆者小學就會畫H漫了)。

這裡就要用到cocos2dx的東西了。首先為每一個下棋的格子設計一個個塊狀的節點,然後設計遊戲主體佈景層:

class ChessNode:public Node<br>class ChessMain:public Layer

作為遊戲棋盤,每一個格子的形態都是一樣的,我只需要將它們拼接成矩陣就成了一個完整的棋盤。因此在遊戲佈景層裡,我開了一個Vector的ChessNode,將其依次緊湊地排列在螢幕上。在遊戲初始狀態時,chess_1.png、chess_2.png是不會顯示的,如圖(截圖我直接使用現成遊戲的截圖):

這樣的棋盤看起來是不是很沒有違和感?

當下棋後,就可以把對應的棋圖顯示出來:

後面發現好像真正的下棋是下在十字交叉處的。。

這部分的注意事項主要就在於觸控檢測與棋盤螢幕大小。觸控的話計算相對棋盤佈景層的座標可以得出下棋的位置。棋盤就以靜態值480px為標準,在其他地方呼叫的時候縮放即可。

#ifndef _CHESSMAIN_H_
#define _CHESSMAIN_H_
#include "cocos2d.h"
#include "Chess.h"
USING_NS_CC;

#define defaultwinsize 480.0
#define chesspicsize 50.0

static Point winsize;

class ChessNode:public Node
{
public:
ChessNode(int playernum=2);

Vector<Sprite *> chesspicarr;
Sprite * basepic;
};

class ChessMain:public Layer
{
public:

Chess *chessdata;

Vector<ChessNode *> basenode;

virtual bool init();
//virtual void onEnter();

void restart();
void updateone(int x,int y);
void updateall();

bool nextstep(int x,int y);
int checkwin();

CREATE_FUNC(ChessMain);
};

#endif //_CHESSMAIN_H_

 

#include "ChessMain.h"

ChessNode::ChessNode(int playernum)
{
basepic=Sprite::create("chess_base_1.png");
basepic->setAnchorPoint(ccp(0,0));
this->addChild(basepic);

char addname[]="chess_1.png";
for(int i=0;i<playernum;i++)
{
addname[6]='0'+i+1;
auto newsprite=Sprite::create(addname);
chesspicarr.pushBack(newsprite);
chesspicarr.back()->setAnchorPoint(ccp(0,0));
this->addChild(chesspicarr.back());

}
}

bool ChessMain::init()
{
winsize=Director::sharedDirector()->getWinSize();

//預設值棋盤
chessdata=new Chess(15,15);

for(int i=0;i<chessdata->w;i++)
{
for(int j=0;j<chessdata->h;j++)
{
basenode.pushBack(new ChessNode());

basenode.back()->setScale((defaultwinsize/chessdata->h)/chesspicsize);
basenode.back()->setPosition(
ccp(defaultwinsize/chessdata->w*i,defaultwinsize/chessdata->h*j)
);
basenode.back()->setAnchorPoint(ccp(0,0));

this->addChild(basenode.back());
}
}

restart(); 

return true;
}
/*
void ChessMain::onEnter()
{
;
}
*/
void ChessMain::restart()
{
chessdata->restart();
updateall();
}
void ChessMain::updateone(int x,int y)
{
for(int i=0;i<chessdata->pnum;i++)
{
if(chessdata->getstatus(x,y)==i+1)
{
basenode.at(x*chessdata->w+y)->
chesspicarr.at(i)->setVisible(true);
}
else
{
basenode.at(x*chessdata->w+y)->
chesspicarr.at(i)->setVisible(false);
}
}
}
void ChessMain::updateall()
{
for(int i=0;i<chessdata->w;i++)
{
for(int j=0;j<chessdata->h;j++)
{
updateone(i,j);
}
}
}

bool ChessMain::nextstep(int x,int y)
{
if(chessdata->isgameend)
{
return false;
}
if(!chessdata->nextstep(x,y))
{
return false;
}
updateone(x,y);
checkwin();

return true;
}

int ChessMain::checkwin()
{
return chessdata->checkwin();
}

/*
bool ChessMain::onTouchBegan(Touch *touch, Event *unused_event)
{
Point pos=convertTouchToNodeSpace(touch);

if(pos.x>defaultwinsize||pos.y>defaultwinsize)
{
return false;
}
int x=chessdata->w*(pos.x/defaultwinsize);
int y=chessdata->h*(pos.y/defaultwinsize);

return nextstep(x,y);
}
*/

這裡的觸控函式會由以後ChessMain的子類重寫。

3、單機遊戲的實現

單機遊戲,只需寫好對手的AI邏輯即可。幸好是五子棋不是圍棋,一般的對局AI很好寫,但是對於大棋盤來說,計算出必勝態仍然非常困難。由於自己主要目的是寫網路端。因此我把單機功能實現後並沒有寫AI,把介面留著的,只接了一個隨機函式,等以後有空自己做個AI邏輯加上。

總的來說這部分就是加上了進入遊戲前的選單以及單機遊戲的選項和遊戲結束的對話方塊:

#ifndef _AIGAMEMAIN_H_
#define _AIGAMEMAIN_H_
#include "cocos2d.h"
#include "ChessMain.h"
USING_NS_CC;

#define defaulttoolwidth 200.0
#define defaulttoolheight 100.0

//遊戲結束選單
class AIGameEndTool:public Layer
{
public:
AIGameEndTool(int type);
bool init(int type);

void gameRestart(Ref* pSender);
void menuCloseCallback(Ref* pSender);
};

//AI遊戲繼承於ChessMain
class AIGameMain:public ChessMain
{
public:
virtual bool init();

virtual bool onTouchBegan(Touch *touch, Event *unused_event);
void nextaistep();
bool checkwin();
CREATE_FUNC(AIGameMain);
};

#endif //_AIGAMEMAIN_H_

現在一個能玩的遊戲已經完成,接下來是重點的網路部分。

4、遊戲通訊協議設計

因為是PC、手機都能玩的遊戲,考慮到糟糕的手機網路環境,通訊採用客戶端單方發起請求,伺服器回覆的方式,使伺服器不用考慮確保手機訊號不好或IP變更的情況,類似於web方式。

遊戲沒有設計固定的使用者,採用的是遊戲每次向伺服器申請一個遊戲ID,使用這個遊戲ID在網際網路上和其他使用者對戰。於是協議報文設計了兩種:普通請求/回覆報文gamequest、遊戲資料包文nextquest。

#include <iostream>
#include <string>
#include <cstring>

#define NEWID (char)1
#define NEWGAME (char)3
#define NEXTSTEP (char)5
#define GETNEXTSTEP (char)6
#define GAMEEND (char)10

#define NEWID_FAIL 0
#define NEWID_SECC 1

#define NEWGAME_FAIL 0
#define NEWGAME_ISFIRST 1
#define NEWGAME_ISSEC 2

#define NEXTSTEP_FAIL 1
#define NEXTSTEP_SEC 1

struct gamequest
{
unsigned int id;
char type;
unsigned int data;
};

struct nextstephead
{
unsigned int id;
char type;
char x;
char y;
char mac;//遊戲資料校驗
short stepno;
};

NEWID:申請一個新的遊戲ID的請求與回覆

NEWGAME:申請開始遊戲的請求與回覆

NEXTSTEP:更新遊戲對局資料的請求與回覆

GETNEXSTEP:獲取遊戲對局資料的請求與回覆

GAMEEND:終止或結束遊戲的請求

關於遊戲請求與遊戲對局時的通訊,因為採用的是請求加回復的方式,伺服器不能主動通知客戶端有新的遊戲開始或是對手已經喜下了下一步棋,因此需要客戶端主動向伺服器獲取相應的資訊。於是這部分被設計為客戶端定時向伺服器傳送更新資料的請求,伺服器一旦接收到請求,就把通過該請求的TCP連線發回去。這樣雖然增加了網路的流量,但為了資料的穩定性必須做出犧牲。好的是該協議報文很小,而且因為是對局遊戲,就算有幾萬人同時在玩,實際單位時間的資料量也不會太多,最重要的是在處理併發資料的情況。

5、伺服器實現:

這是最重要最核心的部分。一個高效、穩定的遊戲伺服器程式直接決定了遊戲的體驗。在實際的遊戲伺服器開發中,遊戲邏輯與網路通訊邏輯可能分工由不同的人員開發。因此,遊戲邏輯與網路通訊邏輯應在保證效率的情況下儘可能地實現低耦合。我這裡雖然是獨立開發的,是因為遊戲的邏輯很簡單,但如果比如去開發一個像GTAOL這樣的遊戲伺服器,本來做網路通訊的人想要做出G他的遊戲邏輯那就相當地困難,需要寫處理世界、物體、角色,還要和遊戲端的邏輯一致,累成狗狗。

所以說遊戲的邏輯與網路的通訊需要儘可能地獨立,就這個五子棋伺服器而言,網路通訊端使用PPC、select、epoll都和遊戲邏輯無關,只要能接收分類並交給遊戲邏輯處理,並將遊戲邏輯處理好的資料發出即可。該伺服器選用的epoll實現的,因篇幅原因,網路通訊部分已經在這篇文章中說明清楚:epoll模型的理解封裝與應用

關於伺服器的遊戲邏輯,首先看看我們的伺服器要做哪些事情:

1、使用者遊戲ID的申請與管理

2、對局資料的處理與管理

大致就以上這兩種事情。但是因為遊戲的客戶端數量很多,不同的客戶端之間進行對局,必須要清晰地處理與管理這些資料。我這裡建立了一個idpool,用於id的儲存於申請,以防發生錯誤給使用者分配無效或是重複的id。

對局資料的處理與管理:

在兩個使用者都有id的情況下,雙方都能申請進行遊戲。這是服務端要做的就是匹配好這些使用者並通知這些使用者開始遊戲。為方便說明,我先把程式碼粘上來:

#ifndef _GAME_H_
#define _GAME_H_

#include<iostream>
#include<cstdio>
#include<cstring>
#include<string>
#include<stdlib.h>
#include<list>

#include "ssock.h"
#include "gameprotocol.h"

using namespace std;

#define idpoollength 1000
#define datapoollength 50

//鏈式IDpool
class idpool
{
list<unsigned int> ids;
public:
idpool()
{
for(int i=1;i<idpoollength;i++)
{
ids.push_back(i);
}
}
unsigned getid()
{
if(ids.empty())
{
return 0;
}
unsigned re=ids.front();
ids.pop_front();
return re;
}
void freeid(unsigned int x)
{
ids.push_front(x);
}

};

//對局匹配類
class p2p
{
unsigned int with[idpoollength];
unsigned int info[idpoollength];
public:
p2p()
{
for(int i=0;i<idpoollength;i++)
{
with[i]=i;
}
}
bool ispair(unsigned int x1)
{
return with[x1]!=x1&&with[x1]!=0;
}
//設定為該id等待匹配
void setwait(unsigned int x1)
{
with[x1]=0;
}
//自動匹配函式
bool makepair(unsigned int x1)
{
for(int i=1;i<idpoollength;i++)
{
if(with[i]==0&&x1!=i)
{
setp2p(x1,i);
return true;
}
}
return false;
}
//設定兩id匹配
void setp2p(unsigned int x1,unsigned x2)
{
with[x1]=x2;
with[x2]=x1;
info[x1]=1;
info[x2]=2;
}
//釋放匹配(單方向)
void freep2p(unsigned int x1)
{
//with[with[x1]]=with[x1];
with[x1]=x1;
}
unsigned int getotherid(unsigned int x1)
{
return with[x1];
}
unsigned int getp2pinfo(unsigned int x1)
{
return info[x1];
}
};

struct step
{
unsigned short x;
unsigned short y;
short stepno;
};
//對於下棋狀態類
class stepstatus
{
step idstep[idpoollength];
public:
stepstatus()
{
for(int i=0;i<idpoollength;i++)
{
idstep[i].stepno=-1;
}
}
bool setstep(unsigned int i,unsigned short xx,unsigned short yy,short sn)
{
idstep[i].x=xx;
idstep[i].y=yy;
idstep[i].stepno=sn;
return true;
}
step *getstep(unsigned int i)
{
return idstep+i;
}
};

//伺服器遊戲主邏輯類
class gamemain:public idpool,public p2p,public stepstatus
{
public:
//報文緩衝資料池,用於自動分配可用的mdata用以儲存待傳送的資料
mdata datapool[datapoollength];
gamemain();
mdata *getdatainpool();
//api函式,釋放用過的mdata到pool中
void freedatainpool(mdata *data);

//資料處理api函式,用於處理網路通訊部分傳入的資料,這個函式是執行緒安全的
mdata *dealdata(mdata *data);
//以下為遊戲資料分類處理的函式
mdata *newid(mdata *data);
mdata *newgame(mdata *data);
bool checkmac(nextstephead *nsh);
mdata *nextstep(mdata *data);
mdata *getnextstep(mdata *data);
mdata *gameend(mdata *data);
};

#endif //_GAME_H_

p2p類:它的作用是用來匹配玩家的。當有客戶端申請進行遊戲時,伺服器會先呼叫makepair函式來尋找可以進行匹配的另一個玩家,如果找到了合適的玩家,接下來就會呼叫setp2p簡歷這兩個玩家有對局關係。如果沒有匹配到,則會呼叫setwait等待其他的使用者進行匹配。該類使用的資料結構為簡單的hash對映。

setpstatus類:用於存放對局資料的類,使用的pool方式,客戶端下棋的資訊將會儲存在這裡,用以客戶端獲取對方下棋的資訊。p2p類的info會直接對映到pool的對應下標。不同id的客戶端查詢資料會相當地迅速。

gamemain類:遊戲的主類。給出api函式dealdata用以接收客戶端的資料並將處理後的資料返回。

#include "game.h"

gamemain::gamemain()
{
//:idpool(),p2p(),stepstatus()
{
for(int i=0;i<datapoollength;i++)
{
datapool[i].len=1;
}
}
}

mdata *gamemain::getdatainpool()
{
for(int i=0;i<datapoollength;i++)
{
if(datapool[i].len==1)
{
return datapool+i;
}
}
return NULL;
}
void gamemain::freedatainpool(mdata *data)
{
data->len=1;
}

mdata *gamemain::dealdata(mdata *data)
{
gamequest *gqh=(gamequest *)data->buf;
printf("this data:type:%d,id:%d\n",gqh->type,gqh->id);
if(gqh->type==NEWID)
{
return newid(data);
}
else if(gqh->type==NEWGAME)
{
return newgame(data);
}
else if(gqh->type==NEXTSTEP)
{
return nextstep(data);
}
else if(gqh->type==GETNEXTSTEP)
{
return getnextstep(data);
}
else if(gqh->type==GAMEEND)
{
return gameend(data);
}
}

mdata *gamemain::newid(mdata *data)
{
mdata *newdata=getdatainpool();
gamequest *rgqh=(gamequest *)newdata->buf;
newdata->len=sizeof(gamequest);

rgqh->type=NEWID;
rgqh->id=0;
rgqh->data=getid();
printf("a new id:%u send,len:%u\n",rgqh->data,newdata->len);
return newdata;
}

mdata *gamemain::newgame(mdata *data)
{
gamequest *gqh=(gamequest *)data->buf;
mdata *newdata=getdatainpool();

gamequest *rgqh=(gamequest *)newdata->buf;
newdata->len=sizeof(gamequest);
rgqh->type=NEWGAME;
if(ispair(gqh->id)||makepair(gqh->id))
{
rgqh->id=getotherid(gqh->id);
rgqh->data=getp2pinfo(gqh->id);
printf("a new game start:%d and %d\n",gqh->id,rgqh->id);
return newdata;
}
setwait(gqh->id);
rgqh->data=NEWGAME_FAIL;
return newdata;
}

bool gamemain::checkmac(nextstephead *nsh)
{
return nsh->mac==(nsh->type^nsh->x^nsh->y^nsh->stepno);
}
mdata *gamemain::nextstep(mdata *data)
{
nextstephead *nsh=(nextstephead *)data->buf;
mdata *newdata=getdatainpool();
newdata->len=0;
printf("nextstep: %d %d %d %d\n",nsh->id,nsh->x,nsh->y,nsh->stepno);
if(checkmac(nsh))
{
if(setstep(nsh->id,nsh->x,nsh->y,nsh->stepno))
{
gamequest *rgqh=(gamequest *)newdata->buf;
newdata->len=sizeof(gamequest);
rgqh->type=NEXTSTEP;
rgqh->data=NEXTSTEP_SEC;
return newdata;
}

}
return newdata;
}

mdata *gamemain::getnextstep(mdata *data)
{
gamequest *gqh=(gamequest *)data->buf;
step *sh=getstep(getotherid(gqh->id));
mdata *newdata=getdatainpool();
if(sh->stepno!=-1)
{
nextstephead *rnsh=(nextstephead *)newdata->buf;
newdata->len=sizeof(nextstephead);

rnsh->type=GETNEXTSTEP;
rnsh->id=getotherid(gqh->id);
rnsh->x=sh->x;
rnsh->y=sh->y;
rnsh->stepno=sh->stepno;
rnsh->mac=rnsh->type^rnsh->x^rnsh->y^rnsh->stepno;
printf("gnextstep: %d %d %d %d\n",rnsh->id,rnsh->x,rnsh->y,rnsh->stepno);
sh->stepno=-1;
return newdata;
}

newdata->len=0;
return newdata;
}

mdata *gamemain::gameend(mdata *data)
{
gamequest *gqh=(gamequest *)data->buf;
mdata *newdata=getdatainpool();
freep2p(gqh->id);
newdata->len=0;
return newdata;
}

這裡的dealdata是執行緒安全的,方便網路通訊部分用的各種方式呼叫。因為這該五子棋伺服器的遊戲邏輯的主要功能就是資料的儲存轉發,沒有什麼需要在後臺一直執行的要求。因此該程式耦合很低,使用很簡答,只需要建立、呼叫處理函式、獲取處理結果即可。

6、網路遊戲功能實現

現在回到遊戲客戶端,前面已經實現的單機遊戲的功能。現在要做的就是加入網路功能,其實就是把單機的ai部分接到伺服器上。

首先是遊戲id的獲取。通過向伺服器傳送NEWID請求。會受到伺服器分配的id。將這個id作為自己的遊戲id,在告知伺服器退出遊戲或是伺服器在長時間未受到該id的情況下自動釋放前都有效。

圖中兩個客戶端分別分配到id2與3。

當客戶端分配到id後,就可以向伺服器發起遊戲匹配請求NEWGAME。為了防止匹配不到玩家,設定傳送匹配請求最多隻維持一分鐘,在一分鐘結束後,客戶端向伺服器發出停止匹配的請求。

當有兩個客戶端在這交叉的時段進行進行匹配,便可能匹配在一起開始遊戲。

遊戲匹配成功後,客戶端將收到伺服器發過來的對局基礎資訊,包括了對手id、先手還是後手。當遊戲開始後,先手的下棋然後將資料提交到伺服器,又後手的更新資料,然後照這樣依次迴圈下去直到遊戲結束。

id2與iD3匹配到了一起。

在遊戲結束時,贏的一方會顯示勝利,輸的顯示失敗,雙方都不再更新資料。退出對局後便能開始下繼續匹配遊戲。

遊戲客戶端需要注意的是對局資料的校驗還有sock連結的問題。當在糟糕的網路環境下,客戶端不應定能獲取到正確的資料,因此要根據資料包總的mac進行校驗。而tcp連結再側重狀態下將時斷時續。因此要注意當連線中斷後及時與伺服器進行重連。

還有關於跨平臺的問題。我將socket封裝成類,不管是win還是linux都是同樣的呼叫方式。在sock類中用ifdef區分開兩個系統的不同api呼叫。

以下是客戶端跨平臺sock的封裝:

#ifndef _MSOCK_H_
#define _MSOCK_H_

#include<iostream>
#include<cstdio>
#include<cstring>
#include<string>

#ifdef WIN32
#include<winsock2.h>
#else
#include<fcntl.h>
#include<sys/ioctl.h>
#include<sys/socket.h>
#include<unistd.h>
#include<netdb.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<sys/types.h>
#define SOCKET int
#define SOCKET_ERROR -1
#define INVALID_SOCKET -1
#endif

using namespace std;

static int networkinit()
{
#ifdef WIN32
WSADATA wsadata={0};
return WSAStartup(MAKEWORD(1,0),&wsadata);
#else
return 0;
#endif
}
static int networkclose()
{
#ifdef WIN32
return WSACleanup();
#endif

return 0;
}

class msock_tcp
{
public:
SOCKET sock;
int info;
sockaddr_in addr;
msock_tcp()
{
newsocket();
addr.sin_family=AF_INET;
}

void newsocket()
{
sock=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
if(sock==INVALID_SOCKET)
{
puts("socket build error");
exit(-1);
}
}

void setioctl(bool x)
{
#ifdef WIN32
if(!x)
{
return;
}
unsigned long ul = 1;
ioctlsocket(sock, FIONBIO, (unsigned long*)&ul);
#else
fcntl(sock, F_SETFL, O_NONBLOCK);
#endif
}
bool setip(string ip)
{
//解析域名IP
hostent *hname=gethostbyname(ip.c_str());
if(!hname)
{
puts("can't find address");
return false;
}//puts(inet_ntoa(addr.sin_addr));
#ifdef WIN32
addr.sin_addr.S_un.S_addr=*(u_long *)hname->h_addr_list[0];
#else
addr.sin_addr.s_addr=*(u_long *)hname->h_addr_list[0];
#endif
return true;
}
void setport(int port)
{
addr.sin_port=htons(port);
}
int mconnect()
{
return connect(sock,(sockaddr *)&addr,sizeof(addr));
}
int msend(const char *data,const int len)
{
info=send(sock,data,len,0);
if(info==SOCKET_ERROR)
{
mclose();
newsocket();
mconnect();
info=send(sock,data,len,0);
}
return info;
}
int msend(const string data)
{
return msend(data.c_str(),data.length());
}
int mrecv(char *data,int len)
{
return recv(sock,data,len,0);
}
int mrecv(char *data)
{
return recv(sock,data,2047,0);
}
int mclose()
{
#ifdef WIN32
return closesocket(sock);
#else
return close(sock);
#endif
}
};

#endif

網路匹配類:

#ifndef _NETWORKSCENE_H_
#define _NETWORKSCENE_H_

#include "cocos2d.h"
#include "NetGameMain.h"
USING_NS_CC;

class NETWorkScene:public Layer
{
public:
msock_tcp *sock;
char rdata[2048];
int rlen;
unsigned int gameid;
unsigned int gameid2;
CCLabelTTF* gameinfo;

virtual bool init();
//從伺服器中獲取id
bool getidonserver();
void showgameid();
//發起匹配遊戲請求
bool findplayer();

void findbutton(Ref* pSender);

//開始新遊戲,進入對局場景
bool newgamestart(bool ismyround);

NETGameMain *gamemain;

//資料以及ui更新
updatequest upq;
void update_quest();
void update(float delta);

CREATE_FUNC(NETWorkScene);
};

#endif // _NETWORKSCENE_H_
#include "NetWorkScene.h"

bool NETWorkScene::init()
{
if(networkinit())
{
CCLOG("network init fail");
return false;
}
sock=new msock_tcp;
sock->setioctl(true);
//我用於測試的centos伺服器
sock->setip("wchrter.oicp.net");//127.0.0.1
sock->setport(5940);
//sock->setip("127.0.0.1");
//sock->setport(5000);

if(sock->mconnect()>=0)
{
CCLOG("sock connect error");
//this->removeFromParentAndCleanup(true);
}
else
{
CCLOG("sock connect secc");
}

gameid=0;

auto fdItem = MenuItemImage::create(
"net_find1.png",
"net_find2.png",
CC_CALLBACK_1(NETWorkScene::findbutton, this));
fdItem->setScale(2.0);
// create menu, it's an autorelease object
auto menu = Menu::create(fdItem, NULL);
winsize=Director::sharedDirector()->getWinSize();
menu->setPosition(ccp(winsize.x/2,winsize.y/2));
this->addChild(menu, 1);

gameinfo = CCLabelTTF::create("", "Arial", 30);
gameinfo->setPosition(ccp(winsize.x/4, winsize.y/2)); 
this->addChild(gameinfo);

scheduleUpdate();
return true;
}
bool NETWorkScene::getidonserver()
{
gamequest quest;
quest.id=0;
quest.type=NEWID;

if(SOCKET_ERROR==sock->msend((char *)&quest,sizeof(quest)))
{
CCLOG("getidonserver error");
return false;
}

return true;
}
void NETWorkScene::showgameid()
{
gameinfo->setString("your\ngame id:\n"+inttostring(gameid));
}
bool NETWorkScene::findplayer()
{
if(gameid==0)
{
if(!getidonserver())
{
return false;
}
return false;
}

gamequest quest;
quest.id=gameid;
quest.type=NEWGAME;
upq.set(quest,30);
return true;
}

void NETWorkScene::findbutton(Ref* pSender)
{
findplayer();
}

bool NETWorkScene::newgamestart(bool ismyround)
{
upq.settle(0);

NETGameMain *newgame=NETGameMain::create();
newgame->setgameid(gameid,gameid2);
newgame->setsock(sock);
newgame->setismyround(ismyround);
Point winsize=Director::sharedDirector()->getWinSize();
newgame->setScale(winsize.y/defaultwinsize);

auto director = Director::getInstance();
auto scene = Scene::create();
scene->addChild(newgame);
director->pushScene(scene);

return true;
}
void NETWorkScene::update_quest()
{
if(upq.end())
{
return ;
}
if(!upq.push())
{
return;
}

if(SOCKET_ERROR==sock->msend((char *)&upq.quest,sizeof(upq.quest)))
{
CCLOG("socket error");
}
return;
}
void NETWorkScene::update(float delta)
{
//CCLOG("JB");
update_quest();

rlen=sock->mrecv(rdata);
if(rlen>0)
{
gamequest *gqh=(gamequest *)rdata;
CCLOG("%d: %d %02x %d\n",rlen,gqh->id,gqh->type,gqh->data);
if(gqh->type==NEWID)
{
gameid=gqh->data;
showgameid();
}
else if(gqh->type==NEWGAME)
{
gameid2=gqh->id;
if(gqh->data==NEWGAME_ISFIRST)
{
newgamestart(true);
}
else if(gqh->data==NEWGAME_ISSEC)
{
newgamestart(false);
}
else
{
CCLOG("findplayer fail");
}
}
}
else
{
//CCLOG("no message");
}
}

網路遊戲對局類:

#ifndef _NETGAMEMAIN_H_
#define _NETGAMEMAIN_H_
#include "cocos2d.h"
#include "ChessMain.h"
#include "msock.h"
#include "gameprotocol.h"
USING_NS_CC;

#define defaulttoolwidth 200.0
#define defaulttoolheight 100.0
#define updatetime 20

//更新類
class updatequest
{
int timecnt;
int timelimit;
public:
gamequest quest;
updatequest()
{
timecnt=0;
timelimit=0;
}
void set(gamequest q,int tle=5)
{
quest=q;
timelimit=tle*updatetime;
timecnt=0;
}
void settle(int tle)
{
timelimit=tle;
}
bool end()
{
if(timelimit<0)
{
return false;
}
if(timecnt<timelimit)
{
return false;
}
return true;
}
bool push(int pt=1)
{
timecnt+=pt;
if(timecnt%updatetime==0)
{
return true;
}
return false;
}
};

//遊戲選單類
class NETGameEndTool:public Layer
{
public:
NETGameEndTool(int type);
bool init(int type);

void gameEnd(Ref* pSender);
};

class NETGameMain:public ChessMain
{
public:
virtual bool init();
virtual void onEnter();

msock_tcp *sock;
char rdata[2048];
int rlen;
//自己id與對局者id
unsigned int gameid;
unsigned int gameid2;
CCLabelTTF* idinfo;
CCLabelTTF* roundinfo;

void setgameid(unsigned int x,unsigned int y);
void setsock(msock_tcp *s);
void setismyround(bool x);

//當前是否為自己回合
bool ismyround;

virtual bool onTouchBegan(Touch *touch, Event *unused_event);

bool isnetsetp;
void nextnetstep(int x,int y);
//勝利檢測
void checkwin();

//資料與ui更新
updatequest upq;
void update_quest();
void update(float delta);

CREATE_FUNC(NETGameMain);
};

string inttostring(int num);

#endif //_AIGAMEMAIN_H_

實現程式碼:

#include "NetWorkScene.h"

bool NETWorkScene::init()
{
if(networkinit())
{
CCLOG("network init fail");
return false;
}
sock=new msock_tcp;
sock->setioctl(true);
//我用於測試的centos伺服器
sock->setip("wchrter.oicp.net");//127.0.0.1
sock->setport(5940);
//sock->setip("127.0.0.1");
//sock->setport(5000);

if(sock->mconnect()>=0)
{
CCLOG("sock connect error");
//this->removeFromParentAndCleanup(true);
}
else
{
CCLOG("sock connect secc");
}

gameid=0;

auto fdItem = MenuItemImage::create(
"net_find1.png",
"net_find2.png",
CC_CALLBACK_1(NETWorkScene::findbutton, this));
fdItem->setScale(2.0);
// create menu, it's an autorelease object
auto menu = Menu::create(fdItem, NULL);
winsize=Director::sharedDirector()->getWinSize();
menu->setPosition(ccp(winsize.x/2,winsize.y/2));
this->addChild(menu, 1);

gameinfo = CCLabelTTF::create("", "Arial", 30);
gameinfo->setPosition(ccp(winsize.x/4, winsize.y/2)); 
this->addChild(gameinfo);

scheduleUpdate();
return true;
}
bool NETWorkScene::getidonserver()
{
gamequest quest;
quest.id=0;
quest.type=NEWID;

if(SOCKET_ERROR==sock->msend((char *)&quest,sizeof(quest)))
{
CCLOG("getidonserver error");
return false;
}

return true;
}
void NETWorkScene::showgameid()
{
gameinfo->setString("your\ngame id:\n"+inttostring(gameid));
}
bool NETWorkScene::findplayer()
{
if(gameid==0)
{
if(!getidonserver())
{
return false;
}
return false;
}

gamequest quest;
quest.id=gameid;
quest.type=NEWGAME;
upq.set(quest,30);
return true;
}

void NETWorkScene::findbutton(Ref* pSender)
{
findplayer();
}

bool NETWorkScene::newgamestart(bool ismyround)
{
upq.settle(0);

NETGameMain *newgame=NETGameMain::create();
newgame->setgameid(gameid,gameid2);
newgame->setsock(sock);
newgame->setismyround(ismyround);
Point winsize=Director::sharedDirector()->getWinSize();
newgame->setScale(winsize.y/defaultwinsize);

auto director = Director::getInstance();
auto scene = Scene::create();
scene->addChild(newgame);
director->pushScene(scene);

return true;
}
void NETWorkScene::update_quest()
{
if(upq.end())
{
return ;
}
if(!upq.push())
{
return;
}

if(SOCKET_ERROR==sock->msend((char *)&upq.quest,sizeof(upq.quest)))
{
CCLOG("socket error");
}
return;
}
void NETWorkScene::update(float delta)
{
//CCLOG("JB");
update_quest();

rlen=sock->mrecv(rdata);
if(rlen>0)
{
gamequest *gqh=(gamequest *)rdata;
CCLOG("%d: %d %02x %d\n",rlen,gqh->id,gqh->type,gqh->data);
if(gqh->type==NEWID)
{
gameid=gqh->data;
showgameid();
}
else if(gqh->type==NEWGAME)
{
gameid2=gqh->id;
if(gqh->data==NEWGAME_ISFIRST)
{
newgamestart(true);
}
else if(gqh->data==NEWGAME_ISSEC)
{
newgamestart(false);
}
else
{
CCLOG("findplayer fail");
}
}
}
else
{
//CCLOG("no message");
}
}

遊戲客戶端就ok了。

7、平臺移植:

整個專案搞定了就是爽哈,平臺移植便是非常輕鬆的事情,只要自己寫的程式碼沒作死,用特定系統或編譯器的api或是語法與庫,平臺移植就相當得快速。尤其是cocos2dx引擎,早已把移植的工作全都準備好了,只需要自己調調錯即可(回想起了以前自己一個人把c++往android上交叉編譯,叫那個苦啊)。

控制檯傻瓜編譯:

編譯成功。

用手機開啟遊戲客戶端,獲取到的id為5。(聯想P780,你值得信賴的充電寶手機)

手機與客戶端實現網路遊戲對局。

哈哈,手機也能和電腦一起聯網玩遊戲了。

這次做的這套五子棋網路遊戲還有很多欠缺的東西,客戶端還缺乏一定的容錯能力,使用者體驗也不夠人性化。在網路方面,通訊的方式並不適合時效性要求較高的遊戲,像一些及時對戰遊戲,請求/回覆的方式需要很頻繁的請求才能保證時效。這樣也沒錯,糟糕的網路環境也不能用來玩這些遊戲。自己對自己的美工挺滿意的,嘿(哪裡有美工啊?這個圖片都算不上好不好)。

總的來說,這是一次很棒的開發經歷,希望畢業以後也能有這樣的閒功夫,去做自己真正想做的。原始碼下載

相關文章