所讀版本:機械工業出版社
這一篇在做什麼
前面幾篇大多在講網絡通信本身,這一篇把焦點轉到多人遊戲裡更實際的一個問題:玩家狀態到底要怎麼同步,才能在「延遲不可避免」的情況下,盡量讓所有人看到合理的結果。
同步的兩條主路
多人同步通常可以粗分成兩種思路:
- 狀態同步
- 指令同步
兩者不是單純誰高級誰落後,而是各自拿不同問題做交換。
狀態同步
狀態同步的核心是:直接同步角色的結果狀態,例如位置、朝向、血量等。
在最簡單的情況下,客戶端會定時把自己的位置發給服務端,再由服務端轉發給其他玩家。其他客戶端收到後,就把對方角色移動到指定位置。
問題:直接設位置會瞬移
這種做法雖然簡單,但很容易讓遠端角色看起來像瞬移,因此才會衍生出一些平滑策略。
跟隨算法
跟隨算法的想法是:收到對方新位置後,不立刻瞬移,而是讓遠端角色以某種速度跟過去。
它看起來比瞬移平順,但問題是誤差可能越積越大。
例如:
- 玩家 1 實際路徑是
A -> B -> C - 玩家 1 經過
B時送出同步消息 - 玩家 2 還看到玩家 1 在
A - 玩家 2 收到消息後,才開始讓遠端角色往
B移動
當玩家 2 畫面上的遠端角色終於到達 B 時,真實玩家可能已經跑到 C 了。
預測算法
另一種思路是預測。也就是說,收到對方當前狀態時,不是只把它補到當前點,而是估算它在下一個時刻應該到哪裡,提早把遠端角色往那個預測點帶。
這樣做的目的,是用一點估算來抵消網絡傳輸延遲帶來的落後感。
代價則是:一旦預測錯了,畫面就需要再修正回真實位置。
指令同步
和狀態同步不同,指令同步傳的不是「結果」,而是「操作」。
例如:
- 玩家 1 按下
W - 客戶端把「前進」這個操作送給服務端
- 服務端再把這個操作轉發給所有客戶端
- 每個客戶端在本地執行同一條指令
這樣理論上只要邏輯一致,大家最後算出來的結果也會一致。
指令同步的問題
問題在於:不同機器的執行速度不一樣,幀率也不一樣。
結果就是:
- 有些人本地跑得快
- 有些人本地跑得慢
- 即使收到的是同一條「前進」指令,最後也可能出現不同畫面結果
所以單純同步操作還不夠,還得進一步處理「時間」這個因素。
幀同步
幀同步可以看成指令同步的一種常見落地形式。它的核心不是只發送操作本身,而是把操作和某個同步幀綁在一起。
也就是說,客戶端不再問「我現在收到了什麼指令」,而是問:
- 這條指令屬於第幾幀
- 當我執行到那一幀時,應該做什麼
為什麼要固定同步幀
只要所有客戶端:
- 使用同一套計算公式
- 在相同的同步幀執行相同指令
那理論上結果就會一致。
一個簡化例子如下:
int frame = 0; //目前執行到第幾幀
float interval = 0.1f; //兩幀之間的理想時間
public void Update()
{
while(Time.deltaTime < frame * interval)
{
FrameUpdate();
frame++;
}
}
它的目標是盡量保證:
- 第
N秒左右時 - 所有客戶端都執行到第
10 * N幀附近
如果某幾幀執行時間拉長,系統就需要補跑後面的幀,盡量把進度追上。
幀同步真正保證的是什麼
幀同步真正想保證的不是「畫面每一刻都毫無差異」,而是:
- 當所有客戶端都執行到同一個同步幀時
- 它們的遊戲狀態應該一致
例如移動公式完全一樣,那麼只要執行了相同幀數,最終位移結果理論上也應該一致。
指令執行時的兩種妥協
即使有了同步幀,現實裡還是會碰到快慢客戶端不同步的問題。這時通常只剩下兩條取捨路線。
延遲執行
讓跑得快的客戶端等待跑得慢的客戶端。
好處是整體一致性更高,壞處是快客戶端的體驗會變差,因為它明明算得動,卻要等別人。
樂觀幀同步
另一條路是更偏「快的人優先」。如果某個慢客戶端送來的指令已經過時,就直接丟棄。
好處是整體節奏更流暢,壞處是慢客戶端可能明顯吃虧,因為它的輸入會更常失效。
網絡延遲沒有真正的終極解
這篇最後點出一個很現實的結論:網絡延遲本身基本無解。
能做的通常只有兩類補救:
減少要傳的數據
傳得越多,丟包與重傳機率通常越高,因此合理壓縮消息量本身就是同步體驗的一部分。
在客戶端做視覺修飾
很多時候,同步系統不是在「消滅延遲」,而是在「掩蓋延遲」。
例如:
- 補間
- 預測
- 延遲顯示
- 平滑修正
這些做法的本質,都是讓玩家更不容易感知到同步誤差。
這一篇先記住的重點
如果只保留最核心的判斷,可以收斂成:
- 狀態同步傳結果,簡單但容易看到瞬移與修正
- 指令同步傳操作,理論一致性高,但對時間控制要求更嚴
- 幀同步是指令同步常見的落地方式
- 多人同步本質上一直都在做「一致性、即時性、體驗感」之間的取捨
- 網絡延遲無法消除,只能盡量減少影響、再用客戶端表現去掩蓋它