MoleServer游戏服务器框架使用教程(三)
在这篇文章中,我们将以我们提供的游戏为例,详细讲解下整个游戏的开发过程,首先我们的客户端是基于cocos2dx-js 3.6版本开发而成,当然你也可以使用其它游戏引擎来做这件事,我们这里主要讲讲整个游戏客户端和服务器是如何交互的。
用IDE打开websocket_demo,在这里,我们的游戏一启动就开始去连接账号服务器了:
socket = new WebSocket(host);
socket.onopen = function(){
socket.send('100');
last_health = new Date();
clearInterval(keepalivetimer);
keepalivetimer = setInterval( function(){keepalive(socket)},1000);
socket.onopen = function(){
socket.send('100');
last_health = new Date();
clearInterval(keepalivetimer);
keepalivetimer = setInterval( function(){keepalive(socket)},1000);
连接成功后开启了一个定时器,这个定时器每秒执行一次,干嘛呢,代码如下:
function keepalive(ws,type=0) {
var time = new Date();
var curtime = last_health.getTime();
if(type == 1)
curtime = last_health_game.getTime();
if(time.getTime() - curtime > health_timeout)
{
if(type == 0)
clearInterval(keepalivetimer);
else
clearInterval(keepalivetimer_game);
}
else
{
if (ws.bufferedAmount == 0) {
ws.send('100');
last_health = time;
last_health_game = time;
}
}
};
var time = new Date();
var curtime = last_health.getTime();
if(type == 1)
curtime = last_health_game.getTime();
if(time.getTime() - curtime > health_timeout)
{
if(type == 0)
clearInterval(keepalivetimer);
else
clearInterval(keepalivetimer_game);
}
else
{
if (ws.bufferedAmount == 0) {
ws.send('100');
last_health = time;
last_health_game = time;
}
}
};
主要看ws_send(‘100’),这是一个心跳消息,基于TCP/IP协议的连接都要靠心跳来维持长连接,但我们游戏框架中已经在内部处理了这个消息,消息号就是100,客户端启动之后,需要每秒发送一次,如果服务器在5秒后没有收到任何心跳消息,那就断定这个客户端已经离开了。关于框架内部如何处理这个心跳消息,可以具体看看molenet网络库。
到这里,我们的游戏客户端的连接就稳定的建立了,然后显示登录框,可以登录,注册,接微信什么的。
输入账号密码后发送下面的消息到账号服务器注册:
var row1 = {};
row1.MsgId = 700;
row1.UserName = this._boxusername.getString();
row1.UserPW = hex_md5(this._boxuserpassword.getString());
socket.send(JSON.stringify(row1));
row1.MsgId = 700;
row1.UserName = this._boxusername.getString();
row1.UserPW = hex_md5(this._boxuserpassword.getString());
socket.send(JSON.stringify(row1));
关于框架所有用到的消息定义都在moleserver/include/Common/defines.h里面,我们具体来看看700有那些消息:
#define IDD_MESSAGE_USER_REGISTER 700 // 用户注册
#define IDD_MESSAGE_USER_REGISTER_SUCCESS 701 // 用户注册成功
#define IDD_MESSAGE_USER_REGISTER_FAIL 702 // 用户注册失败
#define IDD_MESSAGE_SUPER_BIG_MSG 703 // 广播消息
游戏客户端是如何处理这些消息的呢,代码如下:
case 700:
{
switch(obj.MsgSubId)
{
case 701:
{
var loginlayer = new MyMessageBoxLayer();
self.addChild(loginlayer,8);
loginlayer.init("注册成功!");
m_registerLayer.setVisible(false);
MyMainLoginlayer.setVisible(true);
}
break;
case 702:
{
var loginlayer = new MyMessageBoxLayer();
self.addChild(loginlayer,8);
loginlayer.init("注册失败,请检查您的用户名和密码!");
}
break;
default:
break;
}
}
break;
{
switch(obj.MsgSubId)
{
case 701:
{
var loginlayer = new MyMessageBoxLayer();
self.addChild(loginlayer,8);
loginlayer.init("注册成功!");
m_registerLayer.setVisible(false);
MyMainLoginlayer.setVisible(true);
}
break;
case 702:
{
var loginlayer = new MyMessageBoxLayer();
self.addChild(loginlayer,8);
loginlayer.init("注册失败,请检查您的用户名和密码!");
}
break;
default:
break;
}
}
break;
用户注册成功以后,就可以开始进行登录验证了,代码如下:
var row1 = {};
row1.MsgId = 400;
row1.username = m_userloginname;
row1.userpwd = m_userloginpassword;
row1.machinecode = 'html5';
socket.send(JSON.stringify(row1));
row1.MsgId = 400;
row1.username = m_userloginname;
row1.userpwd = m_userloginpassword;
row1.machinecode = 'html5';
socket.send(JSON.stringify(row1));
我们来看看关于400,有哪些消息:
#define IDD_MESSAGE_CENTER_LOGIN 400 // 用户登录消息
#define IDD_MESSAGE_CENTER_LOGIN_SUCESS 401 // 用户登录成功
#define IDD_MESSAGE_CENTER_LOGIN_FAIL 402 // 用户登录失败
400的返回消息很多,我们这里不一一展示,我们来看看游戏客户端是如何处理的,代码较多,我们只截取验证成功后的代码:
case 401:
{
isLoginSuccuss = true;
myselfUserId = obj.UserId;
m_myselfusermoney = obj.money;
m_myselftempusermoney = m_myselfusermoney;
self.MyUserName.setString(m_userloginname);
self.MyUserMoney.setString(m_myselfusermoney);
//MyMainLoginlayer.removeAllChildrenWithCleanup(true);
MyMainLoginlayer.removeFromParent();
self.MyUserName.setVisible(true);
self.MyUserMoney.setVisible(true);
//获取服务器列表
var row1 = {};
row1.MsgId = 800;
socket.send(JSON.stringify(row1));
// if(m_isplaymusic) {
// cc.audioEngine.playMusic(soud_01, true);
// }
}
break;
{
isLoginSuccuss = true;
myselfUserId = obj.UserId;
m_myselfusermoney = obj.money;
m_myselftempusermoney = m_myselfusermoney;
self.MyUserName.setString(m_userloginname);
self.MyUserMoney.setString(m_myselfusermoney);
//MyMainLoginlayer.removeAllChildrenWithCleanup(true);
MyMainLoginlayer.removeFromParent();
self.MyUserName.setVisible(true);
self.MyUserMoney.setVisible(true);
//获取服务器列表
var row1 = {};
row1.MsgId = 800;
socket.send(JSON.stringify(row1));
// if(m_isplaymusic) {
// cc.audioEngine.playMusic(soud_01, true);
// }
}
break;
验证成功后,我们开始获取所有的游戏服务器信息,关于800的消息如下:
#define IDD_MESSAGE_GET_GAMESERVER 800 // 得到游戏服务器列表
#define IDD_MESSAGE_GET_GAMEINFO 801 // 得到游戏信息列表
#define IDD_MESSAGE_GET_GAMEINFO_SUCCESS 802 // 得到游戏信息列表成功
#define IDD_MESSAGE_GET_GAMEINFO_FAIL 803 // 得到游戏信息列表失败
游戏客户端处理如下:
case 800:
{
var MsgSubId = obj.MsgSubId;
if(MsgSubId == 803)
{
var loginlayer = new MyMessageBoxLayer();
self.addChild(loginlayer,8);
loginlayer.init("获取服务器失败,请稍后再试!");
}
else
{
var RoomCount = obj.RoomCount;
console.info("start."+RoomCount);
m_isLoginSuccess = true;
m_gameserver = "ws://"+obj.Room[0].serverip+":"+obj.Room[0].serverport;
//socket.onclose();
console.info("start.");
gameserversocket = new WebSocket(m_gameserver);
gameserversocket.onopen = function(){
gameserversocket.send('100');
last_health_game = new Date();
clearInterval(keepalivetimer_game);
keepalivetimer_game = setInterval( function(){keepalive(gameserversocket,1)},1000);
console.info("connned.");
}
{
var MsgSubId = obj.MsgSubId;
if(MsgSubId == 803)
{
var loginlayer = new MyMessageBoxLayer();
self.addChild(loginlayer,8);
loginlayer.init("获取服务器失败,请稍后再试!");
}
else
{
var RoomCount = obj.RoomCount;
console.info("start."+RoomCount);
m_isLoginSuccess = true;
m_gameserver = "ws://"+obj.Room[0].serverip+":"+obj.Room[0].serverport;
//socket.onclose();
console.info("start.");
gameserversocket = new WebSocket(m_gameserver);
gameserversocket.onopen = function(){
gameserversocket.send('100');
last_health_game = new Date();
clearInterval(keepalivetimer_game);
keepalivetimer_game = setInterval( function(){keepalive(gameserversocket,1)},1000);
console.info("connned.");
}
这里可以获取到你所配置的所有游戏服务器,我们这里只连接一台。游戏服务器连接成功后,和账号服务器一样,开启定时器,每隔一秒发送一条心跳信息,以维持和游戏服务器的长连接。
游戏服务器连接建立成功之后,就用开始用于账号服务器验证的账号和密码进行验证登录。
case 300:
{
if(objgame.MsgSubId == 301)
{
var row1 = {};
row1.MsgId = 500;
row1.UserName = m_userloginname;
row1.UserPW = m_userloginpassword;
row1.DeviceType=1;
gameserversocket.send(JSON.stringify(row1));
}
}
break;
{
if(objgame.MsgSubId == 301)
{
var row1 = {};
row1.MsgId = 500;
row1.UserName = m_userloginname;
row1.UserPW = m_userloginpassword;
row1.DeviceType=1;
gameserversocket.send(JSON.stringify(row1));
}
}
break;
游戏服务器账号验证的返回消息:
#define IDD_MESSAGE_GAME_LOGIN 500 // 用户登录消息
#define IDD_MESSAGE_GAME_LOGIN_SUCESS 501 // 用户登录成功
#define IDD_MESSAGE_GAME_LOGIN_FAIL 502 // 用户登录失败
#define IDD_MESSAGE_GAME_LOGIN_BUSY 503 // 系统忙,用户登录过于频繁
#define IDD_MESSAGE_GAME_LOGIN_EXIST 504 // 用户已经在系统中
#define IDD_MESSAGE_GAME_LOGIN_FULL 505 // 服务器满
#define IDD_MESSAGE_GAME_LOGIN_CLOSE_SERVER 506 // 关闭当前服务器
#define IDD_MESSAGE_GAME_LOGIN_MATCHING_NOSTART 507 // 比赛未开始
#define IDD_MESSAGE_GAME_LOGIN_MATCHING_NOSCROE 508 // 没有达到比赛场所要求的积分
#define IDD_MESSAGE_GAME_LOGIN_MATCHING_NOLEVEL 509 // 没有达到比赛场所要求的等级
#define IDD_MESSAGE_GAME_LOGIN_BANLOGIN 510 // 服务器被封
#define IDD_MESSAGE_GAME_LOGIN_USERBANLOGIN 511 // 玩家账号被封
游戏客户端在收到游戏服务器验证通过的消息后,就发送进入房间消息:
case 501:
{
if(objgame.ID == myselfUserId){
var row1 = {};
row1.MsgId = 900;
row1.MsgSubId=901;
row1.RoomIndex = -1;
row1.ChairIndex = -1;
row1.EnterPWd = "";
row1.Enterfirst=0;
row1.Entersecond=0;
gameserversocket.send(JSON.stringify(row1));
}
}
break;
{
if(objgame.ID == myselfUserId){
var row1 = {};
row1.MsgId = 900;
row1.MsgSubId=901;
row1.RoomIndex = -1;
row1.ChairIndex = -1;
row1.EnterPWd = "";
row1.Enterfirst=0;
row1.Entersecond=0;
gameserversocket.send(JSON.stringify(row1));
}
}
break;
当你发送这个消息后,如果成功后,就会触发moleserver/games/example/CServerServiceManager.cpp文件下的OnProcessEnterRoomMsg接口。游戏逻辑的接口如下:
/// 用于处理用户开始游戏开始消息
virtual void OnProcessPlayerGameStartMes();
/// 用于处理用户进入游戏房间后的消息
virtual void OnProcessPlayerRoomMes(int playerId,Json::Value &mes);
/// 处理用户进入房间消息
virtual void OnProcessEnterRoomMsg(int playerId);
/// 处理用户离开房间消息
virtual void OnProcessLeaveRoomMsg(int playerId);
/// 处理用户断线重连消息
virtual void OnProcessReEnterRoomMes(int playerId);
/// 处理用户断线消息
virtual void OnProcessOfflineRoomMes(int playerId);
/// 处理用户定时器消息
virtual void OnProcessTimerMsg(int timerId,int curTimer);
我们在来看看 OnProcessEnterRoomMsg接口中具体干了些什么:
if(m_gamisrunning == false)
{
LoadGameConfig();
m_cardrecord.push_back(m_CGameLogic.GetCardByColor(rand()%5));
m_g_GameRoom->StartTimer(IDD_TIMER_GAME_STARTING,3);
m_gamisrunning=true;
}
Player *pPlayer = m_g_GameRoom->GetPlayer(playerId);
std::map<uint32,tagJettons>::iterator iter = m_userjettonresult.find(pPlayer->GetChairIndex());
if(iter == m_userjettonresult.end())
m_userjettonresult[pPlayer->GetChairIndex()].clear();
Json::Value root;
root["MsgId"] = IDD_MESSAGE_ROOM;
root["MsgSubId"] = IDD_MESSAGE_ROOM_ENTERGAME;
root["gamestate"] = m_GameState;
root["gamepielement"] = (int32)m_GamePielement;
root["jvindex"] = m_gamejvcount;
Json::Value arrayObj;
for(int i=0;i<5;i++)
{
arrayObj[i] = (int)(m_jettonTrad[i]*10.0f);
}
root["GamePielement"] = arrayObj;
Json::Value arrayObj2;
for(int i=0;i<5;i++)
{
arrayObj2[i] = m_colorrecordcount[i];
}
root["colorrecordcount"] = arrayObj2;
Json::Value arrayObj3;
for(int i=0;i<(int)m_cardrecord.size();i++)
{
arrayObj3[i] = m_cardrecord[i];
}
root["cardrecourdcount"] = arrayObj3;
root["timexiazhu"] = m_timexiazhu;
root["timekaipai"] = m_timekaipai;
root["timejiesuan"] = m_timejiesuan;
//root["unitmoney"] = m_unitmoney;
m_g_GameRoom→SendTableMsg(playerId,root);
看代码,首先开启了游戏定时器,然后将游戏中的一些参数发送给了游戏客户端,游戏中的消息定义都存放在cdefines.h文件中,如下代码:
#define IDD_MESSAGE_ROOM_ENTERGAME IDD_MESSAGE_ROOM+1 // 进入房间消息
#define IDD_MESSAGE_ROOM_STARTJETTON IDD_MESSAGE_ROOM+2 // 开始下注消息
#define IDD_MESSAGE_ROOM_OPENCARD IDD_MESSAGE_ROOM+3 // 开始开牌消息
#define IDD_MESSAGE_ROOM_GAMEOVER IDD_MESSAGE_ROOM+4 // 游戏结束消息
#define IDD_MESSAGE_ROOM_JETTON IDD_MESSAGE_ROOM+5 // 游戏下注消息
#define IDD_MESSAGE_ROOM_CLEARJETTON IDD_MESSAGE_ROOM+6 // 清除下注消息
#define IDD_MESSAGE_ROOM_REENTERGAME IDD_MESSAGE_ROOM+7 // 重回房间消息
游戏消息都是以 IDD_MESSAGE_ROOM开始的, IDD_MESSAGE_ROOM为1000.
关于房间的接口,主要有以下几个:
/// 游戏结束时调用
virtual void GameEnd(bool isupdateuserdata=true) = 0;
/// 游戏开始是调用
virtual void GameStart(void) = 0;
/// 向指定的玩家发送旁观数据
virtual void SendLookOnMes(int index,Json::Value &msg) = 0;
/// 开始一个定时器
virtual bool StartTimer(int timerId,int space) = 0;
/// 关闭一个定时器
virtual void StopTimer(int id) = 0;
/// 关闭所有的定时器
virtual void StopAllTimer(void) = 0;
/// 写入用户积分
virtual bool WriteUserScore(int wChairID, int64 lScore, int64 lRevenue, enScoreKind ScoreKind,int64 pAgentmoney=0,bool isCumulativeResult=true,int64 pcurJetton=0,const char* pgametip="") = 0;
我们这个例子中主要的逻辑就在定时器接口中,一个玩家进入房间后,游戏定时器就打开了,然后整个游戏就是在游戏定时器中进行的。
if(timerId == IDD_TIMER_GAME_STARTING && curTimer <= 0)
{
m_g_GameRoom->StopTimer(IDD_TIMER_GAME_STARTING);
ClearJettonRecord();
m_resultCard = 0;
m_GameState = GAMESTATE_XIAZHU;
m_g_GameRoom->GameStart();
Json::Value root;
root["MsgId"] = IDD_MESSAGE_ROOM;
root["MsgSubId"] = IDD_MESSAGE_ROOM_STARTJETTON;
root["gamestate"] = m_GameState;
root["jvindex"] = m_gamejvcount;
m_g_GameRoom->SendTableMsg(INVALID_CHAIR,root);
m_g_GameRoom->StartTimer(IDD_TIMER_GAME_XIAZHU, m_timexiazhu);
}
else if(timerId == IDD_TIMER_GAME_XIAZHU && curTimer <= 0)
{
m_g_GameRoom->StopTimer(IDD_TIMER_GAME_XIAZHU);
m_resultCard = GetResultCard();
m_colorrecordcount[m_CGameLogic.GetCardColor(m_resultCard)] += 1;
m_GameState = GAMESTATE_KAIPAI;
Json::Value root;
root["MsgId"] = IDD_MESSAGE_ROOM;
root["MsgSubId"] = IDD_MESSAGE_ROOM_OPENCARD;
root["gamestate"] = m_GameState;
root["resultcard"] = m_resultCard;
m_g_GameRoom->SendTableMsg(INVALID_CHAIR,root);
m_g_GameRoom->StartTimer(IDD_TIMER_GAME_KAIPAI, m_timekaipai);
}
else if(timerId == IDD_TIMER_GAME_KAIPAI && curTimer <= 0)
{
m_g_GameRoom->StopTimer(IDD_TIMER_GAME_KAIPAI);
m_GameState = GAMESTATE_KONGXIAN;
TradGame();
m_cardrecord.push_back(m_resultCard);
if((int)m_cardrecord.size() >= 65)
{
m_cardrecord.clear();
m_gamejvcount+=1;
std::map<uint8,int>::iterator iter = m_colorrecordcount.begin();
for(;iter != m_colorrecordcount.end();++iter) (*iter).second = 0;
m_cardrecord.push_back(m_resultCard);
}
m_g_GameRoom->GameEnd();
m_g_GameRoom->StartTimer(IDD_TIMER_GAME_STARTING, m_timejiesuan);
}
这里代码有几个地方要说明一下:
1. SendTableMsg函数,第一个参数如果为具体某个玩家的椅子号,就是单独发送给某个玩家,如果为 INVALID_CHAIR就是发送给所有玩家;
2. StartTimer开始定时器,第一个参数是定时器ID,第二个参数是时间间隔,以秒为单位,但这个定时器接口是每秒都会执行的,这样做,是因为某些游戏需要检查每秒的东西。所以如果要走完你设置的时间间隔,就需要判断 curTimer为0,这样你所设置的才表示走完。
if(timerId == IDD_TIMER_GAME_KAIPAI && curTimer <= 0)
3.GameStart(),GameEnd()这两个函数必须成对使用,当你调用 GameStart后,整个游戏所有玩家的状态和房间的状态就被锁住了,只有调用 GameEnd才会解开所有玩家和房间的状态。比如当你因为异常或者非正常强制关闭服务器的时候,就会导致有些玩家被锁住在房间里,这时你就需要到网站后台:“玩家”-》“玩家管理”-》“玩家列表”-》“玩家解锁”来解锁所有玩家的状态。
我们在这个例子中处理的游戏逻辑非常简单,而且并没有处理机器人的逻辑,我们将在后面的章节中详细讲解机器人和代理方面的操作。
接下来我们看看游戏客户端大游戏逻辑处理代码:
case 1000:
{
console.info(objgame.MsgSubId);
switch(objgame.MsgSubId)
{
case 1001:
case 1007:
{
gamestate = objgame.gamestate;
gamepielement = objgame.gamepielement;
m_GameItemBeiLv = objgame.GamePielement;
var pjvhao = objgame.jvindex + 1;
m_colorrecordcount = objgame.colorrecordcount;
m_cardrecourdcount = objgame.cardrecourdcount;
m_timexiazhu = objgame.timexiazhu;
m_timekaipai = objgame.timekaipai;
m_timejiesuan = objgame.timejiesuan;
// m_myselfgamegonggaostr = objgame.gamegonggao;
{
console.info(objgame.MsgSubId);
switch(objgame.MsgSubId)
{
case 1001:
case 1007:
{
gamestate = objgame.gamestate;
gamepielement = objgame.gamepielement;
m_GameItemBeiLv = objgame.GamePielement;
var pjvhao = objgame.jvindex + 1;
m_colorrecordcount = objgame.colorrecordcount;
m_cardrecourdcount = objgame.cardrecourdcount;
m_timexiazhu = objgame.timexiazhu;
m_timekaipai = objgame.timekaipai;
m_timejiesuan = objgame.timejiesuan;
// m_myselfgamegonggaostr = objgame.gamegonggao;
我们这里由于篇幅所限,只贴部分代码,详细代码请具体看看代码。
当然,这个例子中只展示了整个游戏服务器框架的一些基础功能,框架还提供更多有趣并好玩的其它功能,需要你自己去发现了。
在下一篇教程中,我们将详细讲解游戏机器人和代理分销相关的东西。
欢迎加入QQ群交流:131296225
email:akinggw@126.com