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

摘要:縱觀過去 10 年的遊戲領域,單機向網絡發展已成爲一個很是大的趨勢。然而,爲遊戲添加網絡支持的過程當中每每存在着大量挑戰,這裏將爲你們揭示遊戲引擎網絡開發者的 64 個作與不作。html

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

如下爲譯文:編程

縱觀過去 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 一節詳述」。app

其次,一樣不會限制到某個特定的平臺:事實上,這裏更推薦開發者寫跨平臺引擎,其中就包含了網絡引擎。在實踐中,筆者也曾寫過一個網絡引擎,它能夠在 5 個以上徹底不一樣的平臺上運行,這點將在第六條中進行詳述。框架

再次,由於基於遊戲引擎開發者的視角,因此這裏有個背景是遊戲開發者常常須要爲他們的遊戲開發遊戲引擎。在這個狀況下,大多數建議都是適用的。socket

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

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 官方博客

相關文章
相關標籤/搜索