闲来无事,因自己想要在服务器开发方面进行更深入的学习,积累更丰富的经验。决定写一套网络游戏的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、游戏交互设计与实现

    涉及到游戏交互,这里就要使用到游戏引擎了。首先需要把游戏的一些图片资源大致搞定,这里用画图这画了几个不堪入目的图片资源:wKiom1V7l9uiN4iwAAHMhnNAdys050.jpg    别看这画的丑,我可是用鼠标和window自带的画图画出来的,到时候在游戏中看起来是毫无违和感的(笔者小学就会画H漫了)。

    这里就要用到cocos2dx的东西了。首先为每一个下棋的格子设计一个个块状的节点,然后设计游戏主体布景层:

class ChessNode:public Node
class ChessMain:public Layer

    作为游戏棋盘,每一个格子的形态都是一样的,我只需要将它们拼接成矩阵就成了一个完整的棋盘。因此在游戏布景层里,我开了一个Vector的ChessNode,将其依次紧凑地排列在屏幕上。在游戏初始状态时,chess_1.png、chess_2.png是不会显示的,如图(截图我直接使用现成游戏的截图):

wKioL1V7nOChCM7EAAM5FIcJZ1Q677.jpg

这样的棋盘看起来是不是很没有违和感?

当下棋后,就可以把对应的棋图显示出来:

wKiom1V7mzexs2LGAAPhmb7GBlc756.jpg

    后面发现好像真正的下棋是下在十字交叉处的。。

    这部分的注意事项主要就在于触摸检测与棋盘屏幕大小。触摸的话计算相对棋盘布景层的坐标可以得出下棋的位置。棋盘就以静态值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逻辑加上。

    总的来说这部分就是加上了进入游戏前的菜单以及单机游戏的选项和游戏结束的对话框:

    wKioL1V7ov7hB1VTAAD5lHH5qd8186.jpgwKioL1V7ow_xJcrxAARdcVtwNAk672.jpg    

#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_

    现在一个能玩的游戏已经完成,接下来是重点的网络部分。

    //等后文档的写好了在这里给出网络部分文章的地址以及源码文件,敬请期待。

    一套跨平台五子棋网游的开发经历(二)