1. 程式人生 > >Unity 3D實現幀同步技術

Unity 3D實現幀同步技術

筆者介紹:姜雪偉,IT公司技術合夥人,IT高階講師,CSDN社群專家,特邀編輯,暢銷書作者,國家專利發明人;已出版書籍:《手把手教你架構3D遊戲引擎》電子工業出版社和《Unity3D實戰核心技術詳解》電子工業出版社等。

現在競技類網路遊戲比較火,市面上也出現了很多這種型別的遊戲競賽,提到網路遊戲就回避不了一個問題:同步技術,多個人在一個遊戲場景圍攻一個怪物或者說多人組隊戰鬥等等。現在在移動端的遊戲由於頻寬的限制,一般採用實時同步的方式是狀態同步,也就是說角色的狀態發生改變,才會去傳送訊息。舉個例子:

3D角色一般的動作狀態有:Idle,walk,run,attack等,玩家操作鍵盤或者觸控式螢幕按鈕,會觸發這些動作,一個遊戲場景中會有多個角色,每個角色都有自己的動作狀態,為了讓玩家能夠看到其他玩家在做什麼,需要同步,玩家預設狀態是idle,玩家剛出現時是idle狀態,這個時候,客戶端會把玩家的狀態,位置,方向傳送給伺服器,其他玩家也是一樣的,伺服器接收到資訊後,會把這些資訊傳送給除了它本人之外的其它玩家,這樣我們就可以看到其他玩家的狀態了。如果玩家從idle狀態轉化到walk狀態,這表明玩家的動作狀態發生了變化,這也需要將資訊發給伺服器,伺服器進行群發給其他玩家,這樣其他玩家就可以看到角色開始walk了。接下來如果玩家繼續走,客戶端就不傳送訊息給伺服器了,因為狀態沒發生變化,等狀態再變化時才會傳送訊息給伺服器,然後伺服器再群發訊息,在此過程中,其他客戶端會通過插值的方式把兩個狀態之間的距離實現出來,以此類推。。。。。。這就是所說的狀態同步模式。

下面介紹一下幀同步模式,幀同步含義遊戲客戶端接受來自網路的多個客戶端的操作,如果這些操作在各個客戶端是一樣的,那麼多個客戶端的顯示也就一樣了,這就帶來了同步的效果。所以在這種情況下,各個客戶端的運算要絕對一致,不能依賴諸如本地時間、本地隨機數等等輸入,而要一切以網路來的操作資料為主。

一般來說,大多數的遊戲客戶端引擎,都會定時呼叫一個介面函式,這個函式由使用者填寫內容,用來修改和控制遊戲中各種需要顯示的內容。比如在Unity裡面叫Update(),這類函式通常會在每幀畫面渲染前呼叫,當用戶修改了遊戲中的各個角色的位置、大小後,就在下一幀畫面中顯示出來。而在幀同步的遊戲中,這個Update()

函式依然是存在,只不過裡面大部分的內容,需要挪到另外一個類似的函式中,我們可以稱之為UpdateNet()函式——由網路層不斷的接收伺服器發來的“網路幀”資料包,每收到一個這樣的資料包,就呼叫一次這個UpdateNet()函式,這樣遊戲就從通過本地CPUUpdate()函式的驅動,改為根據網路來的UpdateNet()函式驅動了。顯然,網路發過來的同步幀速度會明顯比本地CPU要慢的多,這裡就對我們的遊戲邏輯開發提出了更高的要求——如何同步的同時,還能保證流暢?

實現UpdateNet函式內容,其實就是定義一個堆疊用於存放網路發過來的訊息,通過幀監測將其資料拿出來使用,因為Update函式明顯比UpdateNet快的多,這就需要我們定義一個時間間隔用於訊息的傳送,比如50毫秒或者100毫米等。

private float AccumilatedTime = 0f;
 
private float FrameLength = 0.05f; //50 miliseconds 
//called once per unity frame 
public void Update() {
    //Basically same logic as FixedUpdate, but we can scale it by adjusting FrameLength 
    AccumilatedTime = AccumilatedTime + Time.deltaTime;
 
    //in case the FPS is too slow, we may need to update the game multiple times a frame 
    while(AccumilatedTime > FrameLength) {
        GameFrameTurn ();
        AccumilatedTime = AccumilatedTime - FrameLength;
    }
}

private void GameFrameTurn() {
    //first frame is used to process actions 
    if(GameFrame == 0) {
        if(LockStepTurn()) {
            GameFrame++;
        }
    } else {
        //update game 
        SceneManager.Manager.TwoDPhysics.Update (GameFramesPerSecond);
         
        List<IHasGameFrame> finished = new List<IHasGameFrame>();
        foreach(IHasGameFrame obj in SceneManager.Manager.GameFrameObjects) {
            obj.GameFrameTurn(GameFramesPerSecond);
            if(obj.Finished) {
                finished.Add (obj);
            }
        }
         
        foreach(IHasGameFrame obj in finished) {
            SceneManager.Manager.GameFrameObjects.Remove (obj);
        }
         
        GameFrame++;
        if(GameFrame == GameFramesPerLocksetpTurn) {
            GameFrame = 0;
        }
    }
}

幀同步遊戲中,由於需要“每一幀”都要廣播資料,所以廣播的頻率非常高,這就要求每次廣播的資料要足夠的小。最好每一個網路幀,能在一個MTU以下,這樣才能有效降低底層網路的延遲。同樣的理由,我們為了提高實時性,一般也傾向於使用UDP而不是TCP協議,這樣底層的處理會更高效。但是,這樣也會帶來了丟包、亂序的可能性。因此我們常常會以冗餘的方式——比如每個幀資料包,實際上是包含了過去2幀的資料,也就是每次發3幀的資料,來對抗丟包。也就是說三個包裡面只要有一個包沒丟,就不影響遊戲。

幀同步實現的過程有個很重要的地方就是邏輯層和表現層一定要分開,表現層先行,邏輯層等發到服務端的指令再處理。幀與幀之間的播放頻率,則由伺服器統一控制,但由於網路抖動等影響,幀的頻率並不是太穩定,為避免播放抖動,幀數控制器需要進行一定的平滑處理。


網路抖動的產生原因:在網路遊戲中,各個客戶端的執行條件和環境往往千差萬別,有的硬體好一些,有的差一些,各方的網路情況也不一致;時不時玩家的網路還會在遊戲過程中,發生臨時的擁堵,我們稱之為“網路抖動”。可能導致客戶端收到“過去時間”裡的一堆網路幀,客戶端需要拿出一定的時間去處理這些堆積的網路幀,因此,客戶端必須要有處理這些堆積起來的網路資料的能力。

實時同步遊戲最重要的是流暢,然而影響遊戲流暢的因素很多,網路頻寬的限制,CPU運算和渲染效率的限制。一般玩家控制的角色的動作,包括當前客戶端控制的角色,還是應該從網路幀裡面獲得行為資料,因為如果玩家愛控制角色不一致的太多,整個遊戲場面就會差更多。很多遊戲中的怪物AI都是根據玩家角色來設定的,所以一旦玩家角色的行為是同步的,那麼大多數的怪物的表現還是一致的。

幀同步遊戲技術,並不存在一種可以讓遊戲流暢的通用做法,而是需要和遊戲具體做很多結合,在減少資料包,優化遊戲快進體驗,控制發包速度上儘量調優。同時還需要和遊戲產品策劃一起,平衡一致性、實時性、公平性的策略,才能真正達到流暢遊戲的目的。

Demo下載地址:連結: https://pan.baidu.com/s/1kVt77wz 密碼: tfsa