大厂如何开发多语言系统

版权声明
本文为“优梦创客”原创文章,您可以自由转载,但必须加入完整的版权声明
更多学习资源请加QQ:1517069595或WX:alice17173获取(企业级性能优化/热更新/Shader特效/服务器/商业项目实战/每周直播/一对一指导)
点赞、关注、分享可免费获得配套学习资源
需求分析与系统设计
需求分析

文字显示
多语言系统中第一个要实现的需求就是文字显示,要针对玩家所使用的母语显示不同的语言文字
技术要点:要获取到玩家手机所使用的语言,然后自动化地根据手机语言配置来显示对应的语言文字
语言切换
要实现语言切换需要提供语言切换UI界面,在点击这个UI界面上语言切换按钮时,所有的语言都要改变
还需要支持的一个功能是格式化字符串的切换,因为上图中的宝箱打开等待时间是根据实际时间填进去的,后面的时间单位也要随着语言的切换而切换
系统设计

用例图
在设计系统之前,先给大家介绍一个系统的分析设计方法:用例图(如上图)
用例图是由一些人形角色以及圆圈来构成的,其中人形角色表示这个系统里的一些角色,圆圈表示这个角色所能做的一些操作
系统设计
用例图上的每一个角色都是要在程序里实现的某个程序模块
比如多语言控制模块,它会有两个功能
1,响应语言切换请求
2,读取数据
快速实现多语言系统

定义多语言数据模型

定义多语言数据常量类,叫做MultiLangConst,所有的多语言数据都会放在这个类里,类中的代码如下
public class MultiLangConst : MonoBehaviour{ //英文字符串表 static Dictionary<string, string> LangMap_en = new Dictionary<string, string>{ {"txt_name", "Name"}, {"txt_level", "Level"}, }; //中文字符串表 static Dictionary<string, string> LangMap_cn = new Dictionary<string, string>{ {"txt_name", "名字"}, {"txt_level", "等级"}, }; // 根据传入的语言类型,以及字符串键名,获取对应语言的字符串内容(值) public static string GetValue(string languageType, string key) { if(languageType == "en") { return LangMap_en[key]; } else if(languageType == "cn") { return LangMap_cn[key]; } return "N/A"; }}
编写多语言控制脚本和处理脚本
创建了多语言数据常量类后,还需要再写一个控制层组件来完成的在不同的语言环境下切换文字的功能
控制层脚本

public class MultiLangCtrl : MonoBehaviour{ public Canvas canvas; // 控制组件以canvas为根节点广播消息 // 向canvas的每一个子节点发送消息,调用它们的OnLanguage?Changed方法 public void SetupLanguage(string langType) { // 思考:这样做好吗? canvas.transform.BroadcastMessage("OnLanguageChanged", langType); }}
要使GameObject能够接收消息就需要再写一个脚本组件,这个组件里有一个方法,名字就叫OnLanguageChanged,这样才就能接收到事件通知
在写脚本之前先来创建UI界面

创建完UI界面后就能发现Canvas是整个UI的根节点,将刚才写的MultiLangCtrl脚本挂载到Canvas上面,设置脚本中的canvas字段是Canvas本身,这样在调用脚本里的SetupLanguage方法时,就会向它下面的每一个节点发送消息
给显示文本组件增加一个脚本,叫MultiLangHandler

它的功能是接收语言改变的通知,并在语言改变时显示对应的语言文字
public class MultiLangHandler : MonoBehaviour{ // 记录这个组件要处理的是字符串表里的那个字符串 public string txtId = ""; // 响应多语言切换 public void OnLanguageChanged(string langType) { // 获取本组件所在的游戏对象上的Text组件 Text text = this.gameObject.getComponent<Text>(); if(text != null) { //根据语言类型、文本ID,获取对应语言的文字 var value = MultiLangConst.GetValue(langType, LangKey); text.text = value; return; } }}
在添加脚本时需要指定一下该文本组件要转换的语言文字(如下图)

添加完脚本后需要给按钮绑定语言切换的事件,以实现在点击按钮时切换语言文字的功能
在Button组件的点击事件里添加上MultiLangCtrl脚本的SetupLanguage方法

并指定对应的语言文字(如下图)

这样在点击语言切换按钮时,就会调用多语言切换的方法

如果把MultiLangHandler里的LangKey更改为”txt_level“,文本显示组件里显示的内容就会变成Level和等级
增加更多字符串进行测试
第三步里主要内容是让大家思考一下:把所有的字符串的都是放在MultiLangConst类里的两个字典里是否合适?
事件的定义、订阅与发布
原本的代码里采用的是BroadcastMessage来广播消息
public void SetupLanguage(string langType){ // 思考:这样做好吗? canvas.tansform.BroadcastMessage("OnLanguageChanged",langType)}
这种方式在处理消息时是把消息发送给下面的每一个组件,这就存在一些性能问题,因为需要检测每一个游戏对象上面挂载的每一个组件,检测其中有没有OnLanguageChanged方法
这样做的性能会比较低,所以要使用另外一种模式:事件订阅和通知模式
public class MultiLangCtrl : MonoBehaviour{ public Canvas canvas; // 控制组件以canvas为根节点广播消息 // 向canvas的每一个子节点发送消息,调用它们的OnLanguage?Changed方法 //public void SetupLanguage(string langType) //{ // 思考:这样做好吗? //canvas.transform.BroadcastMessage("OnLanguageChanged", langType); //} public delegate void OnLanguageChanged(string type); // 定义一个“语言切换”事件 public static OnLanguageChanged onLanguageChanged = null; public void SetupLanguage(string langType) { if(onLanguageChanged != null) { // 发布事件通知 // 那么在哪里订阅事件呢? onLanguageChanged(langType); } }}
会在MultiLangHandler的Start、OnDestroy里订阅和取消事件
public class MultiLangHandler : MonoBehaviour{ // 记录这个组件要处理的是字符串表里的那个字符串 public string txtId = ""; // 响应多语言切换 public void OnLanguageChanged(string langType) { // 获取本组件所在的游戏对象上的Text组件 Text text = this.gameObject.getComponent<Text>(); if(text != null) { //根据语言类型、文本ID,获取对应语言的文字 var value = MultiLangConst.GetValue(langType, LangKey); text.text = value; return; } } public void Start() { // 在程序开始时订阅事件 MultiLangCtrl.onLanguageChanged += OnLanguageChanged; } public void OnDestroy() { // 在程序结束时取消订阅 MultiLangCtrl.onLanguageChanged -= OnLanguageChanged; }}
这样做的好处是:
因为MultiLangHandler只是绑定在那些有文本组件的对象上,所以只有有文本组件的对象能收到事件通知
整体效果测试
为MultiLangConst的两个字典增加一些语言文字,并在场景中创建多个TextUI界面
public class MultiLangConst : MonoBehaviour{ //英文字符串表 static Dictionary<string, string> LangMap_en = new Dictionary<string, string>{ {"txt_name", "Name"}, {"txt_level", "Level"}, {"txt_hp", "HP"}, {"txt_mp","MP"}, {"txt_exp", "EXP"}, }; //中文字符串表 static Dictionary<string, string> LangMap_cn = new Dictionary<string, string>{ {"txt_name", "名字"}, {"txt_level", "等级"}, {"txt_hp", "血值"}, {"txt_mp","魔法值"}, {"txt_exp", "经验值"}, }; // 根据传入的语言类型,以及字符串键名,获取对应语言的字符串内容(值) public static string GetValue(string languageType, string key) { if(languageType == "en") { return LangMap_en[key]; } else if(languageType == "cn") { return LangMap_cn[key]; } return "N/A"; }}

测试效果

实现要点与讨论

语言数据源组件

语言数据源组件是用来存放数据的,上面代码中使用的方案是方案一,方案一就是把所有的多语言数据全部放在程序代码里,这是最差的一个方法
因为多语言数据源组件作为MonoBehaviour脚本是不能热更新的,采用这种方式,代码的维护将非常困难,而且代码里本就不应该放这些数据
这种方案显然是不合适的,所以需要考虑使用第二种方案:把文字数据交给Unity原生提供的ScriptableObject来存储

ScriptableObject是Unity提供的数据存储方案,也能做到热更新,但这种方案有一个明显的缺点:
它的编辑全都是在ScriptableObject的专门编辑器里进行编辑的(如上图),如果只是编辑小批量的数据还是挺方便的,但是数据量大了以后再用这种方式编辑就不太方便了
而且还有一个致命缺陷:
当程序上的这些字段名称变化时就需要重新绑定数据,即使不改变数据名称,只是在上面或下面增加一个字段,也会造成数据丢失
所以这种方法只适合开发一些小游戏或超休闲游戏,如果想要了解这种方案,可以在文末找爱丽丝老师领取我们的炉石传说卡牌数据管理,以及数据管理的十种方式

方案三是一种表驱动方案,在游戏开发中让策划去Excel表里编辑数据,它大概需要五个步骤
1,建立语言字符串表
2,把表数据导出为一种通用格式(如Json或xml格式)
3,在程序里定义一个数据结构
注意:使用表驱动时需要在程序里建立对应的类,比如语言类,不管是Lua还是ILRunTime都需要在程序里建立对应的数据结构,来转换表里的数据到程序的数据结构,这是比较麻烦一点
4,在程序里定义一个数据模型类(数据模型是负责管理程序数据的)
5,程序启动时解析文本数据,转换为”内存中的“字符串表
转换时会存在性能开销,是数据解析的开销,因为文字在解释时要判断数据格式,然后根据数据格式来匹配后面的数据

方案4是建立语言字符串表,然后直接把它导出成内存里的字符串数据,省去了数据解析的过程,而且中间数据模型的定义都是不需要程序员手动完成的
所以方案3中的这些步骤其实都是可以改进的,在我们的《皇室战争》项目框架里通过表驱动,加上工作流的改进,只要一步操作就可以完成表数据的加载
语言控制组件

要控制语言显示就需要检测系统的默认语言
只要调用Application.systemLanguage就能够获得系统的当前语言,然后根据它的语言来通过SetupLanguage切换到对应的语言
语言控制组件还要负责读取语言数据
发布语言切换通知
这是比较重要的一点,因为牵涉到一些性能问题,刚才也已经讲了几种模式
BroadcastMessage模式
BroadcastMessage模式的缺点是要遍历整个Canvas,它是使用多个for循环来遍历每一个对象,以及对象上的每一个组件和方法,检查其中有没有发布消息所对应的方法名称
所以会有性能问题,因为需要通知所有的组件,而不仅仅只是文字组件,这是非常浪费性能的,解决方案就是使用事件订阅发布模式
事件、订阅发布模式
这种模式的问题在于热更新,由于事件订阅、发布模式的事件接收脚本是原生的C#层代码,而控制语言切换的脚本是处于热更层的,是一个热更脚本,它们之间进行交互会有跨域的开销
既然这种方式会有跨域的开销,那么在项目里应该怎么样去做事件切换通知呢?
解决方案

我们的项目里的大部分程序逻辑都是全热更实现的,这样就没有跨域开销了,为什么呢?
因为所有的代码都不跨域,都是在热更工程里完成的
文字显示组件

文字显示组件的基本需求
1,响应语言改变通知
2,处理语言改变
前面已经通过脚本实现了上面的两个需求,但还有一些问题,就是Text组件为什么需要额外挂一个组件来实现多语言切换
这是因为Text组件不支持多语言切换,所以需要额外挂一个组件
那怎样让Text组件从不支持多语言升级到支持多语言呢?

有两种方案,前面实现的是方案2,方案1是继承一个Text组件,来把Text组件替换掉,然后在继承了Text的组件中实现所有的多语言功能
但这种方案也有一些缺点,所有前面使用了方案2,因为方案2的组件化设计对于后续的项目拓展更有利
方案1:继承Text组件

在派生类中实现多语言支持的优点是
简单直观,只要替换组件就可以了
缺点
如果在项目开始时并没有考虑到多语言支持,之后想要通过继承来实现多语言支持就需要把所有原本的Text的组件替换掉,并保留原本Text组件上的所有设置,这是需要手动去做的
方案2:添加一个多语言组件

方案2中需要添一个组件,名叫MyLangText,它的作用是控制Text组件和显示多语言文字,除此之外还有一些高级功能
高级功能
可以通过MyLangText组件实现对Text组件的控制,原理是定义一个TextId字段,这个字段的值就是Text组件要显示多语言字符串的id
MyLangText组件也是多语言系统的控制组件,是用来控制文字的读取和送显的,送显是读取文字并送到Text组件里的Text属性里显示
这种方案的好处是:UI里的文本内容可以完全交给策划同学来设定
但它也是有缺点的:
不能支持格式化的字符串也不支持热重载技术
因为MyLangText组件承担了两个功能
1,记录要显示的文字串id
2,控制文字串id与文字内容的转换
这样做是不太好的,因为一般情况下都不会把程序里的逻辑控制放到原生的C#层里,这样不能热更,而且也不知道策划会加什么样的奇怪需求
把逻辑控制放在原生的C#层就意味着这个组件将来只能控制文字的读取和送显,如果将来有新的需求了,原生的C#脚本也是没有办法更新业务逻辑的
这样就整理出了新需求:
1,多语言要支持格式化字符串
要尽量避免MyLangText组件既处理文本id的存储,又处理文本id到文字的显示
2,支持热重载控制文字表现
支持格式化字符串

如果把格式化字符串的功能放在前面的非热更代码中,也就是MyLangText代码中合不合适呢?
这显然是不合适的,因为策划的需求是多变的,非热更层的代码只能满足今天的需求,而不能满足明天的,所以在写代码时要尽量把控制层的代码放到热更代码里去
支持热重载控制文字表现

很多时候在程序里发现一个bug时就要把程序停掉重新开发,再进行测试,这样效率会比较低,要解决这个问题需要使用到热重载技术
热重载的程序代码是在热更代码程序域里的,但它又高于热更技术实现,因为如果没有框架支持,热更代码在热更时需要把代码停掉,重新启动一次才能使热更代码生效
而热重载可以使代码立刻生效,不需要重新启动,现在的VS2022,还有Unity本身都能够支持热重载,当然了,这是引擎层面和开发环境层面上的,你自己的程序里仍然是不能实现热重载的,除非有项目架构的支持,而我们的项目架构就是能够支持热重载的
写在最后
更多学习资源请加QQ:1517069595或WX:alice17173获取(企业级性能优化/热更新/Shader特效/服务器/商业项目实战/每周直播/一对一指导)
点赞、关注、分享可免费获得配套学习资源
到顶部