首页 > 编程语言 > C/C++开发 > 一套跨平台五子棋网游的开发经历
2015
06-27

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

  闲来无事,因自己想要在服务器开发方面进行更深入的学习,积累更丰富的经验。决定写一套网络游戏的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这样的游戏服务器,本来做网络通信的人想要做出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,你值得信赖的充电宝手机)

  手机与客户端实现网络游戏对局。

  哈哈,手机也能和电脑一起联网玩游戏了。

  这次做的这套五子棋网络游戏还有很多欠缺的东西,客户端还缺乏一定的容错能力,用户体验也不够人性化。在网络方面,通信的方式并不适合时效性要求较高的游戏,像一些及时对战游戏,请求/回复的方式需要很频繁的请求才能保证时效。这样也没错,糟糕的网络环境也不能用来玩这些游戏。自己对自己的美工挺满意的,嘿(哪里有美工啊?这个图片都算不上好不好)。

  总的来说,这是一次很棒的开发经历,希望毕业以后也能有这样的闲功夫,去做自己真正想做的。

  源码下载

编程技巧