因特網的坑:我從編寫X翼戰機VS鈦戰機遊戲中學到的

菜鳥浮出水發表於2014-03-29

當我們啟動“X翼戰機VS鈦戰機”這個專案的時候,我們的目標是去創造第一個多人線上的星球大戰遊戲。為了實現這個目標,除了通過因特網進行聯網對戰以外,還有很多問題需要我們解決。我將回顧我們遇到的所有問題,以及我們是如何解決的,包括最後的結果如何。我希望我的經驗能夠給看這篇文章的讀者帶來幫助。

面臨的問題

“X翼戰機VS鈦戰機”是星球大戰系列的第三部作品。我們最初在開發星球X翼系列遊戲引擎的時候,肯定是沒有考慮因特網的。所以,這是我們第一個需要面對的難題。向一個最初沒有考慮因特網的遊戲引擎裡新增因特網的功能是極其困難的。

我們的第二個難題是遊戲設計的複雜性。我們一直認為我們遊戲引擎的強大之處在於它能夠模擬並創造出一種公平的對戰環境。我們非常自豪的說,在我們設計的任務中,存在大量的不同的飛船,它們有各種不同的能力和行為,但是總體來說是公平的,沒有那一艘飛船會影響遊戲的平衡。我們開發“X翼戰機VS鈦戰機”的目的之一就是實現一個能夠既公平又富有可玩性的多人線上遊戲。我們想要帶給遊戲玩家的多人遊戲體驗,比以前的”死亡匹配賽“更加激烈。為了達到這個目的,玩家所需要的資料量將會極大的增加。

我們清單上的第三個難題是我們沒有專用的伺服器來執行我們的遊戲;我們將不得不使用P2P技術來彌補這一點。為我們預期的玩家數量,提供所需的處理能力和頻寬的伺服器是一筆難以想象的高額花銷。由於我們遊戲發行的證書的性質,不允許玩家自己架設伺服器來執行遊戲。但一個P2P的系統避免了這個問題,可它同時也丟擲另一個技術上的難題,因為每名玩家都需要跟別的玩家進行溝通,而這些玩家不一定在同一個伺服器上。因為因特網沒有多播的能力,傳送同樣的一條訊息到三個不同的目的地,相比傳送到一個目的地需要3倍的頻寬。

第四個難題當然就是因特網本身了。當我們啟動這個專案的時候,我們假設了我們能夠處理200毫秒到1秒的延遲。我們還知道我們的頻寬是有限制,只有28K的調變解調器可以用。這兩個限制是我們設計網路模型一開始最關注的問題,但是最後我們發現,這僅僅是我們需要解決的最簡單的問題罷了。

解決辦法

面對著一堆的難題,我們設計了一個我們自認為可以涵蓋所有問題,並找到滿意的解決方案的網路模型。第一個我們需要解決的也是最重要的一個問題,同時它也是我們後來頭疼的罪魁禍首。那就是我們認為,不該讓網路模型限制我們遊戲關卡設計的複雜性,但是我們也知道沒有辦法把每一個玩家的資料壓縮,然後通過這麼窄的頻寬進行同步。所以我們想出了三個可能的解決方案。第一個選擇,也是我們所知的被其他的遊戲成功運用的方法,我們只傳輸最重要的資料,然後通過預測的方式填充其他不重要的資料。第二個選擇是隻傳輸所需繪製的遊戲世界的資料。第三個選擇是隻傳輸玩家的操作,然後在每個客戶端模擬出這些操作的結果。

第一個選擇需要我們有能力快速決定哪些資料是重要的,哪些不是。在我們之前的遊戲中,玩家被賦予了大量的能力來探尋遊戲世界中的一切。我們甚至給玩家一個能夠實時顯示遊戲世界中所有飛船動向的地圖。除此之外,玩家還能夠通過使用一個叫”追蹤電腦“的東西來查詢任何一艘飛船的當前狀態。如果說我們要解決前面提到的問題的話,我們必須去修改或者刪掉這個功能。

第二個選擇聽起來像是一個可行方案。從玩家飛船的座艙看出去,只能看到很少的一部分物體,而且如果玩家想看到更多的物體,那麼玩家必須是離的很遠,那樣的話就不用太精確的畫出這些物體了。問題是玩家是在一個開放的空間中,並駕駛一艘機動性非常好的飛船。他們能夠非常快的做一個360度迴旋,在那個時候他們就能夠看到遊戲世界中的幾乎所有的物體。我們知道這種只傳輸所需繪製的遊戲世界的資料的方法,在那些室內遊戲中能夠成功,但是我們的遊戲是不會用牆之類的東西把遊戲世界分隔開的。我們還考慮過使用一種霧來分隔玩家的視線,這樣玩家就只能看到一定距離之內的物體了,但是面對現實吧,這不是一個好主意。

第三個選擇立刻吸引了我們。頻寬限制了我們只能傳送玩家的操作,而不用考慮遊戲關卡設計的複雜性。我們以前用過一種類似的技術來讓玩家記錄下戰鬥的過程,然後能夠在“VCR”裡重放戰鬥過程。所以我們知道我們的遊戲引擎是有這種功能和概念的。我們決定對這個辦法做一個快速測試,幾天後我們就完成了我們的第一個多人任務的關卡。

讓玩家做主機

我們做出的第二個重要的決定是讓玩家成為遊戲中的“主機”。我們之前選擇只傳送玩家操作的資料的意思是,每個玩家在點對點系統中對遊戲的操作,將會傳送給其他的所有玩家。因為因特網沒有廣播和多播的能力,所以每一條訊息都會被複制然後傳送N-1次,N是指遊戲中玩家的數量。這也就是說28K的調變解調器的頻寬會被分割成和玩家數量相應的份數。

如果有一個玩家扮演“主機”的角色,那麼我們就能夠明顯的減少其他玩家的頻寬壓力,同時只是稍微增加了做主機的玩家的頻寬壓力。每一個玩家把資料發給主機,主機把所有的資料壓縮排一個大的資料包,然後傳送給每一個客戶端。這種方法的優勢在於,如果主機有一個快速的網路連線,那麼遊戲就可以支援許多低速連線的玩家。在遊戲正式釋出之後,這一點節省了我們大量的開銷,只要主機玩家的頻寬能夠同時支撐起8名通過調變解調器連線遊戲的低速玩家就沒問題。

這種方法的另一個好處是我們不需要去考慮如何在不同玩家的機器之間同步資料,相反,我們只需要關心每名玩家如何把資料同步給主機。我們希望這個方法能夠更早的實現“中途加入遊戲”的機制,但是可惜的是,我們的遊戲沒能實現這一點。

儘管我們的測試是非常輕鬆就通過了,但是我們不認為我們接下來的工作也能這麼容易。我們知道這種方法有它自己的問題。其中最大的一個問題也是我們最為擔心,就是我們之前在“VCR”功能裡已經看到過的,在回放玩家操作的時候,會產生出一些和原本戰鬥完全不同的結果。在以前,這樣的差異bug當然不會帶來什麼樣的害處。比如說,我們也許用了一個本地的布林變數來表示無人機可能的兩種行為。如果這個變數被意外的設定過,那麼結果將會是依賴於棧上這個變數的值而產生出隨機的結果。這種型別的bug通常是不被注意的,除非是在放一部電影。但是在放電影的時候,即使這個bug能夠引起一艘飛船採取與記錄的時候不同的行為。但這個細節會被電影中的其他部分掩蓋掉,就好像那艘飛船本來就應該那樣。

如果這種事情發生在多人遊戲中,玩家將會敏銳的發現兩種截然不同的結果。我們希望用兩種方法來解決這個不一致的問題。首先,我們希望能夠找到這個系統儘可能多的bug,這樣也許可以提前避免這種問題的出現。其次,我們開發了一種方法能夠檢測到這個問題的發生,然後把需要同步的資料再傳送一次。

這個方法的最大優勢是隻需要非常小的頻寬。但我們還需要處理延遲的問題。在做了一些快速的測試之後我們意識到100毫秒的延遲,我們就無法控制飛船了。當你想要射擊一個目標的時候,錯過了準心而你又已經開火了,會讓人產生巨大的挫敗感。我們沒有去改變遊戲的操作方式,而是設計了一種系統,讓玩家在他們的操作和飛船的行為反饋上感覺不到任何延遲。這項技術的關鍵在於使用一種類似所謂的“航位推算”的技術。

我們的解決辦法是維持兩份同樣的遊戲資料。第一份遊戲資料只基於每一個玩家的操作,並且只在資料能夠獲取到的時候進行更新。第二份遊戲資料是當前時間點的遊戲狀態,並且每幀都會進行更新。第二份遊戲資料不能代表玩家的操作,因為玩家操作的資料因為因特網的原因造成了延遲。然而,這份遊戲資料是基於對於玩家行為的預測將會產生什麼樣的結果。延遲越高,兩份遊戲資料的差距越大,預測的版本就越不準確。

我們的方法看起來解決了兩個最常見的網路問題:頻寬和延遲。頻寬限制了我們能夠傳送玩家運算元據的最小值。延遲會造成遊戲世界的資料異常(也就是我們常說的不同步),但是不會影響玩家的戰鬥操作。我們對自己的辦法非常滿意,而且認為我們確實做的非常聰明。

實現這個設計

我們的第一步就是去實現這個網路模型並在我們的區域網進行測試。這個過程進展的非常順利。第一個版本是一個簡單的“同步”模型,在這個模型下所有的玩家必須等待,直到接收到了所有玩家的操作,然後才開始進行遊戲世界的繪製。第一份遊戲世界的拷貝所需傳輸的資料量非常小,只要很小的頻寬就行,但是在明顯的網路延遲下根本就無法工作。當某個玩家幀數比較慢時,還會出現拉回現象,全同步的意思就是所有玩家都向最慢的那個玩家同步。

這個版本非常容易去編碼實現,因為我們沒有實現對遊戲世界的預測,並且我們甚至沒有嘗試去解決延遲問題。此外,我們還使用DriectPlay,這樣在實現一個遊戲場景的繪製,並讓玩家加入的工作中,就只剩下很少的工作要做了。我們讓這個版本非常快的上線了,這樣我們的關卡設計師就可以開始設計多人關卡了。事實上這個同步的版本我們用了很長時間。因為這個版本很容易被測試,所以在當時的開發進度來看,實現一個網路版本優先順序就沒有那麼高了。當我們最終開始編寫我們的網路模組時,我們已經拖後了進度,並且影響了我們之後的一些決定。這也意味著之後我們要完全投身於網路模型,以及使用者介面的開發。

快速實現我們的第一個版本帶來的另一個大的好處是我們能夠做出一些非常漂亮的處理方法去應對那些不同步的bug。由於這些方法的存在以及長時間的測試,我們解決了大部分的bug。我們還能夠實現了多次同步的方式,並且在區域網的情況下,多次同步的速度非常之快,以至於你根本無法注意到不同步的bug出現。

當我們開始編寫網路模組的時候,我們知道,我們的首要任務是基於第一個遊戲世界的拷貝,來建立第二份拷貝。不幸的是,我們的遊戲引擎不支援這個特性,所以實現這個功能花費了很大的代價。但是,一旦我們實現了這個特性,我們就給我們的延遲問題帶來了奇蹟般的效果,在區域網測試的時候,它工作的非常之好!

我們現在擁有了一個在區域網工作的非常好的遊戲的版本。它只需要非常小的頻寬,而且它還能夠容忍500毫秒的延遲,而你壓根感覺不到。鼓起勇氣,我們開始在因特網上簡單的測試它。發現它居然還行!直到數週之後我們開始認真的做一些測試時,才意識到我們的錯誤。

學習到的東西(因特網就是個坑)

第一課:如果所有的玩家都撥打同一個電話號碼,那麼你就不是在測試網路,你實際上是在測試調變解調器和POP伺服器,無論如何你確實不是在測試網路。如果你仔細想一下的話,就會發現這是顯而易見。你的資料包通過調變解調器到達POP伺服器,然後POP伺服器把他們轉發給其他玩家。資料包從未被POP伺服器處理。

當我們最終把我們的遊戲放到真實的網路環境中去,它在幾秒鐘之內就掛了。我們都困惑不解。它在區域網工作的如此之好,甚至能夠允許500毫秒的延遲,為什麼一上因特網就掛了呢。當我們進行一些檢查後,發現了一些難以置信的事實,5到10秒的延遲是常見現象,並且我們發現了一些延遲甚至達到了50秒!在這種延遲的情況下,我們的遊戲當然無法執行了。

甚至有一些包出現了丟失。TCP協議規定資料包一定會被接收,並且它們會被按順序傳送。TCP協議使用一個檢查系統來驗證資料包是否被成功傳送了,並在發生丟失的時候進行重發。按順序傳送的意思是如果前一個資料包需要重發,那麼後一個資料包將會被延遲傳送,直到前一個資料包被接收到。但問題是,一旦因特網連線開始丟包,那麼接下來的包很大概率仍然會發生丟包。這就意味著一個資料包可能需要好幾秒鐘才能夠到達目的地。

第二課:TCP就是惡魔。不要在遊戲裡用TCP協議。你將會用盡你生命剩餘的時間看到滿載13歲少女的泰坦尼克號的悲劇一遍遍的重演。首先,TCP在等待傳送下一個資料包之前不會傳送任何額外的資料包。這就是為什麼我們會看到有5秒延遲的包出現。其次,如果有某個資料包沒有到達目的地,TCP協議不會立刻重發這個資料包。這個方式的考慮是如果一個資料包因為阻塞而發生了丟失,那麼,就沒有必要去重發這個資料包,因為這樣只會增加阻塞。所以這個時候TCP就會停止發包,而開始傳送一些臨時的非常小的檢查網路通暢的包。一旦這些測試包通過了,那麼TCP就會重新開始傳送真正的資料包。這種重發演算法解釋了為什麼我們發現了一些延遲達50秒的資料包。

第三課:使用UDP。應對TCP這個惡魔的辦法看起來也非常簡單。不用TCP,用UDP代替就可以了。不像TCP,UDP是一個不可信的協議。它不做任何事情來保證資料包會被接收到,並且不關心資料包是否按照順序傳送。換句話說,它什麼都不做。所以如果你非常需要傳送一個資料包,你就需要自己去控制重發和檢查機制。關於UDP還有一件煩惱的事。調變解調器的連線使用一種叫做PPP的協議進行連線。當你使用TCP協議通過PPP協議的時候,PPP協議會非常聰明的壓縮資料包的因特網包頭內容,使它從22位元組減少到3位元組(甚至更小)。當你傳送UDP包通過PPP連線的時候,它不會像TCP那樣進行聰明的壓縮,而直接傳送22位元組的包頭。這樣,你使用UDP的時候,你就不應該一次傳送很少的資料,因為這樣會非常浪費頻寬。

我們的網路系統當然需要每一個資料包都能夠被接收到。如果TCP協議能夠使用,這就不是問題。但是用TCP是徹底沒希望的,所以我們必須自己去寫我們的協議來處理檢測和重發機制。不幸的是,我們並沒有馬上意識到這點,我們花了很長時間才明白這點的重要性。

我們的第一步就是換掉TCP,使用UDP。對於DirectPlay,只需要傳遞一個標記給它,就可以換用UDP了。但是,我們的遊戲在第一個資料包丟失之後就會悲劇的掛掉。所以我們實現了一個簡單的重發機制去處理丟包。這樣會好了一點,但是一旦發生一些意外,遊戲就跟之前一樣徹底悲劇了。我們的第一個猜測是DirectPlay忽略了我們的UDP標記,而依然在使用TCP協議。但是檢查後發現,這個罪魁禍首比微軟更加邪惡:是因特網自身的問題。

第四課:UDP比TCP要好,但是UDP也是個坑。雖然開始時我們假設了丟包會偶爾發生,但是因特網的情況更加糟糕。在某一些連線下,有五分之一通過乙太網的包會被丟失。當他們說UDP是不可靠的時候,他們並沒有在開玩笑啊!我們那個簡單的重發系統在這種情況下工作的並不好。重發的包也非常輕易就丟失掉了,並且我們看到了在一些情況下,原包以及重發的4,5個包都一起被丟掉了。因為我們重發了太多的資料包,超出了我們的頻寬限制,然後延遲就開始上升,最後所有的噩夢就開始了。

我們的解決辦法非常簡單而且有效。每一個包都包含上一個包的拷貝,這樣如果有一個包丟失了,那麼下一個到達的包就會送來上一個包的資訊。我們就又可以愉快的玩耍了:)。這需要大約之前頻寬的兩倍,幸運的是,我們本來所需要的頻寬就非常小,所以增加一倍我們還能夠接受。這個方法在連續的兩個包同時丟失的時候也會失敗,但是看起來不會發生這樣的事。如果它真的發生了,我們就用重發的機制。

這個辦法看起來非常奏效!我們最終讓遊戲在因特網上成功的跑起來了!當然因特網的情況比我們想象中更糟,但是我們還能夠處理它。

第五課:當你認為因特網的情況不會更壞時,它就真的變的更壞了。更加廣泛的測試顯示我們還有很多嚴重的問題。顯然,我們的重發機制程式碼裡還有一些bug,因為偶爾有一些玩家會出現丟失連線而且任何資料都無法被髮送的情況。在花費了無數個小時想從我們的程式碼裡找到bug的時候,最後發現我們的程式碼是沒問題的,反而是因為因特網斷開連線了。

有的時候因特網變得非常差,根本無法傳送任何資料包!我們記錄下在10秒到20秒之內只有3或4個包能夠被收到。難怪TCP這時會不再傳送資料,而是傳送檢查包!你在這種情況下怎麼玩遊戲?現在我們確實有一個大問題了。斷開連線這種問題我們確實沒有準備。

幸運的是,這種情況通常非常短,大約幾秒鐘左右。通過調整重發機制的程式碼就能夠處理這種情況。當玩家出現這種情況時,遊戲就停止下來直到重新連線了遊戲,一旦這種情況過去了,他就可以繼續遊戲了。

不幸的是,這種失去連線的狀態可能會持續很長世間,如果真的那樣,我們就無能為力了,最後我們只能把玩家踢出遊戲。這不算是一個真的解決辦法,但是至少一個壞的連線不會毀了所有玩家。

對於我們遊戲的最後一個改良是處理因特網帶來的對遊戲預測不準確的問題。由於延遲能夠變得非常高,需要一種方法來處理預測的遊戲世界拷貝與真實不符的情況。

我們的第一條線索就是之前為了提高效能,做出的存在主伺服器的設計。我們意識到如果每一個玩家都無法順暢的把資料傳送給主伺服器,那麼所有玩家都會延遲,因為主伺服器在沒有收到所有玩家的資料之前是不會向所有玩家傳送壓縮的資料包的。我們最終決定如果一個玩家超過一段時間還沒有把資料發給主伺服器,那麼主伺服器就丟棄掉這個玩家的資料,而把已經獲得的資料通過壓縮傳送給所有玩家。

如果你仔細考慮這個辦法的每一步,你將會意識到這使得遊戲變得非常糟糕。玩家總是非常精確的知道他們自己飛船的位置。畢竟,他們知道他們實際上進行了哪些操作,所以他們準確的知道他們應該飛到哪裡去。但是如果主伺服器的官方遊戲世界拷貝里丟失了他們的輸入操作,而那些操作又關乎他們在遊戲世界位置的準確性,最後,為了保持和其他玩家的同步,伺服器不得不改變這些丟失了操作的玩家的位置。最後的結果就是玩家本地的位置被改變了,這使得他們遊戲世界裡的所有東西的位置都發生了變化,包括星空也會變化位置。

這個被伺服器位移了的效果,被我們戲稱為“星空躍遷”,它非常的令人不安,因為它使得遊戲完全無法進行下去了。最終我們只能妥協,除非一個玩家的資料確實有非常高的延遲,否則我們不會丟掉它,這使得“星空躍遷”非常少見了。但是,後來我們發現,如果這裡也使用我們後來對別的玩家的處理方式的話,也許會更好。

這種位置的瞬間變化,或者叫做“星空躍遷”,在繪製別的玩家身時也經常出現,因為他們的位置總出現預測偏差。在延遲非常低的時候(比如少於200毫秒)這種變化就看不出來,但是隨著延遲的提高,遊戲世界預測的就越不準確,然後這種“躍遷”就變得非常明顯。

為了解決這個問題,我們實現了一種“平滑”的效果。這個平滑演算法記錄我們上一次預測的每一個玩家的位置。它把當前預測的位置更加靠近上一次預測的位置。這使玩家飛船的動作非常平滑,然後看起來非常好,即使是有點不準確也沒關係。

總結

總結非常明顯了:因特網就是個坑。我們對於我們遊戲在糟糕的因特網連線下的表現非常失望。但是回過頭來看,我們就像其他人做的一樣好,在各種限制之下我們努力奠定了遊戲的風格。

缺乏一個專門的伺服器最終變成了一個巨大的問題。在丟失連線的情況持續一定的時間之後,直接傳送整個遊戲世界的狀態要比重發所有丟失的資料包要容易一些。但這個辦法不實用,因為需要這麼做的只有一個玩家,並且無法節省頻寬。一個專門的伺服器可以解決這個問題,並可以提高讓一個玩家加入一場已經開始的遊戲中來的能力。“中途加入”是我們非常想要的一個遊戲特性,但是在沒有一個專門的伺服器時,我們還是感覺它不太實用。

一個專門的伺服器能夠支援更多的玩家。延遲也能夠減少一半左右,因為訊息在重發給別的玩家時不需要再通過調變解調器了,而在有主機玩家的情況下卻是需要。除此之外,一臺專門的伺服器能夠更加容易的認證連線進遊戲的玩家,因為他們只需要關心和伺服器之間的連線。當一個玩家扮演主機時,其他玩家必須關心主機玩家和因特網的連線速度,同時還有他們自己的連線速度。

我們網路模型面對的一個最大問題在於資料包需要按順序進行處理。那些接收到的亂序的資料包,本來能夠用來提高遊戲世界的預測精確度的,但是在“X翼戰機對戰鈦戰機”中是不行的。甚至它們的存在,也會帶來明顯的效能問題。問題在於當按照順序到達的資料包到達的時候,我們需要立刻處理它們,而同時也會有亂序的資料包到達。因為處理器會遍歷每一個資料包,所以這會帶來處理時間上的額外開銷。

如果我們一開始就考慮到上面提及的問題的話,所有的難題都會簡單許多。但是我們是在修改一個已經存在的遊戲引擎,我們被它的特性所限制。如果遊戲引擎對高延遲的處理能夠更加高效,那麼事情就容易許多了。事實上我們需要使用一種靈活的處理時間間隔,這使得在處理高延遲的時候非常低效。除此之外,如果引擎能夠利用亂序的資料來提高遊戲世界預測的精確度的話,一個高延遲,不停重發資料包的過程,就不會被注意到。

我們解決問題的辦法一個優勢是它完全獨立於遊戲邏輯之外。我們傳送的資料包只包含玩家的輸入操作,並且這種技術能夠不做修改就用在任何一種實時遊戲中。這個模型實現中最佳的一塊是我們不需要去擔心遊戲內容發生變化,而引起我們需要去修改網路模組的程式碼。事實上資料包中不包含遊戲的任何特定資料使得玩家在使用機器人去作弊上更加困難。為了獲得一種優勢,一個機器人必須要能夠比人更快的建立一串資料的輸入,在我看來這個在我們的遊戲中非常難做到。

目前Peter Lincroft是Ansible Software公司的主程式和董事長。他畢業於California大學電腦科學專業。他第一次成名於遊戲Pipe Dream,那是他在幾周內獨立完成的遊戲。他在Lawrence Holland公司的Secret Weapons of the Luftwaffe專案組裡繼續編寫他的第一款3D圖形引擎。後來他繼續和Lawrence合作,幫助他們建立了遊戲軟體開發公司,最終成立了Totally Games公司。他在1998年4月之前一直是該公司的技術長。後來他離開去創辦了自己的公司,Ansible Software。他開發的遊戲包括Pipe Dream,Secret Weapons of the Luftwaffe,X-Wing:Star Wars Space Combat Simulator, X-Wing CD, TIE Fighter, TIE Fighter CD, and X-Wing vs. TIE Fighter。這些遊戲賣出了3百多萬份,並且獲得了無數的年度遊戲大獎。TIE Fighter CD最近被PC Gamer雜誌冠以“PC史上最佳的遊戲”的殊榮。

相關文章