使用cocos2dx制作帧同步游戏

发表于2018-04-26
评论1 763浏览

想免费获取内部独家PPT资料库?观看行业大牛直播?点击加入腾讯游戏学院游戏程序行业精英群

711501594
重构了之前的游戏代码,寻思把游戏做成可以网络联机的游戏,于是百度各种资源,想学习一下。学习资料倒是很多,最终选择使用跟 皇室战争 一样的同步方式,进行帧同步。主要记录一下在使用cocos2dx制作帧同步游戏时遇到的问题。

一、修改源码

由于使用的是cocos2dx引擎,看源码,在cocos2dx中帧的时间长短是动态的,这样可以出现动作补偿,让画面更加流畅。

但是这个样子没法进行帧同步,所以必须规定cocos2dx的帧的时间长度,这样在游戏当中的action时间就是一致的了。

查看源码在 CCDirector中:
voidDirector::drawScene()
if (!_paused)
    {
       _eventDispatcher->dispatchEvent(_eventBeforeUpdate);
       _scheduler->update(_deltaTime);
       _eventDispatcher->dispatchEvent(_eventAfterUpdate);
    }

可以看到这个_deltaTime
跟时间长度有关,修改源码函数
voidDirector::calculateDeltaTime()
   if (_nextDeltaTimeZero)
    {
       _deltaTime =0;
       _nextDeltaTimeZero =false;
    }
   else
    {
       //fix time
       _deltaTime =0.02f;
    }

把时间改为固定值,自己计算,这个也就是
 director->setAnimationInterval(1.0 /50);
这个数值。
这样,在游戏中相同的帧数,发生的事情就是一样的了。

二、自己的代码问题

1、经过测试上面的随机数种子方法的确可以达到同步效果,但是!
在不同平台不同机型上运行的时候,是不能同步的。
由于机型差异和内存差异,随机还是不同的,所以要自己写伪随机数!
#ifndef __MyRandom__
#define __MyRandom__
//random
float myRandom0_1();
void setMySeed(unsigned int seed);
unsigned int getRandomTime();
#endif // !__MyRandom__

#include "MyRandom.h"
#define RANDOMMAX 0xff
#define RANDOMA 25
#define RANDOMB 13
unsigned int randomTime = 0;
unsigned int randomSeed = 0;
float myRandom0_1() {
 randomTime++;
 randomSeed = (randomSeed*RANDOMA + RANDOMB) % RANDOMMAX;
 return float(randomSeed) / RANDOMMAX;
}
void setMySeed(unsigned int seed) {
 randomTime = 0;
 randomSeed = seed;
}
unsigned int getRandomTime()
{
 return randomTime;
}
一个简易的伪随机数算法,用来应对不是很平均的随机数产生

2、代码里面的变量一定要初始化!

三、进行实际时间同步

帧同步了,但是实际的时间上,肯定会有一个快一个慢,或者其他原因导致的时间差异。
这就需要在游戏中每隔一段时间检查一下目标与本地的时间差多少,如果对方比本地跑的快,本地就要加速,反之亦然。
这里,
Director::getInstance()->getScheduler()->setTimeScale(1.5f);
这个函数肯定不行,这个是更改帧的时间间隔来加速的。用了帧就没法同步了。

所以只能改
director->setAnimationInterval(1.0 / 50);

自己测试了一下,具体什么原因不清楚,更改这个东西的确能加速或者降速,但是会莫名其妙的被使用 随机数,估计是源码里面某个地方。

所以……继续查看源码,查看框架的循环,发现Application::setAnimationInterval(float interval);这个函数负责设置不同平台的循环时间。修改源码新增一个函数来调用这个函数。于是 实现了同步。

四、不同平台的坑

在win32下,只要更改这个函数就可以修改没帧的时间。但是在其他平台就不适合了。

通过查看源码,发现在iOS平台,帧率是固定的,每秒60。那个定时器之负责 每几帧来执行 框架的帧。所以只能是每帧,每两帧,每三帧。也就是1/60,1/30,1/20,这些帧率才能正常调整。所以在iOS平台,只能是走的快的降速。而不是走的慢的减速。

这里如果用cocos2dx引擎的帧减速 还是正常的,但是如果用加速 不同平台也会出现错误。所以要用加速的话还是要自己修改引擎。

五、指令的同步

指令只能是一方发起指令,预计在当前帧后执行。比如当前是20帧,指令就是预计在30帧执行。把这个指令发送给对方。

对方收到这个指令,判断指令时间是不是在自己的未来,如果是,就也加入到指令列表,等待执行。如果指令时间已经过去,就计算一个新的时间,发指令发回去,告诉对方更新指令。但是如果对方已经执行此命令……

这里使用stepbystep的同步方式进行帧同步,具体原理参考百度,记录一下自己的代码。最开始的时候并没有想用这个方式,只是想只要同步指令,那么游戏就肯定同步了,但是要同步指令就只有这个方法才能同步,要不就会陷入无限发送指令,或者同步失败。
#ifndef __GameDataControler__
#define __GameDataControler__
#define CHACKTIME 7
#include "../Socket/SocketManager.h"
#include "../GameMap/ConmandControler.h"
class GameDataControler
{
public:
 static GameDataControler* getInstance();
 //exit
 static void destroy();
 GameDataControler();
 ~GameDataControler();
 //
 void reset();
 //wait data
 void waitData();
 //net data
 void synData(unsigned int gameTime);
 //get fast tiem
 unsigned int getFastTime();
private:
 static GameDataControler* _g;
 //Send Chack
 void sendChack(unsigned int gameTime);
 //chack conmand
 void chackConmand(unsigned int gameTime);
 //get conmand
 void getConmand(unsigned int gameTime);
 void onRecv(const char* data, int count, unsigned int gameTime);
 //conmand list
 vector<ConmandData> m_conmandNewList;
 vector<ConmandData> m_conmandWaitForChackList;
 vector<ConmandData> m_conmandChackList;
 void onDisconnect();
 void onNewConnection();
 unsigned int m_gameFastTime;
 unsigned int m_chackTime;
 unsigned int m_getChack;
 unsigned int m_errorTime;
};
#endif // !__GameDataControler
#include "GameDataControler.h"
#include "../Utils/LogList.h"
#include "MyRandom.h"
#define ERRORTIME 3
GameDataControler* GameDataControler::_g = nullptr;
GameDataControler::GameDataControler()
{
 reset();
}
GameDataControler::~GameDataControler()
{
 SocketManager::getInstance()->exitSocket();
}
void GameDataControler::reset()
{
 setMySeed(99);
 m_errorTime = 0;
 m_getChack = CHACKTIME;
 m_chackTime = 0;
 m_gameFastTime = 0;
 m_conmandNewList.clear();
 m_conmandWaitForChackList.clear();
 m_conmandChackList.clear();
}
void GameDataControler::waitData()
{
 getConmand(m_errorTime);
}
void GameDataControler::synData(unsigned int gameTime)
{
 GameDataControler::getInstance()->getConmand(gameTime);
 if (m_chackTime == CHACKTIME)
 {
  m_chackTime = 0;
  GameDataControler::getInstance()->chackConmand(gameTime);
  GameDataControler::getInstance()->sendChack(gameTime);
  m_getChack = 0;
 }
 ++m_chackTime;
}
unsigned int GameDataControler::getFastTime()
{
 return m_gameFastTime;
}
void GameDataControler::sendChack(unsigned int gameTime)
{
 int nextchackTime = gameTime + CHACKTIME;
 ConmandData *data = new ConmandData[m_conmandNewList.size() + 1];
 data[0].chack = CHACK;
 data[0].time = gameTime;
 data[0].camp = m_conmandNewList.size();
 data[0].dataSize = sizeof(ConmandData);
 MLOG(String::createWithFormat("\n Random:%f,RandomTime:%d", myRandom0_1(), getRandomTime())->getCString());
 for (unsigned int i = 0; i < m_conmandNewList.size(); i++)
 {
  data[i + 1] = m_conmandNewList.at(i);
  //save chack time
  data[i + 1].camp = nextchackTime;
  data[i + 1].chack = DATAERROR;
 }
 SocketManager::getInstance()->sendMessage((const char*)data, sizeof(ConmandData)*(m_conmandNewList.size() + 1));
 delete[] data;
 //push conmand in chack list
 for (unsigned int i = 0; i < m_conmandNewList.size(); i++)
 {
  m_conmandWaitForChackList.push_back(m_conmandNewList.at(i));
 }
 m_conmandNewList.clear();
}
void GameDataControler::chackConmand(unsigned int gameTime)
{
 if (m_conmandChackList.size() != 0)
 {
  unsigned int idx = m_conmandChackList.size();
  if (m_conmandChackList.size()>m_conmandWaitForChackList.size())
  {
   idx = m_conmandWaitForChackList.size();
  }
  auto ite = m_conmandWaitForChackList.begin();
  auto ite2 = m_conmandChackList.begin();
  for (unsigned int i = 0; i < idx; i++)
  {
   if (ite2->camp <= gameTime)
   {
    if (ite->time == ite2->time)
    {
     if (ite->time < gameTime - m_chackTime + CHACKTIME)
     {
      ite->time = gameTime - m_chackTime + CHACKTIME + 1;
     }
     ConmandControler::getInstance()->gameConmand_addConmand((*ite));
     m_conmandWaitForChackList.erase(ite);
     m_conmandChackList.erase(ite2);
    }
    else
    {
     break;
    }
   }
  }
 }
 //error
 if (m_getChack<gameTime)
 {
  m_errorTime++;
  if (m_errorTime>ERRORTIME)
  {
   //pause wait
   m_errorTime = gameTime;
   CCGI()->gameConmand_wait();
  }
 }
}
void GameDataControler::getConmand(unsigned int gameTime)
{
 auto msg = SocketManager::getInstance()->getMessage();
 while (msg != nullptr)
 {
  switch (msg->getMsgType())
  {
  case NEW_CONNECTION:
   onNewConnection();
   break;
  case DISCONNECT:
   onDisconnect();
   break;
  case RECEIVE:
   onRecv((const char*)msg->getMsgData()->getBytes(), msg->getMsgData()->getSize(), gameTime);
   break;
  default:
   break;
  }
  CC_SAFE_DELETE(msg);
  msg = SocketManager::getInstance()->getMessage();
 }
}
void GameDataControler::onRecv(const char * data, int count, unsigned int gameTime)
{
 ConmandData* charaData = (ConmandData*)data;
 if (charaData->dataSize == sizeof(ConmandData))
 {
  string s;
  switch (charaData->chack)
  {
  case DATABACK:
   //error
   m_conmandNewList.push_back(*charaData);
   break;
  case DATAERROR:
   break;
  case DATANEW:
   //new conmand
   if (charaData->time>gameTime + DELAYFRAME)
   {
    charaData->chack = DATABACK;
    //send ok
    SocketManager::getInstance()->sendMessage(data, count);
    //ok
    m_conmandNewList.push_back(*charaData);
   }
   else {
    charaData->chack = DATABACK;
    //send error
    charaData->time = ConmandControler::getInstance()->getNextTime();
    SocketManager::getInstance()->sendMessage(data, count);
    m_conmandNewList.push_back(*charaData);
   }
   break;
  case CHACK:
   //speed
   /*
   #if (CC_TARGET_PLATFORM != CC_PLATFORM_WIN32)
#else//elif(CC_TARGET_PLATFORM == CC_PLATFORM_WIN32)
   if (charaData->time>gameTime)
   {
    m_gameFastTime = charaData->time;
    Director::getInstance()->resetAnimationInterval(1.0f / (50 + charaData->time - gameTime));
   }
#endif
   */
   if (charaData->time<gameTime)
   {
    //slow
    m_gameFastTime = gameTime + gameTime - charaData->time;
    Director::getInstance()->resetAnimationInterval(0.04f);
   }
   //chack conmand
   m_getChack = charaData->time + CHACKTIME;
   if (charaData->camp != 0)
   {
    for (size_t i = 0; i < charaData->camp; i++)
    {
     m_conmandChackList.push_back(charaData[i + 1]);
    }
   }
   if (m_errorTime>ERRORTIME)
   {
    //back
    CCGI()->gameConmand_resume();
   }
   m_errorTime = 0;
   break;
  default:
   break;
  }
 }
}
void GameDataControler::onDisconnect()
{
 //pause wait
 CCGI()->gameConmand_wait();
}
void GameDataControler::onNewConnection()
{
}
GameDataControler* GameDataControler::getInstance()
{
 if (_g == nullptr)
 {
  _g = new GameDataControler;
 }
 return _g;
}
void GameDataControler::destroy()
{
 if (_g != nullptr)
 {
  _g->reset();
  delete _g;
  _g = nullptr;
 }
}

指令同步类,放到update()里面执行,就可以就行指令同步了
void SceneGameNet::update(float dt){
 if (GameDataControler::getInstance()->getFastTime() == m_gameTime)
 {
  Director::getInstance()->resetAnimationInterval(0.02f);
 }
 GameDataControler::getInstance()->synData(m_gameTime);
 ConmandControler::getInstance()->gameConmand_enConmand(m_gameTime);
 ++m_gameTime;
}
参考资料:http://blog.csdn.net/zhanglongit/article/details/8553440
来自:https://blog.csdn.net/beking00700/article/details/54799938

原文链接

著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

如社区发表内容存在侵权行为,您可以点击这里查看侵权投诉指引

游戏学院公众号二维码
腾讯游戏学院
微信公众号

提供更专业的游戏知识学习平台