接上文http://wchrt.blog.51cto.com/8472636/1661524
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這樣的遊戲服務器,本來做網絡通信的人想要做出GTA的遊戲邏輯那就相當地困難,需要寫處理世界、物體、角色,還要和遊戲端的邏輯一致,累成狗狗。
所以說遊戲的邏輯與網絡的通信需要儘可能地獨立,就這個五子棋服務器而言,網絡通信端使用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的情況下自動釋放前都有效。
當客戶端分配到id後,就可以向服務器發起遊戲匹配請求NEWGAME。爲了防止匹配不到玩家,設置發送匹配請求最多隻維持一分鐘,在一分鐘結束後,客戶端向服務器發出停止匹配的請求。
當有兩個客戶端在這交叉的時段進行進行匹配,便可能匹配在一起開始遊戲。
遊戲匹配成功後,客戶端將收到服務器發過來的對局基礎信息,包括了對手id、先手還是後手。當遊戲開始後,先手的下棋然後將數據提交到服務器,又後手的更新數據,然後照這樣依次循環下去直到遊戲結束。
在遊戲結束時,贏的一方會顯示勝利,輸的顯示失敗,雙方都不再更新數據。退出對局後便能開始下繼續匹配遊戲。
遊戲客戶端需要注意的是對局數據的校驗還有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,你值得信賴的充電寶手機)
手機與客戶端實現網絡遊戲對局。
哈哈,手機也能和電腦一起聯網玩遊戲了。
這次做的這套五子棋網絡遊戲還有很多欠缺的東西,客戶端還缺乏一定的容錯能力,用戶體驗也不夠人性化。在網絡方面,通信的方式並不適合時效性要求較高的遊戲,像一些及時對戰遊戲,請求/回覆的方式需要很頻繁的請求才能保證時效。這樣也沒錯,糟糕的網絡環境也不能用來玩這些遊戲。自己對自己的美工挺滿意的,嘿(哪裏有美工啊?這個圖片都算不上好不好)。
總的來說,這是一次很棒的開發經歷,希望畢業以後也能有這樣的閒功夫,去做自己真正想做的。