使用cocos2dx制作帧同步游戏
发表于2018-04-26
重构了之前的游戏代码,寻思把游戏做成可以网络联机的游戏,于是百度各种资源,想学习一下。学习资料倒是很多,最终选择使用跟 皇室战争 一样的同步方式,进行帧同步。主要记录一下在使用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