using System; using System.IO; using TMPro; using UnityEditor; using UnityEditor.SceneManagement; using UnityEngine; using UnityEngine.SceneManagement; using UnityEngine.TextCore.LowLevel; using UnityEngine.UI; namespace Colosseum.Editor { /// /// UI 폰트 역할 /// public enum UIFontRole { Emphasis, Body, Combat, } /// /// 프로젝트 UI 폰트 에셋 생성 및 적용 도구 /// public static class UIFontSetupTool { private const string InitialCharacterSet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789 " + ".,:;!?+-=*/%()[]{}<>#&_'\"|~" + "검투사콜로세움보스체력마나준비게임시작나가기대기실승리패배연결호스트참가플레이어"; public const string FontRootPath = "Assets/_Game/Fonts"; public const string FontAssetRootPath = "Assets/_Game/Fonts/TMP"; public const string MaruSourceFontPath = FontRootPath + "/MaruBuri-Regular.ttf"; public const string SuseongSourceFontPath = FontRootPath + "/SuseongBatang.ttf"; public const string MaruFontAssetPath = FontAssetRootPath + "/TMP_MaruBuri.asset"; public const string SuseongFontAssetPath = FontAssetRootPath + "/TMP_SuseongBatang.asset"; [MenuItem("Colosseum/UI/TMP 폰트 에셋 생성")] public static void GenerateFontAssetsMenu() { EnsureFontAssets(true); } [MenuItem("Colosseum/UI/UI 폰트 규칙 적용")] public static void ApplyFontRulesMenu() { if (!EnsureFontAssets(true)) { return; } int changedPrefabCount = ApplyPrefabFontRules(); int changedSceneCount = ApplySceneFontRules(); AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); Debug.Log($"[UIFontSetupTool] UI 폰트 규칙 적용 완료. Prefab {changedPrefabCount}개, Scene {changedSceneCount}개를 갱신했습니다."); } [MenuItem("Colosseum/UI/HUD 타이포 보정 적용")] public static void ApplyHudTypographyTuningMenu() { if (!EnsureFontAssets(true)) { return; } int changedPrefabCount = ApplyHudPrefabTuning(); AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); Debug.Log($"[UIFontSetupTool] HUD 타이포 보정 완료. Prefab {changedPrefabCount}개를 갱신했습니다."); } /// /// 역할에 맞는 TMP 폰트를 텍스트에 적용합니다. /// public static bool ApplyRole(TMP_Text text, UIFontRole role) { if (text == null || !TryGetFontAsset(role, out TMP_FontAsset fontAsset)) { return false; } bool changed = false; if (text.font != fontAsset) { text.font = fontAsset; changed = true; } if (text.fontSharedMaterial != fontAsset.material) { text.fontSharedMaterial = fontAsset.material; changed = true; } float targetCharacterSpacing = role == UIFontRole.Emphasis ? GetEmphasisCharacterSpacing(text) : 0f; if (!Mathf.Approximately(text.characterSpacing, targetCharacterSpacing)) { text.characterSpacing = targetCharacterSpacing; changed = true; } if (changed) { EditorUtility.SetDirty(text); } return changed; } /// /// 역할에 맞는 TMP 폰트 에셋을 반환합니다. /// public static bool TryGetFontAsset(UIFontRole role, out TMP_FontAsset fontAsset) { fontAsset = role switch { UIFontRole.Emphasis => AssetDatabase.LoadAssetAtPath(SuseongFontAssetPath), UIFontRole.Body => AssetDatabase.LoadAssetAtPath(MaruFontAssetPath), UIFontRole.Combat => AssetDatabase.LoadAssetAtPath(MaruFontAssetPath), _ => null, }; return fontAsset != null; } private static bool EnsureFontAssets(bool logResult) { EnsureFolder(FontRootPath); EnsureFolder(FontAssetRootPath); bool success = true; success &= CreateFontAssetIfNeeded(MaruSourceFontPath, MaruFontAssetPath, "TMP_MaruBuri", logResult); success &= CreateFontAssetIfNeeded(SuseongSourceFontPath, SuseongFontAssetPath, "TMP_SuseongBatang", logResult); if (success) { AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); } return success; } private static bool CreateFontAssetIfNeeded(string sourceFontPath, string fontAssetPath, string assetName, bool logResult) { TMP_FontAsset existingFontAsset = AssetDatabase.LoadAssetAtPath(fontAssetPath); if (existingFontAsset != null && IsFontAssetUsable(existingFontAsset)) { WarmUpFontAsset(existingFontAsset); return true; } if (existingFontAsset != null) { AssetDatabase.DeleteAsset(fontAssetPath); } Font sourceFont = AssetDatabase.LoadAssetAtPath(sourceFontPath); if (sourceFont == null) { if (logResult) { Debug.LogError($"[UIFontSetupTool] 원본 폰트를 찾을 수 없습니다: {sourceFontPath}"); } return false; } TMP_FontAsset fontAsset = TMP_FontAsset.CreateFontAsset( sourceFont, 90, 9, GlyphRenderMode.SDFAA, 1024, 1024, AtlasPopulationMode.Dynamic, true); fontAsset.name = assetName; AssetDatabase.CreateAsset(fontAsset, fontAssetPath); AttachSubAssets(fontAsset, fontAssetPath); WarmUpFontAsset(fontAsset); if (logResult) { Debug.Log($"[UIFontSetupTool] TMP 폰트 에셋 생성: {fontAssetPath}"); } return true; } private static void WarmUpFontAsset(TMP_FontAsset fontAsset) { if (fontAsset == null) { return; } fontAsset.ReadFontAssetDefinition(); fontAsset.TryAddCharacters(InitialCharacterSet, out _); EditorUtility.SetDirty(fontAsset); } private static void AttachSubAssets(TMP_FontAsset fontAsset, string fontAssetPath) { if (fontAsset == null) { return; } if (fontAsset.material != null && AssetDatabase.GetAssetPath(fontAsset.material) != fontAssetPath) { AssetDatabase.AddObjectToAsset(fontAsset.material, fontAsset); } Texture2D[] atlasTextures = fontAsset.atlasTextures; if (atlasTextures == null) { return; } foreach (Texture2D atlasTexture in atlasTextures) { if (atlasTexture == null || AssetDatabase.GetAssetPath(atlasTexture) == fontAssetPath) { continue; } AssetDatabase.AddObjectToAsset(atlasTexture, fontAsset); } } private static bool IsFontAssetUsable(TMP_FontAsset fontAsset) { if (fontAsset == null || fontAsset.material == null) { return false; } Texture2D[] atlasTextures = fontAsset.atlasTextures; return atlasTextures != null && atlasTextures.Length > 0 && atlasTextures[0] != null; } private static int ApplyPrefabFontRules() { string[] prefabGuids = AssetDatabase.FindAssets("t:Prefab", new[] { "Assets/_Game/Prefabs/UI" }); int changedCount = 0; foreach (string guid in prefabGuids) { string prefabPath = AssetDatabase.GUIDToAssetPath(guid); GameObject root = PrefabUtility.LoadPrefabContents(prefabPath); try { if (ApplyFontRules(root)) { PrefabUtility.SaveAsPrefabAsset(root, prefabPath); changedCount++; } } finally { PrefabUtility.UnloadPrefabContents(root); } } return changedCount; } private static int ApplyHudPrefabTuning() { string[] prefabGuids = AssetDatabase.FindAssets("t:Prefab", new[] { "Assets/_Game/Prefabs/UI" }); int changedCount = 0; foreach (string guid in prefabGuids) { string prefabPath = AssetDatabase.GUIDToAssetPath(guid); GameObject root = PrefabUtility.LoadPrefabContents(prefabPath); try { if (TuneHudTypography(root)) { PrefabUtility.SaveAsPrefabAsset(root, prefabPath); changedCount++; } } finally { PrefabUtility.UnloadPrefabContents(root); } } return changedCount; } private static int ApplySceneFontRules() { string originalScenePath = SceneManager.GetActiveScene().path; string[] sceneGuids = AssetDatabase.FindAssets("t:Scene", new[] { "Assets/Scenes" }); int changedCount = 0; foreach (string guid in sceneGuids) { string scenePath = AssetDatabase.GUIDToAssetPath(guid); Scene scene = EditorSceneManager.OpenScene(scenePath, OpenSceneMode.Single); bool changed = false; foreach (GameObject rootObject in scene.GetRootGameObjects()) { changed |= ApplyFontRules(rootObject); } if (changed) { EditorSceneManager.SaveScene(scene); changedCount++; } } if (!string.IsNullOrEmpty(originalScenePath) && File.Exists(originalScenePath)) { EditorSceneManager.OpenScene(originalScenePath, OpenSceneMode.Single); } return changedCount; } private static bool ApplyFontRules(GameObject root) { bool changed = false; TMP_Text[] texts = root.GetComponentsInChildren(true); foreach (TMP_Text text in texts) { UIFontRole role = DetermineRole(text); changed |= ApplyRole(text, role); } return changed; } private static bool TuneHudTypography(GameObject root) { if (root == null || ContainsIgnoreCase(root.name, "Lobby")) { return false; } bool changed = false; TMP_Text[] texts = root.GetComponentsInChildren(true); foreach (TMP_Text text in texts) { string hierarchyPath = GetHierarchyPath(text.transform); if (root.name == "UI_ActionBar_Item" || root.name == "UI_ActionBar_EvadeItem") { changed |= TuneActionBarTypography(text, hierarchyPath); continue; } if (root.name == "UI_PlayerResources") { changed |= TunePlayerResourcesTypography(text, hierarchyPath); continue; } if (root.name == "UI_HealthBar" || root.name == "UI_ManaBar") { changed |= SetTextStyle(text, 30f, FontStyles.Bold, -1f); continue; } if (root.name == "UI_BossHealthBar") { changed |= TuneBossBarTypography(text, hierarchyPath); } } return changed; } private static UIFontRole DetermineRole(TMP_Text text) { string objectName = text.gameObject.name; string hierarchyPath = GetHierarchyPath(text.transform); if (IsEmphasisText(objectName, hierarchyPath)) { return UIFontRole.Emphasis; } if (IsCombatText(text, objectName, hierarchyPath)) { return UIFontRole.Combat; } return UIFontRole.Body; } private static bool IsEmphasisText(string objectName, string hierarchyPath) { return ContainsIgnoreCase(objectName, "Title") || ContainsIgnoreCase(objectName, "BossName") || ContainsIgnoreCase(objectName, "Victory") || ContainsIgnoreCase(objectName, "GameOver") || ContainsIgnoreCase(hierarchyPath, "/Title") || ContainsIgnoreCase(hierarchyPath, "/BossName"); } private static bool IsCombatText(TMP_Text text, string objectName, string hierarchyPath) { return text.GetComponentInParent