← → · ? · F
Unity Localization 踩坑日志 · ①

三个让 Localization
原地爆炸 的坑

从崩溃时序、切换闪烁、状态污染三个真实场景出发,
看清 Unity Localization 几个没人告诉你的隐藏契约。

Unity 2022.3.62f3 Localization 1.5.3 Addressables 1.22.3
AGENDA

本辑目录

01

不要在 Completed 里切语言

ArgumentException · 同 key 冲突

02

切换语言时的闪烁

StringTable 与 AssetTable 异步竞速

03

退出时别访问 SelectedLocale

ResetState 触发系统语言污染

01
崩溃

不要在 InitializationOperation
的 Completed 回调中
调用 SetSelectedLocale

为什么崩?

Completed 在 Complete() 内部同步触发,调用栈尚未展开。

sequenceDiagram autonumber participant C as Code participant I as Init Op participant V as VAB Dict participant T as Tables C->I: PreloadTables() I->V: Add(bundle A) Note over I,T: Complete() 内部执行 I-->C: Completed 回调(同步) C->T: SetSelectedLocale(B) T->T: ReleaseAllTables() Note right of T: 只释放表引用 <br/>bundle A 仍在字典 T->V: Add(bundle B) V-->T: ArgumentException

调用栈还在 Addressables 完成逻辑里,SetSelectedLocale 又启动了新的异步操作链 —— 撞 key 就崩。

两种修复方式

核心思路:把语言选择搬出 init 流程,或等 init 真正结束。

A · 推荐

IStartupLocaleSelector

把语言选择烘焙进初始化流程,零冗余。

[Serializable]
public class SavedLocaleSelector
    : IStartupLocaleSelector
{
    public Locale GetStartupLocale(
        ILocalesProvider available)
    {
        var code = LoadFromSave();
        if (string.IsNullOrEmpty(code))
            return null;
        return available.GetLocale(
            new LocaleIdentifier(code));
    }
}

→ Inspector 放到 Startup Locale Selectors 列表第一位

B · 兜底

协程 yield 等 init 结束

yield 让 Complete() 完全返回后再切。

IEnumerator ApplySavedLocale()
{
    var op = LocalizationSettings
        .InitializationOperation;

    if (!op.IsDone)
        yield return op;
    // 此时 Complete() 已真正结束

    var target = LocalizationSettings
        .AvailableLocales
        .GetLocale(
            new LocaleIdentifier(code));
    if (target != null)
        LocalizationSettings
            .SelectedLocale = target;
}
坑 ① · 关键原则

Completed 回调本身是安全的同步委托调用,
不要在其中启动新的 Addressables 异步操作链

普通同步 Unity API(SetActive、设置文本、读取已加载的数据)在 Completed 里都没问题。
限制仅在于:不要启动会改变 Addressables 资源状态的操作 —— SetSelectedLocale 内部会这么做,所以是禁区。

02
闪烁

切换语言时
文本和字体
不是同时更新

为什么闪?

SetSelectedLocale 释放旧表后,StringTable 和 AssetTable 走两条独立的异步加载链。

sequenceDiagram autonumber participant U as User participant S as System participant ST as StringTable participant AT as AssetTable U->S: SetSelectedLocale(B) S->S: ReleaseAllTables(A) par 异步并行 S->ST: 加载 B.StringTable S->AT: 加载 B.AssetTable end ST-->S: 50ms 完成 Note over S: 文本已变 <br/>字体还是 A 的 AT-->S: 120ms 完成 Note over S: 字体最终也变 B 的

肉眼可见的"先变字体后变文本"(或反过来)—— 切回 A 语言时同样问题。

让切换瞬间完成

思路:表已经在内存里 → 切语言就是改指针,不会有加载延迟。

A · 推荐

Preload All Tables

LocalizationSettings.asset 里勾选所有语言的 Preload,让所有 StringTable 和 AssetTable 在 init 时全部加载并常驻内存。

切换零延迟
内存占用增加

→ 适合语言数量不多(2-5 种)的项目

B · 替代

代码持有 Table 引用

在代码里持久持有 LocalizedStringTable 等引用,订阅 TableChanged 事件 —— 系统会为新语言重新加载表并通知你,而不是彻底释放。

内存可控
需要手动管理
03
状态污染

退出游戏时
不要访问
SelectedLocale

坑 ③ · 为什么崩 · ① / ②

触发链:ResetState 把缓存清空

退出 Play Mode 时 ResetState() 清空缓存,再次访问 SelectedLocale 会触发完整的重新初始化管线。

flowchart TD A["退出 Play Mode"] --> B["ResetState"] B --> C["m_SelectedLocaleAsync = default"] C --> D{"代码访问 SelectedLocale"} D --> E["GetSelectedLocaleAsync"] E --> F["SelectActiveLocale"]

现在所有坑都准备好了 —— 接下来 SelectActiveLocale 会遍历 Selectors 找语言 → 下一页

坑 ③ · 为什么崩 · ② / ②

Selector 链掉链子,系统语言兜底

Domain 销毁中,自定义 Selector 拿不到上下文,3 个里 2 个返回 null,兜底的那个成了替罪羊。

flowchart TD A["SelectActiveLocale"] --> B["SavedLocaleSelector <br/>→ null"] A --> C["CommandLineSelector <br/>→ null"] A --> D["SystemLocaleSelector <br/>→ ja ✓"] D --> E["SelectedLocaleChanged 事件"] E --> F["回调把系统语言写入存档"] F --> G["💥 状态污染"] style B fill:#f5f5f5,stroke:#d4d4d4,color:#737373 style C fill:#f5f5f5,stroke:#d4d4d4,color:#737373 style G fill:#fef2f2,stroke:#b91c1c,color:#b91c1c,stroke-width:2px

玩家精心选的语言被静默覆盖 —— 下一次启动看到的就是 ja,而且你还不知道为什么。

核心原则:用字符串缓存语言代码

退出时读缓存,绝不碰 Localization 系统。

// Model 层 — 纯字段,不碰 LocalizationSettings
string _languageCode = "zh-CN";
public string CurrentLanguageCode => _languageCode;

// Service 层 — 通过事件同步,退出时先取消订阅再保存
void OnInitialize()
{
    LoadGameSettings();
    LocalizationSettings.SelectedLocaleChanged
        += OnSelectedLocaleChanged;
    Application.quitting += OnApplicationQuitting;
}

void OnSelectedLocaleChanged(Locale locale)
{
    if (locale != null)
        _languageCode = locale.Identifier.Code;
}

void OnApplicationQuitting()
{
    // 先断开事件订阅,阻止 Localization 清理时的回调覆盖
    LocalizationSettings.SelectedLocaleChanged
        -= OnSelectedLocaleChanged;
    SaveGameSettings();  // 再保存
}

关键:先取消订阅,再保存 —— 顺序反了会被 Localization 清理时的回调污染。

SUMMARY

三句话总结

01
Completed 是同步回调,但其中不能启动新的 Addressables 异步操作链。

语言选择走 IStartupLocaleSelector,或 yield 等 init 完全结束。

02
切换语言闪烁 = StringTable 和 AssetTable 异步竞速。

解法是让所有表常驻内存(Preload),或代码持有引用。

03
退出时用字符串缓存语言代码。

断开 Localization 事件订阅后再保存,避免系统语言污染存档。

Q & A

感谢观看

原文:Localization 踩坑日志 ①

Unity 2022.3.62f3 · Localization 1.5.3 · Addressables 1.22.3

← → 翻页 F 全屏 ? 帮助

键盘快捷键

← / →上 / 下页
Space下页
Home / End首页 / 末页
F全屏切换
?显示 / 隐藏帮助
Esc退出帮助 / 全屏