遊戲引擎網路開發者的 64 做與不做 | Part 1 | 客戶端方面

OneAPM官方技術部落格發表於2015-07-22

摘要:縱觀過去 10 年的遊戲領域,單機向網路發展已成為一個非常大的趨勢。然而,為遊戲新增網路支援的過程中往往存在著大量挑戰,這裡將為大家揭示遊戲引擎網路開發者的 64 個做與不做。

【編者按】時下,遊戲網路化已勢不可逆,因此,對於遊戲開發者來說,掌握網路引擎的打造技巧同樣不可避免。近日,Research Industrial Systems Engineering GmbH 安全研究員 Sergey Ignatchenko「擁有 20 年以上的工程經驗」在 IT Hare 上撰文,深入分享了遊戲引擎網路開發的相關經驗,由 OneAPM 工程師翻譯。

以下為譯文:

縱觀過去 10 年的遊戲領域,單機向網路發展已成為一個非常大的趨勢。然而,為遊戲新增網路支援的過程中往往存在著大量挑戰,而據近幾年的工作經驗「不僅參與了這一衍變,同樣也為大量開發者提供資訊支援」來看,許多遊戲開發者甚至都違反了「打造一個優秀網路應用程式」應該堅守的一些基本原則。因此,應用程式往往會面臨著「frozen」UIs、莫名的斷線「在其他程式網際網路訪問正常時」、不定期崩潰,以及峰值期間的伺服器過載等問題。毫無疑問,這些問題將直接影響到玩家的遊戲體驗,同時其直接程度也遠超管理員和圖形開發者的想象。慶幸的是,這些問題處理起來並不複雜,有些甚至是一點就明。

因此,這裡將通過一系列博文來佈道網路開發的某些理念,其中大部分是遊戲引擎開發者不曾留意的。當然對於某些朋友來說,有些觀點可能你已經接觸到了,但毫無疑問的是,它對大量遊戲開發者都是有價值的。因此,對於期望打造出類似遊戲或證券交易這類高互動應用程式的開發者,這些建議值得一讀。

作為系列的第一篇文章,這裡將著重討論不涉及協議的客戶端應用程式網路開發。本系列文章將包括:

  • Protocols and APIs
  • Protocols and APIs (continued)
  • Server-Side (Store-Process-and-Forward Architecture)
  • Server-Side (deployment, optimizations, and testing)
  • Great TCP-vs-UDP Debate
  • UDP
  • TCP
  • Security (TLS/SSL)
  • ……

範圍確定

總的來說,遊戲引擎網路支援是個非常大的主題,因此本系列博文將圈定一個範圍——聚焦擁有客戶端應用程式的遊戲,而不是那些基於 browser-/AJAX 的遊戲,雖然這兩種遊戲在設計上有著很多共同點,但是其中的區別也足夠讓討論分開。本系列博文將嘗試覆蓋遊戲網路層開發的常見理念:

首先,不會只聚焦某種型別的遊戲,比如 MMORPGs;毫無疑問, MMORPGs 確實在討論的範疇中,但是也不乏社交遊戲、多玩家戰略「包括實時和回合制」、賭博類遊戲、證券交易型等等。而出人意料的是,在做網路支援時,這些遊戲存在著大量的共同點。「儘管許多取決於時間控制問題,這點將在 Great TCP-vs-UDP Debate 一節詳述」。

其次,同樣不會限制到某個特定的平臺:事實上,這裡更推薦開發者寫跨平臺引擎,其中就包含了網路引擎。在實踐中,筆者也曾寫過一個網路引擎,它可以在 5 個以上完全不同的平臺上執行,這點將在第六條中進行詳述。

再次,因為基於遊戲引擎開發者的視角,所以這裡有個背景是遊戲開發者經常需要為他們的遊戲開發遊戲引擎。在這個情況下,大多數建議都是適用的。

最後,雖然類似「哪個引擎或者網路引擎是最好的?」這樣的問題已經超出了討論的範疇,但是本系列博文同樣對回答這個問題有所幫助;毫無疑問,答案取決於遊戲的具體需求,因此請詳細閱讀。換句話說:如果你的遊戲引擎或者框架提供了一個支撐網路的方式,這些博文可以作為一個工具對其進行考量,從而弄清其網路實現是否對特定的遊戲有益。

OK,在交代完大致的討論方向後,下面言歸正傳。

1. 在客戶端請使用事件驅動程式設計模型

當下,大多數客戶端 UI 框架都包含一個所謂的「main thread」,或者叫「main loop」,執行於「main thread」之中,而這個「main thread」本質上會處理一些特定的事件「最原始的是 UI 事件」。這種模型存在所有客戶端框架之中,從 Windows GUI、Direct X 和 Cocoa,到 Unity 3D、Android 和 iOS。同時,也確實有一個很好的理由來驅動大家這麼做:因為其他的程式設計模型只能給你帶來噩夢。事實上,在實際工作中,筆者也只碰到了一個「出格」的框架,即最初 Java 的 AWT,而在 AWT 中編寫 APP 的痛苦也眾所周知,有鑑於此,AWT 自始至終也沒有流行起來;實際上,谷歌也確實需要為 Android 開發新的 GUI 框架。

那麼,在給應用程式新增網路支援後,事件驅動模型究竟應該如何轉變?其實,這裡並不需要任何改變。實際生產中,所有遊戲網路通訊邏輯都由訊息傳送和接收構成;而每一個接收到的網路訊息都應該被作為遊戲事件驅動邏輯「除去傳統的 UI 事件,比如滑鼠和鍵盤輸入」的另一個事件。

通常情況下,這個操作可以通過給 main thread 的「message queue」注入一條 message 輕鬆實現。舉個例子,在 Win32 中,這個操作通常由 PostMessage()或者 PostThreadMessage()方法完成。如果你選擇的圖形框架不支援這個理念,你可能需要通過建立你的佇列並進行輪詢進行模擬「舉個例子,Unity3D2012」。對比在單執行緒中強制處理所有事件「同時包含 UI 事件和網路訊息」,將事件作為資料(win32)還是回撥這樣的問題並不重要。NB:如果使用 Unity,這個技巧很少會用到,因為 Unity 內建的網路「已經使用了 Unity 的事件處理執行緒」非常適用於「實時世界模擬real-time world simulator」遊戲;然而根據具體遊戲特徵,使用 Unity 網路做 UDP 傳輸也並不一定就是最好的途徑——特別是那些與實時世界模擬無關的遊戲。

在有些用例中,事件處理執行緒可能與選擇框架的「main thread」相去甚遠,但是這裡需要謹記的是,將所有與邏輯相關事件處理都放到同一個單執行緒中。然而,純通訊相關「與遊戲邏輯完全無關),比如 marshalling、en/decryption 和 (de)compres,儘可能在「main thread」外部處理,在下面的第 3 條中會詳細討論執行緒隔離問題。

2. 別在事件處理執行緒之外呼叫應用程式級別回撥

猶記那年,筆者還「很傻很天真」,那時候負責給一個證券交易業務開發網路框架「PS:別問我為什麼這麼重要的一個任務會交給一個沒經驗的工程師,筆者同樣無解」。開始的時候,新網路庫編寫的確實比較順利,但是在這裡,筆者同樣犯了一個原則錯誤——在應用程式層面呼叫了一個回撥「它本應該是 1 個回撥來響應 sendMessageOverTheNetworkAndCallbackOnReply()-style 函式」。這個蹩腳的錯誤曾一度給後續使用這個框架的同仁帶去了大量麻煩。首先,互動「以及潛在的 races」讓使用它的同事難以理解。其次,給 bugs 和 races 追蹤帶來了大量麻煩。最後,雖然並沒有太壞的影響,而且框架總體執行良好,但是如果沒有這個回撥,開發將變得更加平順。

數年後,筆者一直為大型多玩家遊戲開發網路引擎——同時線上玩家 50 萬,日訊息數 5 億條。而在吸取了之前的經驗後,避免了類似執行緒回撥,所有的工作都井井有條,同時在多平臺切換上也異常平順。

總結:如果你需要從網路層實現一個回撥到應用程式層,首先你需要將事件傳遞給事件處理執行緒「通常情況下就是 main thread」,隨後通過網路層庫呼叫「發源於事件處理執行緒」來處理事件,並在必要時呼叫應用程式級回撥。換句話說,下面才是一個完善的途徑:

network thread –inter-thread-communication –event-processing thread –network-library-call –application-callback –no-thread-sync-needed

而下面雖然可行,但是不利於他人長期使用:

network thread –network-library-call –application-callback –thread-sync-required

在完善的途徑中,回撥只存在事件處理執行緒環境中,這將顯著簡化應用程式開發。所有應用程式級處理都被嚴格確定,從而最大程度地減少 races 出現的可能,同時也減少了應用程式級所必要的同步。上面的過程聽起來可能比較笨重,操作起來也有些繁瑣,但是它可以切實地減少遊戲開發者的後續麻煩。

3. 別從事件處理執行緒呼叫可能阻塞網路的函式

這是網路開發者所有可以提交中影響最大的錯誤之一。如上文所述,你需要在一個單獨的執行緒中處理所有事件。這種操作得當且方便,但麻煩也因此產生,比如:在一個事件處理器中做一個簡單如 gethostbyname() 的呼叫,這個操作在小範圍中不會存在任何問題,但是在有些情況中現實世界的玩家卻會因此阻塞數分鐘之久!如果你在 GUI 執行緒中呼叫這樣一個函式,對於使用者來說,在函式阻塞時,GUI 一直都處於 frozen 或者 hanged 狀態,這從使用者體驗的角度是絕對不允許的。

因此,通過 GUI 來做網路互動時所有函式都應該是非阻塞的,或者位於不同的執行緒中。在這種情況下,你需要讓事件狀態機更加複雜,你可以效率地取得類似「waiting for DNS resolution」這樣的狀態,同時它還需要可以避免「frozen」GUI ,並且可以讓你處理網路延時,包括:

  • 在需要時通知使用者。舉個例子,在等待了 1 秒或者 5 秒後,你知道這裡出現了問題,使用者同樣需要知道這個事情。因此,你最好讓使用者瞭解到你已經發現了這個問題,並著手處理。
  • 在需要時終止操作並重試。
  • 允許使用者優雅地終止請求或應用程式,而不是逼迫他們去使用工作管理員。

需要注意的是,這點看起來似乎與第一條和第二條相違背,但事實上並不是這樣。對於「hey, so should I do it single-threaded or multi-threaded?」這樣的問題,答案是:系統級別網路呼叫要麼是非阻塞的,要麼是來自非事件處理執行緒;同時,所有事件處理必須在事件處理執行緒中完成。這就意味著,如果使用多執行緒,你需要在一個非事件處理的網路處理執行緒中呼叫類似阻塞 recv() 的函式,隨後將呼叫的結果轉換為一個事件,並通過佇列的形式「如上文 1 中介紹」將這個事件傳遞給事件處理執行緒。嚴格來講,decryption/decompression 就要進行這樣的處理,雖然需要去做避免事件處理執行緒成為一個瓶頸的流程,但它通常比只將 encryption/compression 扔到網路處理執行緒中來得更有價效比。

網路執行緒的另一個替代是 non-blocking IO,這裡同樣存在一些需要注意的地方,包括 gethostbyname() 和 getaddrinfo() 在主流平臺中並不存在 non-blocking IO 版本,同時筆者也不認為在客戶端使用 non-blocking 帶來的麻煩會更少。伺服器端將是另一種情景,詳情會在系列博文的第三部分伺服器端討論。

4. 不要將使用者作為免費的錯誤處理程式

在遊戲引擎開發中,很多開發者使用了一個異常簡單的網路錯誤處理途徑。也就是,他們簡單的將錯誤拋到使用者面前,只留下一句「伺服器存在一點問題,請重試」。這個做法是非常討厭的,並且不會帶來任何效果「當然輕鬆了開發者,但是損害了使用者」。除下開發人員太懶,不存在任何理由不將問題在內部解決。在問題產生並給使用者提示後,沒理由不自動重試而要求使用者再次操作。為了通知這個問題,你可以在螢幕的顯著位置進行顯示,或者是彈出一個對話方塊「沒有ok這個按鈕,只有關閉」,同時將在問題解決後自動消失。這樣一來,在問題產生使用者離開後,如果你能短時間解決問題,你不會對使用者體驗產生任何影響。

有人可能爭論不停重試會造成網路阻塞,但是作為一名開發者,你有責任讓使用者體驗變得簡單。當然,你也可以設定一個臨界值,比如 5 分鐘來關閉重試,並提示「對不起,我們已經盡力了,但問題在短時間內無法得到修復」。

綜合上面的 1-3 條,你通常需要在網路處理執行緒中檢測問題,並將它轉換成 1 個事件,並在事件處理執行緒中處理事件,比如顯示一個對話方塊。

5. 為使用者提供有價值的錯誤訊息

從終端使用者的角度來看,「網路不可用」、「連線拒絕」以及「連線終止」沒有任何區別;如果可能的話,你可能是想告訴他們網線未插入或者是伺服器故障或者是兩者之間的一些問題,但是僅僅因為一些專業用語讓使用者無法確定問題真相是完全不可取的。更糟糕的做法是,試圖將技術細節隱藏於一些模稜兩可的話語之間,比如「伺服器有一點問題」和「你丟失了連線」。

總之,切記將錯誤訊息從你能理解的語句轉換到使用者能理解的提示,而不是讓使用者無法辨別各種提示間的區別——讓所有訊息看起來完全相同。

6. 支援多平臺

縱觀當下遊戲領域,單平臺遊戲已經不再有吸引力。即使引擎只為一款遊戲打造,但是你又真的能確定遊戲未來不會過渡到其他平臺?實踐中,讓網路程式碼跨平臺並不是一件難事,因此你沒理由不做多平臺的準備。筆者個人的網路庫就覆蓋了 Windows、Linux、Mac OS X、FreeBSD、iOS 等引擎。

6a. 在客戶端使用 Berkeley Sockets

如果你的遊戲引擎是基於 C 或者 C++,並且將應用程式定義為只 Windows 平臺,那麼就可能嘗試一些 Windows 特有的函式「那些以 WSA*()為字首的」來通訊。請不要這麼做,轉而使用 Berkeley sockets「那些 socket()/connect()/send()/recv()函式」進行取代;關於使用細節,請自行 Google。對於其他提供了跨平臺 APIs 的程式語言,選擇一個合適的網路庫通常不會有太多問題。

7. 提供一個自動升級 APP 的途徑

通常情況下,自動升級並不會考慮為遊戲引擎的一部分。然而,個人覺得將自動升級納入網路層會有一些相應的好處,其原因是:

  • 使用者可能期望多一些選擇「從主題到 DLCs」。
  • 如果可以邊玩邊下載,那麼他們會很開心。
  • 如果下載干涉到娛樂,那他們肯定會不開心。
  • 在你的網路層提供可選下載,你可以優先考慮流量,將下載對遊戲的影響降到最低「在本系列博文的 Part IIb 一節將討論更多技巧」。因為 QoS 並不適用於網際網路,兩個並行的連線很可能產生相互影響。
  • 如果你支援可選下載,他們同樣需要自動化更新,因此結合自動下載和自動更新是件不錯的事情。
  • 因此,在網路引擎實現整個自動更新功能是個不錯的事情。
  • 此外,使用者可以邊玩邊下載,從而最大化了娛樂時間。
  • 上面的推理並不是在任何條件下都成立的,但是我看到了類似系統的實現,同時也取得了非常好的效果。

注意:儘管與網路庫整合,你同樣需要在 HTTP「而不是基於你的協議」上實現初始自動更新「會在遊戲應用啟動前啟動」;這麼操作並不會帶來太多的複雜性,但是很可能會徹底地修改你的協議。

其他在啟動更新上的操作是非常複雜的,因此會單獨開一篇文章來表述、

To Be Continued……

原文連結:64 Network DO’s and DON’Ts for Game Engine Developers. Part I: Client Side

本文系 OneAPM 工程師編譯整理。OneAPM 是應用效能管理領域的新興領軍企業,能幫助企業使用者和開發者輕鬆實現:緩慢的程式程式碼和 SQL 語句的實時抓取。想閱讀更多技術文章,請訪問 OneAPM 官方部落格

相關文章