14 KiB
22 · 本地化系统(Localization System)
命名空间
BaseGames.Localization
所属文档集 ← 返回索引 · 总览
Package 依赖com.unity.localization(≥ 1.4)
依赖系统BaseGames.UI·BaseGames.Dialogue
目录
- 系统总览
- 语言包配置
- StringTable 结构
- LanguageManager — 语言切换管理器
- UI 组件本地化
- 对话系统本地化桥接
- 字体与排版规范
- 数字 / 货币 / 时间格式
- SaveData 扩展
- 编辑器工作流
1. 系统总览
本地化系统基于 Unity Localization 包构建,提供运行时语言切换和零硬编码字符串的文本管理方案。
本地化系统架构:
├─ LocalizationSettings(Unity 包)
│ ├─ StringTableCollection → 按功能域划分的字符串表集合
│ ├─ AssetTableCollection → 本地化资产(字体、图片)
│ └─ Locale 列表 → zh-CN / en / ja(初始三语言)
│
├─ LanguageManagerSO → SO 单例,封装语言切换、持久化
├─ LocalizedStringComponent → TextMeshPro 文本组件的本地化桥接
├─ DialogueLocalizationBridge → DialogueSequenceSO → 本地化键 映射
└─ LocalizationKeys → 所有键名静态常量(编译期检查)
设计原则:
- UI 文本 →
LocalizedStringReference(Unity 内置组件,在 Inspector 中选键名) - 对话文本 →
DialogueLocalizationBridge(键 ="dialogue.{sequenceId}.{lineIndex}") - 代码内不直接写任何面向玩家的字符串,统一走
LocalizationKeys
2. 语言包配置
在 Assets/Settings/Localization/ 下配置 Unity Localization:
Localization/
├── LocalizationSettings.asset → LocalizationSettings(项目唯一)
├── Locales/
│ ├── zh-CN.asset → SimpleChinese(简体中文,默认语言)
│ ├── en.asset → English
│ └── ja.asset → Japanese
└── Tables/
├── UI_Table/ → UI 通用文本
├── Dialogue_Table/ → NPC / 系统对话
├── Items_Table/ → 物品名称与描述
├── Combat_Table/ → 战斗相关文本(HP 不足、BOSS 名等)
└── Settings_Table/ → 设置页面文本
Locale 创建规范
// 编辑器脚本:自动创建 Locale
// Tools > Zeling > Localization > Create Locales
[MenuItem("Tools/Zeling/Localization/Create Locales")]
static void CreateLocales()
{
var codes = new[] { ("zh-CN", "简体中文"), ("en", "English"), ("ja", "日本語") };
foreach (var (code, name) in codes)
{
var locale = Locale.CreateLocale(new SystemLanguage());
locale.LocaleName = name;
// ... 创建资产
}
}
3. StringTable 结构
键名规范
{域}.{子域}.{词条}
UI:
ui.menu.start = "开始游戏"
ui.menu.continue = "继续"
ui.menu.settings = "设置"
ui.menu.quit = "退出"
ui.hud.soul_label = "灵魂"
ui.hud.geo_label = "Geo"
ui.map.fast_travel = "快速旅行"
ui.map.undiscovered = "???"
ui.item.found = "获得物品"
ui.ability.unlocked = "获得能力"
ui.boss.defeated = "BOSS 已击败"
对话:
dialogue.{sequenceId}.{lineIndex}
dialogue.town_elderbug_01.0 = "又来了,年轻的旅者。"
dialogue.town_elderbug_01.1 = "王国啊……它已不再是从前的样子。"
物品:
item.simple_key.name = "简单的钥匙"
item.simple_key.desc = "一把普通的钥匙,不知锁的是什么门。"
item.pale_ore.name = "苍白矿石"
item.pale_ore.desc = "稀有的矿石,蕴含奇特的力量。"
战斗:
combat.parry_success = "弹反!"
combat.boss.false_knight = "伪骑士"
设置:
settings.language = "语言"
settings.sfx_volume = "音效音量"
settings.bgm_volume = "音乐音量"
settings.fullscreen = "全屏"
settings.vibration = "手柄震动"
LocalizationKeys 常量类(编译期保障)
public static class LocalizationKeys
{
// UI
public const string UI_MENU_START = "ui.menu.start";
public const string UI_MENU_CONTINUE = "ui.menu.continue";
public const string UI_MAP_FAST_TRAVEL = "ui.map.fast_travel";
public const string UI_ITEM_FOUND = "ui.item.found";
public const string UI_BOSS_DEFEATED = "ui.boss.defeated";
// 物品
public const string ITEM_SIMPLE_KEY_NAME = "item.simple_key.name";
// 战斗
public const string COMBAT_PARRY_SUCCESS = "combat.parry_success";
// 格式化辅助
/// <summary> 返回对话行键名 "dialogue.{id}.{idx}" </summary>
public static string DialogueLine(string sequenceId, int lineIndex)
=> $"dialogue.{sequenceId}.{lineIndex}";
/// <summary> 返回物品名称键名 </summary>
public static string ItemName(string itemId) => $"item.{itemId}.name";
/// <summary> 返回物品描述键名 </summary>
public static string ItemDesc(string itemId) => $"item.{itemId}.desc";
}
4. LanguageManager — 语言切换管理器
LanguageManagerSO 是项目全局语言管理的 SO 单例,通过 LocalizationSettings 操作语言切换:
[CreateAssetMenu(menuName = "Localization/LanguageManager")]
public class LanguageManagerSO : ScriptableObject
{
[Header("支持的语言代码")]
public string[] supportedLocales = { "zh-CN", "en", "ja" };
[Header("默认语言")]
public string defaultLocale = "zh-CN";
[Header("事件频道")]
[SerializeField] StringEventChannelSO _onLanguageChanged;
/// <summary> 当前语言代码(如 "zh-CN") </summary>
public string CurrentLocale
=> LocalizationSettings.SelectedLocale?.Identifier.Code ?? defaultLocale;
/// <summary> 设置语言并持久化 </summary>
public void SetLanguage(string localeCode)
{
var locale = LocalizationSettings.AvailableLocales
.Locales.Find(l => l.Identifier.Code == localeCode);
if (locale == null) return;
LocalizationSettings.SelectedLocale = locale;
PlayerPrefs.SetString("SelectedLocale", localeCode);
PlayerPrefs.Save();
_onLanguageChanged?.Raise(localeCode);
}
/// <summary> 游戏启动时恢复上次语言 </summary>
public void RestoreSavedLanguage()
{
string saved = PlayerPrefs.GetString("SelectedLocale", defaultLocale);
SetLanguage(saved);
}
/// <summary> 同步获取本地化字符串(非异步,需提前加载表) </summary>
public string Get(string key, string tableCollection = "UI_Table")
{
var result = LocalizationSettings.StringDatabase
.GetLocalizedString(tableCollection, key);
return string.IsNullOrEmpty(result) ? $"[{key}]" : result;
}
/// <summary> 带参数格式化 </summary>
public string GetFormatted(string key, string tableCollection, params object[] args)
{
string template = Get(key, tableCollection);
return string.Format(template, args);
}
}
5. UI 组件本地化
5.1 TextMeshPro 文本自动本地化
推荐方式:在 TextMeshProUGUI 同 GameObject 上添加 LocalizeStringEvent(Unity Localization 包内置组件):
Inspector:
LocalizeStringEvent
├─ String Reference: UI_Table / "ui.menu.start"
└─ Update String: TextMeshProUGUI.SetText(自动连接)
语言切换时 Unity 自动更新所有绑定了 LocalizeStringEvent 的 TMP 组件。
5.2 运行时动态文本
对于需要在代码中动态设置的文本:
// 物品获取提示(动态拼接物品名)
public class ItemPickupNotification : MonoBehaviour
{
[SerializeField] TMP_Text _label;
[SerializeField] LanguageManagerSO _langMgr;
public void Show(string itemId)
{
// 正确:通过 LocalizationKeys 获取键名,再查询
string itemName = _langMgr.Get(LocalizationKeys.ItemName(itemId), "Items_Table");
string template = _langMgr.Get(LocalizationKeys.UI_ITEM_FOUND, "UI_Table");
_label.text = string.Format(template, itemName); // "{0} 已获取"
}
}
6. 对话系统本地化桥接
DialogueLocalizationBridge 在 DialogueSequenceSO 加载时,将文本内容替换为本地化版本:
[RequireComponent(typeof(DialoguePlayer))]
public class DialogueLocalizationBridge : MonoBehaviour
{
[SerializeField] LanguageManagerSO _langMgr;
public DialogueLine[] Localize(string sequenceId, DialogueLine[] rawLines)
{
var localized = new DialogueLine[rawLines.Length];
for (int i = 0; i < rawLines.Length; i++)
{
localized[i] = rawLines[i]; // 值类型浅拷贝
string key = LocalizationKeys.DialogueLine(sequenceId, i);
string text = _langMgr.Get(key, "Dialogue_Table");
// 如果找到本地化键,覆盖原始文本;否则保留 DialogueSequenceSO 中的备用文本
if (!text.StartsWith("[")) // "[key]" 表示未找到
localized[i].text = text;
}
return localized;
}
}
对话文本存储规范:
DialogueSequenceSO.lines[i].text存储备用文本(英文或开发者备注)- 正式文本统一在 Dialogue_Table 中维护
- 新增 NPC/对话时,同步在所有语言的表中添加条目(即使暂时只有一种语言文本)
7. 字体与排版规范
字体资产布局
Assets/Fonts/
├── zh-CN/
│ ├── NotoSerifSC-Regular_SDF.asset → 正文(宋体风格)
│ ├── NotoSerifSC-Bold_SDF.asset → 标题/加重
│ └── PixelFont_CJK_SDF.asset → 像素风(HUD 标签,伤害数字)
├── en/
│ ├── TrajanPro3-Regular_SDF.asset → 正文
│ └── TrajanPro3-Bold_SDF.asset → 标题
└── ja/
├── NotoSerifJP-Regular_SDF.asset
└── NotoSerifJP-Bold_SDF.asset
本地化字体切换(AssetTable)
通过 Unity Localization 的 AssetTableCollection 实现运行时字体切换:
AssetTableCollection: Fonts_Table
键名: zh-CN en ja
"font.body" → NotoSerifSC → TrajanPro3-Regular → NotoSerifJP
"font.title" → NotoSerifSC-Bold → TrajanPro3-Bold → NotoSerifJP-Bold
"font.hud" → PixelFont_CJK → PixelFont_EN → PixelFont_JP
LocalizeFontGroup 组件(在 Canvas 根节点)统一更新所有 TMP 字体资产:
public class LocalizeFontGroup : MonoBehaviour
{
[Serializable]
struct FontEntry { public string key; public TMP_Text[] targets; }
[SerializeField] FontEntry[] _entries;
[SerializeField] LanguageManagerSO _langMgr;
void OnEnable()
=> LocalizationSettings.SelectedLocaleChanged += OnLocaleChanged;
void OnDisable()
=> LocalizationSettings.SelectedLocaleChanged -= OnLocaleChanged;
void OnLocaleChanged(Locale locale)
{
foreach (var entry in _entries)
{
var fontOp = LocalizationSettings.AssetDatabase
.GetLocalizedAssetAsync<TMP_FontAsset>("Fonts_Table", entry.key);
fontOp.Completed += op =>
{
foreach (var t in entry.targets) t.font = op.Result;
};
}
}
}
CJK 排版注意事项
TMP_Text.lineSpacing:CJK 字体建议 +5%(行高较高)- 中文不允许行首标点(句号/逗号/右括号)→ TMP 开启
Line Breaking Rule选Chinese Simplified - 日文全角字符宽度 = 中文字符宽度,可复用布局
- 对话框固定宽度,文本内容通过 Unity Localization 中的 Smart String 处理变量替换
8. 数字 / 货币 / 时间格式
public static class LocalizationFormat
{
/// <summary> 伤害数字显示(统一不使用千位分隔符,保持像素风格简洁) </summary>
public static string Damage(int value) => value.ToString();
/// <summary> Geo 货币显示 </summary>
public static string Geo(int amount)
{
string locale = LocalizationSettings.SelectedLocale?.Identifier.Code ?? "zh-CN";
return locale switch
{
"zh-CN" => $"{amount} Geo",
"ja" => $"{amount}ジオ",
_ => $"{amount} Geo",
};
}
/// <summary> 游戏内时间(存档时长) </summary>
public static string PlayTime(float seconds)
{
int h = (int)(seconds / 3600);
int m = (int)(seconds % 3600 / 60);
int s = (int)(seconds % 60);
return $"{h:D2}:{m:D2}:{s:D2}";
}
}
9. SaveData 扩展
"localization": {
"selectedLocale": "zh-CN"
}
语言设置存储于
PlayerPrefs而非 SaveData,以便在读取存档前即可恢复语言(主菜单阶段)。
10. 编辑器工作流
本地化键扫描器
Tools > Zeling > Localization > Scan Missing Keys:
- 扫描所有
DialogueSequenceSO,为每行对话生成期望键名 - 扫描所有
ItemDataSO,生成期望item.{id}.name/desc键 - 与 StringTable 实际条目对比,输出缺失清单
- 提供 [一键创建缺失条目] 按钮(默认文本 = SO 中的备用文本)
翻译导入 / 导出工具
Tools > Zeling > Localization > Export CSV:将所有 StringTable 导出为 CSV,每行格式:
Key,zh-CN,en,ja
ui.menu.start,开始游戏,Start Game,スタート
dialogue.town_elderbug_01.0,又来了年轻的旅者。,Back again...,また来たな…
Tools > Zeling > Localization > Import CSV:导入翻译后的 CSV,自动更新对应 StringTable。
语言预览模式
Inspector 顶部下拉菜单切换预览语言(不修改 PlayerPrefs),仅在 Editor Play Mode 下生效,用于对话策划的快速校验。