Online Game Development in C++ 第五部分總結
教程案例框架描述
該套教程做了一個簡單的汽車控制系統,沒有用到物理模擬。用油門和方向控制汽車的加速度和轉向,同時還有一些空氣阻力和滾動摩擦力的設置增加了真實感。汽車的位置是通過加速度和時間等計算出來的。
關鍵的參數包括:加速度,速度,質量,最大驅動力,最小轉彎半徑等。
詳細的計算方式就不細說了,重點是沒有用到物理模擬。
聯網遊戲要面對的兩大問題,以及模擬這些困難的方法
1. 網絡更新頻率
在每一個Actor中都能設置其網絡更新(Replicate)的頻率(前提是該Actor需要Replicate)。
具體方法是在BeginPlay中加入以下語句:
if (HasAuthority()) { NetUpdateFrequency= 1;//默認值是10 }
(註意NetUpdateFrequency是針對屬性的Replicate的,和RPC無關)
這個頻率和遊戲運行幀率相比較來說要低很多。盡管這個值可以手動設置的更高,但是更新的越頻繁,給網絡帶來的負擔越大,在多人聯網遊戲中必須合理分配帶寬,而不能無限制地提高某一個actor的數據傳輸量。
在一個對操作比較敏感的遊戲中(例如極品飛車)如果不做任何處理,僅依靠0.1秒一次的更新是無法保證把本地的遊戲對象的運動完美復制到另外一端的。玩家的操作是會隨時發生很細膩的變化的,僅0.1秒更新一次輸入會產生很不連續的跳動,其對操控對象造成的影響會產生更大的偏差,多種因素(例如油門和轉向)造成的誤差累積起來會相差很遠。
正因為要克服這個問題,所以在開發時需要刻意將問題“誇大”,模擬這個困難,把更新頻率調低,比如設置成1次/秒,更容易看出采取一些方法前後的效果差別。
2. 網絡延遲
一方發出的操作指令通過路由到達另外一方需要一定的時間,這個時間就是網絡延遲,通常以毫秒記。如果延遲過大,而不做任何處理,會看到操作對象有明顯的“跳躍”。
延遲是網絡環境決定的,在代碼上無法避免延遲,但是我們可以做到讓操作對象平滑地移動,雖然整體遲一些,但仍是連貫的。
為了誇大這個問題,我們可以輸入以下console命令模擬網絡延遲:Net PktLag=xxxx。 該值的單位是毫秒,例如我們可以設置為1000來模擬一秒鐘的網絡延遲。
Actor Role
教程中的一幅圖對Actor Role總結的很好
圖中上方綠色的框表示服務端,下方藍色和紅色的框分別表示兩個客戶端。
綠色方塊代表一些在服務端控制其移動的對象,例如Listen Server自己控制的Pawn,或者移動的平臺、機關等。
藍色小人表示藍色客戶端的pawn,紅色小人表示紅色客戶端的pawn。
三種Actor Role的同步方案
為了解決後面的一系列問題,將“輸入”和“狀態”分別進行封裝。
“輸入”封裝為Move,包括油門數值、轉向數值、DeltaTime、時間標簽。
“狀態”封裝為State,包括Transform,速度,最後的輸入。
這裏只進行方法上的描述,具體代碼就不列出了。
1. Authority
服務端所有的Actor都是Authority。自己控制移動的Actor當然是Authority,而其他玩家的Pawn的控制也是提交到服務端進行計算後再次同步給所有客戶端的,而且服務器計算出來的就是標準,所以也稱為Authority。
服務端自己控制的Actor自然可以在服務端本地讓其平滑移動。但這個Actor在客戶端怎麽移動呢?實際上它在客戶端的Role(即Remote Role)就變成了Simulated Proxy。
具體情況和處理方法見後面的Simulated Proxy。
2.Autonomous Proxy
客戶端自己控制的Actor是Autonomous proxy(自治代理)。
Auonomouse Proxy它可以首先獲取輸入,所以在本地就可以平滑地模擬移動,然後在Tick中,創建一個Move,通過一個Reliable 的RPC函數SendMove將Move提交到服務端運行。由於是在Tick中,而且是Reliable函數,所以在服務端執行的頻率和Tick是一致的,這是唯一比較耗費帶寬的操作,帶來的好處就是服務端也會平滑精確地響應其輸入。
在該RPC函數中,會改變一個ServerState屬性,是前面封裝的State類型結構體。
而ServerState屬性是一個OnRep函數,每次更新它,就會在客戶端觸發另外一個函數OnRep_ServerState(),在這個函數中覆蓋客戶端自己計算的汽車狀態。
需要註意的是ServerState是屬性,它的Replicate的頻率並不是每個Tick一次,而是使用網絡更新頻率。雖然頻率不高,但是在網絡延遲不嚴重的情況下,服務端和客戶端計算出來的位置應該不會有太大差別,所以這個覆蓋行為也不會讓玩家有明顯的感覺。
但是如果考慮到網絡延遲,上述方法仍不能完美應對。
可以手動將網絡延遲模擬為1秒,就會明顯感覺到車輛在移動和轉彎時頻繁地跳回之前的某個狀態,非常難以控制,而且根本談不上平滑。
原因也很好理解,因為根據上述方法的描述,服務端會定期覆蓋本地的車輛狀態,而因為網絡延遲的原因,服務端的“反應”總是慢半拍。比如客戶端車輛已經啟動,並且走了一段距離了,服務端的車輛才剛剛啟動,那麽在同步時,服務端的車輛位置和客戶端的車輛位置有一定的差距,生硬的去覆蓋當然會產生一個跳躍,並且這個問題會持續下去,以至於根本無法進行正常的操作。
我們雖然無法避免網絡延遲,但是我們有辦法將操作變得平滑,不再跳躍。教程稱之為:“Keeping ahead of the server”,但我寧願稱之為“緩存操作”。方法概述如下:
在客戶端本地的Tick中創建Move(和之前是一樣的),然後將Move緩存入一個數組(不同的地方),然後依照前面的方法,本地模擬,再通過RPC在服務端模擬。在同步ServerState時多做一些事情:在Autonomous Proxy中對比ServerState的LastMove中的時間標簽和前面緩存的Move數組,時間早於LastMove的都清理掉(因為這些在服務端已經得到了執行)。然後通過一個For循環在本地瞬間模擬剩下的未執行的Move數組。
對比這個操作和直接生硬的從服務端往客戶端覆蓋,就會發現這個操作實際上把服務端還沒接收到的一系列操作在本地瞬間重演了,並在服務端的結果上進行偏移給與客戶端,這樣雖然增加了計算量,但是保證了本地運動是平滑的,同時也最大程度的保證了服務器的權威性——因為仍是在服務器計算的結果之上進行的偏移。
3. Simulated Proxy
Simulated Proxy只存在於客戶端(因為服務端的都是Authority),它無法受到自己控制,是從服務端同步來的(不管是誰控制的,可能是其他客戶端,也可能是直接由服務器控制),所以叫做“模擬代理”。
方法1:
簡單粗暴的做法是讓其同步移動。
C++的做法是在構造函數中加入 bReplicateMovement = true;
藍圖中是在屬性中搜索ReplicateMovement,將其打鉤。
這樣雖然可以同步其移動,但是因為同步頻率的問題,在客戶端會看到類似定格動畫的移動,很不平滑。
方法2:
註意該方法需要設置bReplicateMovement=false。不讓Movement自動同步,而是手動處理。
正常的處理方法是滯後一次更新,對物體位置進行Linear Interpolation,對旋轉進行Slerp。這樣Simulated Proxy的移動雖然會慢一點,但是好處是移動會平滑很多。
方法3:
註意該方法需要設置bReplicateMovement=false。不讓Movement自動同步,而是手動處理。
更好的辦法是利用Hermite Cubic Spline Interpolation。
重點公式:
Derivative(曲線斜率)= DeltaLocation/DeltaAlpha Velocity (速度)= DeltaLocation/DeltaTime DeltaAlpha = DeltaTime/TimeBetweenLastUpdates(兩個點之間的時間差) 推倒得出: Derivative = Velocity * TimeBetweenLastUpdates 註意虛幻裏的速度默認單位是m/s,而位置的單位是cm,所以在速度轉換為位置的時候一定要記得*100。
虛幻裏提供了兩個函數可以幫助我們模擬Hermite Cubic Spline,分別是:
FMath::CubicInterp()
和
FMath::CubicInterpDerivative()。
前者用來求插值的位置,後者用來求插值的速度,具體用法如下:
FVector NewLocation = FMath::CubicInterp(初始位置,初始Derivative,目標位置,目標Derivative,比例);
FVector NewDerivative = FMath::CubicInterpDerivative(初始位置,初始Derivative,目標位置,目標Derivative,比例);
求出的NewDerivative轉換成速度也很簡單,直接除以(TimeBetweenLastUpdates*100)即可。
但是如果講Velocity的方向設置為旋轉方向,倒車時候就會瞬間調轉車頭,這是我們不希望看到的。所以旋轉上最好還是結合第二種方法來做。總之旋轉上沒有非常好的平滑方法。
需要知道,上述這寫方法都是針對非常低的同步頻率(測試使用的是1次/秒)來處理的,在正常同步頻率下(10次/秒)的表現還都是非常好的。
Online Game Development in C++ 第五部分總結