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
相关推荐
沉默金鱼9 小时前
Unity实用技能-模型
unity·游戏引擎
阿里云云原生9 小时前
AgentRun:如何利用 AI Agent 构建现代化的舆情分析解决方案?
人工智能·unity·游戏引擎
在路上看风景10 小时前
2.8 预渲染
unity
老朱佩琪!10 小时前
Unity代理模式
unity·游戏引擎·代理模式
技术小甜甜11 小时前
【Godot】【入门】GDScript 快速上手(只讲游戏里最常用的 20% 语法)
android·游戏·编辑器·游戏引擎·godot
老朱佩琪!15 小时前
Unity命令模式
unity·游戏引擎·命令模式
世洋Blog15 小时前
Unity编辑器基础
unity·c#·编辑器·游戏引擎
老朱佩琪!16 小时前
Unity责任链模式
unity·设计模式·责任链模式
WarPigs16 小时前
Unity NetCode for GameObject笔记
笔记·unity·游戏引擎
龙智DevSecOps解决方案18 小时前
Perforce《2025游戏技术现状报告》Part 5:创意工作者在用什么工具以及如何看待游戏引擎与生成式AI(附免费下载)
游戏引擎·游戏开发·软件开发·perforce·ai创作·龙智