新聞中心
閑來無事,因自己想要在服務(wù)器開發(fā)方面進(jìn)行更深入的學(xué)習(xí),積累更豐富的經(jīng)驗(yàn)。決定寫一套網(wǎng)絡(luò)游戲的c/s。

昌圖網(wǎng)站建設(shè)公司創(chuàng)新互聯(lián),昌圖網(wǎng)站設(shè)計(jì)制作,有大型網(wǎng)站制作公司豐富經(jīng)驗(yàn)。已為昌圖1000多家提供企業(yè)網(wǎng)站建設(shè)服務(wù)。企業(yè)網(wǎng)站搭建\成都外貿(mào)網(wǎng)站制作要多少錢,請找那個售后服務(wù)好的昌圖做網(wǎng)站的公司定做!
因?yàn)橹饕康氖欠?wù)器的開發(fā),因此游戲我選用規(guī)則較為簡單、畫面特效沒有要求的回合制游戲:五子棋。我曾經(jīng)在剛接觸編程的時(shí)候自己在控制臺 下做過這個游戲,當(dāng)時(shí)寫的ai特nb我自己根本下不贏他。確定是制作五子棋了, 但是還要滿足跨平臺的特性,畢竟移動互聯(lián)時(shí)代,得終端者得天下。游戲做成全平臺才能更好的將各種玩家聚集在一起??缙脚_?b/s是人們通常會第一個想到的 跨平臺方式,的確現(xiàn)在市面上有很多基于b/s的頁游,大部分使用的是flash作為游戲引擎。但手機(jī)上很少有人使用瀏覽器玩游戲。(其實(shí)根本不會 flash,html也爛得很,曾經(jīng)給別人用php做的數(shù)據(jù)管理網(wǎng)站根本就沒有像樣的界面)于是選擇了c++的跨平臺游戲引擎cocos2dx,這引擎簡 單好用,而且因?yàn)槭莄++作為游戲邏輯,移植特方便,以前也用過這個引擎(某比賽)。最終選用的版本是cocos2d-x 3.4。
既然是網(wǎng)絡(luò)游戲的服務(wù)器,那么就得高效,而且是在linux下,因此我選epoll模型進(jìn)行服務(wù)端的開發(fā),epoll的部分寫在這篇文章里:epoll模型的理解與封裝實(shí)現(xiàn),使用的linux系統(tǒng)為CENT OS 6.4,內(nèi)核為linux2.6。
關(guān)于游戲開發(fā)步驟的思考:
按照自己以前習(xí)慣的套路來說,通信方式與協(xié)議的設(shè)計(jì)應(yīng)該是放在首位的,然后是服務(wù)器、再到客戶端(沒有美工)。
而自己以前曾經(jīng)玩到很多的單機(jī)游戲,更新版本后,游戲便增加了網(wǎng)絡(luò)游戲功能。這似乎說明了很多游戲與網(wǎng)絡(luò)協(xié)議之間是相互獨(dú)立的。甚至網(wǎng)絡(luò)協(xié)議是根據(jù)實(shí)際的游戲邏輯設(shè)計(jì)的,而不是游戲根據(jù)協(xié)議來設(shè)計(jì)自身的邏輯。
最終決定先把單機(jī)的版本做出來。于是制定了如下的開發(fā)流程:
1、游戲的算法與數(shù)據(jù)結(jié)構(gòu)設(shè)計(jì)與實(shí)現(xiàn)
2、游戲交互設(shè)計(jì)與實(shí)現(xiàn)
3、單機(jī)游戲的實(shí)現(xiàn)
4、游戲通信協(xié)議設(shè)計(jì)
5、服務(wù)器實(shí)現(xiàn)(不可忽略掉的重點(diǎn),自己寫游戲的目的)
6、網(wǎng)絡(luò)游戲功能實(shí)現(xiàn)
7、平臺移植
1、游戲的算法與數(shù)據(jù)結(jié)構(gòu)設(shè)計(jì)與實(shí)現(xiàn):
五子棋這個游戲是一個二維平面上的游戲,我們將棋盤看做一個數(shù)組,每一個格子的狀態(tài)分為兩種:沒棋和有棋,有棋因不同玩家而區(qū)別(數(shù)量不限,可直接作為多人多子棋的游戲基類)
代碼:
- //Chess.h
- #ifndef _CHESS_H_
- #define _CHESS_H_
- #include "cocos2d.h"
- USING_NS_CC;
- //下棋坐標(biāo)狀態(tài)結(jié)構(gòu)體
- struct Chesspos
- {
- int x,y;
- int player;//該步數(shù)所屬玩家
- Chesspos(){};
- Chesspos(int px,int py,int pp)
- {
- x=px;
- y=py;
- player=pp;
- }
- };
- class Chessway
- {
- Chesspos *way;//路徑數(shù)組
- int totallen;//總長度
- int len;//當(dāng)前步數(shù)
- public:
- Chessway(int totalnum);
- ~Chessway(void);
- void setempty();
- bool addway(int x,int y,int player);//添加步數(shù)
- 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);//獲取游戲狀態(tài)
- bool checklen(int x,int y);
- int checkwin();//判斷游戲是否結(jié)束并返回勝利玩家
- };
- #endif //_CHESS_H_
檢測勝利的邏輯很簡單:找到一個下有棋的位置,檢查這個位置下、右、左下、右下是否有連續(xù)相等的5個棋,即為游戲勝利。游戲一旦勝利是不可以繼續(xù)下棋的,所以只會有一個玩家勝利。下面給出判斷代碼:
- //Chess.cpp
- //勝利檢測代碼
- bool Chess::checklen(int x,int y)
- {
- for(int i=1;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
- {
- 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
- {
- 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
- {
- 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
- {
- for(int j=0;j
- {
- if(board[i][j])
- {
- if(checklen(i,j))
- {
- isgameend=true;
- return board[i][j];
- }
- }
- }
- }
- return 0;
- }
#p#
2、游戲交互設(shè)計(jì)與實(shí)現(xiàn)
涉及到游戲交互,這里就要使用到游戲引擎了。首先需要把游戲的一些圖片資源大致搞定,這里用畫圖這畫了幾個不堪入目的圖片資源:
別看這畫的丑,我可是用鼠標(biāo)和window自帶的畫圖畫出來的,到時(shí)候在游戲中看起來是毫無違和感的(筆者小學(xué)就會畫H漫了)。
這里就要用到cocos2dx的東西了。首先為每一個下棋的格子設(shè)計(jì)一個個塊狀的節(jié)點(diǎn),然后設(shè)計(jì)游戲主體布景層:
- class ChessNode:public Node
class ChessMain:public Layer
作為游戲棋盤,每一個格子的形態(tài)都是一樣的,我只需要將它們拼接成矩陣就成了一個完整的棋盤。因此在游戲布景層里,我開了一個Vector 的ChessNode,將其依次緊湊地排列在屏幕上。在游戲初始狀態(tài)時(shí),chess_1.png、chess_2.png是不會顯示的,如圖(截圖我直接 使用現(xiàn)成游戲的截圖):
這樣的棋盤看起來是不是很沒有違和感?
當(dāng)下棋后,就可以把對應(yīng)的棋圖顯示出來:
后面發(fā)現(xiàn)好像真正的下棋是下在十字交叉處的。。
這部分的注意事項(xiàng)主要就在于觸摸檢測與棋盤屏幕大小。觸摸的話計(jì)算相對棋盤布景層的坐標(biāo)可以得出下棋的位置。棋盤就以靜態(tài)值480px為標(biāo)準(zhǔn),在其他地方調(diào)用的時(shí)候縮放即可。
- #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
chesspicarr; - Sprite * basepic;
- };
- class ChessMain:public Layer
- {
- public:
- Chess *chessdata;
- Vector
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
- {
- 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();
- //默認(rèn)值棋盤
- chessdata=new Chess(15,15);
- for(int i=0;i
w;i++) - {
- for(int j=0;j
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
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
w;i++) - {
- for(int j=0;j
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);
- }
- */
這里的觸摸函數(shù)會由以后ChessMain的子類重寫。
#p#
3、單機(jī)游戲的實(shí)現(xiàn)
單機(jī)游戲,只需寫好對手的AI邏輯即可。幸好是五子棋不是圍棋,AI很好寫,能很快計(jì)算出必勝態(tài)。由于自己主要目的是寫網(wǎng)絡(luò)端。因此我把單機(jī)功能實(shí)現(xiàn)后并沒有寫AI,把接口留著的,只接了一個隨機(jī)函數(shù),等以后有閑情把AI邏輯加上。
總的來說這部分就是加上了進(jìn)入游戲前的菜單以及單機(jī)游戲的選項(xiàng)和游戲結(jié)束的對話框:
- #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
- {
- 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();
- //默認(rèn)值棋盤
- chessdata=new Chess(15,15);
- for(int i=0;i
w;i++) - {
- for(int j=0;j
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
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
w;i++) - {
- for(int j=0;j
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);
- }
- */
現(xiàn)在一個能玩的游戲已經(jīng)完成,接下來是重點(diǎn)的網(wǎng)絡(luò)部分。
#p#
4、游戲通信協(xié)議設(shè)計(jì)
因?yàn)槭荘C、手機(jī)都能玩的游戲,考慮到糟糕的手機(jī)網(wǎng)絡(luò)環(huán)境,通信采用客戶端單方發(fā)起請求,服務(wù)器回復(fù)的方式,使服務(wù)器不用考慮確保手機(jī)信號不好或IP變更的情況,類似于web方式。
游戲沒有設(shè)計(jì)固定的用戶,采用的是游戲每次向服務(wù)器申請一個游戲ID,使用這個游戲ID在互聯(lián)網(wǎng)上和其他用戶對戰(zhàn)。于是協(xié)議報(bào)文設(shè)計(jì)了兩種:普通請求/回復(fù)報(bào)文gamequest、游戲數(shù)據(jù)報(bào)文nextquest。
- #include
- #include
- #include
- #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;//游戲數(shù)據(jù)校驗(yàn)
- short stepno;
- };
NEWID:申請一個新的游戲ID的請求與回復(fù)
NEWGAME:申請開始游戲的請求與回復(fù)
NEXTSTEP:更新游戲?qū)謹(jǐn)?shù)據(jù)的請求與回復(fù)
GETNEXSTEP:獲取游戲?qū)謹(jǐn)?shù)據(jù)的請求與回復(fù)
GAMEEND:終止或結(jié)束游戲的請求
關(guān)于游戲請求與游戲?qū)謺r(shí)的通信,因?yàn)椴捎玫氖钦埱蠹踊貜?fù)的方式,服務(wù)器不能主動通知客戶端有新的游戲開始或是對手已經(jīng)喜下了下一步棋,因 此需要客戶端主動向服務(wù)器獲取相應(yīng)的信息。于是這部分被設(shè)計(jì)為客戶端定時(shí)向服務(wù)器發(fā)送更新數(shù)據(jù)的請求,服務(wù)器一旦接收到請求,就把通過該請求的TCP連接 發(fā)回去。這樣雖然增加了網(wǎng)絡(luò)的流量,但為了數(shù)據(jù)的穩(wěn)定性必須做出犧牲。好的是該協(xié)議報(bào)文很小,而且因?yàn)槭菍钟螒?,就算有幾萬人同時(shí)在玩,實(shí)際單位時(shí)間的 數(shù)據(jù)量也不會太多,最重要的是在處理并發(fā)數(shù)據(jù)的情況。
#p#
5、服務(wù)器實(shí)現(xiàn):
這是最重要最核心的部分。一個高效、穩(wěn)定的游戲服務(wù)器程序直接決定了游戲的體驗(yàn)。在實(shí)際的游戲服務(wù)器開 發(fā)中,游戲邏輯與網(wǎng)絡(luò)通信邏輯可能分工由不同的人員開發(fā)。因此,游戲邏輯與網(wǎng)絡(luò)通信邏輯應(yīng)在保證效率的情況下盡可能地實(shí)現(xiàn)低耦合。我這里雖然是獨(dú)立開發(fā) 的,是因?yàn)橛螒虻倪壿嫼芎唵?,但如果比如去開發(fā)一個像GTAOL這樣的游戲服務(wù)器,本來做網(wǎng)絡(luò)通信的人想要做出GTA的游戲邏輯那就相當(dāng)?shù)乩щy,需要寫處 理世界、物體、角色,還要和游戲端的邏輯一致,累成狗狗。
所以說游戲的邏輯與網(wǎng)絡(luò)的通信需要盡可能地獨(dú)立,就這個五子棋服務(wù)器而言,網(wǎng)絡(luò)通信端使用PPC、select、epoll都和游戲邏輯無 關(guān),只要能接收分類并交給游戲邏輯處理,并將游戲邏輯處理好的數(shù)據(jù)發(fā)出即可。該服務(wù)器選用的epoll實(shí)現(xiàn)的,因篇幅原因,網(wǎng)絡(luò)通信部分已經(jīng)在這篇文章中 說明清楚:epoll模型的理解封裝與應(yīng)用。
關(guān)于服務(wù)器的游戲邏輯,首先看看我們的服務(wù)器要做哪些事情:
1、用戶游戲ID的申請與管理
2、對局?jǐn)?shù)據(jù)的處理與管理
大致就以上這兩種事情。但是因?yàn)橛螒虻目蛻舳藬?shù)量很多,不同的客戶端之間進(jìn)行對局,必須要清晰地處理與管理這些數(shù)據(jù)。我這里建立了一個idpool,用于id的儲存于申請,以防發(fā)生錯誤給用戶分配無效或是重復(fù)的id。
對局?jǐn)?shù)據(jù)的處理與管理:
在兩個用戶都有id的情況下,雙方都能申請進(jìn)行游戲。這是服務(wù)端要做的就是匹配好這些用戶并通知這些用戶開始游戲。為方便說明,我先把代碼粘上來:
- #ifndef _GAME_H_
- #define _GAME_H_
- #include
- #include
- #include
- #include
- #include
- #include
- #include "ssock.h"
- #include "gameprotocol.h"
- using namespace std;
- #define idpoollength 1000
- #define datapoollength 50
- //鏈?zhǔn)絀Dpool
- class idpool
- {
- list
ids; - public:
- idpool()
- {
- for(int i=1;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
- {
- with[i]=i;
- }
- }
- bool ispair(unsigned int x1)
- {
- return with[x1]!=x1&&with[x1]!=0;
- }
- //設(shè)置為該id等待匹配
- void setwait(unsigned int x1)
- {
- with[x1]=0;
- }
- //自動匹配函數(shù)
- bool makepair(unsigned int x1)
- {
- for(int i=1;i
- {
- if(with[i]==0&&x1!=i)
- {
- setp2p(x1,i);
- return true;
- }
- }
- return false;
- }
- //設(shè)置兩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;
- };
- //對于下棋狀態(tài)類
- class
網(wǎng)站名稱:一套跨平臺五子棋網(wǎng)游的開發(fā)經(jīng)歷
文章出自:http://www.dlmjj.cn/article/codespe.html


咨詢
建站咨詢
