Unity I2多语言拆分方案【内存、包体⬇️】

I2 Localization 多语言资源拆分方案

📋 目录


I2 Localization 官方架构原理

核心数据结构

I2 Localization 插件使用以下核心数据结构管理多语言:

复制代码
┌─────────────────────────────────────────────────────────────────┐
│                    LanguageSourceData                            │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │  mLanguages: List<LanguageData>                          │    │
│  │  ├── [0] { Name: "English", Code: "en-US" }              │    │
│  │  ├── [1] { Name: "简体中文", Code: "zh-CN" }              │    │
│  │  └── [2] { Name: "日本語", Code: "ja-JP" }                │    │
│  └─────────────────────────────────────────────────────────┘    │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │  mTerms: List<TermData>                                  │    │
│  │  ├── [0] { Term: "btn_ok",                               │    │
│  │  │         Languages: ["OK", "确定", "はい"] }            │    │
│  │  ├── [1] { Term: "btn_cancel",                           │    │
│  │  │         Languages: ["Cancel", "取消", "キャンセル"] }  │    │
│  │  └── ...                                                 │    │
│  └─────────────────────────────────────────────────────────┘    │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │  mDictionary: Dictionary<string, TermData>               │    │
│  │  用于快速查找 Term                                        │    │
│  └─────────────────────────────────────────────────────────┘    │
└─────────────────────────────────────────────────────────────────┘

翻译查找流程

复制代码
┌──────────────────────────────────────────────────────────────────┐
│  LocalizationManager.GetTranslation("btn_ok")                    │
└──────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌──────────────────────────────────────────────────────────────────┐
│  遍历 Sources 列表(支持多个 LanguageSourceData)                 │
│  for (int i = 0; i < Sources.Count; i++)                         │
│  {                                                               │
│      if (Sources[i].TryGetTranslation(term, out translation))    │
│          return translation;                                     │
│  }                                                               │
└──────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌──────────────────────────────────────────────────────────────────┐
│  LanguageSourceData.TryGetTranslation(term)                      │
│  ├── 1. 从 mDictionary 查找 TermData                             │
│  ├── 2. 获取当前语言索引 CurrentLanguageIndex                     │
│  └── 3. 返回 termData.Languages[CurrentLanguageIndex]            │
└──────────────────────────────────────────────────────────────────┘
                              │
                              ▼
                         返回 "确定"

TermData 结构详解

每个 TermData 存储一个 Key 对应的所有语言翻译:

csharp 复制代码
public class TermData
{
    public string Term;           // Key 名称,如 "btn_ok"
    public eTermType TermType;    // 类型:Text, Texture, Audio 等
    public string Description;    // 描述(仅 Editor)
  
    // 🔑 核心:所有语言的翻译存储在同一个数组中
    public string[] Languages;    // ["OK", "确定", "はい", ...]
    public byte[] Flags;          // 每个翻译的标记位
}

关键点Languages 数组的索引与 mLanguages 列表的索引一一对应。

语言切换原理

csharp 复制代码
// 官方语言切换流程
LocalizationManager.CurrentLanguage = "简体中文";

// 内部实现:
// 1. 找到语言索引
int langIndex = Source.GetLanguageIndex("简体中文");  // 返回 1

// 2. 后续所有翻译查询都使用这个索引
string translation = termData.Languages[langIndex];  // Languages[1] = "确定"

Localize 组件的更新机制

当语言切换时,I2 通过以下机制通知所有 Localize 组件更新:

复制代码
┌────────────────────────────────────────────────────────────────────┐
│  LocalizationManager.CurrentLanguage = "简体中文"                   │
└────────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌────────────────────────────────────────────────────────────────────┐
│  SetLanguageAndCode(languageName, languageCode)                    │
│  ├── mCurrentLanguage = languageName                               │
│  ├── mLanguageCode = languageCode                                  │
│  └── LocalizeAll(Force)  // 🔑 触发全局更新                         │
└────────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌────────────────────────────────────────────────────────────────────┐
│  LocalizeAll()                                                     │
│  // 使用 Unity API 查找场景中所有 Localize 组件                      │
│  Localize[] locals = Resources.FindObjectsOfTypeAll<Localize>();   │
│  foreach (var local in locals)                                     │
│  {                                                                 │
│      local.OnLocalize(Force);  // 逐个通知更新                      │
│  }                                                                 │
│  OnLocalizeEvent?.Invoke();    // 触发全局事件(可选监听)            │
└────────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌────────────────────────────────────────────────────────────────────┐
│  Localize.OnLocalize(Force)                                        │
│  ├── 检查:LastLocalizedLanguage == CurrentLanguage ?              │
│  │   └── 如果相同且非强制,跳过更新                                  │
│  ├── 获取翻译:LocalizationManager.GetTranslation(FinalTerm)        │
│  └── 更新目标:mLocalizeTarget.DoLocalize(translation)             │
│      └── 例如:Text.text = translation                             │
└────────────────────────────────────────────────────────────────────┘

核心代码(LocalizationManager_Language.cs):

csharp 复制代码
public static void SetLanguageAndCode(string LanguageName, string LanguageCode, ...)
{
    if (mCurrentLanguage != LanguageName || mLanguageCode != LanguageCode || Force)
    {
        mCurrentLanguage = LanguageName;
        mLanguageCode = LanguageCode;
      
        // 🔑 通知所有 Localize 组件更新
        LocalizeAll(Force);
    }
}

核心代码(LocalizationManager_Translation.cs):

csharp 复制代码
public static void LocalizeAll(bool Force = false)
{
    // 查找场景中所有 Localize 组件(包括未激活的)
    Localize[] Locals = (Localize[])Resources.FindObjectsOfTypeAll(typeof(Localize));
  
    for (int i = 0; i < Locals.Length; i++)
    {
        Locals[i].OnLocalize(Force);
    }
  
    // 触发全局事件,供其他脚本监听
    if (OnLocalizeEvent != null)
        OnLocalizeEvent();
}

Localize 组件的更新逻辑(Localize.cs):

csharp 复制代码
public void OnLocalize(bool Force = false)
{
    // 避免重复更新:如果语言没变且非强制,跳过
    if (!Force && LastLocalizedLanguage == LocalizationManager.CurrentLanguage)
        return;
      
    LastLocalizedLanguage = LocalizationManager.CurrentLanguage;
  
    // 获取新翻译
    MainTranslation = LocalizationManager.GetTranslation(FinalTerm);
  
    // 更新目标组件(如 Text、Image 等)
    mLocalizeTarget.DoLocalize(this, MainTranslation, SecondaryTranslation);
}

关键设计点

机制 说明
Resources.FindObjectsOfTypeAll 查找所有 Localize 组件,包括未激活的
LastLocalizedLanguage 缓存上次语言,避免重复更新
OnLocalizeEvent 全局事件,供自定义脚本监听语言变化
mLocalizeTarget 抽象目标接口,支持 Text、Image、Audio 等多种类型

官方方案的问题

单一 Asset 架构

官方设计将所有数据存储在一个 Asset 中:

复制代码
Resources/
└── I2Languages.prefab (或 .asset)
    └── LanguageSourceData
        ├── mLanguages: [en-US, zh-CN, ja-JP, ko-KR, ...]  (25种语言)
        └── mTerms: [5000个 TermData,每个包含25种翻译]

问题分析

问题 具体表现
包体积膨胀 5,000 Key × 25 语言 × 50 字节 ≈6.25 MB 但用户实际只需 1 种语言 ≈ 0.25 MB
内存浪费 运行时所有语言数据常驻内存,实际只用 1/25
加载缓慢 启动时反序列化 6MB+ 数据
热更粒度粗 改一个语言的一个词,需更新整个 6MB 文件

拆分方案设计

核心思路

TermData.Languages[] 数组按语言拆分到独立文件:

复制代码
改造前:                              改造后:
┌────────────────────┐               ┌─────────────────────┐
│ I2Languages.asset  │               │ I2Languages.asset   │ (仅语言列表)
│ ├── mLanguages     │               │ └── mLanguages      │
│ │   [en-US, zh-CN] │               │     [en-US, zh-CN, ja-JP...] │
│ └── mTerms         │               │     (无 mTerms)     │
│     ├── btn_ok     │               ├─────────────────────┤
│     │   Languages: │               │ en-US.asset         │
│     │   [0]="OK"   │     ──────►   │ ├── mLanguages: [en-US] │
│     │   [1]="确定" │               │ └── mTerms          │
│     │   [2]="はい" │               │     └── btn_ok      │
│     └── ...        │               │         Languages[0]="OK" │
└────────────────────┘               ├─────────────────────┤
                                     │ zh-CN.asset         │
                                     │ ├── mLanguages: [zh-CN] │
                                     │ └── mTerms          │
                                     │     └── btn_ok      │
                                     │         Languages[0]="确定" │
                                     └─────────────────────┘

文件结构

复制代码
AssetBundle/SplitLanguage/
├── I2Languages.asset     # 索引:仅包含语言列表(mLanguages)
├── en-US.asset           # 英语:包含所有 Key + 英语翻译
├── zh-CN.asset           # 中文:包含所有 Key + 中文翻译
├── ja-JP.asset           # 日语:包含所有 Key + 日语翻译
└── ...

核心改造点

修改 Import_CSV 方法(唯一代码改动)

官方 Import_CSV 方法签名:

csharp 复制代码
// 官方版本(三个参数)
public string Import_CSV(string Category, List<string[]> CSV, 
    eSpreadsheetUpdateMode UpdateMode = eSpreadsheetUpdateMode.Replace)

我们添加了第四个参数 splitCode

csharp 复制代码
// 改造版本(四个参数)
public string Import_CSV(string Category, List<string[]> CSV, 
    eSpreadsheetUpdateMode UpdateMode = eSpreadsheetUpdateMode.Replace,
    string splitCode = "")  // 🔑 新增:指定只导入某种语言

新增代码(约 5 行):

csharp 复制代码
// LanguageSourceData_Import_CSV.cs 第 90-94 行
// 在解析语言列时,跳过非目标语言

GoogleLanguages.UnPackCodeFromLanguageName(langToken, out LanName, out LanCode);

// ========== 新增代码 START ==========
if (!string.IsNullOrEmpty(splitCode) && LanCode != splitCode)
{
    LanIndices[i] = -1;  // 标记为跳过
    continue;
}
// ========== 新增代码 END ==========

int LanIdx = GetLanguageIndexFromCode(LanCode);
// ... 后续逻辑不变

设计要点

  • 向后兼容:splitCode 默认为空,行为与官方版本完全一致
  • 最小侵入:只添加 5 行代码,不修改任何官方逻辑
  • 通过 LanIndices[i] = -1 标记跳过,复用官方的跳过机制

多语言切分工具

I2UpdateTool - 核心拆分工具

提供 Unity Editor 菜单入口:Tools/I2Localize Split

csharp 复制代码
public class I2SplitTool : EditorWindow
{
    [MenuItem("Tools/I2Localize Split")]
    private static void ShowWindow()
    {
        var window = GetWindow<I2SplitTool>("多语言切分工具");
        window.Show();
    }
}

核心拆分逻辑

csharp 复制代码
/// <summary>
/// 按语言拆分多语言配置
/// </summary>
/// <param name="csvString">完整的 CSV 数据(包含所有语言)</param>
/// <param name="firstSheet">是否为第一个表(决定 Replace 还是 Merge)</param>
private void SplitLocalConfig(string csvString, bool firstSheet)
{
    var outputPath = "Assets/AssetBundle/SplitLanguage";
  
    // 1. 清空输出目录
    if (Directory.Exists(outputPath))
        Directory.Delete(outputPath, true);
    Directory.CreateDirectory(outputPath);

    // 2. 解析 CSV
    List<string[]> csv = LocalizationReader.ReadCSV(csvString);
    var csvLanguages = new List<string[]>() { csv[0] };  // 只取表头

    // 3. 创建索引文件(仅包含语言列表,不包含 Key 和翻译)
    //    csvLanguages 只有表头一行:["Key", "Type", "Desc", "en-US", "zh-CN", ...]
    var i2LanguageAsset = CreateInstance<LanguageSourceAsset>();
    i2LanguageAsset.mSource.Import_CSV(string.Empty, csvLanguages);
    AssetDatabase.CreateAsset(i2LanguageAsset, 
        Path.Combine(outputPath, "I2Languages.asset"));

    // 4. 🔑 核心:为每种语言创建独立的 Asset
    foreach (var language in _newSourceData.mLanguages)
    {
        if (!language.IsEnabled()) continue;
  
        // 创建新的 LanguageSourceAsset
        var tempSource = CreateInstance<LanguageSourceAsset>();
  
        // 使用 splitCode 参数,只导入当前语言
        tempSource.mSource.Import_CSV(
            string.Empty, 
            csv,
            eSpreadsheetUpdateMode.Replace, 
            language.Code);  // 🔑 关键:传入语言代码
  
        // 保存为独立文件:en-US.asset, zh-CN.asset, ja-JP.asset ...
        var output = Path.Combine(outputPath, language.Code + ".asset");
        AssetDatabase.CreateAsset(tempSource, output);
    }

    AssetDatabase.SaveAssets();
}

更新流程

复制代码
┌────────────────────────────────────────────────────────────────┐
│  1. 点击 "拆分多语言数据"                                       │
└────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌────────────────────────────────────────────────────────────────┐
│  2. SplitLocalConfig() 执行拆分                                 │
│     ├── 创建 I2Languages.asset(索引)                          │
│     ├── 创建 en-US.asset                                        │
│     ├── 创建 zh-CN.asset                                        │
│     ├── 创建 ja-JP.asset                                        │
│     └── ...                                                     │
└────────────────────────────────────────────────────────────────┘

运行时加载

LanguageLoader - 按需加载管理

csharp 复制代码
public static class LanguageLoader
{
    private static LanguageSourceAsset _source;

    /// <summary>
    /// 初始化多语言系统
    /// </summary>
    public static void Init()
    {
        // 1. 加载索引文件(语言列表,用于语言切换时查询)
        LoadLanguageIndex();
  
        // 2. 加载当前语言的翻译数据
        LoadCurrentLanguage();
    }

    /// <summary>
    /// 加载索引文件(仅包含语言列表,用于语言名称和代码的映射)
    /// </summary>
    private static void LoadLanguageIndex()
    {
        // I2Languages.asset 仅包含 mLanguages 列表,体积极小(约 2KB)
        // 作用:提供语言代码到语言名称的映射(如 "en-US" → "English")
        var asset = LoadAsset<LanguageSourceAsset>(
            Path.Combine("SplitLanguage", "I2Languages.asset"));
        asset.mSource.Awake();  // 注册到 LocalizationManager.Sources
    }

    /// <summary>
    /// 加载当前语言的翻译数据
    /// </summary>
    private static void LoadCurrentLanguage()
    {
        var code = GetLanguageCode();  // 如 "en-US"
  
        // 加载对应语言的 Asset(如 en-US.asset,约 250KB)
        var path = Path.Combine("SplitLanguage", code + ".asset");
        _source = LoadAsset<LanguageSourceAsset>(path);
  
        // Fallback:找不到时使用英语
        if (_source == null)
        {
            _source = LoadAsset<LanguageSourceAsset>(
                Path.Combine("SplitLanguage", "en-US.asset"));
            LocalizationManager.SetLanguageAndCode("English (United States)", "en-US");
        }
  
        _source.mSource.Awake();  // 注册到 LocalizationManager.Sources
    }

    /// <summary>
    /// 运行时切换语言
    /// </summary>
    public static void SwitchLanguage(string code)
    {
        // 1. 卸载当前语言
        UnloadLanguage();
  
        // 2. 加载新语言
        var path = Path.Combine("SplitLanguage", code + ".asset");
        _source = LoadAsset<LanguageSourceAsset>(path);
  
        if (_source != null)
        {
            var languageName = LocalizationManager.GetLanguageFromCode(code);
            LocalizationManager.SetLanguageAndCode(languageName, code);
            _source.mSource.Awake();
        }
    }

    /// <summary>
    /// 卸载当前语言资源
    /// </summary>
    private static void UnloadLanguage()
    {
        if (_source != null)
        {
            _source.mSource.OnDestroy();  // 从 Sources 列表移除
            UnloadAsset(_source);
            _source = null;
        }
    }
}

加载时序图

复制代码
游戏启动
    │
    ▼
LanguageLoader.Init()
    │
    ├──► LoadLanguageIndex()
    │    └── 加载 I2Languages.asset (仅语言列表, ~2KB)
    │        └── 注册到 LocalizationManager.Sources[0]
    │        └── 作用:提供语言代码↔名称映射
    │
    └──► LoadCurrentLanguage()
         └── 加载 en-US.asset (Key + 英语翻译, ~250KB)
             └── 注册到 LocalizationManager.Sources[1]
    │
    ▼
翻译查询
    │
    ▼
LocalizationManager.GetTranslation("btn_ok")
    │
    ├── Sources[0] (I2Languages) → 没有 Term 数据,跳过
    └── Sources[1] (en-US) → 找到 Term,返回 "OK"
    │
    ▼
用户切换语言 → LanguageLoader.SwitchLanguage("zh-CN")
    │
    ├── UnloadLanguage() → 卸载 en-US.asset
    └── LoadLanguage() → 加载 zh-CN.asset
    │
    ▼
LocalizationManager.GetTranslation("btn_ok") → 返回 "确定"

收益分析

包体积对比

指标 官方方案 拆分方案 优化幅度
初始包大小 6.25 MB ~0.25 MB ↓ 96%
运行时内存 6.25 MB ~0.25 MB ↓ 96%
启动加载时间 ~500 ms ~30 ms ↓ 94%

按 5,000 Key × 25 语言 × 50 字节计算

拆分后:I2Languages.asset (~2KB) + 单语言 Asset (~250KB)

热更新对比

更新场景 官方方案 拆分方案
修改英语一个词 下载 6.25 MB 下载 0.25 MB
新增一种语言 下载 6.25 MB 下载 0.25 MB
修复日语翻译 下载 6.25 MB 下载 0.25 MB
相关推荐
jtymyxmz14 小时前
《Unity Shader》12.5 Bloom 效果
unity·游戏引擎
jtymyxmz16 小时前
《Unity Shader》12.6 运动模糊
unity·游戏引擎
jtymyxmz18 小时前
《Unity Shader》12.4.2 实现
unity·游戏引擎
sindyra20 小时前
Unity UGUI 之 Canvas Scaler
unity·游戏引擎
在路上看风景1 天前
2.Square Grid
unity
程序猿阿伟1 天前
《突破Unity热更新瓶颈:底层函数调用限制与生态适配秘籍》
unity·游戏引擎
龙智DevSecOps解决方案1 天前
Perforce《2025游戏技术现状报告》Part 3:不同行业挑战以及Unreal、Godot、自研游戏引擎的应用趋势
游戏引擎·godot·游戏开发·perforce
在路上看风景1 天前
13. UGUI合批
unity
jtymyxmz2 天前
《Unity Shader》12.2调整屏幕的亮度、饱和度和对比度
unity·游戏引擎