Unity3D中實現幀同步 - Part 2

lauhonyeung發表於2015-07-01

http://jjyy.guru/unity3d-lock-step-part-2/

概覽 在上次實現的幀同步模型當中,遊戲幀率和通訊頻率(也就是幀同步長度)長度是固定間隔的。但實際上,每個玩家的延遲和效能都不同的。在update中會跟蹤兩個變數。第一個是玩家通訊的時長。第二個則是遊戲的效能時長。

移動平均數 為了處理延遲上的波動,我們想快速增加幀同步回合的時長,同時也想在低延遲的時候減少。如果遊戲更新的節奏能夠根據延遲的測量結果自動調節,而不是固定值的話,會使得遊戲玩起來更加順暢。我們可以累加所有的過去資訊得到”移動平均數”,然後根據它作為調節的權重。

每當一個新值大於平均數,我們會設定平均數為新值。這會得到快速增加延遲的行為。當值小於當前平均值,我們會通過權重處理該值,我們有以下公式:

newAverage=currentAverage∗(1–w)+newValue∗(w) 其中0

在我的實現中,我設定w=0.1。而且還會跟蹤每個玩家的平均數,而且總是使用所有玩家當中的最大值。這裡是增加新值的方法:

public void Add(int newValue, int playerID) { if(newValue > playerAverages[playerID]) { //rise quickly playerAverages[playerID] = newValue; } else { //slowly fall down playerAverages[playerID] = (playerAverages[playerID] * (9) + newValue * (1)) / 10; } } 為了保證計算結果的確定性,計算只使用整數。因此公式調整如下:

newAverage=(currentAverage∗(10–w)+newValue∗(w))/10 其中0

而在我的例子中,w=1。

執行時間平均數 每次遊戲幀更新的時間是由執行時間平均數決定的。如果遊戲幀要變得更長,那麼我們需要降低每次幀同步回合更新遊戲幀的次數。另一方面,如果遊戲幀執行得更快了,每次幀同步回合可以更新遊戲幀的次數也多了。對於每次幀同步回合,最長的遊戲幀會被新增到平均數中。每次幀同步回合的第一個遊戲幀都包含了處理動作的時間。這裡使用Stopwatch來計算流逝的時間。

private void ProcessActions() { //process action should be considered in runtime performance gameTurnSW.Start ();

...

//finished processing actions for this turn, stop the stopwatch
gameTurnSW.Stop ();

}

private void GameFrameTurn() { ...

//start the stop watch to determine game frame runtime performance
gameTurnSW.Start();

//update game
...

GameFrame++;
if(GameFrame == GameFramesPerLockstepTurn) {
    GameFrame = 0;
}

//stop the stop watch, the gameframe turn is over
gameTurnSW.Stop ();
//update only if it's larger - we will use the game frame that took the longest in this lockstep turn
long runtime = Convert.ToInt32 ((Time.deltaTime * 1000))/*deltaTime is in secounds, convert to milliseconds*/ + gameTurnSW.ElapsedMilliseconds;
if(runtime > currentGameFrameRuntime) {
    currentGameFrameRuntime = runtime;
}

//clear for the next frame
gameTurnSW.Reset();

} 注意到我們也用到了Time.deltaTime。使用這個可能會在遊戲以固定幀率執行的情況下與上一幀時間重疊。但是,我們需要用到它,這使得Unity為我們所做的渲染以及其他事情都是可測量的。這個重疊是可接受的,因為只是需要更大的緩衝區而已。

網路平均數 拿什麼作為網路平均數在這裡不太明確。我最終使用了Stopwatch計算從玩家傳送資料包到玩家確認動作的時間。這個幀同步模型傳送的動作會在未來兩個回合中執行。為了結束幀同步回合,我們需要所有玩家都確認了這個動作。在這之後,我們可能會有兩個動作等待對方確認。為了解決這個問題,用到了兩個Stopwatch。一個用於當前動作,另一個用於上一個動作。這被封裝在ConfirmActions類當中。當幀同步回合往下走,上一個動作的Stopwatch會成為這一個動作的Stopwatch,而舊的”當前動作Stopwatch”會被複用作為新的”上一個動作Stopwatch”。

public class ConfirmedActions { ... public void NextTurn() { ... Stopwatch swapSW = priorSW;

    //last turns actions is now this turns prior actions
    ...
    priorSW = currentSW;

    //set this turns confirmation actions to the empty array
    ...
    currentSW = swapSW;
    currentSW.Reset ();
}

} 每當有確認進來,我們會確認我們接收了所有的確認,如果接收到了,那麼就暫停Stopwatch。

public void ConfirmAction(int confirmingPlayerID, int currentLockStepTurn, int confirmedActionLockStepTurn) { if(confirmedActionLockStepTurn == currentLockStepTurn) { //if current turn, add to the current Turn Confirmation confirmedCurrent[confirmingPlayerID] = true; confirmedCurrentCount++; //if we recieved the last confirmation, stop timer //this gives us the length of the longest roundtrip message if(confirmedCurrentCount == lsm.numberOfPlayers) { currentSW.Stop (); } } else if(confirmedActionLockStepTurn == currentLockStepTurn -1) { //if confirmation for prior turn, add to the prior turn confirmation confirmedPrior[confirmingPlayerID] = true; confirmedPriorCount++; //if we recieved the last confirmation, stop timer //this gives us the length of the longest roundtrip message if(confirmedPriorCount == lsm.numberOfPlayers) { priorSW.Stop (); } } else { //TODO: Error Handling log.Debug ("WARNING!!!! Unexpected lockstepID Confirmed : " + confirmedActionLockStepTurn + " from player: " + confirmingPlayerID); } } 傳送平均數 為了讓一個客戶端向其他客戶端傳送平均數,Action介面修改為一個有兩個欄位的抽象類。

[Serializable] public abstract class Action { public int NetworkAverage { get; set; } public int RuntimeAverage { get; set; }

public virtual void ProcessAction() {}

} 每當處理動作,這些數字會加到執行平均數。然後幀同步回合以及遊戲幀回合開始更新

private void UpdateGameFrameRate() { //log.Debug ("Runtime Average is " + runtimeAverage.GetMax ()); //log.Debug ("Network Average is " + networkAverage.GetMax ()); LockstepTurnLength = (networkAverage.GetMax () * 2/two round trips/) + 1/minimum of 1 ms/; GameFrameTurnLength = runtimeAverage.GetMax ();

//lockstep turn has to be at least as long as one game frame
if(GameFrameTurnLength > LockstepTurnLength) {
    LockstepTurnLength = GameFrameTurnLength;
}

GameFramesPerLockstepTurn = LockstepTurnLength / GameFrameTurnLength;
//if gameframe turn length does not evenly divide the lockstep turn, there is extra time left after the last
//game frame. Add one to the game frame turn length so it will consume it and recalculate the Lockstep turn length
if(LockstepTurnLength % GameFrameTurnLength > 0) {
    GameFrameTurnLength++;
    LockstepTurnLength = GameFramesPerLockstepTurn * GameFrameTurnLength;
}

LockstepsPerSecond = (1000 / LockstepTurnLength);
if(LockstepsPerSecond == 0) { LockstepsPerSecond = 1; } //minimum per second

GameFramesPerSecond = LockstepsPerSecond * GameFramesPerLockstepTurn;

PerformanceLog.LogGameFrameRate(LockStepTurnID, networkAverage, runtimeAverage, GameFramesPerSecond, LockstepsPerSecond, GameFramesPerLockstepTurn);

} 更新:支援單個玩家 自從本文發出以來,增加了單人模式得支援。

特別感謝redstinggames.com的Dan提供。可以在以下看到修改:

Single Player Update diff

原始碼 Source code on bitbucket – Dynamic Lockstep Sample

相關文章