使用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 =
}
else
{
//fix time
_deltaTime =0.02f;
}
..
把時間改為固定值,自己計算,這個也就是
director->setAnimationInterval(1.0 /50);
這個數值。
這樣,在遊戲中相同的幀數,發生的事情就是一樣的了。
二、自己的程式碼問題
1、在進行幀同步的時候,一定要統一隨機數種子
正在遊戲中為了每次產生的效果不同都要用
setSand(time(NULL));
來讓隨機數種子隨機。但是這裡不行,一定要統一種子,比如
setSand(999);
1、經過測試上面的隨機數種子方法的確可以達到同步效果,但是!!
在不同平臺不同機型上執行的時候,是不能同步的。
由於機型差異和記憶體差異,隨機還是不同的,所以要自己寫偽隨機數!!
#ifndef __MyRandom__
#define __MyRandom__
//random
float myRandom0_1();
void setMySeed(unsigned int seed);
unsigned int getRandomTime();
#endif // !__MyRandom__
#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、一定要在相同位置使用CCRANDOM0_1();
3、程式碼裡面的變數一定要初始化!!
4、粒子系統,一定要在相同位置使用,粒子系統會使用隨機數。
三、進行實際時間同步
幀同步了,但是實際的時間上,肯定會有一個快一個慢,或者其他原因導致的時間差異。
這就需要在遊戲中每隔一段時間檢查一下目標與本地的時間差多少,如果對方比本地跑的快,本地就要加速,反之亦然。
這裡,
Director::getInstance()->getScheduler()->setTimeScale(1.5f);
這個函式肯定不行,這個是更改幀的時間間隔來加速的。用了幀就沒法同步了。
所以只能改
director->setAnimationInterval(1.0 / 50);
自己測試了一下,具體什麼原因不清楚,更改這個東西的確能加速或者降速,但是會莫名其妙的被使用 隨機數,估計是原始碼裡面某個地方。
所以...繼續檢視原始碼
參考http://blog.csdn.net/zhanglongit/article/details/8553440
檢視框架的迴圈
發現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;
}