Files
zeling_v2/Assets/_Game/Scripts/Localization/LocalizationManager.cs
Joywayer 446fd5dcd0 feat: Add WorldStateFlagAttribute and custom property drawer for enhanced dialogue management
- Implemented WorldStateFlagAttribute to mark string fields as world state flags.
- Created NarrativeNPCEditor for custom inspector to visualize dialogue version activation states.
- Developed WorldStateFlagDrawer to provide dropdown menu for known flags in the inspector.
- Introduced ActorModule for managing DialogueActorSO assets, including viewing, creating, and deleting actors.
- Added DialogueModule for managing DialogueSequenceSO assets with detailed previews and action bars.
- Established QuestModule for managing QuestSO assets, including objectives and branches.
- Implemented QuestManagerPostprocessor to automatically refresh QuestManager's quest list on asset changes.
2026-05-24 00:36:11 +08:00

234 lines
9.8 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Assets/Scripts/Localization/LocalizationManager.cs
// 本地化管理器(运行时 JSON 文件驱动)。
//
// 数据格式(放在 Resources/Localization/{Language}/{TableName}.json
// {
// "entries": [
// { "key": "ui_start", "value": "开始游戏" },
// { "key": "ui_settings", "value": "设置" }
// ]
// }
//
// 推荐用法(通过 ServiceLocator 获取 ILocalizationService 实例):
// ServiceLocator.GetOrDefault<ILocalizationService>()?.Get("ui_start")
// ServiceLocator.GetOrDefault<ILocalizationService>()?.SetLanguage(Language.English)
//
// 便捷静态方法(内部仍走 ServiceLocator推荐在热路径之外使用
// LocalizationManager.Get("ui_start")
// LocalizationManager.Get("dlg_hero", "Dialogue")
using System;
using System.Collections.Generic;
using UnityEngine;
using BaseGames.Core;
using BaseGames.Core.Save;
namespace BaseGames.Localization
{
/// <summary>
/// 本地化管理器MonoBehaviour挂在 Persistent 场景)。
/// 实现 ILocalizationService + ISaveable通过 ServiceLocator 注册。
/// 语言偏好持久化到 SaveData.Settings.Language不使用 PlayerPrefs。
/// </summary>
public class LocalizationManager : MonoBehaviour, ILocalizationService, ISaveable
{
// 默认语言:回退链:当前语言 → 英语 → 直接返回 key
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();
// ILocalizationService 实例事件
private event Action<Language> _onLanguageChanged;
event Action<Language> ILocalizationService.OnLanguageChanged
{
add => _onLanguageChanged += value;
remove => _onLanguageChanged -= value;
}
// ── 生命周期 ──────────────────────────────────────────────────────────
private void Awake()
{
if (ServiceLocator.GetOrDefault<ILocalizationService>() != null) { Destroy(gameObject); return; }
ServiceLocator.Register<ILocalizationService>(this);
}
private void OnEnable()
{
ServiceLocator.GetOrDefault<ISaveableRegistry>()?.Register(this);
}
private void OnDisable()
{
ServiceLocator.GetOrDefault<ISaveableRegistry>()?.Unregister(this);
}
private void OnDestroy()
{
ServiceLocator.Unregister<ILocalizationService>(this);
}
// ── ILocalizationService ──────────────────────────────────────────────
public Language CurrentLanguage => _currentLanguage;
/// <summary>切换游戏语言并通知所有订阅者刷新文本。</summary>
public void SetLanguage(Language language)
{
if (_currentLanguage == language) return;
_currentLanguage = language;
_onLanguageChanged?.Invoke(language);
}
/// <summary>
/// 获取本地化字符串(显式接口实现)。
/// 查找顺序:当前语言 → 回退语言English→ 直接返回 key。
/// </summary>
string ILocalizationService.Get(string key, string table)
{
if (string.IsNullOrEmpty(key)) return string.Empty;
// 1. 尝试当前语言
if (TryGetFromTable(_currentLanguage, table, key, out string text))
return text;
// 2. 回退到 English
if (_currentLanguage != _fallbackLanguage &&
TryGetFromTable(_fallbackLanguage, table, key, out text))
return text;
// 3. 最终回退:原始 key
return key;
}
// ── ISaveable ─────────────────────────────────────────────────────────
public void OnSave(SaveData data)
{
if (data?.Settings == null) return;
data.Settings.Language = _currentLanguage.ToString();
}
public void OnLoad(SaveData data)
{
if (data?.Settings == null || string.IsNullOrEmpty(data.Settings.Language)) return;
if (Enum.TryParse<Language>(data.Settings.Language, out var lang))
SetLanguage(lang);
}
// ── 静态便捷方法 ─────────────────────────────────────────────────────────
/// <summary>
/// 静态快捷获取本地化字符串。委托给 ILocalizationService 实例;服务未注册时直接返回 key。
/// </summary>
public static string Get(string key, string table = "UI")
=> ServiceLocator.GetOrDefault<ILocalizationService>()?.Get(key, 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();
/// <summary>
/// 编辑器工具专用:不依赖运行时服务实例,直接从 Resources 读取本地化文本。
/// 结果缓存在静态字典中,同一编辑器会话内同一表只加载一次。
/// 找不到时返回 null区别于运行时的 key 回退,便于调用方判断是否显示 key
/// </summary>
public static string GetEditorPreview(string key, string table = "UI",
Language language = Language.ChineseSimplified)
{
if (string.IsNullOrEmpty(key)) return null;
var dict = GetEditorTable(language, table)
?? GetEditorTable(Language.English, table); // 中文缺失时英文回退
if (dict == null) return null;
dict.TryGetValue(key, out var value);
return value; // 找不到 key 时返回 null
}
private static System.Collections.Generic.Dictionary<string, string> GetEditorTable(
Language language, string table)
{
string cacheKey = $"{language}/{table}";
if (s_editorPreviewCache.TryGetValue(cacheKey, out var cached))
return cached; // 已缓存(可能是 null 占位,表示文件不存在)
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;
s_editorPreviewCache[cacheKey] = dict;
return dict;
}
#endif
private bool TryGetFromTable(Language language, string table, string key, out string value)
{
var cacheKey = $"{language}/{table}";
if (!_cache.TryGetValue(cacheKey, out var dict))
{
dict = LoadTable(language, table);
_cache[cacheKey] = dict; // 即使加载失败也存入空字典,避免每帧重试
}
if (dict != null && dict.TryGetValue(key, out value)) return true;
value = null;
return false;
}
/// <summary>
/// 从 Resources/Localization/{language}/{table}.json 加载字符串表。
/// 返回 null 表示文件不存在。
/// </summary>
private static Dictionary<string, string> LoadTable(Language language, string table)
{
string path = $"Localization/{language}/{table}";
var asset = Resources.Load<TextAsset>(path);
if (asset == null) return null;
var parsed = JsonUtility.FromJson<StringTableJson>(asset.text);
if (parsed?.entries == null) return null;
var dict = new Dictionary<string, string>(parsed.entries.Count, StringComparer.Ordinal);
foreach (var entry in parsed.entries)
if (!string.IsNullOrEmpty(entry.key))
dict[entry.key] = entry.value ?? string.Empty;
return dict;
}
// ── 序列化辅助类型 ────────────────────────────────────────────────────
[Serializable]
private class StringTableJson
{
public List<StringEntry> entries;
}
[Serializable]
private class StringEntry
{
public string key;
public string value;
}
}
}