Hey,大家好!我是 Bill “LtRandolph” Clark,一名英雄聯盟的游戲工程師。許多 Rioter 工程師關注大量的內容需要直接發送給玩家問題——這是兩個我最近最喜歡的例子之一,包括最新的冠軍Jhin及項目重構的支持。而我的團隊使得這個過程變得又快有簡單。
我們有一個簡單的目標:即允許參加游戲試玩項目的暴民,能夠創建兩倍于任何給定的LoL(英雄聯盟)補丁的內容。這說起來容易,但是執行起來卻是一個充滿挑戰的任務。
今天,我們討論實現這一目標我們所鋪設的基礎:Riot 游戲數據服務器(GDS)。雖然這是一篇技術文章,但是我會站在一個較高的層次來解釋這個問題。如果你是一個在做多系統間傳送數據工作的工程師,我希望這能讓你特別感興趣。
游戲數據:
首先,我們了解一些背景。在LoL的工作中,存在兩種類型的游戲數據:一種是 key-value 對,被稱為屬性數據(如 Black Cleaver HP 獎金是300),另一種是不透明的二進制數據(如,大文本、動畫和視頻)。在這篇文章中,我們只討論屬性數據,二進制數據處理是未來潛在的一篇博文。
在LoL的所有歷史中,屬性數據由一堆松散、混亂的文件組成,這些文件存儲在一個大的名為 DATA 的文件夾中。
早期,我們將數據存儲在.ini的文件中(對,就是 Windows 下 .ini的文件格式)。類似如下所示:
當然,我創建這個例子是為了強調一些我們在編輯.ini文件時遇到的一些共同的問題。這離用戶友的界面相差甚遠。編輯原始文本時非常容易混亂——缺乏重要的內容,而其他字段又重復。設計者們每天不得不處理這種混亂,這里總共有 977 種法術,這些功能(當然忽略)位于“MissileEffect=AnnieBasicAttack_mis.troy”行中,在很早的LoL開發中,每個冠軍涉及一個令人愉快的場景:“Death=Cardmaster_Death.wav。”
下面是當前數據系統面臨的一些關鍵問題:
1、使用 Notepad++ 來編輯屬性數據
2、對已存在的字段沒有清晰的定義
3、缺乏類型安全
4、多人同時編輯同一個文件時會有合并沖突問題
5、繁瑣的并行版本(活躍(Live)版、公測版(PBE)及內部版本)
6、松散的文件鏈接;只有短名稱和隱藏的搜索路徑
游戲數據服務器
我們特意設計一個游戲數據服務器系統來定位這些問題。最基礎的是 RiotGameDataServer.exe——一段運行在每個開發者機器上的小程序。它以一個 Riot 拳頭形式顯示在工具欄,它所做的工作是連接屬性數據與計算機上的程序
GDS 為其他工具抽取出文件和數據的管理方式,所以這些工具只需要關注傳送過來數據的展示和編輯。我將其類比于操作系統窗口創建的抽象來思考,所以一個開發者只需要關注窗口該如何顯示即可。GDS 工具還包括許多內部開發工具,也有第三方工具,如 Maya 和 Photoshop。它們都通過基于 JSON 格式數據的 RPC API 與 GDS 進行通信。
關于一份整潔的 RPC API,我們可以很容易的通過使用一個名為Swagger的標準來生成一個文檔頁,它列出了所有的有效函數。這是 GDS 暴露的一小部分函數子集:
GDS 屬性數據存在一個名叫 PROPERTIES 的文件夾中。該文件夾最終將包含所有英雄聯盟的屬性數據。當一個工具需要識別出在什么情況下 Black Cleaver 是 Pantheon 最喜歡的武器時,它就會給 localhost:1300(GDS 監聽的端口)發一個 HTTP 請求。當接收到一個“get?path=Items/BlackCleaver”請求時,GDS 就會去 PROPERTIES/Items/BlackCleaver.json文件中查找。響應結果類似如下所示:
當某個工具想要改變 Black Cleaver 造成的傷害值時,該工具需要發送另一條命令到localhost(或 127.0.0.1)的 1300 端口上,這次需要發送的指令是“et&path=PROPERTIES/Items/BlackCleaver.FlatHPMod&value=1000.”。GDS 工具將從源碼控制工具(Perforce)中獲取到指定的文件,并編輯對應的值,然后將成功或失敗的結果返回給頁面。因此,任何工具都可以很簡單的修改屬性數據文件,而不需要考慮數據的格式,文件操作或者其他復雜的因素。
這樣,我們就很容易創建工具,如RiotEditor,來解決問題 #1:使用 Notepad++ 來編輯屬性數據。
屬性標記
對任何給定的類型,非常重要的一點是識別出其實際在的字段,這樣我們才能知道用戶可以編輯什么。為了完成這項工作,我們維護了一個環環相扣的宏與magic 模板集合,該集合允許我們在工程代碼里面直接標注類型。大概形式如下所示:
注意宏:PROPERTY_CLASS,PROPERTY_START,PROPERTY及 PROPERTY_END。它們負責兩項主要任務:
1、告知類定義了哪些出口,類的哪些字段應該是可編輯的。
2、告知屬性加載系統內存的偏移量,以便在運行時載入屬性值。
PROPERTY 宏可通過特定的簡單模板函數自動推斷類型。我們可以引用復雜類型,如BoundingVolume,只需要提供它們擁有的子屬性的標注。我們也可以跳過某些字段,如mRuntimeNumber,這意味著它們不會在 GDS 中暴露出來。
這是 GDS 中使用的 JSON 定義的結果:
屬性標記解決了問題 #2 和 #3:分別是對已存在的字段沒有清晰的定義和沒有類型安全。
層
GDS 除了為其他工具抽取出文件和數據的管理方式外,還做了一件相當酷的事情,就是為 Riot 開發工程師提供一項技術,我們稱之為“層”。一層代表了一個可以關閉或打開的功能,我們可以為一個新的冠軍,一種新皮膚,一個游戲模型或一次大的重新平衡創建一個層。然后,當一個內容創建者在某一功能上工作時,他們可以告訴 GDS,例如,“激活”APItemRework層。
之后,GDS 會對APItemRework層包含的文件任何改變打上標簽。在磁盤上,這看起來像一個文件,我們稱之為RabadonsDeathcap.json,挨著該文件的另一個文件,名為RabadonsDeathcap.APItemRework.json。在第二個層文件中,GDS 簡單的標記每個被改變的字段為 delta。保存前后兩個值就是為了解決之后的合并沖突。這兩個文件并排看起來如下所示:
由于我們捕獲了單個字段的改變,我們不再需要擔心多個 Rioters 同時修改同一個文件了,除非他們修改完全相同的字段。如果他們修改了同一個字段,我們也存儲了修改前后的值,所以可以識別出沖突。這樣做的好處是可以防止一個發包以后的bug:在創建 DJ Sona,團隊意外的將其狀態恢復到了上一個版本。
現在,我們解決了問題 #4:多人同時編輯同一個文件時的合并沖突問題。
分層,讓我們捕獲了某一特定功能的所有改變。為了真正發布這些功能,我們需要引入一個概念,叫做“游戲版本”。一個游戲版本定義了一個完整的打開層的集合。每個游戲版本保存了一個層名稱的簡單 JSON 列表。在任何給定的時間點,我們維護幾個主要的游戲版本:
1、Alpha:內部測試、準備發布到公測服務器上的功能集合。
2、Beta:當前公測服務器上的功能集合,如Jhin。要注意的是,Beta 版繼承了 Release 版的功能列表,所以它擁有最近更新的功能,類似于季前賽。
3、Release:當前正式服務器上的功能集合,如閃亮新補丁包 6.3。
還有一個很酷的特性是一個功能從一個版本遷移到另一版本只需要在我們層管理窗口執行一次拖拽操作即可,有了這個以后,我們不再需要在某個功能發生改變時,需將成百上千個文件從一個地方拖到另一個地方了。
這樣就很容易解決了問題 #5:全文件覆蓋的并發版本(活躍版、公測版及內部版本)
總結:
希望這篇文章能讓你體會到我們是如何使得LoL開發更有效率的。對于細心的讀者,你可能發現我們沒有深入討論問題 #6:松散的文件鏈接;只有短名稱和隱藏的搜索路徑。我留下這個問題沒解決是因為這個問題比預期的更麻煩——存在冗余、避免不必要的代碼重構、增加數據遷移、補丁大小等問題,因此,值得專門為此寫一篇完整的博客。
如果你對我們如何改變英雄聯盟中混亂的游戲數據的某方面感興趣,請務必在評論中告知我們。