遊戲開發中的多語言文本管理2

AiurTemplar 2024-03-29 15:46:38

前一陣子在做新項目的時候想了一下目前的項目在本地化文本管理方面遇到的諸多問題,正好又碰到了一個新朋友來問這個問題,翻出了7年前我寫的一篇博客,現在回頭看的話感覺當時寫的太簡陋了,因此決定重新分享我的個人經驗,在多語言文本管理中遇到的問題,以及我建議的解決方法。

中心化管理:看起來很美

市面上最常見的多語言文本管理的手段是中心化的管理手段,即多語言文本完全脫離其所被使用的場合,作爲一個單獨的表單或文件存在,任何需要顯示文本的地方,都以key爲唯一的索引從這個文本中來獲取對應的本地化語言的文本。

在舉一個具體的例子之前,我們要明確一個觀點:一個項目中的文本主要分兩種:UI文本和數據文本。UI文本指的是界面上各種控件顯示的文本,數據文本則是需要策劃管理的遊戲數據的文本。除此之外還有第三種文本:服務器文本,這個屬于比較特殊的情況,不在下面的例子中討論,文章結尾的地方會專門說明。

假如我們在做一款RPG遊戲,有英雄和技能兩個表,最終導出了三個文件,那麽一個典型的中心化管理的多語言文本可能是這樣設計的文件結構:

hero.json

skill.json

text.json

我們只看hero.json和text.json來說明問題。

假設一個英雄非常簡單,只有兩個屬性:名字、技能,那麽英雄的數據可能長這樣:

hero_id = 1

hero_name = "text_hero_name_001"

hero_skill = [1,2,3]

本文重點關注的即是hero_name的部分,這是需要多語言管理的部分。顯然"text_hero_name_001"是一個key,其指向的真實文本儲存于text.json中。

那麽text.json長什麽樣?如果我們用同一個text.json來存儲全部多語言的文本,那麽可能長得類似這樣(Dictionary/Object風格):

text_hero_name_001 =

"zh_CN" : "王老王",

"en_US" : "King Old King",

{

}

也可能是這樣(CSV風格):

key,"zh_CN","en_US",

text_hero_name_001,"王老王","King Old King",

也可能是每個語言對應一個text.json文件,這種就不再舉例贅述,但不管哪種,本質都是一樣的:文本脫離于其所被使用的環境,進行中心化管理,並且在被使用的地方通過Key來索引。

這看起來沒有任何問題,也是業界的主流做法,但在我這些年遊戲開發的經驗中卻覺得這樣做的問題非常大,甚至可以說這都是程序思維的産物,能實現需求但卻基本沒考慮策劃和UI的維護成本,下面就開始詳細的闡述這種方式的弊端。

問題一:脫離使用環境

假設你是一個UI同學,現在做了一個簡單的彈出層,有標題、正文和確定按鈕,你需要三個文本來描述這個界面:

標題:確認充值?

正文:做遊戲不賺錢,就是交個朋友。確認充值648鑽?

按鈕:確認

那麽按照上述的管理方式,界面上可能是這樣的:

標題:text_dialogue_title_iap_001

正文:text_dialogue_content_iap_001

按鈕:text_dialogue_button_iap_001

一個UI同學在UI編輯器或遊戲引擎中長久的面對這種不可讀、不可排版的文本,長此以往對心靈會産生什麽樣的打擊不言而喻——當然,這跟程序沒關系,程序也不在乎。

假如UI抱怨的非常凶,要求程序在編輯環境中也要默認把這堆看不懂的key給顯示成簡體中文,而程序勉爲其難的也給做了,那麽至少解決了UI同學的問題,但前文提過,UI文本只是一個項目中的一部分,還有另外一個大頭:數據文本。

還是剛才的hero.json的例子,假如我們這個RPG項目有100個英雄,那麽我們在hero.json中能看到的是什麽呢?是任何一個英雄你都不知道他的名字是什麽,只能看到從text_hero_name_001到text_hero_name_100這種意義不明的東西。

作爲一個策劃,你不太可能記得住一個英雄的ID,反而記住他的名字要容易得多。那麽現在你要去找一個英雄的數據,你要怎麽辦?你可能要去text.json中找到這個英雄的名字,再看對應的key來識別他的ID,再去hero.json中找到這個英雄的數據。

這時候假如有個策劃不那麽講究,沒按照規範去好好的給hero_name對應的文本建key,並且當時他偷摸就提交了,誰也不知道,本來應該叫做"text_hero_name_100",但這個策劃給起成了"text_hero_name_wanglaowang",那你找起來就會想殺人了。

這時候還有更大的問題,如果一個策劃想要知道一個英雄有什麽技能,那怎麽辦?因爲技能表裏也是不帶名字的,他就只能先想辦法找到英雄的ID,然後再找到技能的ID,然後再去看技能的名字。

這種套娃操作每多一層,策劃的心理都會多一層崩潰。

除此之外,脫離使用環境還使得你的項目需要翻譯外包的時候看似容易,只要把text.json丟過去就行了,但因爲翻譯的人員不知道這個文本是用在哪裏的,因此很容易出現翻譯完了結合上下文意義錯誤或UI的顯示出現異常的情況。這部分的溝通成本也是相當巨大的。

問題二:單一職能原則

假設我們還是上面那個做充值對話框的UI同學。這個項目肯定不止這樣一個對話框,所以我們還會做很多很多其他的對話框,比如購買各種東西的確認對話框、各種危險操作的確認對話框、需要選擇個數或者填寫內容再提交的對話框等等。

做多了之後我們就會發現,這些對話框都有一個確定按鈕,而且按鈕的文本內容可能都一樣,都是“確定”,但卻每次都要起一個新的text的key。當這個UI同學打開了text.json後,發現有幾十上百個不同key的文本都叫“確定”的時候,他一定會懷疑這種做法是不是有問題。

最後UI們商量了一下,決定所有的確定按鈕都用一個key,比如這個key叫做“text_dialogue_button_confirm”。大家覺得工作簡單多了,所有的確定按鈕都用這一個文本,不用再弄大量臃腫的文本了。

直到再過了一段時間,策劃提了一個需求,充值界面的確認按鈕不能只寫“確認”,要寫“確認,我家有礦”。策劃認爲簡單的改文本就行,于是就把“text_dialogue_button_confirm”的內容給直接改了。

改完了之後發現項目出了大問題,所有的對話框的確認按鈕的文本都變成了“確認,我家有礦”。然後QA爸爸就提著刀過來了。

QA爸爸把這個問題捅到了項目主管那裏,等到項目主管發現這個問題之後,決定檢查一下所有的text.json裏面的文本,要確認下到底有哪條key是不止被用了一次的,但卻發現很難做到這樣,因爲項目進展到這個階段,text.json裏面可能已經有上萬條數據了,你既沒法簡單的知道裏面有沒有完全沒被引用實際上已經冗余了的條目(比如刪了一個技能,但技能的名字沒刪),也沒法確認裏面的某一個條目有沒有被多次引用(類似上面的確認的問題,策劃和美術都會有這個情況),從而導致改動的時候引發意料之外的問題。

當然,你可以硬性的規定,凡是要複用的text都放到同一個文件裏(比如它叫general_text.json),凡是應該被用且應該只被引用一次的text都放在text.json裏,但畢竟只要key是人爲的手寫的,就無法完全避免這個問題,很快兩個文件裏面可能又會出現混亂——text.json裏面出現了被引用多次的key,而general_text.json裏面出現了冗余的key。

假設這時候有一個程序同學覺得可以從工具上來入手解決這個問題,于是他寫了一個工具,從此text.json的key都是自動管理的了,策劃同學可以方便的在各個數據表中添加、修改、刪除文本,key的關聯都是自動完成的,或者說text.json的key完全都由這個工具自動管理了,key甚至對策劃來說是不可見的,從而從根本上避免了策劃手誤的問題。

這聽起來很美好是吧?但其實並不是的,因爲這個方案依然有問題。

從原則上來說我個人是非常不建議項目在常規的開發工作流中加入一些自己開發的工具的,除非是在這方面有相當豐富經驗的開發團隊,或者這個工具本身足夠簡單、不進入工作流(比如用了一次就不用了的“日抛型”工具)。原因有三點。

首先是這個工具的可維護性問題。這些工具在開發之時只是爲了盡快的解決某些小問題,並沒有嚴格的當做一款可以交付使用的産品來對待,因此缺乏文檔,並且開發的也相對隨意(用戶是內部開發人員而不是遊戲玩家)。這個工具本身需要測試但缺乏測試,它的完整性和可靠性需要支付相當大的成本,而這部分成本本來是可以去開發玩家可以體驗到的功能的。這還只是短期的問題,如果我們從長計議的話,短期少做點功能,把工具做好,也沒什麽問題,但長期問題實際上更大。當這個工具被用了幾個月甚至幾年之後,不出bug的概率幾乎等于0,而當初寫這個工具的人可能早已不在這個項目組甚至離職了,而由于缺乏文檔且開發隨意,接手維護這個工具的人可能完全無法上手,這種情況下項目就陷入了一個兩難的境地。還有很多情況下,隨著工作流的調整或人員的變化,慢慢的這個工具也會被不斷的叠代,但其叠代的速度往往是滯後于團隊的變化的,甚至最終成爲拖後腿的卡點——你不得不用它,而你又明知道它已經不好用了。

其次是這個工具的可靠性問題。這些工具所依賴的開發環境過于複雜,公司的一次停電、遊戲引擎的一次升級、一個髒數據的寫入、一個策劃不小心的誤操作(比如把ID填重複了),都有可能産生大量的問題,這些問題是最早做工具的同學所意料不到的(大家都知道,程序員是樂觀的),而爲了解決這些問題所有的策劃都不得不停工,並且問題解決的代價和效果也不得而知(比如可能導致json中的所有排序都變了,雖然實際上沒變化因爲json裏面排序不重要,但QA爸爸能看到的就是一個上萬行的json文件的每一行都變了,而導致QA爸爸提著刀過來)。

最後一旦你開始依賴這種小工具後,往往會接二連三的做一大堆工具,前面的兩個問題很快會變成多個問題,很快就會陷入按下葫蘆起了瓢的狀態,甚至當出了問題之後你都不知道究竟是哪個小工具的鍋,開發工具的人抱怨使用工具的人提的問題模糊而不確定(甚至懷疑是使用者自己的問題),使用工具的人抱怨開發工具的人做的破玩意不靠譜,最終所有人都難受。

因此,如果你必須要把某個工具加入工作流中,那麽如果有外部的、成熟的、有長期維護的解決方案,盡量不要自己造輪子,甯可花點錢買解決方案,也比自己花精力去做這些事情要強,否則當你過了幾個月甚至幾年後,一定會爲當初自己的決定後悔——假如你還在這個項目組的話。

要相信你的問題別人都早就遇到過了,用別人造好的輪子總是比自己造輪子強,人類社會就是這麽進步的。

問題三:依賴性問題

假如你是一個策劃,已經設計好了一個新的英雄,以及他的技能,現在要開始配表了。

既然要加英雄,那麽理所當然的你打開了hero.json,但是當你加到一半的時候發現加不下去了,因爲hero_name需要一個key,你必須先去text.json裏面配置好這個文本,再填回到hero.json中來才能完成配置。提交的時候你必須同時提交這兩個文件,否則英雄的名字就會顯示錯誤,顯然這是會被QA爸爸暴揍的。

等你搞完了之後又發現這個英雄要加技能,那必須又先去配技能表。而配技能表的時候又發現了必須要去先去text.json裏面寫好技能的名字。

而現實情況中這個套娃的情況往往更嚴重,以我目前的項目爲例,我需要新加一個寶箱,這個寶箱有【名字】有【描述】,這個寶箱有一個對應的道具ID,道具有【名字】有【描述】,這個寶箱裏面是一件新時裝,這個時裝有【名字】有【描述】,這個新時裝有對應的道具ID, 道具有【名字】有【描述】,這個新時裝有對應的時裝碎片,這個碎片有【名字】有【描述】,這個碎片有對應的道具 ID , 道具有【名字】有【描述】。等這一套折騰完,你會發現最耗費精力的不是寶箱->時裝->碎片的套娃,而是不管你幹啥都需要去text.json裏面加名字,而這對于策劃來說也是非常痛苦的。

當然,在刪除配置的時候策劃也會面對同樣的噩夢,要刪一個東西就要刪大量對應的text,而這種刪除操作的危險性在上面的一個問題中已經提到過了,因此最終往往會演變成“冗余就冗余,只加不減就好了, 這樣至少不會出錯”的情況。這會導致外包成本急劇增加,因爲你也不知道哪些文本有用,哪些文本沒用的。

更嚴重的是這會讓你的項目很屎,而這屎不是餵給玩家的,是餵給策劃的。大家都知道這裏面充滿了屎,但誰也沒法認出來究竟哪些是屎。一個敢餵自己屎的策劃,對玩家能做出多麽喪心病狂的決策都是有可能的。

當一個東西變得反直覺的時候,大概率有更好的方案可以去替代它。

問題四:沖突問題

極端的情況下,不管你改客戶端的什麽表,都需要同時的去改text.json。而text.json只有一個,所有策劃共用,因此沖突的幾率極高,同時類似SVN的版本管理工具對json文件的比對支持的也不是特別好(你可能需要專門的json語意比對工具),于是策劃的作業變成了一場噩夢,你只是想把自己改的東西提交上去而已,但卻發現這竟然如此困難。

而更大的問題是,你不只是提交就完事了,你還需要合並呢,合並的時候更是一場噩夢。

雖然靠培訓每個人都要學會提交、合並、解決沖突可以來克服這個困難,但畢竟這個困難實際上……有可能壓根就不存在,而只是因爲一開始設計的偷懶導致的沒必要的困難。你讓程序把所有代碼都寫在一個文件裏他們自然不幹,那爲什麽策劃和UI的所有文本都在一個文件裏他們就幹了呢?——因爲不用他們維護,這是個屁股決定腦袋的問題。

問題五:唯一性問題

由于大量的文本都堆在一個文件裏,爲了保持key的可讀性和唯一性,命名就成了一大難題,其困難程度甚至比美術同學考慮美術資源的命名還困難,因爲美術資源可以分文件夾,但key都堆在一起。

想找到一套完美的可以描述所有東西的key的方法不是不行,但這種結果大概率會讓key變得非常冗長。比如美術可能考慮把界面的名字或者prefab的名字加進去,策劃則要把是哪個表的哪個id的哪個字段用到的加進去。很快你的項目就會出現一個神奇的現象:大部分的字符串的key甚至比其本身的內容還要長的多的多,整個text.json文件奇大無比,但“三斤鴨子兩斤嘴”,肉沒多少。這種情況的體驗就像是大家上班的時候互相不叫昵稱也不叫姓名,而是互相喊身份證號的完整號碼來互相溝通一樣,顯得非常蠢。

那怎麽辦?

解決方法我覺得很簡單,去中心化,直接對症下藥,需要完成以下幾個需求:

文本不再中心化全放在一起,而是分散開來,根據各個模塊分散到各個文件中,並且把程序設計成只能訪問自己相關模塊的文本。這樣即能解決一個大文件策劃互相沖突的問題,也能解決濫用key的引用導致無法追蹤每個key都被哪裏用到了問題。一言以蔽之,把text從全局的改成本地的。

文本盡量貼近其所被使用的場合,甚至可以完全免掉key是最好的。這樣既不用費勁去給key取名字,也不用去琢磨key的冗余或被多次引用的問題了,也不用去開發勞什子自動關聯key的工具了。

于是hero.json會變成什麽樣子?大概會變成這樣:

hero_id = 1

hero_name =

"zh_CN" : "王老王"

"en_US" : "King Old King"

{

}

hero_skill = [1,2,3]

回頭一看,這其實就是我七年前貼的文章中《爐石傳說》的做法。暴雪在《星際爭霸2》中采用的還是中心化的管理辦法,而《爐石傳說》他們選擇了另外的做法,我猜是他們吃屎吃夠了。

如果策劃用Excel來管理多語言的話,需要寫插件,否則一個格子裏面寫Dictonary/Object風格的東西會很蛋疼;或者策劃可以在多列中配置語言,但最終把多列導出成Dictionary/Object風格的內容。但是畢竟長痛不如短痛,總比搞好幾個表來回貼key要舒服得多。

有人可能說了,這樣策劃要每次都去寫"zh_CN"和"en_US"這種文本,不很容易錯嗎?但你想想,是寫這樣固定的文本容易錯,還是每次都要依照一個規則去想一個新的key更容易錯呢?

還有人可能會說,這樣客戶端不管當前語言是什麽,都會加載全部的語言文本,有點浪費資源。但根據我個人的理解,占內存大頭的永遠不會是文本文本,而是二進制文件,因此這方面的性能問題也可以忽略不計。反而這可能成爲一個好處,即切換語言不需要重啓客戶端更容易實現了,但畢竟我不是專業的程序,這裏可能還涉及到加載字體等問題,這裏要視項目最初的需求是否需要不重啓客戶端就能切換語言了。

好了,策劃的同學解決完了,那麽UI同學怎麽辦?

比較簡單的做法是擴展一個支持多語言的控件,比如原本系統的文本控件只能輸入一套文本,讓程序擴展一下可以寫多套文本即可,同時默認顯示中文。比如一個確定按鈕,本來UI拉上來一個button後直接在text裏面寫“確定”就行了,現在的話會有多行的text,其中第一行是"zh_CN"的,UI同學要在這裏寫"確定",然後在第二行"en_US"裏面寫"Confirm"。同時還可以支持切換預覽多語言效果。實際上很多多語言插件都是用類似的思路去做的,比如Unity的I2 Language(這裏不是打廣告,我沒用過,但看demo感覺挺好用的,還支持圖片和音頻的多語言,而且這也是Unity Asset Store中最受歡迎的本地化插件)。

UI文本本地化之後的一個問題就是外包的時候我們還是需要導出文本,因此在一開始設計結構的時候需要盡量設計一個不依賴于Key同時還可以方便導出導入的結構,單獨把每個界面prefab的文本導出,翻譯完成後再導回來即可。上面提過的I2 Language貌似還支持谷歌翻譯以及導入導出功能(這插件本質上還是中心化管理文本的,只是界面上隱藏了),感興趣的同學可以自己試試。

但是以上畢竟只是腦洞,我決定在新項目中試一下用這種方式來管理文本的實際效果如何。因爲都是我一個人做的,所以UI的多語言文本我決定不寫在控件上,而寫在代碼裏,一個prefab的代碼把需要用到的文本統一在一個地方以Dictonary/Object的風格來聲明好,方便後面調用。而策劃配表的部分,我決定在Excel中每個文本一列,再導出的時候把同一個key的文本合並成Dictonary/Object風格的變量。

這裏實際也引申出了另外兩個關于UI實現問題。

第一個問題是Prefab這個東西往往是客戶端程序和UI同學都要打交道的,這裏非常容易出現問題,比如UI同學一不小心把某個節點隱藏了,QA同學提了BUG,程序同學絞盡腦汁的去調查問題最終發現居然不是自己的鍋。解決方案業內也有一些,有的比較笨的辦法是由程序同學來拼界面,這樣避免UI同學染指Prefab,但拼完了難免坐標不對,UI同學要再調整一下;有的方法則是參考策劃的工作流,把UI的編輯過程獨立于遊戲引擎之外,把界面當做是資源文件導入到項目中,從而實現了UI同學和程序同學作業對象的分離。也有其他的做法,比如對UI設計師和UI程序員的要求更高,雙方必須緊密的結對作業,而不是各自分屬不同的部門。還有的做法是UI在PhotoShop裏面做完了工作後策劃來拼界面,把和程序對接的工作交給了策劃,從而UI不用考慮上傳SVN的問題。還有的做法是逼著UI同學去學寫代碼,比如至少會用個藍圖啥的,從而避免程序同學染指Prefab。反正各種奇奇怪怪的做法都有,哪種是最好的不好說,要視團隊和項目的具體情況而定。以上的種種方法中我個人最傾向的是哪種?如果是UI表現十分重要的遊戲,那麽提高對UI和程序人才素質的要求,強制其結對作業共同對UI的結果負責,是比較好的解決手段,能夠實現質量和性能都比較出色的UI界面,但這樣對人才的要求很高(相當于半個TA),大部分的團隊不一定能做到,這種情況下另外一個選擇是退而求其次,將UI的設計工作交給策劃,只保證功能性但不保證美觀度,在人力捉襟見肘的小公司/獨立團隊或功能無限龐雜的大項目中這也是個不錯的方法。不管哪種,其本質都是要適應UI工作的特殊性,這不是一個可以簡單的“美術vs程序”二元割裂的工作,而是必須有機的結合在一起才能完成的綜合性工作,一切想著切割UI和程序的工作流的方案都注定不是最優的。

第二個問題是界面的很多狀態和內容應該是以資源驅動爲主導還是以代碼驅動爲主導?比如一個界面默認狀態是應該隱藏的,那麽是UI同學把隱藏給勾上比較好,還是程序同學在代碼裏初始化的部分寫上強制隱藏好?按照我的傾向性的話,如果不影響性能的前提下,我傾向于用代碼來控制。UI資源本身畢竟是靜態的,因此凡是動態的東西都應該由代碼來控制,這也是爲什麽多語言文本我覺得寫在代碼中比挂在界面上要好的原因,因爲涉及到多語言,本身就已經不是靜態的文本了。

祝我好運吧emmmm,前途未蔔,不知道還會踩到多少坑,但總比現在的情況舒服。

(另外別問我爲什麽我不用I2 Language,因爲Godot天下第一)

寫在最後:關于服務器文本

所謂的服務器文本,即並列于UI文本和數據文本之外的第三類文本,這種文本常見的是停服公告、發給玩家的郵件、緊急維護通知跑馬燈等種種由于具體寫什麽文本完全無法預估而只能在服務器端設置文本,客戶端收到什麽就顯示什麽的文本。這類文本大部分屬于運營工具的範疇。

那麽這類文本如何實現本地化?

這首先要看遊戲服務器的結構,是各個語言的用戶都混在一個服務器裏面玩?還是每個語言的用戶自己有自己的服務器?如果是後者的話那麽很簡單,每個語言的服務器發對應語言的公告就好了(甚至可能是完全不同的運營商),如果是前者那麽這涉及到另外一個問題:客戶端能否切換多語言?如果能的話,比如我用英文客戶端登錄之後,系統發給我的郵件可能是英語寫的,這時候我切換成中文客戶端重登後,看到的這封郵件是英文的還是中文的呢?確定了這個需求之後,才能設計服務器端文本的多語言是應該如何處理的。

只要提前考慮好,程序做起來都是很容易的。難的都是已經在線上跑了一陣子了又不得不改,這才是最難受的。

0 阅读:6

AiurTemplar

簡介:遊戲制作人