多轮审查和修复

This commit is contained in:
2026-05-12 15:34:08 +08:00
parent f55d2a57c3
commit ebbbb7332e
805 changed files with 838724 additions and 1905 deletions

View File

@@ -0,0 +1,24 @@
using System;
namespace BaseGames.Localization
{
/// <summary>
/// 本地化服务接口。通过 ServiceLocator 注册,供 UI 和游戏系统获取本地化文本。
/// </summary>
public interface ILocalizationService
{
/// <summary>当前激活的语言。</summary>
Language CurrentLanguage { get; }
/// <summary>
/// 获取本地化字符串。查找顺序:当前语言 → 回退语言English→ 直接返回 key。
/// </summary>
string Get(string key, string table = "UI");
/// <summary>切换游戏语言并通知所有订阅者刷新文本。</summary>
void SetLanguage(Language language);
/// <summary>语言切换时触发。</summary>
event Action<Language> OnLanguageChanged;
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: bd601a913eab0be4896b058f8c6fe8d5
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,14 @@
namespace BaseGames.Localization
{
/// <summary>
/// 游戏支持的语言列表。
/// 添加新语言时同步在 Resources/Localization/ 下创建对应子目录和 JSON 表文件。
/// </summary>
public enum Language
{
ChineseSimplified,
English,
Japanese,
Korean,
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 19c40515404112049b98f2632226fc78
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,12 @@
using BaseGames.Core.Events;
namespace BaseGames.Localization
{
/// <summary>
/// 语言切换事件频道。
/// 用法LocalizationManager 切换语言后广播UI 组件订阅并刷新文本。
/// 创建资产路径Assets/Data/Events/EVT_LanguageChanged.asset
/// </summary>
[UnityEngine.CreateAssetMenu(menuName = "Events/LanguageChanged")]
public class LanguageEventChannelSO : BaseEventChannelSO<Language> { }
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 828bc10f39b0bcc418209e0e7350220f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,177 @@
// Assets/Scripts/Localization/LocalizationManager.cs
// 本地化管理器(运行时 JSON 文件驱动)。
//
// 数据格式(放在 Resources/Localization/{Language}/{TableName}.json
// {
// "entries": [
// { "key": "ui_start", "value": "开始游戏" },
// { "key": "ui_settings", "value": "设置" }
// ]
// }
//
// 用法(静态 Facade保持调用兼容
// LocalizationManager.Get("ui_start") → "开始游戏"
// LocalizationManager.Get("dlg_hero", "Dialogue") → Dialogue 表中的对应文本
//
// 服务调用(通过 ServiceLocator
// ServiceLocator.GetOrDefault<ILocalizationService>()?.SetLanguage(Language.English)
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。
/// 保留静态 Get() Facade现有调用方无需修改。
/// </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();
// ── 静态事件代理(向后兼容静态订阅方式)─────────────────────────────────
/// <summary>语言切换时触发(静态代理,可通过 LocalizationManager.OnLanguageChanged += 订阅)。</summary>
public static event Action<Language> OnLanguageChanged;
// ILocalizationService 显式实现:委托给静态事件
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>
public string Get(string key, string table = "UI")
{
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);
}
// ── 静态 Facade保持现有调用方不变────────────────────────────────────
/// <summary>
/// 静态快捷获取本地化字符串。委托给 ILocalizationService 实例;服务未注册时直接返回 key。
/// </summary>
public static string Get(string key, string table = "UI")
=> ServiceLocator.GetOrDefault<ILocalizationService>()?.Get(key, table) ?? key;
// ── 内部 ─────────────────────────────────────────────────────────────
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; // 即使加载失败也存入空字典,避免每帧重试
}
return dict != null && dict.TryGetValue(key, out value);
}
/// <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;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 8dcd182e23f515147886cce01e26854d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: