游戏本地化的实现

作者:gutenberg
2016-09-01
20 51 10

引言

本文产自游戏古登堡计划,我们是致力于弭除游戏中语言障碍的志愿者组织,欢迎各位加入我们

游戏本地化

所谓游戏本地化,是指为适应目标国家的玩家群体而对游戏进行调整修改的过程。狭义上讲,主要指游戏内文本图片等资源的翻译;广义上讲,也包括为了目标国家的游戏市场环境和法律法规而做出的内容与功能调整。

对国内玩家来说,将游戏本地化简单理解成汉化也没有什么问题。由于众所周知的原因,国内的主机/单机游戏市场刚刚才从黑暗低迷的时期走出,许多方面百废待兴,许多游戏并没有完善的官方中文支持。很多人可能不太理解游戏开发商为了支持完善的本地化支持,究竟需要哪些工作,这也是笔者写作本文的主要目的。游戏古登堡的主要使命之一,也正是帮助更多的优秀游戏加入官方中文支持,让国内的玩家能够更好地享受游戏内容。

对本地化相关功能提供相对完善的官方支持应当做到下面几点:

  1. 实现游戏内容层和逻辑层的分离,存放为单独的本地化文件
    不要将涉及到游戏内容的代码与游戏逻辑相关的代码混淆在一起。并且将需要本地化为多国语言的游戏文本和游戏素材单独保存为易于编辑修改的专有格式。专有格式一般会是一个保存着 key-value 形式数据的文件,key 为文本索引,value 为翻译后的目标语言文本,游戏会从指定路径读取这个专有格式的文件,并通过检索 key 来确定某个特定位置显示什么样的文本。很多游戏直接使用 XML 格式;有的则简单使用每行一句 key = value 语句的形式。这个专门文件应当保存在指定路径下,并以特殊规则命名,类似地,对图片,语音文件等的本地化也应以相同方式处理。专门的本地化文件之于游戏有点像网页编程中控制格式的 css 文件之于 html 文件,好处在于,不必将游戏逻辑暴露给负责文本翻译的人(他们很可能毫无技术背景),这样可以大大降低后期维护的难度:只需要文本编辑器就能修改游戏的语言,交给第三方的翻译公司或社区进行本地化工作变成相当简单的工作。
  2. 充分考虑语言差异造成的本地化问题
    开发商需要充分考虑语言差异造成的本地化问题。游戏中的各自 UI 元素,诸如下拉列表、径向菜单等在展开或收缩都会用到必要的空间,为了取得最佳的本地化效果,需要协同游戏文本的翻译者,来根据显示效果进行适配,这样才能彻底免除图像覆盖等形式的显示差错。如果以英文为标准长度,多数亚洲文字会比英文稍短,而欧洲国家的语言一般会比英文更长:阿拉伯语通常会比英语长上25%,法语和德语通常会比英语长 20% 左右,意大利语会长 15%,德语长 20%,西班牙语长 25%。中日文字一般会更短,但由于语言体系存在根本差异,也会有一些其他问题,比如,字母文字拥有大量可用的像素字体,而汉字在 8px 以下就很难找到合适字体。还有很多细微问题:阿拉伯文字从右往左显示的问题;换行处理问题(有些游戏会使用空格来判断是否应当自动换行,而中文并不使用空格来分词)等等。对 cjk 字符,由于巨大的字符使用量,还需要单独制作专门的字库。
  3. 根据文化差异和政策规定进行内容调整
    笔者并不赞成为了迎合受众市场而对游戏内容进行大幅度修改,但这种做法其实并不鲜见。例如,大家所熟悉的魂斗罗中主角形象是以第一滴血主角来设计的,但在欧版中,却变成了令人啼笑皆非的机甲形象。随着游戏文化价值方面属性的上升,大家会更倾向于享受原汁原味的游戏内容,但为了适应不同国家的文化和政策,仍然会有许多需要注意的地方:例如,对一些血腥暴力元素的修改,对一些具有不同含义的图标进行修改(比如,在大部分国家和地区,“大拇指”意味着赞许。但在有些地方,这却是种挑衅的手势。)等等。

很多游戏并非以常规渠道同玩家见面,其中文版也多由民间汉化组或盗商提供,其制作流程是有别于官方的本地化流程的。民间汉化组需要以非常规手段解包和重新打包游戏,需要翻译和修改的文本需要自己提取和重新封包,字库问题也要独立解决。因此,对民间汉化组来说,是否能够进行汉化,一方面取决于游戏本身的框架对中文的支持程度,另外一方面也取决于汉化组本身的技术实力。大家可以阅读下面这篇文章感受民间汉化的艰难与不易:

官方中文支持所面临的困难

对开发商来说,游戏本地化有助于开拓非母语市场,商业上的意义自不必赘言。以国内情况为例,只消去观察游戏商城和各大游戏社区的用户评论区看看有多少“求汉化”发发言就能多少感受到需求之旺盛。

pknigth

素质平平的仿dqb游戏《传送门骑士》在更新中推出汉化后在国内大卖

但推进官方中文支持的推进其实也还面临着相当多的困难:

CJK(中日韩文)字符的独特性

纯粹从技术角度来讲,本地化系统非常容易实现。但是,如果不是在开发阶段就考虑到相关的问题,尤其对包含大量文本的游戏来说,后期才进行本地化工作会变成一个相当繁琐的工作。幸运的是,随着全球化趋势,本地化工作已经日益成为一个常规考虑事项,但不幸的是,但由于中文与西方字母文字的天然差异,中文支持会需要考虑一些无中文背景的西方开发者可能难以考虑到的额外的情况:诸如扩大游戏字库,不同的分行处理算法,预留更多的显示范围等等。日韩文字其实也面临相似的问题,但中文问题尤其严峻:以像素字体为例,日文有大量的可用免费字体供开发者选择,但中文几乎找不到特别合适的全字号解决方案(一般是不消除锯齿使用小字号宋体)。

开发商对国内市场的信任与评估问题

过去这些年,海外游戏作品缺少正常途径引入国内,即便目前环境逐步好转,但进入中国市场的困难和复杂程度已经给他们留下了非常深刻的印象。中国市场的确潜力巨大,但其活跃的正版玩家用户数量仍然与其巨大的人口总量十分不匹配,尽管市场在逐渐回暖,但当下而言,国内的盗版玩家数量还是远远多于正版玩家用户。根据 steamspy 的统计,中国的 steam 用户已经逼近千万,但平均拥有游戏数不到10款;作为对比,美国玩家平均拥有游戏数为40款左右。正式进入国内市场需要经过许多内容上的审核,并做出相应调整,因此也需要找到熟悉中国市场的国内合作伙伴,对开发商而言,本地化绝非仅仅翻译游戏内容那样简单,翻译游戏内容只是其中最主要的必不可少的一个环节,其他事项过于复杂也会动摇他们执行本地化的兴趣和信心。

高昂的翻译费用

成本取决于文本的内容规模,也取决于文本质量。

根据国外游戏翻译公司 lingo24 的一篇文章,他们的报价大抵为:

  • 英文到法文,意大利文,德文和西班牙文:每千词 $160-$275(有赖于文本类型和对翻译者的专业性要求)
  • 英文到丹麦文,荷兰文,芬兰文,冰岛文,日文,韩文,挪威文,瑞士文:每千词 $190-$325(有赖于文本类型和对翻译者的专业性要求)

505 发行游戏目前一般都支持官方中文,质量中等偏上(例如前段时间的 Abzu 就是通过这个渠道制作的官方中文,翻译质量尚可,但存在少量缺漏),主要通过 localizedirect 翻译,也可以参考他们首页的报价单,如英文翻译为中文,每千字报价为 120 欧元,约合 134 美元。

一些涉及大量文字的游戏在翻译方面会支持一笔相当高的开支,例如,以剧情见长的永恒之柱如果使用商业翻译公司来制作中文版本,假使按 50 万单词计算,仅仅制作中文翻译文本就需要花费超过 5 万美元。

如果拥有大量的玩家受众,也考虑承受社区翻译良莠不齐的风险,也可以通过向玩家社区开发本地化文件接口的形式来获取翻译方面的支持,但多数时候,情况会更加复杂。

民间汉化的转正也困难重重

很多来自民间汉化组和盗商的中文版本,质量上存在良莠不齐的问题,加之缺少与官方的沟通渠道,难以被采用为正式的中文版,同广大玩家见面。在进行游戏古登堡计划中,我们有机会同很多开发者交流。当提及国内有大量的民间汉化组在制作他们作品的中文版时,他们多数人乐见这样的情况,对中国有如此多的爱好者能肯定他们的游戏作品表示开心,但正式的官中支持会是个更复杂的问题,需要更多的交流沟通,需要对翻译质量更审慎的评估和认证,这些都是民间汉化组可能难以做到的。另外,正如前文提到的,需要开发方做一些额外的并非零成本的准备工作(字体授权的申请/购买,字库制作,市场调研与政策问题解决等等),很多工作会和汉化组的传统操作流程相悖。盗商抱着以汉化来吸引人气的动机,更难积极去推进官方中文支持的进程。

接下来的部分主要面向游戏开发者,我将简单介绍如何在 unity 和 gms 这两款引擎中实现本地化系统。

Unity 的本地化系统

本教程主要面向对 unity 和 C# 有一定使用经验的用户。

正如前文所述,我们首先要指定一种用来保存 key-value 形式数据的本地化文件格式。既可以使用简单的 .ini 格式,也可以使用 json 或者 xml(自定义格式也可以,但你可能需要花更多时间来专门编写解析它的函数)。不同格式都有各自的优势劣势,我们主要从三个方面考虑:是否便于人类编辑修改;是否方便程序来读取解析;功能上是否有拓展空间,是否有足够的弹性适应千奇百怪的需求。.ini 虽然易于阅读修改,但缺乏弹性;json 计算机解析起来很容易,但不利于人类阅读。本教程中使用 .xml 格式。它解析起来很方便,而且虽然初看上去不是那么便于阅读,但却很容易转换成其他更容易手动编辑的格式。此外,我们这里使用的引擎是 unity,而 C# 对 xml 的支持非常良好,解析起来很容易。

XML format

我们来定义下用于本地化的 xml 文件的结构。我会尽量让例子保持简明。将下面的文件命名为 ENGLISH.xml 并保存到游戏项目assets路径下的某个指定位置:

 
<!--?xml version="1.0" encoding="utf-8"?-->
<language lang="english" id="0">
  <!-- Main Menu -->
  <text key="MAIN_TITLE">My Game Title</text>
  <text key="CONTINUE">Continue</text>
  <text key="START_NEW_GAME">Start New Game</text>
  <text key="OPTIONS">Options</text>
  <text key="QUIT">Quit</text>
</language>

目前的文件结构看起来非常简单。解析和手动编辑都不难。xml 的根节点定义了语言的种类和 id 属性,会用于在游戏中设置语言类型。不同的 id 对应不同的语言文件。而拥有 key 属性的 text 节点则用于保存游戏中实际可见的文本。这个文件会在 Asset 路径下保存为 ENGLISH.xml,而如果你制作了一个用于中文的文件,也可以将其保存为 CHINESE.xml

接下来我们来实现在游戏中利用 key-value 对来读取语言文件。

编写本地化的类

下面提供了一个简单的类,用于读取保存在 xml 中的文本。你只需要将其作为脚本绑定到到场景中一个不可见的 GameObject 上就可以了。将下面的代码保存为 LocalizationManager.cs:

 
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using System.Xml.Linq;
 
public class LocalizationManager : MonoBehaviour
{
    public static LocalizationManager Instance { get { return instance; } }
    public int currentLanguageID = 0;
    [SerializeField]
    public List languageFiles = new List();
    public List languages = new List();
 
    private static LocalizationManager instance;   // GameSystem local instance
 
    void Awake()
    {
        instance = this;
        DontDestroyOnLoad(this);
        // This will read  each XML file from the languageFiles list<> and populate the languages list with the data
        foreach (TextAsset languageFile in languageFiles)
        {
            XDocument languageXMLData = XDocument.Parse(languageFile.text);
            Language language = new Language();
            language.languageID = System.Int32.Parse(languageXMLData.Element("Language").Attribute("ID").Value);
            language.languageString = languageXMLData.Element("Language").Attribute("LANG").Value;
            foreach (XElement textx in languageXMLData.Element("Language").Elements())
            {
                TextKeyValue textKeyValue = new TextKeyValue();
                textKeyValue.key = textx.Attribute("key").Value;
                textKeyValue.value = textx.Value;
                language.textKeyValueList.Add(textKeyValue);
            }
            languages.Add(language);
        }
    }
    // GetText will go through each language in the languages list and return a string matching the key provided 
    public string GetText(string key)
    {
        foreach(Language language in languages)
        {
            if (language.languageID == currentLanguageID)
            {
                foreach(TextKeyValue textKeyValue in language.textKeyValueList)
                {
                    if (textKeyValue.key == key)
                    {
                        return textKeyValue.value;
                    }
                }
            }
        }
        return "Undefined";
    }
}
// Simple Class to hold the language metadata
[System.Serializable]
public class Language
{
    public string languageString;
    public int languageID;
    public List textKeyValueList = new List();
}
// Simple class to hold the key/value pair data
[System.Serializable]
public class TextKeyValue
{
    public string key;
    public string value;
}

脚本的一些细节解释如下:

这个类使用了 DontDestroyOnLoad(this);,因此不必担心载入新场景会让脚本失效。你可以将用于本地化的游戏对象放在第一个游戏场景中,或者配合某个场景管理工具来方便地使用。

使用 [SerializeField]languageFile List<> 更方便,因此你可以在场景编辑中自由拖拽 xml 文本。这个脚本使用了 [System.Serializable] 将语言的子类放入 Unity 的监视器中,这样就能可视化地查看数据,有助于调试修改。

变量 currentLanguageID 用来指定使用哪种语言文件。你可以按照自己的需求来设置这个数值,但它必须和 xml 中你使用的 id 数值相匹配。我将默认值设为0,此时它会使用英文文件,而 English.xml 中的 id 即为 0。你既可以通过侦测系统语言的方法来设置语言类型,也可以在菜单中加入一个选项,允许玩家自己更改语言设置。

TyNTlcX

脚本位于监视器中,你可以在语言文件列表中拖拽 xml 文件。

本地化 UI 文本的组建

在向游戏对象 GameObject 添加 UI 文本组建时你也需要一个脚本,保存下面代码为 LocalizationUIText.cs:

using UnityEngine;
using UnityEngine.UI;
 
[RequireComponent(typeof (Text))]
public class LocalizationUIText : MonoBehaviour
{
    public string key;

    void Start()
    {
        // Get the string value from localization manager from key & set text component text value to the returned string value
        GetComponent().text = LocalizationManager.Instance.GetText(key);
    }
}

无论是包含文本组件的按钮,标签还是别的什么,凡是包含文本的游戏对象,都可以向其添加该脚本。只需要在脚本中调用 LocalizationManager 中的 GetText() 就可以将标记该文本组件,并向其返回本地化之后的文本。

如果某个你添加过脚本的游戏对象还没有绑定文本组件,那么 [RequireComponent(typeof (Text))] 这行代码则会自动添加一个。

XL7jTFk

将脚本添加到绑定了 UI 文本组件的游戏对象上,并设置 key 为你在 xml 文件中使用的值

你可以为每个特别的文本组建使用单独的key,在游戏中,显示的内容会自动替换为本地化后的文本。

在其他脚本中也可以使用 LocalizationManager,凡是涉及文本显示的地方,只需要调用 GetText() 并适应你想要在 xml 中使用的 key 即可。

string localizedString = LocalizationManager.Instance.GetText("START_NEW_GAME");

它会通过 LocalizationManager 读取当前游戏文本id currentLanguageID 指定的文本(假设是英文),并返回字符串"Start New Game"。

GMS 游戏的本地化实现

思路大体类似 unity 中的实现,我们这里简单地使用 .ini 格式来作为本地化文件。

XREpmQz

下面在游戏中读取文本的脚本由来自 reddit 的 DragonCoke 提供:

在需要本地化的地方调用的 text_local() 实现:

///text_local("section","identifier",[massimport])
if (argument_count != 3 || argument[2] = 0)
    {
    ini_open("localization/"+global.language);
    }
var var_importedtext = ini_read_string(argument[0],argument[1],"-1errormessage");

if (var_importedtext = "-1errormessage") //try to reimport from english if missing
    {
    ini_close();
    ini_open("localization/english.txt")
    var_importedtext = ini_read_string(argument[0],argument[1],"MISSINGNO#CHECK FILE INTEGRITY");
    ini_close();
    if (argument_count == 3 && argument[2] == true)
        {ini_open("localization/"+global.language);}
    }
else if (argument_count != 3)
    {ini_close();}

return var_importedtext;
///text_local_massimport()
ini_open("localization/"+global.language);
///text_local_massimport_end()
ini_close();

DragonCoke 也提供了将游戏文本从 .ini 中导出为 .csv 格式的工具,这样就能很方便地用电子表格进行协同编辑了(虽然并非最佳的解决方案,例如游戏古登堡计划就使用了专门的翻译协作工具)。

工具下载见这里

来源与扩展阅读

近期点赞的会员

 分享这篇文章

您可能还会对这些文章感兴趣

参与此文章的讨论

  1. Feihong 2016-09-02

    干货!

  2. eastecho 2016-09-02

    古登堡,加油!

  3. Hyy 2016-09-02

    厉害

  4. 浑身难受 2016-09-03

    看到一堆国旗我就心痒

  5. 花开半生 2016-09-03

    厉害!

  6. pg7go 2016-09-03

    赞一个!

  7. labmemno009 2016-09-04

    +1s

  8. JoyTJoy 2016-09-08

    写的很棒,感谢~~

  9. Vladislav/:8-) 2019-03-22 微信会员

    我们的俄文本地化工作室:https://rgloc.com/
    欢迎访问!

  10. q531620267 2019-12-11

    你好,我想知道GMS2怎么本地化,文章中写的GMS我看不懂。。。

您需要登录或者注册后才能发表评论

登录/注册