UI系统优化

This commit is contained in:
2026-05-25 11:54:37 +08:00
parent c7057db27d
commit 3c812cfb41
130 changed files with 4738 additions and 477 deletions

View File

@@ -1,4 +1,4 @@
// Assets/Scripts/Localization/LocalizationManager.cs
// Assets/Scripts/Localization/LocalizationManager.cs
// 本地化管理器(运行时 JSON 文件驱动)。
//
// 数据格式(放在 Resources/Localization/{Language}/{TableName}.json
@@ -15,9 +15,11 @@
//
// 便捷静态方法(内部仍走 ServiceLocator推荐在热路径之外使用
// LocalizationManager.Get("ui_start")
// LocalizationManager.Get("dlg_hero", "Dialogue")
// LocalizationManager.Get("dlg_hero", LocalizationTable.Dialogue)
// LocalizationManager.GetFormat("REWARD_GOLD", LocalizationTable.UI, 100)
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using BaseGames.Core;
@@ -36,10 +38,17 @@ namespace BaseGames.Localization
private Language _currentLanguage = Language.ChineseSimplified;
private readonly Language _fallbackLanguage = Language.English;
// 双层缓存:languageKey("ChineseSimplified/UI") → (key → value)
private readonly Dictionary<string, Dictionary<string, string>> _cache = new();
// 双层缓存:(Language, tableName) 结构体键 → (key → value)
// 使用值类型 CacheKey 代替字符串插值,消除每次 Get 调用的 string 堆分配。
private readonly Dictionary<CacheKey, Dictionary<string, string>> _cache = new();
// ILocalizationService 实例事件
// LanguageEventChannelSO语言切换时向 SO 驱动的 UI 组件广播。
// 在 Persistent 场景预制体的 Inspector 中拖入 EVT_LanguageChanged.asset。
[SerializeField]
[Tooltip("语言切换事件频道EVT_LanguageChanged.asset。切换语言时广播订阅此频道的 UI 组件自动刷新文本。")]
private LanguageEventChannelSO _languageEventChannel;
// ILocalizationService 实例事件C# 订阅,供不方便引用 SO 的组件使用)
private event Action<Language> _onLanguageChanged;
event Action<Language> ILocalizationService.OnLanguageChanged
{
@@ -78,8 +87,22 @@ namespace BaseGames.Localization
if (_currentLanguage == language) return;
_currentLanguage = language;
_onLanguageChanged?.Invoke(language);
_languageEventChannel?.Raise(language);
// 同步到设置文件ISettingsService 存储 locale code 字符串)
ServiceLocator.GetOrDefault<ISettingsService>()?.SetLanguage(LanguageToLocaleCode(language));
}
/// <summary>将 Language 枚举转换为标准 locale code 字符串。</summary>
public static string LanguageToLocaleCode(Language language) => language switch
{
Language.ChineseSimplified => "zh-CN",
Language.Japanese => "ja-JP",
Language.Korean => "ko-KR",
Language.English => "en-US",
_ => "en-US",
};
/// <summary>
/// 获取本地化字符串(显式接口实现)。
/// 查找顺序:当前语言 → 回退语言English→ 直接返回 key。
@@ -101,6 +124,118 @@ namespace BaseGames.Localization
return key;
}
/// <summary>
/// 尝试获取本地化字符串(显式接口实现)。
/// 返回 false 时 value 为 nullkey 不会作为 value 返回。
/// </summary>
bool ILocalizationService.TryGet(string key, out string value, string table)
{
if (string.IsNullOrEmpty(key)) { value = null; return false; }
if (TryGetFromTable(_currentLanguage, table, key, out value)) return true;
if (_currentLanguage != _fallbackLanguage &&
TryGetFromTable(_fallbackLanguage, table, key, out value)) return true;
value = null;
return false;
}
/// <summary>
/// 获取带格式化参数的本地化字符串(显式接口实现)。
/// 格式化失败时静默返回原始模板字符串。
/// </summary>
string ILocalizationService.GetFormat(string key, string table, params object[] args)
{
string template = ((ILocalizationService)this).Get(key, table);
if (args == null || args.Length == 0) return template;
try { return string.Format(template, args); }
catch (Exception e)
{
#if UNITY_EDITOR || DEVELOPMENT_BUILD
Debug.LogWarning($"[LocalizationManager] GetFormat '{key}' 格式化失败: {e.Message}");
#else
_ = e;
#endif
return template;
}
}
/// <summary>
/// 获取带数量的复数形式本地化字符串(显式接口实现)。
/// 先查找 "{key}_one"count==1或 "{key}_other"count≠1找不到则回退到基础 key。
/// 模板以 string.Format(template, count) 展开。
/// </summary>
string ILocalizationService.GetPlural(string key, int count, string table)
{
string pluralKey = count == 1 ? $"{key}_one" : $"{key}_other";
if (TryGetFromTable(_currentLanguage, table, pluralKey, out string text) ||
(_currentLanguage != _fallbackLanguage &&
TryGetFromTable(_fallbackLanguage, table, pluralKey, out text)))
{
try { return string.Format(text, count); }
catch { return text; }
}
// 回退到基础 key
string baseText = ((ILocalizationService)this).Get(key, table);
try { return string.Format(baseText, count); }
catch { return baseText; }
}
/// <summary>
/// 同步预热指定语言的所有已知本地化表,不阻塞检测已缓存表(显式接口实现)。
/// </summary>
void ILocalizationService.PreloadTables(Language language)
{
foreach (var table in LocalizationTable.All)
{
var ck = new CacheKey(language, table);
if (_cache.ContainsKey(ck)) continue;
var dict = LoadTable(language, table);
#if UNITY_EDITOR || DEVELOPMENT_BUILD
if (dict == null)
Debug.LogWarning(
$"[LocalizationManager] PreloadTables{language}/{table} 未找到," +
$"请确认 Resources/Localization/{language}/{table}.json 存在。");
#endif
_cache[ck] = dict;
}
}
/// <summary>
/// 异步分帧预热:每帧加载一个表,不阻塞主线程(显式接口实现)。
/// 建议在 Loading Screen 的协程中调用。
/// </summary>
void ILocalizationService.PreloadTablesAsync(Language language, Action onComplete)
=> StartCoroutine(PreloadTablesRoutine(language, onComplete));
private IEnumerator PreloadTablesRoutine(Language language, Action onComplete)
{
foreach (var table in LocalizationTable.All)
{
var ck = new CacheKey(language, table);
if (_cache.ContainsKey(ck)) { yield return null; continue; }
string path = $"Localization/{language}/{table}";
var request = Resources.LoadAsync<TextAsset>(path);
yield return request;
var asset = request.asset as TextAsset;
var dict = asset == null ? null : ParseTableText(asset.text);
#if UNITY_EDITOR || DEVELOPMENT_BUILD
if (dict == null)
Debug.LogWarning(
$"[LocalizationManager] PreloadTablesAsync{language}/{table} 未找到," +
$"请确认 Resources/Localization/{language}/{table}.json 存在。");
#endif
_cache[ck] = dict;
}
onComplete?.Invoke();
}
// ── ISaveable ─────────────────────────────────────────────────────────
public void OnSave(SaveData data)
{
@@ -118,75 +253,91 @@ namespace BaseGames.Localization
// ── 静态便捷方法 ─────────────────────────────────────────────────────────
/// <summary>
/// 静态快捷获取本地化字符串。委托给 ILocalizationService 实例;服务未注册时直接返回 key。
/// 表名建议使用 <see cref="LocalizationTable"/> 中的常量。
/// </summary>
public static string Get(string key, string table = "UI")
public static string Get(string key, string table = LocalizationTable.UI)
=> ServiceLocator.GetOrDefault<ILocalizationService>()?.Get(key, table) ?? key;
/// <summary>
/// 静态快捷尝试获取本地化字符串。服务未注册时返回 false。
/// </summary>
public static bool TryGet(string key, out string value, string table = LocalizationTable.UI)
{
var svc = ServiceLocator.GetOrDefault<ILocalizationService>();
if (svc != null) return svc.TryGet(key, out value, table);
value = null;
return false;
}
/// <summary>
/// 静态快捷获取带格式化参数的本地化字符串。
/// 服务未注册时以 key 作为模板直接格式化后返回。
/// </summary>
public static string GetFormat(string key, string table, params object[] args)
{
var svc = ServiceLocator.GetOrDefault<ILocalizationService>();
if (svc != null) return svc.GetFormat(key, table, args);
if (args == null || args.Length == 0) return key;
try { return string.Format(key, args); }
catch { return key; }
}
/// <summary>
/// 静态快捷获取带数量的复数形式本地化字符串。
/// 服务未注册时直接返回 key。
/// </summary>
public static string GetPlural(string key, int count, string table = LocalizationTable.UI)
=> ServiceLocator.GetOrDefault<ILocalizationService>()?.GetPlural(key, count, table) ?? key;
// ── 编辑器预览(不依赖 ServiceLocator 实例)────────────────────────────
#if UNITY_EDITOR
// 编辑器预览缓存:"{language}/{table}" → (key → value)
// 生命周期与编辑器进程相同域重载时自动清空static 字段随域重载重置)。
private static readonly System.Collections.Generic.Dictionary<
string,
System.Collections.Generic.Dictionary<string, string>> s_editorPreviewCache = new();
private static readonly Dictionary<string, Dictionary<string, string>> s_editorPreviewCache = new();
/// <summary>
/// 编辑器工具专用:不依赖运行时服务实例,直接从 Resources 读取本地化文本。
/// 结果缓存在静态字典中,同一编辑器会话内同一表只加载一次。
/// 找不到时返回 null区别于运行时的 key 回退,便于调用方判断是否显示 key
/// </summary>
public static string GetEditorPreview(string key, string table = "UI",
public static string GetEditorPreview(string key, string table = LocalizationTable.UI,
Language language = Language.ChineseSimplified)
{
if (string.IsNullOrEmpty(key)) return null;
var dict = GetEditorTable(language, table)
?? GetEditorTable(Language.English, table); // 中文缺失时英文回退
?? GetEditorTable(Language.English, table);
if (dict == null) return null;
dict.TryGetValue(key, out var value);
return value; // 找不到 key 时返回 null
return value;
}
private static System.Collections.Generic.Dictionary<string, string> GetEditorTable(
Language language, string table)
public static Dictionary<string, string> GetEditorTable(Language language, string table)
{
string cacheKey = $"{language}/{table}";
if (s_editorPreviewCache.TryGetValue(cacheKey, out var cached))
return cached; // 已缓存(可能是 null 占位,表示文件不存在)
return cached;
string path = $"Localization/{language}/{table}";
var asset = Resources.Load<TextAsset>(path);
if (asset == null)
{
s_editorPreviewCache[cacheKey] = null; // 记录"不存在",避免重复尝试
return null;
}
var parsed = JsonUtility.FromJson<StringTableJson>(asset.text);
if (parsed?.entries == null)
{
s_editorPreviewCache[cacheKey] = null;
return null;
}
var dict = new System.Collections.Generic.Dictionary<string, string>(
parsed.entries.Count, System.StringComparer.Ordinal);
foreach (var entry in parsed.entries)
if (!string.IsNullOrEmpty(entry.key))
dict[entry.key] = entry.value ?? string.Empty;
var dict = asset == null ? null : ParseTableText(asset.text);
s_editorPreviewCache[cacheKey] = dict;
return dict;
}
/// <summary>编辑器工具:清除编辑器预览缓存(修改 JSON 文件后手动刷新时调用)。</summary>
public static void ClearEditorPreviewCache() => s_editorPreviewCache.Clear();
#endif
// ── 内部缓存查找 ──────────────────────────────────────────────────────
private bool TryGetFromTable(Language language, string table, string key, out string value)
{
var cacheKey = $"{language}/{table}";
if (!_cache.TryGetValue(cacheKey, out var dict))
var ck = new CacheKey(language, table);
if (!_cache.TryGetValue(ck, out var dict))
{
dict = LoadTable(language, table);
_cache[cacheKey] = dict; // 即使加载失败也存入空字典,避免每帧重试
_cache[ck] = dict; // 即使加载失败也存入空字典,避免每帧重试
}
if (dict != null && dict.TryGetValue(key, out value)) return true;
value = null;
@@ -201,9 +352,16 @@ namespace BaseGames.Localization
{
string path = $"Localization/{language}/{table}";
var asset = Resources.Load<TextAsset>(path);
if (asset == null) return null;
return asset == null ? null : ParseTableText(asset.text);
}
var parsed = JsonUtility.FromJson<StringTableJson>(asset.text);
/// <summary>
/// 将 JSON 文本解析为 key→value 字典(内部共享解析逻辑)。
/// 返回 null 表示格式无效。
/// </summary>
private static Dictionary<string, string> ParseTableText(string jsonText)
{
var parsed = JsonUtility.FromJson<StringTableJson>(jsonText);
if (parsed?.entries == null) return null;
var dict = new Dictionary<string, string>(parsed.entries.Count, StringComparer.Ordinal);
@@ -214,6 +372,29 @@ namespace BaseGames.Localization
return dict;
}
// ── 缓存键(值类型,消除字符串插值 GC──────────────────────────────
private readonly struct CacheKey : IEquatable<CacheKey>
{
private readonly Language _language;
private readonly string _table;
public CacheKey(Language language, string table)
{
_language = language;
_table = table;
}
public bool Equals(CacheKey other)
=> _language == other._language &&
string.Equals(_table, other._table, StringComparison.Ordinal);
public override bool Equals(object obj)
=> obj is CacheKey other && Equals(other);
public override int GetHashCode()
=> HashCode.Combine((int)_language, _table);
}
// ── 序列化辅助类型 ────────────────────────────────────────────────────
[Serializable]
@@ -230,4 +411,3 @@ namespace BaseGames.Localization
}
}
}