Unity游戏本地化实战:基于Luban与QFramework的Excel驱动方案
1. 项目概述:为什么我们需要一个Excel驱动的本地化系统?
做Unity游戏开发,尤其是面向全球市场的项目,本地化(Localization)从来都不是一个“锦上添花”的功能,而是决定产品能否成功出海的关键一环。我经历过不止一个项目,早期图省事,把UI文本、对话、道具描述直接硬编码在C#脚本或者挂在Prefab上,结果到了要支持多语言的时候,整个团队都傻眼了——到处找字符串、翻译版本管理混乱、运行时切换语言卡顿甚至崩溃。这种“技术债”一旦欠下,后期偿还的成本极高。
所以,当项目启动时,如果有多语言需求,我的第一原则就是:必须建立一个中心化、数据驱动、易于协作的本地化系统。而“Excel表格驱动”正是实现这一目标的黄金方案。它把所有的文本内容从代码和场景中剥离出来,集中管理在一张或多张结构清晰的表格里。策划、翻译人员可以直接在Excel里工作,无需接触Unity编辑器或代码;程序则通过工具链,将Excel数据高效、无错地转换为游戏运行时可用的格式。
这次要聊的实战方案,核心是Luban和QFramework这两个框架的组合。Luban是一个强大的配置表代码生成工具,它能将Excel(或CSV、JSON等)格式的配置数据,自动生成强类型的C#数据结构代码和高效的二进制数据文件。QFramework则是我非常推崇的一套Unity开发框架,其架构思想清晰,提供的工具集能极大地提升开发效率,它的ILocalization接口和配套管理器,为多语言切换提供了优雅的运行时支持。
简单来说,这个系统的流程是:策划在Excel里填表 -> Luban自动生成代码和二进制数据 -> QFramework加载并管理这些数据 -> 游戏UI通过QFramework的接口动态获取对应语言的文本。整个链路清晰、自动化程度高,且性能优异。下面,我就来拆解这套方案的每一个环节,分享其中的设计思路、实操细节以及我踩过的那些坑。
2. 系统核心架构与工具选型解析
2.1 为什么是Luban + QFramework?
在动手之前,我们先要理解为什么选择这两个工具,而不是Unity自带的Localization包或者其他Asset Store的插件。
首先看Luban。它的核心价值在于“配置即代码”和“极致的数据加载性能”。
- 强类型安全:Luban会根据你的Excel表结构,生成对应的C#类。比如你有一列
id,一列text_cn,一列text_en,它会生成一个LocalizationConfig类,包含Id(int)、TextCn(string)、TextEn(string)这三个属性。你在代码里访问时是config.TextCn,而不是config["text_cn"],避免了字符串拼写错误,也获得了IDE的智能提示和编译时检查。 - 二进制序列化:Luban默认将Excel数据序列化为紧凑的二进制格式(如
.bytes文件)。相比直接解析JSON或CSV,二进制加载速度更快,内存占用更小,这对于包含成千上万条文本的本地化表来说,优势明显。 - 强大的数据校验与转换:你可以在Excel中定义数据类型(int, string, bool, list等),Luban在生成时会进行严格校验,避免策划填错格式。它还支持复杂的数据结构(如嵌套、多态),未来如果本地化需要关联图标、音频等资源,扩展起来也很方便。
- 命令行与CI/CD集成:Luban可以通过命令行调用,这意味着你可以轻松地将配置表导出流程集成到Jenkins、GitLab CI等自动化流水线中,实现“提交Excel -> 自动生成 -> 打包”的全流程自动化。
再看QFramework。它是一个轻量而强大的框架,其ILocalization接口设计得非常简洁。
- 接口抽象,解耦彻底:QFramework定义了一个
ILocalization接口,核心方法就是GetText(string key)。你的UI文本组件(无论是UGUI Text,TextMeshPro,还是自定义组件)只依赖这个接口,而不关心底层数据是来自Excel、JSON还是AssetBundle。这带来了巨大的灵活性。 - 内置管理器与便捷扩展:QFramework提供了
LocalizationManager来管理当前语言和多个语言数据源。我们只需要实现一个基于Luban数据的ILocalization具体类(例如LubanLocalization),并将其注册到管理器即可。 - 与UI框架无缝集成:QFramework的
UIKit系统管理UI界面,我们可以很方便地在界面打开、刷新时,调用本地化接口更新文本,甚至可以实现文本的“热更新”(不重启游戏切换语言)。
这个组合,相当于用Luban解决了“数据从哪里来、如何高效使用”的问题,用QFramework解决了“数据怎么被游戏消费、如何优雅管理”的问题。两者分工明确,边界清晰。
2.2 Excel表格的结构设计
表格结构是系统的基石,设计得好,后续开发和维护能省一半的力气。我的经验是设计两张核心表:
第一张表:localization_text.xlsx(文本内容表)这是存储所有需要翻译的文本的核心表。
| id (key) | desc (描述) | text_cn (简体中文) | text_en (英文) | text_ja (日文) | ... |
|---|---|---|---|---|---|
| 1001 | 主界面-开始按钮 | 开始游戏 | Start Game | ゲームスタート | ... |
| 1002 | 主界面-设置按钮 | 设置 | Settings | 設定 | ... |
| 1003 | 道具-生命药水描述 | 恢复50点生命值 | Restores 50 HP | HPを50回復する | ... |
| ... | ... | ... | ... | ... | ... |
设计要点:
id(主键):必须是唯一数字或字符串。我强烈推荐使用数字ID,因为它在代码中作为int或uint类型,比较和查找效率比字符串高,且不易出错。可以使用分段ID来区分模块,例如1xxx代表UI,2xxx代表道具等。desc(描述列):这列非常重要,是给策划和开发自己看的注释,说明这个文本用在哪里。Luban生成代码时会忽略这列,但它能极大提升表格的可读性和维护性。- 语言列 (
text_xx):列名遵循text_+语言代码的规范(如text_en,text_zh-CN)。语言代码建议使用标准代码,方便与Unity的System.Globalization或第三方翻译平台对接。 - 空单元格处理:如果某种语言下某个词条尚未翻译,可以留空。在生成的代码逻辑里,我们需要实现一个“回退机制”,比如当前语言为空时,显示默认语言(如英文)的文本。
第二张表:localization_font.xlsx(字体与样式表)不同语言可能需要不同的字体,比如中文用思源黑体,英文用Arial,日文用MS Gothic。字体资源管理同样重要。
| id | language_code | font_asset_path | font_size_offset | line_spacing_offset |
|---|---|---|---|---|
| 1 | zh-CN | Assets/Fonts/SourceHanSansCN-Regular.asset | 0 | 0 |
| 2 | en | Assets/Fonts/Arial.asset | -2 | 1.1 |
| 3 | ja | Assets/Fonts/UDDigiKyokashoN-R.asset | 0 | 1.2 |
这张表可以扩展,比如加入bold_font_path(粗体字体)、font_material(特殊材质)等字段,以满足更复杂的排版需求。
注意:字体文件(.ttf/.otf)需要先在Unity中创建为Font Asset(对于TextMeshPro)或直接导入为Font(对于Legacy Text)。这里的
font_asset_path填的是Unity工程内的资源路径。
3. Luban配置与代码生成实战
3.1 环境搭建与项目配置
首先,你需要安装Luban。推荐使用其命令行工具。你可以从GitHub Release页面下载对应操作系统的可执行文件(如luban.exefor Windows),或者通过.NET Core的全局工具安装:dotnet tool install -g Luban.Client。
接下来,在Unity项目的同级目录或某个专门的管理目录下,创建Luban的配置文件。一个典型的目录结构如下:
YourGameProject/ ├── UnityProject/ (你的Unity工程) │ └── Assets/ │ └── ... └── GameConfig/ (配置表管理目录) ├── Datas/ (存放Excel源文件) │ ├── localization_text.xlsx │ └── localization_font.xlsx ├── Gen/ (Luban生成的代码和数据的输出目录) │ ├── C#代码 │ └── bytes数据文件 └── luban.conf (Luban配置文件)luban.conf配置文件详解:
{ "option": { "inputDataDir": "Datas", // Excel源文件目录 "outputCodeDir": "Gen/Code", // 生成C#代码的目录 "outputDataDir": "Gen/DataBytes", // 生成二进制数据的目录 "codeTarget": "client-unity", // 目标平台,针对Unity优化 "dataTarget": "bin", // 数据格式,二进制 "namingConvention": { "cs": "PascalCase" // C#代码使用帕斯卡命名法 } }, "group": [ { "name": "client", "tables": ["localization_text.xlsx", "localization_font.xlsx"] // 要处理的表 } ] }3.2 定义Luban Schema(模式文件)
Luban的强大之处在于其Schema文件(通常是.xml格式),它定义了如何解析Excel表格。我们在Datas目录下创建一个__tables__.xml文件(Luban会识别这个特殊名称的文件)。
<?xml version="1.0" encoding="utf-8"?> <schema> <!-- 定义文本表 --> <table name="localization_text" input="localization_text.xlsx" mode="one"> <!-- 定义主键字段id,类型为int --> <var name="id" type="int"/> <!-- desc列仅用于注释,不导出到最终数据 --> <var name="desc" type="string" comment="true"/> <!-- 定义多语言文本字段,类型为string。value=“”表示如果单元格为空,则用空字符串填充 --> <var name="text_cn" type="string" value=""/> <var name="text_en" type="string" value=""/> <var name="text_ja" type="string" value=""/> <!-- 可以在这里继续添加其他语言列,如text_ko, text_fr等 --> </table> <!-- 定义字体表 --> <table name="localization_font" input="localization_font.xlsx" mode="one"> <var name="id" type="int"/> <var name="language_code" type="string"/> <var name="font_asset_path" type="string"/> <var name="font_size_offset" type="int"/> <var name="line_spacing_offset" type="float"/> </table> </schema>关键参数解释:
mode="one":表示这张表里的每一行都是一个独立的配置项。对于本地化文本表,这是最常用的模式。comment="true":标记该字段仅为注释,Luban在生成最终数据文件时会忽略此列,但生成的C#类中仍会包含这个字段(可能标记为[Comment]特性),方便我们查阅。
3.3 执行生成与导入Unity
配置好后,在命令行中进入GameConfig目录,执行生成命令:
luban -c luban.conf如果一切顺利,你会在Gen/Code目录下看到生成的C#代码文件(例如LocalizationText.cs,LocalizationFont.cs),在Gen/DataBytes目录下看到对应的二进制数据文件(例如localization_text.bytes,localization_font.bytes)。
接下来是将生成物导入Unity:
- 将
Gen/Code下的所有.cs文件复制到Unity项目的Assets/Scripts/Generated/Config目录(目录可自定)。这些是纯C#代码,不依赖Unity API,放在任何Assets下的脚本目录都行。 - 将
Gen/DataBytes下的所有.bytes文件复制到Unity项目的Assets/StreamingAssets/Config目录。StreamingAssets文件夹在打包后会原封不动地包含在发布包中,并且可以通过Application.streamingAssetsPath路径读取,是存放配置数据的理想位置。
实操心得:我强烈建议将整个
GameConfig目录纳入版本控制(如Git)。并且,将Luban生成命令写成一个简单的脚本(如generate.bat或generate.sh),甚至集成到Unity Editor的菜单中(通过UnityEditor.MenuItem),这样策划同学修改完Excel后,一点按钮就能自动生成并导入,极大提升协作效率。
4. 基于QFramework的运行时系统实现
4.1 实现Luban数据加载器
Luban生成的代码里,会包含一个Tables类,它提供了加载所有配置表数据的功能。我们需要创建一个适配器,将Luban的数据加载到QFramework的本地化管理体系中。
首先,在Unity中创建一个LubanLocalization.cs脚本,实现QFramework的ILocalization接口。
using QFramework; using System.Collections.Generic; using UnityEngine; // 引入Luban生成的代码命名空间 using Game.Config.Generated; namespace Game.Localization { public class LubanLocalization : ILocalization { // 存储当前语言的所有键值对 private Dictionary<string, string> mTextMap = new Dictionary<string, string>(); // 存储当前语言的字体配置 private LocalizationFontConfig mCurrentFontConfig; // 当前语言代码,例如 "en", "zh-CN" public string Language { get; private set; } = "en"; // 实现ILocalization接口的核心方法 public string GetText(string key) { if (mTextMap.TryGetValue(key, out string value)) { // 如果当前语言文本为空,尝试回退到英文 if (string.IsNullOrEmpty(value) && Language != "en") { // 这里假设Luban生成的类里有一个静态的Tables实例 var cfg = Tables.Instance.LocalizationTextTable.GetById(int.Parse(key)); if (cfg != null && !string.IsNullOrEmpty(cfg.TextEn)) { return cfg.TextEn; } } return value; } // 如果找不到key,返回key本身并打一个警告,方便排查 Debug.LogWarning($"[Localization] Text key '{key}' not found."); return $"#{key}"; } // 切换语言 public void SwitchLanguage(string languageCode) { if (Language == languageCode) return; Language = languageCode; ReloadLanguageData(); // 通知所有监听者语言已切换 QFramework.TypeEventSystem.Global.Send(new LanguageChangedEvent(languageCode)); } // 加载指定语言的数据 private void ReloadLanguageData() { mTextMap.Clear(); var textTable = Tables.Instance.LocalizationTextTable.DataList; // 根据当前语言代码,选择对应的列名 string fieldName = GetFieldNameByLanguageCode(Language); foreach (var cfg in textTable) { string key = cfg.Id.ToString(); string value = GetTextByFieldName(cfg, fieldName); mTextMap[key] = value; } // 加载字体配置 LoadFontConfig(); } // 根据语言代码映射到Luban生成的类中的属性名 private string GetFieldNameByLanguageCode(string code) { // 简单的映射,可以根据需要扩展 switch (code) { case "zh-CN": case "zh-Hans": return "TextCn"; case "en": return "TextEn"; case "ja": return "TextJa"; default: return "TextEn"; // 默认回退到英文 } } // 使用反射动态获取属性值(简化版,实际可优化) private string GetTextByFieldName(LocalizationTextConfig cfg, string fieldName) { var prop = cfg.GetType().GetProperty(fieldName); return prop?.GetValue(cfg) as string ?? string.Empty; } // 加载字体配置 private void LoadFontConfig() { var fontList = Tables.Instance.LocalizationFontTable.DataList; mCurrentFontConfig = fontList.Find(f => f.LanguageCode == Language); if (mCurrentFontConfig == null) { // 找不到对应语言配置,使用第一个或默认配置 mCurrentFontConfig = fontList.Count > 0 ? fontList[0] : null; Debug.LogWarning($"[Localization] Font config for language '{Language}' not found, using default."); } } // 获取当前语言的字体资源路径 public string GetFontAssetPath() { return mCurrentFontConfig?.FontAssetPath; } // 获取字体大小偏移量 public int GetFontSizeOffset() { return mCurrentFontConfig?.FontSizeOffset ?? 0; } } // 语言切换事件,用于通知UI更新 public struct LanguageChangedEvent { public string LanguageCode; public LanguageChangedEvent(string code) { LanguageCode = code; } } }这个LubanLocalization类做了几件关键事情:
- 封装Luban数据:它内部持有一个字典,在切换语言时,将Luban表中对应语言列的所有数据预加载到字典中,这样
GetText时就是O(1)的查找,非常高效。 - 实现回退机制:当获取的文本为空时,尝试返回英文文本,确保玩家至少能看到内容,而不是空字符串。
- 管理字体配置:同时加载并管理对应语言的字体配置信息。
- 事件驱动:语言切换后,发送一个全局事件
LanguageChangedEvent,UI组件可以监听这个事件来刷新自身显示。
4.2 集成到QFramework架构并初始化
接下来,我们需要在游戏启动时,初始化Luban的Tables和我们的LubanLocalization,并将其注册到QFramework的架构中。
创建一个GameArchitecture类,继承自Architecture<GameArchitecture>,这是QFramework框架的入口。
using QFramework; using Game.Config.Generated; using Game.Localization; using UnityEngine; namespace Game { public class GameArchitecture : Architecture<GameArchitecture> { protected override void Init() { // 1. 初始化Luban Tables (加载二进制数据) // 注意:Tables.Instance.Loader需要你实现一个从StreamingAssets加载bytes文件的方法 // 这里是一个示例实现 Tables.Instance.Initialize(new LubanTableLoader()); // 2. 注册本地化服务 var localization = new LubanLocalization(); // 默认设置为英文,也可以从玩家存档中读取上次设置的语言 localization.SwitchLanguage("en"); this.RegisterInstance<ILocalization>(localization); // 3. 注册其他系统... // this.RegisterInstance<IAudioSystem>(new AudioSystem()); // ... } } // 自定义的Luban表加载器,从StreamingAssets读取.bytes文件 public class LubanTableLoader : ITableLoader { public ByteBuf Load(string file) { // 构建文件在StreamingAssets中的路径 string path = System.IO.Path.Combine(Application.streamingAssetsPath, "Config", $"{file}.bytes"); // 注意:在Unity中,同步读取StreamingAssets在某些平台(如WebGL)可能有问题。 // 实际项目中建议使用UnityWebRequest异步加载,这里为简化示例使用同步读取。 if (System.IO.File.Exists(path)) { byte[] bytes = System.IO.File.ReadAllBytes(path); return new ByteBuf(bytes); } else { Debug.LogError($"[Luban] Config file not found: {path}"); return null; } } } }最后,在游戏启动场景中创建一个空的GameObject,挂载GameArchitecture组件(QFramework会自动生成这个组件脚本),确保它在所有场景中最早初始化。
4.3 UI文本组件的本地化绑定
现在,数据和服务都准备好了,我们如何在UI上使用呢?以UGUI Text为例,我们可以创建一个LocalizedText组件。
using QFramework; using UnityEngine; using UnityEngine.UI; namespace Game.Localization.UI { [RequireComponent(typeof(Text))] public class LocalizedText : MonoBehaviour { [SerializeField] private string mTextKey; // 在Inspector中配置的文本Key,如 "1001" private Text mTextComponent; private ILocalization mLocalization; private void Awake() { mTextComponent = GetComponent<Text>(); mLocalization = GameArchitecture.Interface.GetUtility<ILocalization>(); if (mLocalization == null) { Debug.LogError("[LocalizedText] ILocalization not found in architecture."); return; } // 监听语言切换事件 QFramework.TypeEventSystem.Global.Register<LanguageChangedEvent>(OnLanguageChanged).UnRegisterWhenGameObjectDestroyed(gameObject); } private void Start() { // 初始时更新一次文本 UpdateText(); } private void OnLanguageChanged(LanguageChangedEvent e) { UpdateText(); // 如果需要,也可以在这里根据新的语言更新字体 UpdateFont(); } private void UpdateText() { if (!string.IsNullOrEmpty(mTextKey) && mTextComponent != null && mLocalization != null) { mTextComponent.text = mLocalization.GetText(mTextKey); } } private void UpdateFont() { var lubanLocalization = mLocalization as LubanLocalization; if (lubanLocalization != null && mTextComponent != null) { string fontPath = lubanLocalization.GetFontAssetPath(); if (!string.IsNullOrEmpty(fontPath)) { // 异步加载字体资源,实际项目应使用资源管理系统(如Addressables) var font = Resources.Load<Font>(fontPath); // 假设字体放在Resources文件夹 if (font != null) { mTextComponent.font = font; } // 应用字体大小偏移 mTextComponent.fontSize += lubanLocalization.GetFontSizeOffset(); } } } // 提供一个公共方法,允许在运行时动态改变Key(比如根据道具ID显示不同描述) public void SetKey(string newKey) { if (mTextKey != newKey) { mTextKey = newKey; UpdateText(); } } } }将这个脚本挂载到任何一个有Text组件的GameObject上,在Inspector中填入对应的文本ID(如1001)。游戏运行时,这个Text就会自动显示当前语言下的正确文本,并且在切换语言时自动更新。
对于TextMeshPro (TMP),原理完全相同,只需要创建一个LocalizedTextMeshPro组件,将Text组件替换为TMP_Text组件,并在UpdateFont方法中加载TMP_FontAsset即可。
5. 高级功能、优化与避坑指南
5.1 动态参数与文本格式化
游戏文本中经常需要插入动态参数,比如“玩家{0}获得了{1}个金币”。我们的系统需要支持这种格式化。
方案一:在Excel中预留占位符,在C#中使用string.Format。在Excel中,文本写成:“玩家{0}获得了{1}个金币”。 在代码中:
string rawText = mLocalization.GetText("2001"); // 获取带占位符的文本 string formattedText = string.Format(rawText, playerName, goldCount); mTextComponent.text = formattedText;这种方式简单直接,但要求翻译人员理解{0}、{1}的含义,并且顺序不能错。
方案二:使用命名参数,提高可读性和翻译容错性。我们可以扩展GetText方法,支持字典参数。
// 在ILocalization接口和LubanLocalization实现中添加方法 string GetText(string key, Dictionary<string, object> parameters);在Excel中,文本写成:“玩家{playerName}获得了{goldCount}个金币”。 在代码中:
var parameters = new Dictionary<string, object> { { "playerName", "张三" }, { "goldCount", 100 } }; string formattedText = mLocalization.GetText("2001", parameters);实现时,我们可以用正则表达式(如\{(\w+)\})匹配出所有命名参数,然后用参数值替换。这样对翻译者更友好,参数顺序也无关。
实操心得:我推荐方案二。虽然实现稍复杂,但大大降低了协作成本。你可以在
LubanLocalization的GetText方法中实现一个简单的模板引擎。记得对参数值进行HTML转义(如果UI支持富文本),防止注入问题。
5.2 图片、音频等资源的本地化
本地化不止于文字,UI图标、背景音乐、语音也可能因地区而异。我们可以在Excel中扩展这套系统。
创建localization_asset.xlsx表:
| id | asset_type | key | asset_path_cn | asset_path_en | ... |
|---|---|---|---|---|---|
| 1 | sprite | ui_icon_shop | Assets/Sprites/CN/icon_shop.png | Assets/Sprites/EN/icon_shop.png | ... |
| 2 | audio | bgm_main | Assets/Audio/CN/bgm_main.ogg | Assets/Audio/EN/bgm_main.ogg | ... |
然后,在LubanLocalization类中增加GetAssetPath(string key)等方法。在UI层,我们同样可以创建LocalizedImage、LocalizedAudioPlayer等组件,它们监听语言切换事件,并根据key从本地化管理器获取当前语言对应的资源路径,然后通过资源管理系统(如Addressables)加载并设置资源。
5.3 性能优化与内存管理
- 懒加载与缓存:
LubanLocalization在SwitchLanguage时一次性加载所有文本到字典。对于文本量极大的游戏,这可能在切换语言时造成卡顿。可以考虑“懒加载”策略,即第一次请求某个Key的文本时才去Luban表中查找并缓存。但这样GetText的复杂度从O(1)变成了O(n)或O(log n)。一个折中方案是按模块加载,比如只预加载主界面、设置界面等当前活跃模块的文本。 - 二进制数据优化:Luban生成的二进制数据已经非常紧凑。确保在打包时,这些
.bytes文件没有被Unity重新压缩(在Import Settings中设置AssetBundle为Uncompressed或使用LZ4)。 - 字体加载:字体文件通常较大。不要每次切换语言都同步加载字体,这会导致卡顿。应该使用异步加载(如
Addressables.LoadAssetAsync),并在加载期间显示一个默认字体或加载指示器。 - 对象池化文本组件:对于频繁创建销毁的UI(如列表项),其中的
LocalizedText组件在Awake/Start时都会执行查找和注册事件的操作。确保在对象销毁时正确注销事件监听(示例代码中使用了UnRegisterWhenGameObjectDestroyed),避免内存泄漏。
5.4 常见问题与排查技巧
问题1:切换语言后,部分UI文本没有刷新。
- 排查:首先检查该文本的GameObject上是否挂载了
LocalizedText组件,并且Text Key是否正确。然后,在LocalizedText组件的OnLanguageChanged方法中打日志,看是否被触发。如果没有触发,检查LocalizedText的Awake是否执行(GameObject是否处于激活状态),以及事件注册是否成功。 - 技巧:在
LubanLocalization的SwitchLanguage方法最后,可以打印出当前加载的语言和文本数量,便于确认切换是否成功。
问题2:运行时提示“Text key 'XXX' not found”。
- 排查:
- 检查Excel表中是否存在ID为
XXX的行。 - 检查Luban生成的数据文件是否是最新的(策划改完Excel后有没有重新生成并导入Unity)。
- 检查
GetText时传入的key类型。如果你的key是int类型的ID(如1001),但在Excel中id列被设置成了string类型,或者反之,都会导致查找失败。确保代码中的key类型与Luban Schema中定义的类型匹配。
- 检查Excel表中是否存在ID为
- 技巧:在
LubanLocalization的ReloadLanguageData方法中,可以遍历加载的字典并打印前几条记录,确认数据是否正确加载。
问题3:中文显示为乱码或“口口口”。
- 排查:这几乎肯定是字体问题。
- 检查
localization_font表中对应语言的font_asset_path路径是否正确。 - 检查该字体资源是否真的包含了中文字形。Unity默认的Arial字体不包含中文,需要导入或设置回退字体。
- 对于TextMeshPro,确保使用的是TMP_FontAsset,并且其“Atlas Population Mode”设置正确,包含了所需的字符集。
- 检查
- 技巧:在Unity Editor中,可以写一个编辑器工具,遍历所有配置的字体路径,检查资源是否存在,并预览其包含的字符集。
问题4:Luban生成失败,报错“字段类型不匹配”。
- 排查:仔细查看Luban的错误信息,它会指出具体哪个Excel文件的哪一行哪一列有问题。常见原因有:
- 在定义为
int的列里填了字符串。 - 单元格格式错误,例如看起来是数字,但实际是文本格式。
- Excel中存在合并单元格,Luban可能无法正确解析。
- 在定义为
- 技巧:要求策划同学在填写Excel时,使用“清除格式”功能,确保单元格是纯净的数据。可以编写一个Excel预处理脚本,在Luban生成前自动检查数据格式。
这套基于Luban+QFramework的Excel驱动本地化系统,从项目中期接入我负责的一个海外发行项目以来,已经稳定运行了两年多,支撑了超过10个语种的版本。它的核心优势在于将内容与逻辑分离,并通过自动化工具链保证了数据和代码的一致性。对于策划和翻译而言,他们只需要面对熟悉的Excel;对于程序而言,获得的是强类型、高性能、易维护的代码和数据。这种分工明确的协作模式,是应对复杂游戏本地化需求的一剂良药。当然,没有银弹,你需要根据自己项目的规模和特点,对上述方案进行裁剪和扩展,比如集成本地化翻译管理平台(如LocalizeDirect, Crowdin)的API,实现真正的在线协作与更新,那就是另一个更宏大的话题了。