在使用Unity开发游戏的过程中,本地化是必不可少的。网络上也有很多的本地化工具,本次我介绍的是Unity官方提供的Localization插件,大家可以在Package Manager进行安装

一、语言配置,本地化表创建

在Project Setting中找到Localization,(需要先创建这个Localization Setting文件)点击Locale Generator选择需要本地化的语言。创建好后会得到这些文件,这些文件可以用于切换语言(后面做切换语言界面时会用),先把英语拖入作为默认语言。

打开本地化表工具,创建本地化表

创建UILocalization和ScriptLocalization两个本地化表,分别用作UI和代码本地化。会得到以下文件

可以在表格中添加一些需要本地化的文本(这里只是先试试,先不要添加太多,后面会用Excel进行管理),注意最左侧的Key值,这个key值后续用来确定文本

二、使用Localize String Event进行本地化

给UI上的文本挂载LocalizeStringEvent脚本,点击其中最上面的String Reference搜索之前表格中创建的本地化文本。搜索Key值或者本地化的文本都可以搜得到。

注:如果显示不完整,可以调节右下角的小球

然后将LocalizeStringEvent的Update String调整为Text.text,就是要刷新的脚本。任何的string都可以刷新,Text Mesh Pro也是可以的

运行游戏查看效果,可以临时先使用右上角的下拉框调节本地化语言,界面上的文本会实时刷新。

三、UI本地化

我先将本地化的需求暂时分成两类

1,UI本地化:UI界面上面固定不变的文本

2,Script本地化:代码中实时更改的文本,包括(String.Format+数值),动态弹出的提示语,NPC对话,物品名字等

为了后续教程,我们暂时约定在制作界面时,UI本地化的内容正常命名,Script本地化的对象以$开头命名,例如:

首先,需要知道有哪些文本需要本地化,并给它们自动挂载上LocalizeStringEvent脚本。可以在Editor目录下创建一个扩展脚本实现此功能。

using System.Collections.Generic;using System.IO;using UnityEditor;using UnityEngine;using UnityEngine.Events;using UnityEngine.Localization;using UnityEngine.Localization.Components;using UnityEngine.UI;public class LocalizeTextEditor : EditorWindow{    private string outputFilePath = "LocalizedText.txt"; // 指定的txt文件路径    private List<string> localizedTextEntries = new List<string>();    [MenuItem("Custom/Localize Text")]    private static void ShowWindow()    {        GetWindow("Localize Text");    }    private void OnGUI()    {        GUILayout.Label("Localize Text Editor", EditorStyles.boldLabel);        if (GUILayout.Button("Localize Selected GameObjects"))        {            LocalizeSelectedGameObjects();        }        if (GUILayout.Button("Save Localized Text to File"))        {            SaveLocalizedTextToFile();        }        if (GUILayout.Button("Print Script Localize Selected GameObjects"))        {            PrintScriptLocalize();        }    }    private void LocalizeSelectedGameObjects()    {        localizedTextEntries.Clear();        GameObject[] selectedObjects = Selection.gameObjects;        foreach (GameObject selectedObject in selectedObjects)        {            Text[] textComponents = selectedObject.GetComponentsInChildren(true);            foreach (Text textComponent in textComponents)            {                if (!textComponent.name.StartsWith("$"))                {                    // 需要本地化的Text                    if(textComponent.gameObject.GetComponent() == null)                    {                        LocalizeStringEvent localizeEvent = textComponent.gameObject.AddComponent();                        // 标记对象为“已修改”                        EditorUtility.SetDirty(selectedObject);                    }                    // 添加到列表                    string entry = $"{selectedObject.name}\t{textComponent.name}\t{textComponent.text}";                    localizedTextEntries.Add(entry);                }            }        }    }    private void SaveLocalizedTextToFile()    {        if(!File.Exists(outputFilePath))        {            File.Create(outputFilePath).Dispose();        }        using (StreamWriter writer = new StreamWriter(outputFilePath, true))        {            foreach (string entry in localizedTextEntries)            {                writer.WriteLine(entry);            }        }        Debug.Log($"Localized text entries saved to {outputFilePath}");    }    private void PrintScriptLocalize()    {        GameObject[] selectedObjects = Selection.gameObjects;        foreach (GameObject selectedObject in selectedObjects)        {            Text[] textComponents = selectedObject.GetComponentsInChildren(true);            foreach (Text textComponent in textComponents)            {                if (textComponent.name.StartsWith("$"))                {                    Debug.Log($"{selectedObject.name}\t{textComponent.name}\t{textComponent.text}");                }            }        }    }}

我本来还想自动给LocalizeStringEvent的Update String赋值,但是未能实现。如果哪位高人有办法可以在评论区指出

全选需要本地化的UI预制体,依次点击扩展窗口的第1和第2个按钮,可以给非$开头的Text(你们如果命名规则不一样,请自行修改脚本)自动挂载LocalizeStringEvent脚本

这些Text中的内容会输出到txt中,可以查看(注:txt默认应该在项目根路径)

四、使用Excel表格管理本地化文本

右键点击本地化表的选项卡,选择导出CSV

如果使用Office,导出时选择UTF-8即可。如果和我一样使用WPS,请另存为xlsx格式,不然部分语言会乱码。

将之前txt中的内容复制到表格中,由于我输出的是\t,内容会自动分布到表格的不同列,大家可以把根据预制体名字来给Key起名。

丢给AI或者翻译软件翻译之后,对于没有空格的语言,可以使用alt+回车在适当位置输入换行,例如:中文,日语等。Unity的自适应换行只会在有空格的地方换行

对于WPS用户,我们需要将xlsx转回CSV才能在Unity中读取,这里使用python的pandas库进行处理。

import pandas as pd # 读取 Excel 文件df = pd.read_excel("UILocalization.xlsx", sheet_name="UILocalization") # 将 DataFrame 写入 CSV 文件df.to_csv('dataUI.csv', encoding='utf-8', index=False)df2 = pd.read_excel("ScriptLocalization.xlsx", sheet_name="ScriptLocalization")df2.to_csv('dataScript.csv', encoding='utf-8', index=False)

这里还处理了后面的Script本地化文件,大家还没看完后续教程的,可以先注释后两行代码

在本地化表导出CSV的上面还有导入CSV,这里就不截图了,导入之后就可以得到我们刚才在Office或WPS中编辑的表格

五、Script本地化

还是全选所有UI预制体,这次点击第三个按钮,会在控制台中输出所有$开头的Text。这些Text都是在代码中更改内容的,只能对其查找所有引用,然后逐个更改。

那么,如何在代码中获取本地化的内容呢,我们需要创建一个单例的LocalizationManager,提供一个获取本地化内容的函数。

以防止有编程小白,这里介绍一下单例模式,单例模式就是类最多只有一个对象,并且有一个指向该对象的静态指针。在Unity中可以用以下代码实现一个简单的单例模式

using UnityEngine;public class Singleton : MonoBehaviour where T : Singleton {    protected static T _instance;    public static T Ins {        get {            return _instance;        }    }    protected virtual void Awake() {        _instance = (T)this;    }    protected virtual void OnDestroy() {        if (_instance == this) {            _instance = null;        }    }}

在场景中创建一个(DontDestroyOnLoad)不会销毁的对象,并挂载LocalizationManager脚本

using System.Collections;using System.Collections.Generic;using UnityEngine;using UnityEngine.Localization;using UnityEngine.Localization.Settings;using UnityEngine.Localization.Tables;using UnityEngine.ResourceManagement.AsyncOperations;namespace UI{    public class LocalizationManager : Singleton    {        private StringTable ScriptStringTable;          //代码本地化表        void Start()        {            if (LocalizationSettings.AvailableLocales.Locales.Count > 0)            {                GetLocalizationTable();            }            else            {                LocalizationSettings.InitializationOperation.Completed += OnLocalizationInitialized;            }            LocalizationSettings.SelectedLocaleChanged += OnLocaleChanged;        }        protected override void OnDestroy()        {            base.OnDestroy();            LocalizationSettings.SelectedLocaleChanged -= OnLocaleChanged;        }        ///         /// 本地化初始化完成        ///         private void OnLocalizationInitialized(AsyncOperationHandle handle)        {            if (handle.Status == AsyncOperationStatus.Succeeded)            {                Debug.Log("Localization initialized successfully!");                GetLocalizationTable();            }            else            {                Debug.LogError("Localization initialization failed.");            }        }        ///         /// 切换语言        ///         private void OnLocaleChanged(Locale newLocale)        {            GetLocalizationTable();        }        ///         /// 获取本地化表        ///         public void GetLocalizationTable()        {            ScriptStringTable = LocalizationSettings.StringDatabase.GetTable("ScriptLocalization");            //Debug.LogWarning(ScriptStringTable.GetEntry("CommonTip_NoItem").GetLocalizedString());        }        ///         /// 获取本地化文本        ///         public string GetLocalizedString(string key)        {            return ScriptStringTable.GetEntry(key).GetLocalizedString();        }    }}

这里我们注册了两个事件,一个是LocalizationSettings.InitializationOperation.Completed,这个是LocalizationSettings初始化完成时调用。由于本地化插件是异步初始化,代码运行到start时不一定初始化完成,此处通过判断可用语言是否大于0来判断有没有初始化完成。

另一个事件是LocalizationSettings.SelectedLocaleChanged,这个是LocalizationSettings切换语言是调用。这两个事件都会执行获取本地化表的操作。对于之前在代码中更改的Text,可用调用GetLocalizedString来获取本地化文本。

例如:

物品名字要进行本地化,key值是GameItem_ID,每个物品ID不同

和之前使用表格管理UILocalization表类似,我们也使用表格管理ScriptLocalization表。

六、语言切换界面

创建一个这样的UI界面,我设计的是每个按钮都能点。当前使用的语言是绿的。

给每个按钮拖上一个Locale,就是之前最开始创建的用于切换语言的

        ///         /// 刷新按钮状态        ///         public void RefreshItemChooseState()        {            Locale currentLocale = LocalizationSettings.SelectedLocale;            foreach (var item in languageItems)            {                item.SetChooseState(currentLocale == item.locale);            }        }        ///         /// 设置语言        ///         public void SetLanguage(Locale locale)        {            if(locale == LocalizationSettings.SelectedLocale)            {                return;            }            LocalizationSettings.Instance.SetSelectedLocale(locale);            Client.Ins.Player.PlayerLanguage = locale.LocaleName;            RefreshItemChooseState();        }

获取当前语言:LocalizationSettings.SelectedLocale,获取之后和每个按钮上面拖入的Locale比对,一样的就是当前语言

设置语言:LocalizationSettings.Instance.SetSelectedLocale(locale);把拖入的Locale设置进去,就可以切换语言。

以下为拓展内容:

设置完语言之后,最好能够保存下来,用户下次启动自动使用。这里把locale.LocaleName,也就是语言的名字保存下来。

大家可以用任意方式保存一个string,我这边用的json,这里就涉及到游戏存档设计了,超出了本文的讨论范围。大家可以自行设计游戏存档,反正能够存取当前语言名字(public string PlayerLanguage)就行了

这里仍然使用了之前提到的LocalizationSettings.InitializationOperation.Completed事件,在LocalizationSettings初始化完成之后设置为保存的语言。

        ///         /// 本地化初始化完成        ///         private void OnLocalizationInitialized(AsyncOperationHandle handle)        {            if (handle.Status == AsyncOperationStatus.Succeeded)            {                Debug.Log("Localization initialized successfully!");                UseSaveLanguage();            }            else            {                Debug.LogError("Localization initialization failed.");            }        }        ///         /// 使用保存的语言        ///         private void UseSaveLanguage()        {            var locales = LocalizationSettings.AvailableLocales.Locales;            foreach (var locale in locales)            {                if (locale.LocaleName == PlayerLanguage)                {                    LocalizationSettings.Instance.SetSelectedLocale(locale);                    break;                }            }        }