perf: 优化语言切换性能并修复实时翻译丢失 - 预构建O(1)查找字典、缓存组件检查和字体状态、改用FindObjectsOfType、调整SetLanguage顺序为LanguageChanged先于RefreshAll

This commit is contained in:
ayuan9957 2026-07-17 10:49:53 +08:00
parent 33a1681f3d
commit de46915c70

View File

@ -18,14 +18,23 @@ public class LocalizationManager : MonoBehaviour
public string source;
public string localized;
public object[] formatArgs;
public bool skipDropdown; // cached: this Text is a Dropdown caption/item
public bool skipInputField; // cached: this Text is inside an InputField
public bool fontApplied; // cached: font has been set at least once
}
private class DropdownState
{
public List<string> sources = new List<string>();
public List<string> localized = new List<string>();
public bool fontApplied;
}
// Pre-built lookup dictionaries for O(1) translation
private Dictionary<string, string> reverseTable;
private Dictionary<string, string[]> normalizedTable;
private Dictionary<string, string> normalizedReverseTable;
public const string PreferenceKey = "UI.Language";
private const string FontResourcePath = "Localization/NotoSansCJKsc-Regular";
@ -542,6 +551,37 @@ public class LocalizationManager : MonoBehaviour
instance = this;
language = Mathf.Clamp(PlayerPrefs.GetInt(PreferenceKey, 0), 0, 3);
font = Resources.Load<Font>(FontResourcePath);
BuildLookupTables();
}
private void BuildLookupTables()
{
reverseTable = new Dictionary<string, string>();
normalizedTable = new Dictionary<string, string[]>();
normalizedReverseTable = new Dictionary<string, string>();
foreach (var pair in table)
{
// Normalized key → values (for fuzzy Translate)
string nk = System.Text.RegularExpressions.Regex.Replace(pair.Key, @"\s+", " ").Trim();
if (!normalizedTable.ContainsKey(nk))
normalizedTable[nk] = pair.Value;
if (pair.Value == null) continue;
for (int i = 0; i < pair.Value.Length; i++)
{
if (pair.Value[i] == null) continue;
// Exact value → source key (for exact GetSourceText)
if (!reverseTable.ContainsKey(pair.Value[i]))
reverseTable[pair.Value[i]] = pair.Key;
// Normalized value → source key (for fuzzy GetSourceText)
string nv = System.Text.RegularExpressions.Regex.Replace(pair.Value[i], @"\s+", " ").Trim();
if (!normalizedReverseTable.ContainsKey(nv))
normalizedReverseTable[nv] = pair.Key;
}
}
}
private void Start()
@ -553,13 +593,10 @@ public class LocalizationManager : MonoBehaviour
private void LateUpdate()
{
// Covers canvases instantiated after the current frame and avoids duplicate bars.
LocalizationLanguageTestBar.EnsureForActiveCanvases();
// Periodically refresh localization for dynamically loaded UI (e.g. student panels).
if (++refreshFrameCounter >= 30)
if (++refreshFrameCounter >= 60)
{
refreshFrameCounter = 0;
LocalizationLanguageTestBar.EnsureForActiveCanvases();
RefreshAll();
}
}
@ -570,8 +607,10 @@ public class LocalizationManager : MonoBehaviour
instance.language = Mathf.Clamp(value, 0, 3);
PlayerPrefs.SetInt(PreferenceKey, instance.language);
PlayerPrefs.Save();
instance.RefreshAll();
// Fire LanguageChanged FIRST so handlers rebuild content (table rows, operation messages, etc.),
// then RefreshAll catches all texts including the newly created ones.
if (LanguageChanged != null) LanguageChanged();
instance.RefreshAll();
}
public static string Get(string source)
@ -700,10 +739,10 @@ public class LocalizationManager : MonoBehaviour
public void RefreshAll()
{
Text[] texts = Resources.FindObjectsOfTypeAll<Text>();
Text[] texts = FindObjectsOfType<Text>();
for (int i = 0; i < texts.Length; i++) Localize(texts[i]);
Dropdown[] dropdowns = Resources.FindObjectsOfTypeAll<Dropdown>();
Dropdown[] dropdowns = FindObjectsOfType<Dropdown>();
for (int i = 0; i < dropdowns.Length; i++) Localize(dropdowns[i]);
}
@ -716,14 +755,11 @@ public class LocalizationManager : MonoBehaviour
if (table.TryGetValue(source, out values) && values != null && language < values.Length && values[language] != null)
return values[language];
// Fuzzy: try normalized lookup
// Fuzzy: O(1) normalized lookup
string normalized = System.Text.RegularExpressions.Regex.Replace(source, @"\s+", " ").Trim();
foreach (KeyValuePair<string, string[]> pair in table)
{
string nk = System.Text.RegularExpressions.Regex.Replace(pair.Key, @"\s+", " ").Trim();
if (nk == normalized && pair.Value != null && language < pair.Value.Length && pair.Value[language] != null)
return pair.Value[language];
}
string[] normValues;
if (normalizedTable.TryGetValue(normalized, out normValues) && normValues != null && language < normValues.Length && normValues[language] != null)
return normValues[language];
return source;
}
@ -732,31 +768,13 @@ public class LocalizationManager : MonoBehaviour
{
if (string.IsNullOrEmpty(value) || table.ContainsKey(value)) return value;
// Exact match
foreach (KeyValuePair<string, string[]> pair in table)
{
if (pair.Value == null || pair.Value.Length <= 1) continue;
for (int i = 1; i < pair.Value.Length; i++)
{
if (object.ReferenceEquals(pair.Value[i], value)) return pair.Key;
if (pair.Value[i] != null && pair.Value[i] == value) return pair.Key;
}
}
// O(1) exact reverse lookup
string key;
if (reverseTable.TryGetValue(value, out key)) return key;
// Fuzzy match: normalize whitespace (newlines, tabs, multiple spaces → single space)
// O(1) fuzzy reverse lookup
string normalizedValue = System.Text.RegularExpressions.Regex.Replace(value, @"\s+", " ").Trim();
foreach (KeyValuePair<string, string[]> pair in table)
{
if (pair.Value == null || pair.Value.Length <= 1) continue;
string normalizedKey = System.Text.RegularExpressions.Regex.Replace(pair.Key, @"\s+", " ").Trim();
if (normalizedKey == normalizedValue) return pair.Key;
for (int i = 1; i < pair.Value.Length; i++)
{
if (pair.Value[i] == null) continue;
string normalizedVal = System.Text.RegularExpressions.Regex.Replace(pair.Value[i], @"\s+", " ").Trim();
if (normalizedVal == normalizedValue) return pair.Key;
}
}
if (normalizedReverseTable.TryGetValue(normalizedValue, out key)) return key;
return value;
}
@ -765,34 +783,40 @@ public class LocalizationManager : MonoBehaviour
{
if (text == null || !text.gameObject.scene.IsValid()) return;
// Dropdown caption and item texts are handled by Localize(Dropdown).
Dropdown dropdown = text.GetComponentInParent<Dropdown>();
if (dropdown != null && (dropdown.captionText == text || dropdown.itemText == text))
return;
int id = text.GetInstanceID();
TextState state;
if (!textStates.TryGetValue(id, out state))
{
// First encounter: do expensive checks once and cache
Dropdown dropdown = text.GetComponentInParent<Dropdown>();
InputField input = text.GetComponentInParent<InputField>();
state = new TextState
{
source = text.text,
localized = text.text,
skipDropdown = dropdown != null && (dropdown.captionText == text || dropdown.itemText == text),
skipInputField = input != null
};
textStates.Add(id, state);
}
else if (text.text != state.localized)
{
// Text was externally changed; update source but keep cached skip flags
state.source = GetSourceText(text.text);
state.formatArgs = null;
}
// InputField children are managed by their owning panels to avoid
// interfering with user input and placeholder rendering.
if (text.GetComponentInParent<InputField>() != null) return;
if (state.skipDropdown) return;
if (state.skipInputField) return;
if (font != null)
// Apply font only once per Text
if (font != null && !state.fontApplied)
{
Color savedColor = text.color;
text.font = font;
text.color = savedColor;
text.verticalOverflow = VerticalWrapMode.Overflow;
}
int id = text.GetInstanceID();
TextState state;
if (!textStates.TryGetValue(id, out state))
{
state = new TextState { source = text.text, localized = text.text };
textStates.Add(id, state);
}
else if (text.text != state.localized)
{
state.source = GetSourceText(text.text);
state.formatArgs = null;
state.fontApplied = true;
}
state.localized = state.formatArgs == null
@ -1041,7 +1065,7 @@ public class LocalizationManager : MonoBehaviour
if (font != null)
if (font != null && !state.fontApplied)
{
Text[] texts = dropdown.GetComponentsInChildren<Text>(true);
for (int i = 0; i < texts.Length; i++)
@ -1051,6 +1075,7 @@ public class LocalizationManager : MonoBehaviour
texts[i].color = savedColor;
texts[i].verticalOverflow = VerticalWrapMode.Overflow;
}
state.fontApplied = true;
}
dropdown.RefreshShownValue();