1. 程式人生 > >多人遊戲對戰技術(坦克大戰、狀態同步)

多人遊戲對戰技術(坦克大戰、狀態同步)

  用狀態同步的方式實現一個坦克大戰的小遊戲,這也是一次全新的嘗試,從遊戲的效果來看,在正常的網路速度下效果符合預期。這裡跟大家分享下游戲客戶端中用到的關鍵技術點。

一、 同步方式的選擇,狀態同步or 幀同步?

  狀態同步: 同步的是遊戲中的各種狀態,遊戲邏輯由伺服器實現,只是將計算後的結果同步給客戶端,客戶端根據收到的狀態,同步本地的遊戲狀態。

實現狀態同步的一般流程是:

    客戶端上傳操作至伺服器 ====> 伺服器收到後按固定頻率計算遊戲行為的結果,然後以廣播的形式以固定頻率下發至客戶端 ====>客戶端根據收到的狀態,渲染遊戲畫面

  值得注意的是,狀態同步是一種不嚴謹的同步

,只是保證了各種狀態的一致性,並不能保證各客戶端在同一時刻顯示同樣的畫面。比如子彈擊中坦克,那麼狀態同步能保證子彈確實擊中了坦克,但有可能有的客戶端先看到擊中的畫面,有的客戶端後看到。所以狀態同步對網路的要求不高。還有一點,伺服器實現遊戲邏輯的計算,安全性相對客戶端實現遊戲邏輯計算高很多。

  幀同步: 同步的是操作指令,指令包含當前的幀索引。客戶端上傳指令至伺服器,伺服器通過廣播下發至每個客戶端, 客戶端再根據指令,本地計算遊戲邏輯並進行渲染。幀同步確保伺服器每幀下發的指令是一致的。如果出現網路不好的情況,導致幀在本地積壓,或者客戶端同時收到多個幀資訊,則選擇加快速度渲染,或者直接跳至最新的一幀(不建議,除非斷線重連)。

實現幀同步的一般流程是:

    同步隨機數種 (用於遊戲中暴擊等計算) ====> 客戶端上傳操作指令 ====> 伺服器按固定頻率廣播所有客戶端的操作 ====> 客戶端根據收到的操作,進行邏輯運算並渲染遊戲畫面

  嚴格的幀同步每個幀都會等待所有的玩家上傳操作至伺服器後,才會廣播指令至各客戶端,這樣的就能嚴格保證各個玩家的操作是同步的,但是,這樣做的壞處也是顯而易見的,這局遊戲的網路延遲,就是網路最差的那個玩家的網路延遲,這樣對網路好的玩家顯得不公平,而且遊戲的操作手感也不好。所以延伸出來的樂觀鎖幀同步方式,就是用來保證網路好的玩家不受延遲高的玩家的影響。樂觀鎖就是預設在定時器觸發的那一刻,所有的玩家操作已經上傳,然後再廣播至各個玩家。對於延遲高的玩家,會感覺操作延遲,但對於網路情況良好的玩家,會感覺很順暢。至於說這對於網路不好的玩家不公平,只能說網路不好就不要玩這種對於網路要求高的遊戲嘛,當然得優先保證大多數玩家的體驗了。

對於坦克大戰採用狀態同步的考量:

    對於多人線上小遊戲來說,其實這兩種同步方式都可以。但考慮到實際應用場景,我們還是選擇了狀態同步。第一、遊戲邏輯在服務端實現,所以我們更新一款遊戲,直接更新服務端就好了。第二、對於移動端的網路不穩定,所以選擇對網路要求稍微低的狀態同步。

  對於坦克大戰我們也把一些遊戲邏輯交給了客戶端,我們選擇將坦克位置由伺服器廣播至客戶端,而子彈這個不是很關鍵的資訊,我們是直接將開火指令廣播至客戶端,客戶端直接本地渲染,直到收到來自伺服器的生命週期結束的訊息為止(擊中了障礙物)。這部分放到客戶端直接渲染,是因為像子彈這種型別的元素,方向跟運動速度不變,除非碰到障礙物消失。所以,沒有必要將子彈的資訊,也通過服務端廣播至客戶端。一個遊戲子彈有很多,僅僅附帶位置資訊,那也是很佔用頻寬的。其實後續可以考慮,子彈生命結束也由客戶端控制。至於他擊中的物體會怎麼樣,這個由服務端判斷

二、客戶端與伺服器時間的校準

  多人對戰遊戲,需要一個統一的時間軸來運轉整個遊戲世界。而這個時間軸,就是伺服器的時間軸。所以CS之間的時間同步,對整個遊戲的執行是至關重要的。

  服務端時間 = 客戶端時間 + RTT/2 + difftime

Created with Raphaël 2.1.2客戶端客戶端服務端服務端seq = 1, ts = t1ack = 1, ts = t2

  這裡t1 為客戶端傳送請求的時間, t2為服務端收到請求的時間,t3為客戶端收到ack的時間。則有:

     t3-t1 = RTT

    t1 + RTT/2 + diff = t2

  那麼根據以上公式可以算出,客戶端與服務端的時間差diff.這樣就同步了客戶端與服務端的時間。

  這裡預設客戶端到服務端的延遲跟服務端到客戶端的延遲是一樣的,可以多做幾次,取平均值。

三、客戶端的狀態同步的實現

  不同於單機遊戲,多人對戰遊戲需要同步各個客戶端的資訊,因此理論上來說沒法做到像單機遊戲那種立即操作立即顯示。客戶端的操作需要經過服務端的確認才能顯示。整個過程是這樣的:

Created with Raphaël 2.1.2客戶端操作push操作至等待佇列原等待佇列為空?從佇列中取出隊頭併發送至服務端服務端返回ACK,並進行邏輯運算yes

           客戶端上傳至服務端

Created with Raphaël 2.1.2服務端廣播遊戲狀態客戶端儲存最新收到的兩個包介於這兩個包之間做插值,並渲染

           服務端廣播至客戶端

  這裡是分開兩個過程,客戶端的操作不會立即對客戶端渲染產生影響,可以對比下單機遊戲的過程是這樣的:

  操作 ==> 本地進行遊戲邏輯運算 ==> 渲染遊戲畫面==>操作 …

  這裡的網路資料包包括 狀態 + 操作 + 服務端時間, 狀態包括坦克的位置、方向、速度, 操作包括坦克開火、移動、停止,每發生一個動作,就將動作push到佇列裡等待發送。

  可以注意到,只有確保服務端收到一個操作之後,客戶端才會從佇列裡取出下一個操作傳送。如果不用佇列進行快取,則有可能出現在一個週期內傳送多個操作的情況。

四、客戶端做插值平滑遊戲畫面

  由於服務端廣播至客戶端的幀為15幀/s, 如果按照這樣的頻率做本地渲染,這樣得到的效果是讓人無法接受的。看到的遊戲畫面是不連續的。也許有人認為視訊25幀/s就已經非常流暢了,那麼把服務端的廣播頻率設為25幀/s就可以了。其實,視訊的顯示與遊戲的顯示機制是不一樣的,視訊可以由一幅幅畫連續播放得到,而這幅畫是由連續曝光得到,所以是一段時間的資訊,而遊戲是直接顯示,就是一個時間點的資訊。所以遊戲的幀率太少會讓人覺得很卡,一頓一頓的。

  要平滑的顯示遊戲畫面,會選擇一個插值的操作。就是基於兩個已知的狀態做插值,讓本來的兩幀資料,在渲染的時候,細分成多幀資料。服務端廣播幀率為15幀/s, 而本地渲染的是大約60幀/s.我們做個完美的假設,假設收到來自服務端的資料,倒數第二幀坦克位置為(X0,Y0); 最新收到的一幀中坦克的位置為(X1,Y1);之間相隔1/15s。由於客戶端幀率是60幀/s, 故理論上可以服務端的一幀變為客戶端的四幀。但是,會發現由於js是單執行緒的,其定時器無法做到完美的定時,js的定時器也是一個坑。它每隔dt個時間段,渲染一次遊戲畫面,而dt是小範圍波動的。所以有:

這裡寫圖片描述

  其中 Xnow 是現在需要渲染的x座標位置,ts是兩個訊息自帶的時間戳的差值,基本為66ms, 而dt的累加永遠要小於等於ts,當dt的累加大於ts時,說明渲染時間過長,而坦克的x座標已經到達X1的位置了,因此不需要再移動,故當dt的累加大於ts時做無效處理。

五、過程中可改進的點

1.傳輸的資訊沒有進一步壓縮

   用的都是json格式,而且玩家的數目與需要廣播的訊息是成線性關係的,所以需要進一步壓縮,才能更好的支援數量很多的玩家。也是節省玩家流量的一個措施。

2.可以提前渲染一至兩幀的影象

  目前是完全根據服務端的資訊來進行渲染,包括自己操縱的那輛坦克。可以通過客戶端預測和與服務端的協調,來提前一兩幀顯示自己操縱的那輛坦克的位置。為何不走另外一個極端,即馬上操作,馬上得到?因為這不是單機遊戲,需要與伺服器保持一致,只能提前顯示一至兩幀,也就是最多提前顯示120ms的畫面,再多的話就有問題,會導致坦克在服務端的位置與客戶端的位置相差過大,進一步導致整個遊戲時間的不一致。

3.幾乎同時收到服務端多個幀的資料

   這個問題在mac 本上出現過,抓包發現本來間隔66ms的幀資料,有時候會只間隔幾毫秒。這樣導致的現象就是,坦克會一頓一頓,出現閃現。對於該問題偶然出現會有辦法改進,假如短時間內收到3個包,那麼直接丟棄前兩個包。這樣坦克的畫面會突然加快,但總比閃現效果要好。如果經常出現,則目前是無解的,客戶端沒收到來自服務端的幀資料,是做等待處理的,看起來是閃現的效果。只能通過提高網路質量或者網絡卡裝置來避免這個問題。