Unity3D中實現幀同步 - Part 2
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
相關文章
- 【UNITY3D 遊戲開發之四】有關實現2D幀序列幀播放相關—Animating TiledtextureUnity3D遊戲開發
- 網路遊戲同步方式(幀同步和狀態同步)遊戲
- 幀同步遊戲的設計遊戲
- 手遊幀同步的研究[轉]
- css3實現逐幀動畫CSSS3動畫
- css3 實現逐幀動畫CSSS3動畫
- GO語言實現區塊鏈Part6 Transactions 2Go區塊鏈
- 使用PowerBI_Embed實現Web訪問報表 part 2Web
- 在 DotNetty 中實現同步請求Netty
- Qt 中實現非同步雜湊器QT非同步
- Unity3D中暫停時的動畫及粒子效果實現Unity3D動畫
- Unity3d主城玩家位置同步Unity3D
- GO語言實現區塊鏈Part2 Proof-of-WorkGo區塊鏈
- unity3d中對像之間的相互作用的實現Unity3D
- 手遊中載具物理同步的實現方案
- 「譯」 MotionLayout 介紹 (Part IV) 深入理解關鍵幀
- 幀中繼(FR)中繼
- requestAnimationFrame實現一幀的函式節流requestAnimationFrame函式
- HarmonyOS:幀率和丟幀分析實踐
- 編譯實踐學習 Part2編譯
- Webshell-Part1&Part2Webshell
- PHP實現非同步PHP非同步
- wasm + ffmpeg實現前端擷取視訊幀功能ASM前端
- Log4j 2使用Disruptor實現非同步日誌非同步
- JS非同步控制流及async實現細節分析(2)JS非同步
- Unity3D 2D實戰遊戲開發Unity3D遊戲開發
- Node中非同步和同步的實現非同步
- Java 中佇列同步器 AQS(AbstractQueuedSynchronizer)實現原理Java佇列AQS
- Node中console.log的同步實現
- Swift 中如何利用閉包實現非同步回撥?Swift非同步
- 如何在Java中實現非同步任務排程?Java非同步
- 幀動畫的多種實現方式與效能對比動畫
- 王者榮耀技術分析(二):遊戲的幀同步遊戲
- CyclicBarrier – 同步屏障實現分析
- Java實現非同步呼叫Java非同步
- CyclicBarrier - 同步屏障實現分析
- 實現 VUE 中 MVVM - step2 - DepVueMVVM
- Spring Boot中如何優雅地實現非同步呼叫?Spring Boot非同步