This document is about: QUANTUM 3
SWITCH TO

Frames

概述

Quantum的預測-回滾架構能夠有效降低延遲。Quantum總是會回滾並重新模擬幀。這是實現確定性的必要條件,並涉及伺服器對玩家輸入的驗證。一旦伺服器確認了玩家的輸入或覆蓋/替換了輸入(僅在輸入未及時送達伺服器的情況下),該幀所有玩家的驗證後輸入將被發送至客戶端。收到驗證後的輸入後,最後一個已驗證的幀將使用確認的輸入向前推進。

注意: 如果玩家的輸入未及時送達伺服器或無法通過驗證,該輸入將被回滾。

幀的類型

Quantum將幀分為兩種類型:

  • 已驗證幀;及
  • 預測幀。

已驗證

已驗證 幀是一個 可信 的模擬幀。已驗證幀保證在所有客戶端模擬中具有確定性且完全一致。已驗證模擬僅在收到伺服器確認的輸入後才會模擬下一個已驗證幀;因此,它的推進速度與從伺服器到客戶端的往返時間(RTT/2)成正比。

一個幀被視為已驗證,必須同時滿足以下兩個條件:

  • 該幀的 所有 玩家輸入均已通過伺服器驗證;及
  • 該幀之前的所有幀均已驗證。

如果僅驗證了 部分 玩家的輸入,則該幀不會被視為已驗證幀。

預測

已驗證幀 不同,預測幀 不需要伺服器確認的輸入。這意味著預測幀會根據本地模擬中累積的增量時間進行預測並向前推進。

Unity端的API提供了多種預測幀的訪問方式,詳見以下API說明。

  • Predicted : 模擬的「頭部」,基於同步的時鐘。
  • PredictedPrevious(預測幀 - 1):用於主時鐘對齊插值(大多數視圖會使用此幀以保持平滑,因為Unity的本地時鐘可能與伺服器時鐘略有偏差。Quantum使用獨立的時鐘運行,並與伺服器時鐘同步,且會平滑校正)。
  • PreviousUpdatePredicted: 這是上一次調用Session.Update時的「Predicted/Head」幀(包含校正後的數據)。用於錯誤校正插值(大多數情況下不會出現錯誤)。

API

已驗證幀預測幀 的概念同時存在於模擬和視圖中,但API略有不同。

模擬

在模擬中,可以通過Frame類訪問當前模擬幀的狀態。

方法 返回值 描述
IsVerified 布林值 如果該幀在所有客戶端上具有確定性且使用伺服器確認的輸入,則返回true。
IsPredicted 布林值 如果該幀是本地預測幀,則返回true。

視圖

在視圖中,可以通過QuantumRunner.Default.Game.Frames訪問 已驗證幀預測幀

方法 描述
Verified 可信的模擬幀,在所有客戶端上完全一致。
Predicted 基於同步Quantum時鐘的本地模擬「頭部」。不同客戶端之間可能不一致。
PredictedPrevious 預測幀 - 1
用於主時鐘對齊插值,大多數視圖會使用此幀以保持平滑。由於Unity的本地時鐘可能與伺服器時鐘略有偏差,Quantum使用獨立的時鐘運行,並與伺服器時鐘同步,且會平滑校正
PreviousUpdatePredicted 上一次調用Session.Update時的「Predicted/Head」幀的重新模擬版本。在發生回滾時,用於「校正」該幀持有的數據。視圖會使用此幀進行錯誤校正插值——這是一種安全措施,通常很少需要。

Using Frame.User

可以通過在Frame.User.cs中添加數據來擴展Frame。但這樣做時,還需要實現幀所使用的相應初始化、分配和序列化方法。

C#

partial void InitUser() // Initialize the Data

partial void SerializeUser(FrameSerializer serializer) // De/Serialize the Data
partial void CopyFromUser(Frame frame) // Copy to next Frame

partial void AllocUser() // Allocate space
partial void FreeUser() // Free allocated space

注意: 在幀中添加過多數據會影響性能(序列化/反序列化),並可能影響延遲加入的玩家。

範例

這是一個非常簡單的範例,不需要手動分配記憶體。

C#

namespace Quantum {

    unsafe partial class Frame    {
        public byte[] Grid => _grid;
        private byte[] _grid;

        partial void InitUser() {
            _grid = new byte[RuntimeConfig.GridSize];
        }

        partial void SerializeUser(FrameSerializer serializer)
        {
            serializer.Stream.SerializeArrayLength<Byte>(ref _grid);
            for (int i = 0; i < Grid.Length; i++)
            {
                serializer.Stream.Serialize(ref Grid[i]);
            }
        }

        partial void CopyFromUser(Frame frame)
        {
            Array.Copy(frame._grid, _grid, frame._grid.Length);
        }
    }
}
Back to top