定幀帶來的問題
游戲的心跳,往往因當(dāng)前一幀運算量的不同擎淤,而有不同的delta時間奢啥。但是定幀則要求每間隔固定時間執(zhí)行一次運算。
我們對于定幀的常規(guī)實現(xiàn)之一如下:
// 邏輯定幀的時長
float logicFrameStep;
float timeCount;
// 顯示幀的心跳函數(shù)
void Update(float deltaTime)
{
timeCount += deltaTime;
while (timeCount >= logicFrameStep)
{
// 邏輯心跳
LogicTick();
timeCount -= logicFrameStep;
}
}
通過此種方式嘴拢,可以實現(xiàn)邏輯定幀的實現(xiàn)桩盲。
但是,當(dāng)游戲需要平滑顯示而邏輯卻需要定幀運算時席吴,矛盾便產(chǎn)生了赌结。由于邏輯幀定時長執(zhí)行,而現(xiàn)實幀不定時長孝冒,所以在同一顯示幀中可能會運行n個邏輯幀柬姚,且n個邏輯幀的總時長不一定等于該顯示幀的時長。
雖然定幀的方式可以確保每一邏輯幀運算后庄涡,所有數(shù)據(jù)都是正確的(起碼站在邏輯的角度是這樣)量承,但是根據(jù)上圖可以發(fā)現(xiàn),顯示幀是會存在時間富余的(邏輯幀3在顯示幀1內(nèi)是沒有執(zhí)行完的)穴店。
因此從顯示的角度看撕捍,顯示幀1只進行了2幀邏輯,而顯示幀2則進行了4幀邏輯迹鹅,雖然顯示幀1與2之間的時間比為5:7卦洽。正是因為顯示幀內(nèi) 邏輯定幀的比值 與 時間的比值 不同贞言,從而導(dǎo)致了顯示層的一個大問題 —— 抖動斜棚。
顯示平滑的處理
針對抖動,筆者曾嘗試用數(shù)學(xué)的方式進行矯正,但是結(jié)果證明效果并不理想弟蚀,運算量提升不說蚤霞,效率也不太高(使用的是最小二乘法义钉,但是它顯然不太可以勝任我們的需求)昧绣。
筆者也曾使用變速的方法讓移動平滑,即當(dāng)前數(shù)據(jù)離目標(biāo)數(shù)據(jù)越遠(yuǎn)捶闸,則以更高速度接近夜畴,否則接近速度降低税灌。但是這樣一來會產(chǎn)生與邏輯相比失真的問題洛勉,且速度的變動公式會很大程度影響體驗粘秆,因而該方案也被我們的項目拋棄了(這種方案是我從《夢幻西游》的攝像機移動上學(xué)來的)翻擒。
后來筆者意識到自己把簡單問題復(fù)雜化了巩趁。其實問題的核心就是數(shù)值要在指定事件內(nèi)變化指定大小炉菲。既然抖動是當(dāng)前幀的顯示與邏輯不匹配造成的拍霜,那么讓顯示延遲個1-2幀嘱丢,不就有可以修復(fù)這種抖動了嗎,畢竟延遲顯示1-2幀還不太會影響用戶體驗(我不信《守望先鋒》里早這么1幀你就會變成神槍手)祠饺。
核心代碼如下:
// 經(jīng)過平滑后的結(jié)果值
public double Value;
// 插入新值
// duration代表邏輯幀時長
// target代表這一邏輯幀的目標(biāo)值
public void Insert(float duration, double target)
{
__waitSeconds += duration;
__targetValue = target;
}
// 核心心跳
// 利用__waitSeconds計算當(dāng)前顯示幀對應(yīng)的值Value
public void Tick(float delta)
{
if (__waitSeconds <= delta)
{
Value = __targetValue;
return;
}
double p = delta / __waitSeconds;
Value = Value * (1 - p) + __targetValue * p;
__waitSeconds -= delta;
}
每個顯示幀調(diào)用Insert
方法收集數(shù)據(jù)越驻。而每個顯示幀則調(diào)用Tick
方法計算平滑后的結(jié)果值Value
。
結(jié)尾
最終經(jīng)過測試道偷,這種方法實現(xiàn)簡單缀旁,利用1顯示幀的緩沖達到了符合項目的平滑需求。所以記錄下來勺鸦,為自己曾經(jīng)走過的彎路默哀诵棵。
希望下次碰到問題的時候,不會把自己帶入太深的彎路 :P祝旷。