閒來無事,因自己想要在服務器開發方面進行更深入的學習,積累更豐富的經驗。決定寫一套網絡游戲的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
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這樣的游戲服務器,本來做網絡通信的人想要做出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的情況下自動釋放前都有效。
圖中兩個客戶端分別分配到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,你值得信賴的充電寶手機)
手機與客戶端實現網絡游戲對局。
哈哈,手機也能和電腦一起聯網玩游戲了。
這次做的這套五子棋網絡游戲還有很多欠缺的東西,客戶端還缺乏一定的容錯能力,用戶體驗也不夠人性化。在網絡方面,通信的方式並不適合時效性要求較高的游戲,像一些及時對戰游戲,請求/回復的方式需要很頻繁的請求才能保證時效。這樣也沒錯,糟糕的網絡環境也不能用來玩這些游戲。自己對自己的美工挺滿意的,嘿(哪裡有美工啊?這個圖片都算不上好不好)。
總的來說,這是一次很棒的開發經歷,希望畢業以後也能有這樣的閒功夫,去做自己真正想做的。
源碼下載