最近,我在抄王者荣耀玩。而多国语言自然是一个避不开的问题。
在这次的实现中,UI系统我采用了FairyGui。
FairyGUI通过“字符串表”和“分支”功能提供了多国语言解决方案。
FairyGUI把用到的所有文字导出到一个xml文件中,然后为每个外国语言翻译一个相应的xml文件(字符串表),只要在运行时加载相应的xml文件,就可以将所有UI上的文字自动切换到相应的语言。
当然多国语言不止有文字,还有图片等资源。FairyGUI可以为每个外国语言设置一个分支(假设所有外国语言都需要使用不同的图片),每个分支上可以使用不同的图片、布局等,只要执行UIPackage.branch = "en";,打开UI时就会相应分支的UI。这样就实现了非文字类多国语言的实现。
这案相当惊艳,对我有不小的启发。
虽然FairyGUI已经有了相当完美的多国语言方案,但是游戏不仅仅只有UI,一些非UI部分同样会有多国语言的需求,比如不同国家玩家看到的英雄的Tips, 一些3D素材等。
因此,业务逻辑还需要一个自己的多国语言模块,这部分我6年前就实现过。
但是,FairyGUI的多国语言方案让我意识到一个事实,多国语言应该是一种和业务逻辑无关的需求。因此理论上,业务程序员不需要关心多国语言的存在,仅策划和美术关心就够了。
之前的设计中,所有多国语言的显示,都需要业务程序员干预,比如写一条local str = multi_lan.get(key)
。
如果显示多国语言的UI设计有变动,业务程序员就要相应的增加或删减multi_lan.get
语句。
这违反了Open-Close原则,同时增加了业务程序的心智负担。
我希望这次可以更近一步,业务程序员完全不需要干预。
我重新思考了一下整个游戏的运行流程后发现,屏幕上的所有显示内容其实都是来源于配置。游戏逻辑代码中不需要也不应该去写死每一个字符串(文字或资源路径)。
这样只要我们从配置入手,就可以在底层彻底解决多国语言的问题。
虽然,新的设计要比之前复杂很多。但是,大部分开销和复杂度都在离线(打表)逻辑,运行时代价不高。
按以往的经验,程序需要的配置表往往不是策划直接操作的第一格式。
策划大都是先用Excel来配置相关数据,之后再使用程序提供的配置生成工具,将Excel文件转换为程序使用的配置表格式。
这次多国语言的主角就是这个配置生成工具。
由于Excel的特性, 一般在使用Excel文件作配置表时,都会使用关系型数据库的思路来操作。即先设计表结构,再填充表内容。
比如一个Hero表的Excel文件格式可能是下面这样:
HeroID | HeroName | HeroTips | HeroSpeed |
---|---|---|---|
int(a) | string(c) | string(c) | int(s) |
1000 | 刘备 | 刘皇叔是大哥 | 100 |
1001 | 关羽 | 关云长是二哥 | 50 |
1002 | 张飞 | 张翼德是小弟 | 50 |
第一行用于标识每一列数据的字段名。
第二行用于标识每一个字段的类型(int, string), 以及是被客户端(c、a), 还是服务端(s、a)使用。
第三行开始(包括)就是真正的配置数据。
按照之前的多国语言思路,整个流程大致是这样的。
先将上述Hero表拆分为Hero表和Language表。Hero表用来配置Hero相关信息的,Language表用于配置多国语言信息。
Hero表:
HeroID | HeroName | HeroTips | HeroSpeed |
---|---|---|---|
int(a) | string(c) | string(c) | int(s) |
1000 | HeroName_1000 | HeroTips_1000 | 100 |
1001 | HeroName_1001 | HeroTips_1001 | 50 |
1002 | HeroName_1002 | HeroTips_1002 | 50 |
Language表:
Key | Value |
---|---|
string(c) | string(c) |
HeroName_1000 | 刘备 |
HeroName_1001 | 关羽 |
HeroName_1002 | 张飞 |
HeroTips_1000 | 刘皇叔是大哥 |
HeroTips_1001 | 关云长是二哥 |
HeroTips_1002 | 张翼德是小弟 |
然后在需要显示HeroName和HeroTips的地方(即使HeroTips是资源路径同样适用)使用如下代码:
代码语言:javascript复制local id = 1000
local name = Language[Hero[id].HeroName]
local tips = Language[Hero[id].HeroTips]COPY
上述工作流有不少弊端:
1. 程序不满足Open-Close原则,业务程序员需要关心哪些字段是需要做多国语言处理的,一旦UI改了表现设计,可能需要程序修改代码。相比之下,改了UI表现的FairyGUI却不需要修改业务逻辑代码。
2. 对策划不友好,策划需要手工维护Hero表和Language表之间的同步,人类极不擅长这类工作。这使用两表之间同步极易出错,而且不易发现(只有在运行时用到配错的那一行数据时,才能发现错误)。就算是使用自动化测试都不太可能覆盖表中100%的数据。
3. 由于人类极不擅长Hero和Language表之间的同步,导致策划在修改Language表时,往往只增加不删除,这会导致Language越来越大。以我的经验来讲,这种情况是存在的,尤其是对于半路接手的策划。
4. 由于所有跟文字相关的内容都被移到Language表中,这导致Hero表的可读性下降,往往打开一个Hero.xls之后,你找不到“刘备”是哪个。策划们的通常做法是再加一个字段, 这个字段即不用于客户端也不用于服务端, 仅用增加可读性。但是,增加了一个字段的同时,又增加了维护数据之间同步的工作量,出错的概率更大了。
在新的多国语言设计中,我为Excel文件引入了lan类型。
lan类型不仅表明这个字段是一个字符串类型,还表明这个字段对于每个外国语言都有一个不同的值。
我们的Hero表最终配置如下:
HeroID | HeroName | HeroTips | HeroSpeed |
---|---|---|---|
int(a) | lan(c) | lan(c) | int(s) |
1000 | 刘备 | 刘皇叔是大哥 | 100 |
1001 | 关羽 | 关云长是二哥 | 50 |
1002 | 张飞 | 张翼德是小弟 | 50 |
是的,策划的工作仅仅是把需要进行多国语言处理的字段由string改成lan即可(当然根据需要可以扩展为lan[]等列表类型)。
配置生成工具会有一个选项叫做导出多国语言文件,用于导出所有表中类型为lan的字段。
配置生成工具总是会在输出文件的末尾追加新添加的文字。而已经翻译过的文字保持不变。
导出的语言文件Lan.xlsx如下:
CN |
---|
刘备 |
关羽 |
张飞 |
刘皇叔是大哥 |
关云长是二哥 |
张翼德是小弟 |
在Lan.xlsx中添加要支持的语言及翻译内容,例如添加英语支持:(注:配置生成工具不会也不应该修改已经翻译过的文字, 如果某一列已经删除,可以将这一列移动第二个Sheet中去,以做备份)
CN | EN |
---|---|
刘备 | Liu Bei |
关羽 | Guan Yu |
张飞 | Zhang Fei |
刘皇叔是大哥 | Uncle Liu is the eldest brother |
关云长是二哥 | Guan Yunchang is the second brother |
张翼德是小弟 | Zhang Yide is the younger brother |
再使用配置生成工具中的导出配置文件功能, 生成客户端和服务端需要使用的配置即可(这里可以使用增量导出功能, 参考Makefile的做法)。
至于配置生成工具到底如何工作,采用不同的配置文件格式有不同的做法。
以Lua为例,我们导出的配置文件如下:
代码语言:javascript复制--hero.lua
local lan = require LAN .. ".hero"
local M = {
[1000] = {
HeroID = 1000,
HeroName = lan.HeroName_1000,
HeroTips = lan.HeroTips_1000,
HeroSpeed = 100,
},
[1001] = {
HeroID = 1001,
HeroName = lan.HeroName_1001,
HeroTips = lan.HeroTips_1001,
HeroSpeed = 50,
},
[1002] = {
HeroID = 1002,
HeroName = lan.HeroName_1002,
HeroTips = lan.HeroTips_1002,
HeroSpeed = 50,
},
}
return M
--cn/hero.lua
local M = {
HeroName_1000 = '刘备',
HeroName_1001 = '关羽',
HeroName_1002 = '张飞',
HeroTips_1000 = '刘皇叔是大哥',
HeroTips_1001 = '关云长是二哥',
HeroTips_1002 = '张翼德是小弟',
}
return M
--en/hero.lua
local M = {
HeroName_1000 = 'Liu Bei',
HeroName_1001 = 'Guan Yu',
HeroName_1002 = 'Zhang Fei',
HeroTips_1000 = 'Uncle Liu is the eldest brother',
HeroTips_1001 = 'Guan Yunchang is the second brother',
HeroTips_1002 = 'Zhang Yide is the younger brother',
}
return MCOPY
有同学说,你这个和策划配出来的XML格式并没有什么不同啊,优势在哪里。
以写代码而论,本质上你写的高级语言和汇编并没有什么不同。为什么你要写高级语言呢,因为写的效率高,出错概率小。
有了这个思路,再次对比上面新旧两种多国语言方案的优劣:
新的多国语言方案,策划只需要做两件事就能保证一定正确:1. 配置正确的lan类型。2. 给出正确的翻译文本
旧的多国语言方案, 同步Language和Hero表有负担,一旦同步错误不容易发现,没有特殊手段清理Language表中废弃的行,Hero.xls表失去可读性等各种缺点。