feat: 本地化字体兼容
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
using System.Collections;
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using AibisDream.Utility;
|
||||
using UnityEngine;
|
||||
@@ -11,22 +12,14 @@ using UnityEditor;
|
||||
|
||||
namespace AibisDream.Framework
|
||||
{
|
||||
/// <summary>
|
||||
/// csv 本地化列类别。集中声明"哪些 Locale 归到哪一列",csv 数据类按此枚举取字段。
|
||||
/// </summary>
|
||||
public enum LocaleKind
|
||||
{
|
||||
Cn,
|
||||
En,
|
||||
Ja,
|
||||
Ru,
|
||||
Es,
|
||||
PtBr
|
||||
}
|
||||
|
||||
public static class LocalizationKit
|
||||
{
|
||||
private const string LocalizationPrefix = "l10n.";
|
||||
private static Task<bool> currentLanguageSwitchTask;
|
||||
private static int languageSwitchVersion;
|
||||
private static bool managedLocaleChange;
|
||||
private static bool typographyLoaderConfigured;
|
||||
private static bool localeChangedSubscribed;
|
||||
|
||||
public static string CurrentLanguageCode { get; private set; } = "zh-Hans";
|
||||
public static Locale CurrentLocale => LocalizationSettings.SelectedLocale;
|
||||
@@ -78,8 +71,14 @@ namespace AibisDream.Framework
|
||||
{
|
||||
#if UNITY_EDITOR
|
||||
EditorApplication.playModeStateChanged += OnPlayModeChanged;
|
||||
if (EditorApplication.isPlayingOrWillChangePlaymode)
|
||||
{
|
||||
EnsureTypographyLoaderConfigured();
|
||||
SubscribeLocaleChanged();
|
||||
RefreshCurrentLanguageCodeAsync();
|
||||
}
|
||||
#else
|
||||
LocalizationSettings.SelectedLocaleChanged += OnLocaleChanged;
|
||||
SubscribeLocaleChanged();
|
||||
RefreshCurrentLanguageCodeAsync();
|
||||
#endif
|
||||
}
|
||||
@@ -88,6 +87,14 @@ namespace AibisDream.Framework
|
||||
{
|
||||
UpdateCurrentLanguageCode(locale);
|
||||
EnumEventSystem.Global.Send(SettingChangeEvent.LocaleChanged, locale);
|
||||
|
||||
if (!managedLocaleChange && locale != null)
|
||||
{
|
||||
EnsureTypographyLoaderConfigured();
|
||||
LocaleKind localeKind = GetLocaleKind(locale.Identifier.Code);
|
||||
if (!TypographyService.IsActiveFor(localeKind))
|
||||
currentLanguageSwitchTask = TypographyService.LoadAndActivateAsync(localeKind);
|
||||
}
|
||||
}
|
||||
|
||||
private static void UpdateCurrentLanguageCode(Locale locale)
|
||||
@@ -109,32 +116,95 @@ namespace AibisDream.Framework
|
||||
{
|
||||
if (playMode == PlayModeStateChange.EnteredPlayMode)
|
||||
{
|
||||
LocalizationSettings.SelectedLocaleChanged += OnLocaleChanged;
|
||||
EnsureTypographyLoaderConfigured();
|
||||
SubscribeLocaleChanged();
|
||||
RefreshCurrentLanguageCodeAsync();
|
||||
}
|
||||
else if (playMode == PlayModeStateChange.ExitingPlayMode)
|
||||
{
|
||||
LocalizationSettings.SelectedLocaleChanged -= OnLocaleChanged;
|
||||
UnsubscribeLocaleChanged();
|
||||
TypographyService.ResetForTests();
|
||||
typographyLoaderConfigured = false;
|
||||
currentLanguageSwitchTask = null;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
public static async void SwitchLanguage(string localeCode)
|
||||
public static void SwitchLanguage(string localeCode)
|
||||
{
|
||||
_ = SwitchLanguageAsync(localeCode);
|
||||
}
|
||||
|
||||
public static Task<bool> SwitchLanguageAsync(string localeCode)
|
||||
{
|
||||
int requestVersion = Interlocked.Increment(ref languageSwitchVersion);
|
||||
currentLanguageSwitchTask = SwitchLanguageCoreAsync(localeCode, requestVersion);
|
||||
return currentLanguageSwitchTask;
|
||||
}
|
||||
|
||||
private static async Task<bool> SwitchLanguageCoreAsync(string localeCode, int requestVersion)
|
||||
{
|
||||
// 等待本地化系统初始化完成
|
||||
await LocalizationSettings.InitializationOperation.Task;
|
||||
|
||||
var selectedLocale =
|
||||
LocalizationSettings.AvailableLocales.Locales.Find(locale => locale.Identifier.Code == localeCode);
|
||||
if (!selectedLocale)
|
||||
{
|
||||
Debug.LogWarning($"Locale with code {localeCode} not found.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (selectedLocale)
|
||||
EnsureTypographyLoaderConfigured();
|
||||
bool typographyReady = await TypographyService.LoadAndActivateAsync(GetLocaleKind(localeCode));
|
||||
if (requestVersion != languageSwitchVersion)
|
||||
return false;
|
||||
|
||||
managedLocaleChange = true;
|
||||
try
|
||||
{
|
||||
LocalizationSettings.SelectedLocale = selectedLocale;
|
||||
}
|
||||
else
|
||||
finally
|
||||
{
|
||||
Debug.LogWarning($"Locale with code {localeCode} not found.");
|
||||
managedLocaleChange = false;
|
||||
}
|
||||
|
||||
await Task.Yield();
|
||||
Canvas.ForceUpdateCanvases();
|
||||
if (!typographyReady)
|
||||
{
|
||||
Debug.LogWarning(
|
||||
$"[LocalizationKit] Locale 已切换为 {localeCode},但字体 Profile 加载失败,保留上一套字体。");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void EnsureTypographyLoaderConfigured()
|
||||
{
|
||||
if (typographyLoaderConfigured)
|
||||
return;
|
||||
|
||||
TypographyService.ConfigureLoader(new ResourceSystemTypographyProfileLoader());
|
||||
typographyLoaderConfigured = true;
|
||||
}
|
||||
|
||||
private static void SubscribeLocaleChanged()
|
||||
{
|
||||
if (localeChangedSubscribed)
|
||||
return;
|
||||
|
||||
LocalizationSettings.SelectedLocaleChanged += OnLocaleChanged;
|
||||
localeChangedSubscribed = true;
|
||||
}
|
||||
|
||||
private static void UnsubscribeLocaleChanged()
|
||||
{
|
||||
if (!localeChangedSubscribed)
|
||||
return;
|
||||
|
||||
LocalizationSettings.SelectedLocaleChanged -= OnLocaleChanged;
|
||||
localeChangedSubscribed = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -250,6 +320,24 @@ namespace AibisDream.Framework
|
||||
public static IEnumerator WaitUntilReady()
|
||||
{
|
||||
yield return LocalizationSettings.InitializationOperation;
|
||||
|
||||
Task<bool> languageTask;
|
||||
do
|
||||
{
|
||||
languageTask = currentLanguageSwitchTask;
|
||||
while (languageTask != null && !languageTask.IsCompleted)
|
||||
yield return null;
|
||||
} while (languageTask != currentLanguageSwitchTask);
|
||||
|
||||
EnsureTypographyLoaderConfigured();
|
||||
if (!TypographyService.IsReady)
|
||||
{
|
||||
Task<bool> typographyTask = TypographyService.LoadAndActivateAsync(
|
||||
GetLocaleKind(LocalizationSettings.SelectedLocale?.Identifier.Code));
|
||||
while (!typographyTask.IsCompleted)
|
||||
yield return null;
|
||||
}
|
||||
|
||||
yield return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
using System.Threading.Tasks;
|
||||
using UnityEngine.ResourceManagement.AsyncOperations;
|
||||
|
||||
namespace AibisDream.Framework
|
||||
{
|
||||
internal sealed class ResourceSystemTypographyProfileLoader : ITypographyProfileLoader
|
||||
{
|
||||
public async Task<TypographyProfile> LoadAsync(string key)
|
||||
{
|
||||
AsyncOperationHandle<TypographyProfile> handle =
|
||||
ResourceSystem.LoadPersistentAsync<TypographyProfile>(key);
|
||||
try
|
||||
{
|
||||
await handle.Task;
|
||||
if (handle.Status == AsyncOperationStatus.Succeeded && handle.Result != null)
|
||||
return handle.Result;
|
||||
|
||||
ResourceSystem.EarlyReleasePersistent(key);
|
||||
return null;
|
||||
}
|
||||
catch
|
||||
{
|
||||
ResourceSystem.EarlyReleasePersistent(key);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public void Release(string key)
|
||||
{
|
||||
ResourceSystem.EarlyReleasePersistent(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c9dd51810ef0cc94398175f3597a978e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -140,6 +140,15 @@ namespace AibisDream.Framework
|
||||
public static void EarlyRelease<T>(AsyncOperationHandle<T> handle)
|
||||
=> ActiveLoader.EarlyRelease(handle);
|
||||
|
||||
/// <summary>
|
||||
/// 提前释放持久化 Loader 中指定 Key 的资源。
|
||||
/// </summary>
|
||||
public static void EarlyReleasePersistent(string key)
|
||||
{
|
||||
if (_persistentLoader != null)
|
||||
_persistentLoader.EarlyRelease(key);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2964c7221d2d9d44aaa0da470729509b
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "AibisDream.Typography.Runtime",
|
||||
"rootNamespace": "AibisDream.Framework",
|
||||
"references": [
|
||||
"Unity.TextMeshPro"
|
||||
],
|
||||
"includePlatforms": [],
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": false,
|
||||
"overrideReferences": false,
|
||||
"precompiledReferences": [],
|
||||
"autoReferenced": true,
|
||||
"defineConstraints": [],
|
||||
"versionDefines": [],
|
||||
"noEngineReferences": false
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 118b0de59b1eb6e438aec021b8e3f39b
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,10 @@
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace AibisDream.Framework
|
||||
{
|
||||
public interface ITypographyProfileLoader
|
||||
{
|
||||
Task<TypographyProfile> LoadAsync(string key);
|
||||
void Release(string key);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0de13fe1f11da524abe267418428556b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,40 @@
|
||||
using System;
|
||||
|
||||
namespace AibisDream.Framework
|
||||
{
|
||||
/// <summary>
|
||||
/// 项目支持的本地化语言类别。多个 Locale 可以共享同一套排版资源。
|
||||
/// </summary>
|
||||
public enum LocaleKind
|
||||
{
|
||||
Cn,
|
||||
En,
|
||||
Ja,
|
||||
Ru,
|
||||
Es,
|
||||
PtBr
|
||||
}
|
||||
|
||||
public enum TypographyProfileKind
|
||||
{
|
||||
ZhHans,
|
||||
Japanese,
|
||||
Latin,
|
||||
Russian
|
||||
}
|
||||
|
||||
public static class TypographyLocaleMapping
|
||||
{
|
||||
public static TypographyProfileKind GetProfileKind(LocaleKind localeKind) => localeKind switch
|
||||
{
|
||||
LocaleKind.Cn => TypographyProfileKind.ZhHans,
|
||||
LocaleKind.Ja => TypographyProfileKind.Japanese,
|
||||
LocaleKind.Ru => TypographyProfileKind.Russian,
|
||||
LocaleKind.En or LocaleKind.Es or LocaleKind.PtBr => TypographyProfileKind.Latin,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(localeKind), localeKind, null)
|
||||
};
|
||||
|
||||
public static string GetProfileKey(LocaleKind localeKind) =>
|
||||
$"Typography/Profile/{GetProfileKind(localeKind)}";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 65c00ba464255484193f0350427b5e11
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,89 @@
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
|
||||
namespace AibisDream.Framework
|
||||
{
|
||||
[DisallowMultipleComponent]
|
||||
[RequireComponent(typeof(TMP_Text))]
|
||||
public sealed class LocalizedTypography : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private TypographyRole role = TypographyRole.Body;
|
||||
[SerializeField] private bool applyMetrics = true;
|
||||
[SerializeField, HideInInspector] private bool baselineCaptured;
|
||||
[SerializeField, HideInInspector] private float baseFontSize;
|
||||
[SerializeField, HideInInspector] private float baseLineSpacing;
|
||||
[SerializeField, HideInInspector] private float baseCharacterSpacing;
|
||||
|
||||
private TMP_Text target;
|
||||
|
||||
public TypographyRole Role => role;
|
||||
public TMP_Text Target => target != null ? target : target = GetComponent<TMP_Text>();
|
||||
|
||||
private void Reset()
|
||||
{
|
||||
target = GetComponent<TMP_Text>();
|
||||
CaptureBaseline();
|
||||
}
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
target = GetComponent<TMP_Text>();
|
||||
// Prefab variants can override TMP metrics without overriding the
|
||||
// baseline serialized by the source prefab. Capture the fully
|
||||
// resolved instance values before the first profile is applied.
|
||||
CaptureBaseline();
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
if (!baselineCaptured)
|
||||
CaptureBaseline();
|
||||
TypographyService.Register(this);
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
TypographyService.Unregister(this);
|
||||
}
|
||||
|
||||
public void ApplyProfile(TypographyProfile profile)
|
||||
{
|
||||
TMP_Text text = Target;
|
||||
TypographyStyle style = profile != null ? profile.Resolve(role) : null;
|
||||
if (text == null || style == null || !style.IsUsable)
|
||||
return;
|
||||
|
||||
if (!baselineCaptured)
|
||||
CaptureBaseline();
|
||||
TypographyService.ApplyStyle(
|
||||
text,
|
||||
style,
|
||||
applyMetrics,
|
||||
baseFontSize,
|
||||
baseLineSpacing,
|
||||
baseCharacterSpacing);
|
||||
}
|
||||
|
||||
public void CaptureBaseline()
|
||||
{
|
||||
TMP_Text text = Target;
|
||||
if (text == null)
|
||||
return;
|
||||
|
||||
baseFontSize = text.fontSize;
|
||||
baseLineSpacing = text.lineSpacing;
|
||||
baseCharacterSpacing = text.characterSpacing;
|
||||
baselineCaptured = true;
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
public void ConfigureForEditor(TypographyRole configuredRole, bool shouldApplyMetrics = true)
|
||||
{
|
||||
role = configuredRole;
|
||||
applyMetrics = shouldApplyMetrics;
|
||||
CaptureBaseline();
|
||||
UnityEditor.EditorUtility.SetDirty(this);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9fe642db0d3d2b54fa6d1afaf8c85e4a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,84 @@
|
||||
using System.Collections.Generic;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
|
||||
namespace AibisDream.Framework
|
||||
{
|
||||
[CreateAssetMenu(fileName = "TypographyProfile", menuName = "AIBIS Dream/Typography/Profile")]
|
||||
public sealed class TypographyProfile : ScriptableObject
|
||||
{
|
||||
[SerializeField] private TypographyProfileKind profileKind;
|
||||
[SerializeField] private List<TypographyStyle> styles = new();
|
||||
|
||||
public TypographyProfileKind ProfileKind => profileKind;
|
||||
public IReadOnlyList<TypographyStyle> Styles => styles;
|
||||
|
||||
public TypographyStyle Resolve(TypographyRole role)
|
||||
{
|
||||
TypographyStyle body = null;
|
||||
if (styles == null)
|
||||
return null;
|
||||
|
||||
foreach (TypographyStyle style in styles)
|
||||
{
|
||||
if (style == null || !style.IsUsable)
|
||||
continue;
|
||||
|
||||
if (style.Role == role)
|
||||
return style;
|
||||
if (style.Role == TypographyRole.Body)
|
||||
body = style;
|
||||
}
|
||||
|
||||
return body;
|
||||
}
|
||||
|
||||
public IEnumerable<string> GetValidationErrors()
|
||||
{
|
||||
if (styles == null || styles.Count == 0)
|
||||
{
|
||||
yield return $"{name}: 没有配置任何 TypographyStyle。";
|
||||
yield break;
|
||||
}
|
||||
|
||||
var seenRoles = new HashSet<TypographyRole>();
|
||||
foreach (TypographyStyle style in styles)
|
||||
{
|
||||
if (style == null)
|
||||
{
|
||||
yield return $"{name}: 存在空的 TypographyStyle。";
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!seenRoles.Add(style.Role))
|
||||
yield return $"{name}: Role {style.Role} 重复配置。";
|
||||
if (!style.IsUsable)
|
||||
yield return $"{name}: Role {style.Role} 没有字体。";
|
||||
}
|
||||
|
||||
if (!seenRoles.Contains(TypographyRole.Body))
|
||||
yield return $"{name}: 缺少 Body 回退样式。";
|
||||
}
|
||||
|
||||
public void Configure(TypographyProfileKind kind, IEnumerable<TypographyStyle> configuredStyles)
|
||||
{
|
||||
profileKind = kind;
|
||||
styles = configuredStyles != null
|
||||
? new List<TypographyStyle>(configuredStyles)
|
||||
: new List<TypographyStyle>();
|
||||
}
|
||||
|
||||
internal void ApplyFallbacks()
|
||||
{
|
||||
if (styles == null)
|
||||
return;
|
||||
|
||||
var configuredFonts = new HashSet<TMP_FontAsset>();
|
||||
foreach (TypographyStyle style in styles)
|
||||
{
|
||||
if (style?.Font != null && configuredFonts.Add(style.Font))
|
||||
style.ApplyFallbacks();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5d25e769249077945915a5c2c793b0e6
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace AibisDream.Framework
|
||||
{
|
||||
public enum TypographyRole
|
||||
{
|
||||
Body,
|
||||
Dialogue,
|
||||
Terminal,
|
||||
WorldText,
|
||||
MiniGame
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e847b9aaee116be4c8d8d135ae0defb9
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,179 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
|
||||
namespace AibisDream.Framework
|
||||
{
|
||||
public static class TypographyService
|
||||
{
|
||||
private static readonly HashSet<LocalizedTypography> Bindings = new();
|
||||
private static ITypographyProfileLoader loader;
|
||||
private static int switchVersion;
|
||||
private static string currentProfileKey;
|
||||
private static string latestRequestedProfileKey;
|
||||
private static TypographyProfile currentProfile;
|
||||
|
||||
public static TypographyProfile CurrentProfile => currentProfile;
|
||||
public static bool IsReady => currentProfile != null;
|
||||
public static string CurrentProfileKey => currentProfileKey;
|
||||
|
||||
public static void ConfigureLoader(ITypographyProfileLoader profileLoader)
|
||||
{
|
||||
loader = profileLoader ?? throw new ArgumentNullException(nameof(profileLoader));
|
||||
}
|
||||
|
||||
public static bool IsActiveFor(LocaleKind localeKind) =>
|
||||
currentProfile != null &&
|
||||
string.Equals(
|
||||
currentProfileKey,
|
||||
TypographyLocaleMapping.GetProfileKey(localeKind),
|
||||
StringComparison.Ordinal);
|
||||
|
||||
public static TypographyStyle Resolve(TypographyRole role) => currentProfile?.Resolve(role);
|
||||
|
||||
public static bool Apply(TMP_Text text, TypographyRole role, bool applyMetrics = true)
|
||||
{
|
||||
if (text == null)
|
||||
return false;
|
||||
|
||||
TypographyStyle style = Resolve(role);
|
||||
if (style == null || !style.IsUsable)
|
||||
return false;
|
||||
|
||||
ApplyStyle(text, style, applyMetrics, text.fontSize, text.lineSpacing, text.characterSpacing);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static void Register(LocalizedTypography binding)
|
||||
{
|
||||
if (binding == null)
|
||||
return;
|
||||
|
||||
Bindings.Add(binding);
|
||||
if (currentProfile != null)
|
||||
binding.ApplyProfile(currentProfile);
|
||||
}
|
||||
|
||||
public static void Unregister(LocalizedTypography binding)
|
||||
{
|
||||
if (binding != null)
|
||||
Bindings.Remove(binding);
|
||||
}
|
||||
|
||||
public static async Task<bool> LoadAndActivateAsync(LocaleKind localeKind)
|
||||
{
|
||||
int requestVersion = ++switchVersion;
|
||||
string profileKey = TypographyLocaleMapping.GetProfileKey(localeKind);
|
||||
latestRequestedProfileKey = profileKey;
|
||||
|
||||
if (IsActiveFor(localeKind))
|
||||
return true;
|
||||
if (loader == null)
|
||||
{
|
||||
Debug.LogError("[TypographyService] 尚未配置 ITypographyProfileLoader。");
|
||||
return false;
|
||||
}
|
||||
|
||||
TypographyProfile loadedProfile;
|
||||
try
|
||||
{
|
||||
loadedProfile = await loader.LoadAsync(profileKey);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Debug.LogError($"[TypographyService] 加载字体 Profile 失败:{profileKey}\n{exception}");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (requestVersion != switchVersion)
|
||||
{
|
||||
if (!string.Equals(profileKey, currentProfileKey, StringComparison.Ordinal) &&
|
||||
!string.Equals(profileKey, latestRequestedProfileKey, StringComparison.Ordinal))
|
||||
{
|
||||
loader.Release(profileKey);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (loadedProfile == null)
|
||||
{
|
||||
Debug.LogError($"[TypographyService] 字体 Profile 不存在或加载失败:{profileKey}");
|
||||
return false;
|
||||
}
|
||||
|
||||
string previousKey = currentProfileKey;
|
||||
loadedProfile.ApplyFallbacks();
|
||||
currentProfile = loadedProfile;
|
||||
currentProfileKey = profileKey;
|
||||
RefreshBindings();
|
||||
Canvas.ForceUpdateCanvases();
|
||||
|
||||
await Task.Yield();
|
||||
if (!string.IsNullOrEmpty(previousKey) &&
|
||||
!string.Equals(previousKey, profileKey, StringComparison.Ordinal))
|
||||
{
|
||||
loader.Release(previousKey);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
internal static void ApplyStyle(
|
||||
TMP_Text text,
|
||||
TypographyStyle style,
|
||||
bool applyMetrics,
|
||||
float baseFontSize,
|
||||
float baseLineSpacing,
|
||||
float baseCharacterSpacing)
|
||||
{
|
||||
text.font = style.Font;
|
||||
text.fontSharedMaterial = style.MaterialPreset != null
|
||||
? style.MaterialPreset
|
||||
: style.Font.material;
|
||||
|
||||
if (applyMetrics)
|
||||
{
|
||||
text.fontSize = baseFontSize * style.FontSizeMultiplier;
|
||||
text.lineSpacing = baseLineSpacing + style.LineSpacingOffset;
|
||||
text.characterSpacing = baseCharacterSpacing + style.CharacterSpacingOffset;
|
||||
}
|
||||
|
||||
text.SetAllDirty();
|
||||
if (text.gameObject.activeInHierarchy)
|
||||
text.ForceMeshUpdate();
|
||||
}
|
||||
|
||||
private static void RefreshBindings()
|
||||
{
|
||||
if (Bindings.Count == 0)
|
||||
return;
|
||||
|
||||
var stale = new List<LocalizedTypography>();
|
||||
foreach (LocalizedTypography binding in Bindings)
|
||||
{
|
||||
if (binding == null)
|
||||
stale.Add(binding);
|
||||
else
|
||||
binding.ApplyProfile(currentProfile);
|
||||
}
|
||||
|
||||
foreach (LocalizedTypography binding in stale)
|
||||
Bindings.Remove(binding);
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
public static void ResetForTests()
|
||||
{
|
||||
if (loader != null && !string.IsNullOrEmpty(currentProfileKey))
|
||||
loader.Release(currentProfileKey);
|
||||
loader = null;
|
||||
currentProfile = null;
|
||||
currentProfileKey = null;
|
||||
latestRequestedProfileKey = null;
|
||||
switchVersion++;
|
||||
Bindings.Clear();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9c15158900e46184da7832703b02d0a1
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,65 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
|
||||
namespace AibisDream.Framework
|
||||
{
|
||||
[Serializable]
|
||||
public sealed class TypographyStyle
|
||||
{
|
||||
[SerializeField] private TypographyRole role;
|
||||
[SerializeField] private TMP_FontAsset font;
|
||||
[SerializeField] private Material materialPreset;
|
||||
[SerializeField] private List<TMP_FontAsset> fallbackFonts = new();
|
||||
[SerializeField, Min(0.01f)] private float fontSizeMultiplier = 1f;
|
||||
[SerializeField] private float lineSpacingOffset;
|
||||
[SerializeField] private float characterSpacingOffset;
|
||||
|
||||
public TypographyStyle(
|
||||
TypographyRole role,
|
||||
TMP_FontAsset font,
|
||||
Material materialPreset = null,
|
||||
IEnumerable<TMP_FontAsset> fallbackFonts = null,
|
||||
float fontSizeMultiplier = 1f,
|
||||
float lineSpacingOffset = 0f,
|
||||
float characterSpacingOffset = 0f)
|
||||
{
|
||||
this.role = role;
|
||||
this.font = font;
|
||||
this.materialPreset = materialPreset;
|
||||
this.fallbackFonts = fallbackFonts != null
|
||||
? new List<TMP_FontAsset>(fallbackFonts)
|
||||
: new List<TMP_FontAsset>();
|
||||
this.fontSizeMultiplier = Mathf.Max(0.01f, fontSizeMultiplier);
|
||||
this.lineSpacingOffset = lineSpacingOffset;
|
||||
this.characterSpacingOffset = characterSpacingOffset;
|
||||
}
|
||||
|
||||
public TypographyRole Role => role;
|
||||
public TMP_FontAsset Font => font;
|
||||
public Material MaterialPreset => materialPreset;
|
||||
public IReadOnlyList<TMP_FontAsset> FallbackFonts => fallbackFonts;
|
||||
public float FontSizeMultiplier => Mathf.Max(0.01f, fontSizeMultiplier);
|
||||
public float LineSpacingOffset => lineSpacingOffset;
|
||||
public float CharacterSpacingOffset => characterSpacingOffset;
|
||||
public bool IsUsable => font != null;
|
||||
|
||||
internal void ApplyFallbacks()
|
||||
{
|
||||
if (font == null)
|
||||
return;
|
||||
|
||||
font.fallbackFontAssetTable ??= new List<TMP_FontAsset>();
|
||||
font.fallbackFontAssetTable.Clear();
|
||||
if (fallbackFonts == null)
|
||||
return;
|
||||
|
||||
foreach (TMP_FontAsset fallback in fallbackFonts)
|
||||
{
|
||||
if (fallback != null && fallback != font && !font.fallbackFontAssetTable.Contains(fallback))
|
||||
font.fallbackFontAssetTable.Add(fallback);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 50206284a91c06448b41453dbf51dc44
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -83,7 +83,7 @@ namespace AibisDream.UI
|
||||
|
||||
private void PrepareContent()
|
||||
{
|
||||
_owner?.SetHeader("terminal_header_chapter", "REPAIR_STATION_07 - AUTH_VERIFIED");
|
||||
_owner?.SetHeader("terminal_header_chapter", "AUTH_VERIFIED");
|
||||
LoadChapters();
|
||||
RefreshViews();
|
||||
}
|
||||
|
||||
@@ -74,7 +74,7 @@ namespace AibisDream.UI
|
||||
|
||||
public void Show()
|
||||
{
|
||||
_owner?.SetHeader("termnial_header_record", "REPAIR_STATION_07 - AUTH_VERIFIED");
|
||||
_owner?.SetHeader("termnial_header_record", "AUTH_VERIFIED");
|
||||
if (panelAnimator != null)
|
||||
{
|
||||
panelAnimator.Show(onComplete: () => StartCoroutine(DrawRecordsWhenReady()));
|
||||
|
||||
@@ -38,7 +38,7 @@ namespace AibisDream.UI
|
||||
new UIFormOption { prop = "ja-JP", label = "日本語" },
|
||||
new UIFormOption { prop = "ru", label = "Русский" },
|
||||
new UIFormOption { prop = "es", label = "Español" },
|
||||
new UIFormOption { prop = "pt-BR", label = "Português (Brasil)" },
|
||||
new UIFormOption { prop = "pt-BR", label = "Português" },
|
||||
};
|
||||
|
||||
private static readonly UIFormOption[] TextSpeedOptions =
|
||||
@@ -115,7 +115,7 @@ namespace AibisDream.UI
|
||||
|
||||
private void PrepareContent()
|
||||
{
|
||||
_owner?.SetHeader("terminal_header_setting", "REPAIR_STATION_07 - AUTH_VERIFIED");
|
||||
_owner?.SetHeader("terminal_header_setting", "AUTH_VERIFIED");
|
||||
InitControls();
|
||||
RefreshControls();
|
||||
RefreshFooterButtons();
|
||||
|
||||
@@ -49,11 +49,11 @@ namespace AibisDream.UI
|
||||
|
||||
if (panelAnimator != null)
|
||||
{
|
||||
panelAnimator.Show(prepare: () => _owner?.SetHeader("termnial_header_start", "REPAIR_STATION_07 - AUTH_VERIFIED"));
|
||||
panelAnimator.Show(prepare: () => _owner?.SetHeader("termnial_header_start", "AUTH_VERIFIED"));
|
||||
}
|
||||
else
|
||||
{
|
||||
_owner?.SetHeader("termnial_header_start", "REPAIR_STATION_07 - AUTH_VERIFIED");
|
||||
_owner?.SetHeader("termnial_header_start", "AUTH_VERIFIED");
|
||||
gameObject.SetActive(true);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user