聲明:本文所用Unity版本為
2021.3.2f1c1
。Mac使用的是MacBook Pro (16-inch, 2019)
涩哟,CPU型號為2.3GHz 八核Intel Core i9
索赏。Windows所用CPU型號為11th Gen Intel(R) Core(TM) i7-11700K @ 3.60GHz
。
幀同步與時間
幀同步贴彼,游戲開發(fā)常見的同步方案潜腻。簡單來說就是讓各個客戶端的邏輯部分,按照固定的速度進行心跳器仗。在這里融涣,時間就是控制每個客戶端邏輯幀率的唯一變量。
在Unity開發(fā)中精钮,因為幀同步需要定幀威鹿,所以最常見的定幀的方式有兩種:使用Unity的FixUpdate
,或著自己管理時間轨香。
FixUpdate
只需要把它的DeltaTime
設(shè)置成所需要的時間即可忽你,我們著重說第二種:自己管理時間。
double time; // 時間累積
double interval; // 邏輯幀間隔
void Update(float delta)
{
time += delta;
while (time >= interval)
{
time -= interval;
LogicTick(); // 邏輯幀心跳
}
}
正如上面代碼展示臂容,我們通過累積時間科雳,每滿足一次邏輯幀的幀率時間,就進行一次邏輯心跳脓杉,并將累積時間減去一個邏輯幀間隔時間糟秘。這里函數(shù)的參數(shù),就可以傳入UnityEngine.Time.deltaTime
球散。
問題的出現(xiàn)
現(xiàn)在問題來了尿赚,當(dāng)我實際去運行的時候,發(fā)現(xiàn)同樣的一套代碼所寫的邏輯,mac端就是比windows端延遲感更明顯吼畏。
為了查明問題,我專門實時顯示了兩端與服務(wù)器的RTT嘁灯,結(jié)果發(fā)現(xiàn)RTT均在30ms以內(nèi)泻蚊,說明問題不在這里。
之后我才去監(jiān)測二者每次邏輯心跳所對應(yīng)的邏輯幀號與服務(wù)器幀號丑婿,結(jié)果大出意料:mac端的時間比windows端過得要慢性雄,而且隨著時間的累積效應(yīng),mac端會越來越卡(類似于延遲時間越來越大)羹奉。
對這個問題進行排查秒旋,我將問題定位到了UnityEngine.Time.deltaTime
。從我們對時間的理解诀拭,當(dāng)Time.timeScale
為1時迁筛,Time.unscaledDeltaTime
與Time.deltaTime
與應(yīng)當(dāng)是一致的。但是當(dāng)我們在某一個心跳函數(shù)內(nèi)比較這兩個值時會發(fā)現(xiàn)耕挨,Time.unscaledDeltaTime
并不總是等于Time.deltaTime
细卧,時長會比這個大。而且每次出現(xiàn)不一致時筒占,Time.deltaTime
的值總是一樣的贪庙。
所以最后,問題被鎖定到了Time
設(shè)置里的Maximum Allowed Timestep
選項翰苫。
PS:其實我曾經(jīng)一度以為是跨平臺導(dǎo)致Unity底層提供的deltaTime出現(xiàn)了偏差止邮。
Maximum Allowed Timestep的說明
關(guān)于Maximum Allowed Timestep
這個配置的說明,Unity有一份官方的說明奏窑。結(jié)合我之前轉(zhuǎn)發(fā)的Unity的函數(shù)執(zhí)行順序导披,我們可以簡單理解為:
FixedUpdate
在1次Update
可能會執(zhí)行N次,N約等于Time.deltaTime / Time.fixedDeltaTime
。假如Time.deltaTime
變大缨硝,會導(dǎo)致下一幀FixedUpdate
的執(zhí)行次數(shù)N變大新症,CPU的性能消耗變大,從而又會影響當(dāng)前Update
的耗時滑沧。這種糟糕的情況可能需要相當(dāng)長的時間才能緩解過來。所以Maximum Allowed Timestep
限制Time.deltaTime
的最大值巍实,N的最大值也固定了滓技,防止出現(xiàn)“最壞”的情況。
解決方案
為了解決這個問題棚潦,我們所需要做的就是有一個方案能夠替換掉Time.deltaTime
令漂。
為什么不使用Time.unscaledDeltaTime
?因為在具體的項目中,我們依舊可能會用到Time.timeScale
叠必,所以一切從項目實際出發(fā)(如果你的項目無所謂荚孵,那就大膽用吧)。
下面給出我的解決方案:
private long _lastFrameTicks;
private const double Ticks2Second = 0.0000001;
private double _delta;
public double Delta => _delta * UnityEngine.Time.timeScale;
public void Tick()
{
long currFrameTicks = System.DateTime.Now.Ticks;
_delta = (currFrameTicks - _lastFrameTicks) * Ticks2Second;
_lastFrameTicks = currFrameTicks;
}
通過使用DateTime.Now.Ticks
纬朝,計算兩次心跳之間的時間間隔收叶,再將時間間隔的單位轉(zhuǎn)換為秒,即可作為delta時間去使用共苛。
其實按照這個方案深入下去判没,依舊有很多點會決定我們Delta計算的正確性。比如這個Tick執(zhí)行的時機隅茎,Unity內(nèi)所有Update執(zhí)行的順序等澄峰。但是這些基本都不會影響我們幀同步內(nèi)時間的累積計算:多個客戶端同時開始幀同步運算,那么過去固定時間后辟犀,這些客戶端的幀號一定是相同的俏竞,而不會出現(xiàn)開頭我所描述的客戶端幀號不一致的bug。