Project_WL/Assets/WL/Editor/HudBuilder.cs

418 lines
29 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.

using System.Text;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
using UnityEngine.UI;
using WL.Player;
using WL.UI;
namespace WL.EditorTools
{
/// <summary>
/// HUD 배치 빌더 (PD #727 · #728 · 레퍼런스 XP Hero).
/// ① 캐릭터 변경 버튼을 우측 카메라 버튼 아래로 이동
/// ② 좌상단 플레이어 레벨 배지 + 경험치 바 (PlayerProgress · PlayerProgressSettings.asset)
/// ③ 하단 메뉴 5종 (강화 · 무기 · 제작 · 던전 · 상점) — 배경 = GUI Pro SuperCasual `Button_SkillBtn_Dark`,
/// 아이콘 = 같은 팩 스프라이트 조합 (PD 첨부 「강화」 버튼 예시: 위 아이콘 · 아래 라벨)
/// 여러 번 실행해도 같은 결과가 나오도록, 만든 오브젝트는 이름으로 찾아 재사용하고 옛 더미 자식은 지운다.
/// 기존 HUD 규격을 따른다: 캔버스 UI_Hud(1920×1080 기준 · match 0.5) · SafeArea 루트 · 여백 32.
/// </summary>
public static class HudBuilder
{
private const string SettingsPath = "Assets/WL/Settings/PlayerProgressSettings.asset";
private const string SpriteRoot = "Assets/WL/UI/GUIPro/";
private const float Margin = 32f;
private const float ButtonSize = 140f;
private const float Gap = 24f;
// 하단 버튼 규격 (캔버스 단위 · 배경 스프라이트 128×107 비율 유지)
private const float SideW = 148f, SideH = 140f; // 강화·무기·던전·상점 (PD: 좌우 크기 축소 · 2·4번째와 중앙 아이콘 간격 확보)
private const float CenterW = 240f, CenterH = 280f; // 제작(가운데 · 프레임 없음 · 배지+검+별+배너 조합 · 바닥 정렬)
private const float RowInset = 0.015f; // 하단 줄 좌우 여백 비율 (줄을 넓혀 슬롯 간격 확보)
[MenuItem("WL/HUD - 레벨 표기·하단 메뉴 5종·캐릭터 버튼 이동 (PD #727·#728)")]
public static void MenuBuild() { Debug.Log(Build()); }
public static string Build()
{
var sb = new StringBuilder();
var canvas = FindCanvas("UI_Hud");
if (canvas == null) return "[HudBuilder] UI_Hud 캔버스를 찾지 못했다";
Transform safe = canvas.transform.Find("SafeArea");
if (safe == null) return "[HudBuilder] SafeArea 루트를 찾지 못했다";
Font font = FindFont(safe);
// ① 캐릭터 변경 버튼 → 카메라 버튼 아래
var cam = safe.Find("Btn_CameraMode") as RectTransform;
var chr = safe.Find("Btn_CharacterToggle") as RectTransform;
if (cam != null && chr != null)
{
float camH = cam.sizeDelta.y > 0f ? cam.sizeDelta.y : ButtonSize;
chr.anchorMin = new Vector2(1f, 1f);
chr.anchorMax = new Vector2(1f, 1f);
chr.pivot = new Vector2(1f, 1f);
chr.sizeDelta = new Vector2(ButtonSize, ButtonSize);
chr.anchoredPosition = new Vector2(cam.anchoredPosition.x, cam.anchoredPosition.y - camH - Gap);
sb.AppendLine("① Btn_CharacterToggle → 우상단 카메라 버튼 아래 " + chr.anchoredPosition);
}
else sb.AppendLine("① 버튼을 찾지 못했다 (cam=" + (cam != null) + " chr=" + (chr != null) + ")");
// ② 플레이어 진행 데이터 + 좌상단 레벨 표기
var settings = AssetDatabase.LoadAssetAtPath<PlayerProgressSettings>(SettingsPath);
if (settings == null)
{
settings = ScriptableObject.CreateInstance<PlayerProgressSettings>();
AssetDatabase.CreateAsset(settings, SettingsPath);
sb.AppendLine("② PlayerProgressSettings.asset 생성 (시작 Lv " + settings.startLevel + " · 필요 " + settings.baseXpToNext + ")");
}
var progress = Object.FindFirstObjectByType<PlayerProgress>();
if (progress == null)
{
var go = new GameObject("PlayerProgress");
progress = go.AddComponent<PlayerProgress>();
sb.AppendLine("② PlayerProgress 오브젝트 생성");
}
var so = new SerializedObject(progress);
so.FindProperty("settings").objectReferenceValue = settings;
so.ApplyModifiedPropertiesWithoutUndo();
// PD #729 탑바 — 좌측 가장자리 상단 = 레벨 아이콘 조합(PD 지정: Icon_Crown + Slider_Level02_Icon_Badge_Yellow + Icon_Leaf_Left/Right + 숫자)
Sprite icLevelBadge = LoadSprite("Icons/Slider_Level02_Icon_Badge_Yellow.png", Vector4.zero);
Sprite icLevelCrown = LoadSprite("Icons/Icon_Crown.png", Vector4.zero);
Sprite icLeafL = LoadSprite("Icons/Icon_Leaf_Left.png", Vector4.zero);
Sprite icLeafR = LoadSprite("Icons/Icon_Leaf_Right.png", Vector4.zero);
Sprite resBar = LoadSprite("Frame/ResourceBar_Single_Bg.png", new Vector4(11f, 11f, 11f, 11f));
Sprite rhombusBg = LoadSprite("Frame/BorderFrame_Rhombus_Bg.png", Vector4.zero);
Sprite rhombusIn = LoadSprite("Frame/BorderFrame_Rhombus_Inner.png", Vector4.zero);
var level = GetOrCreate(safe, "HUD_Level");
level.anchorMin = new Vector2(0f, 1f); level.anchorMax = new Vector2(0f, 1f); level.pivot = new Vector2(0f, 1f);
level.anchoredPosition = new Vector2(Margin, -Margin);
level.sizeDelta = new Vector2(150f, 150f);
DestroyChild(level, "Badge"); DestroyChild(level, "XpBar_BG"); // #727 임시 회색 배지·바 제거
// 그리기 순: 잎(뒤) → 방패 배지 → 왕관 → 숫자
// PD 2차: 잎은 아주 조금 위로(-14→-8) · 왕관은 조금 아래로(56→46)
var leafL = GetOrCreateIcon(level, "Leaf_L", icLeafL, new Vector2(-46f, -8f), new Vector2(74f, 96f), Color.white);
var leafR = GetOrCreateIcon(level, "Leaf_R", icLeafR, new Vector2(46f, -8f), new Vector2(74f, 96f), Color.white);
var lvBadge = GetOrCreateIcon(level, "Badge_Shield", icLevelBadge, new Vector2(0f, -8f), new Vector2(104f, 110f), Color.white);
var lvCrown = GetOrCreateIcon(level, "Crown", icLevelCrown, new Vector2(0f, 38f), new Vector2(74f, 58f), Color.white); // PD 3차: 왕관 조금 더 아래(46→38)
var levelText = GetOrCreateText(lvBadge.rectTransform, "LevelText", font, 46, FontStyle.Bold, "1");
levelText.rectTransform.anchoredPosition = new Vector2(0f, 2f); // PD 3차: 숫자를 아이콘 중앙으로(-8→+2)
leafL.transform.SetSiblingIndex(0); leafR.transform.SetSiblingIndex(1);
lvBadge.transform.SetSiblingIndex(2); lvCrown.transform.SetAsLastSibling();
// PD #729 2차: "우리 게임에는 경험치가 없다" → 경험치 바 제거 (레벨 아이콘만)
DestroyChild(level, "XpBar_BG");
Text xpText = null; Image fillImg = null;
// 우상단 = 골드 · 캐시 · 특수 재화 (오른쪽 가장자리부터 일정 간격 · 다이아 = 마름모 프레임 틴트 + 어두운 바)
var currency = GetOrCreate(safe, "HUD_Currency");
currency.anchorMin = new Vector2(1f, 1f); currency.anchorMax = new Vector2(1f, 1f); currency.pivot = new Vector2(1f, 1f);
currency.anchoredPosition = new Vector2(-Margin, -Margin); currency.sizeDelta = new Vector2(3f * 200f + 2f * 22f, 60f);
string[] curIds = { "gold", "cash", "special" };
Color[] curOuter = { new Color(1.00f, 0.84f, 0.16f), new Color(0.18f, 0.72f, 1.00f), new Color(0.42f, 0.42f, 0.53f) };
Color[] curInner = { new Color(1.00f, 0.95f, 0.45f), new Color(0.45f, 0.95f, 1.00f), new Color(0.62f, 0.62f, 0.72f) };
Text[] curTexts = new Text[3];
for (int c = 0; c < 3; c++)
{
var grp = GetOrCreate(currency, "Cur_" + curIds[c]);
grp.anchorMin = new Vector2(1f, 0.5f); grp.anchorMax = new Vector2(1f, 0.5f); grp.pivot = new Vector2(1f, 0.5f);
grp.anchoredPosition = new Vector2(-(2 - c) * (200f + 22f), 0f); grp.sizeDelta = new Vector2(200f, 60f);
var gbar = GetOrCreate(grp, "Bar");
var gbarImg = SetImage(gbar, new Color32(14, 16, 16, 255)); // 흰 바탕 스프라이트를 어둡게 틴트 (PD 첨부 = 검은 바)
if (resBar != null) { gbarImg.sprite = resBar; gbarImg.type = Image.Type.Sliced; }
gbar.anchorMin = new Vector2(0f, 0.5f); gbar.anchorMax = new Vector2(1f, 0.5f); gbar.pivot = new Vector2(0.5f, 0.5f);
gbar.anchoredPosition = new Vector2(16f, 0f); gbar.sizeDelta = new Vector2(-32f, 44f);
var dia = GetOrCreateIcon(grp, "Diamond", rhombusBg, new Vector2(0f, 0f), new Vector2(64f, 66f), curOuter[c]);
dia.rectTransform.anchorMin = new Vector2(0f, 0.5f); dia.rectTransform.anchorMax = new Vector2(0f, 0.5f);
dia.rectTransform.anchoredPosition = new Vector2(14f, 0f);
var diaIn = GetOrCreateIcon(dia.rectTransform, "Inner", rhombusIn, Vector2.zero, new Vector2(40f, 40f), curInner[c]);
var val = GetOrCreateText(gbar, "Value", font, 26, FontStyle.Bold, "0");
val.rectTransform.offsetMin = new Vector2(40f, 0f); val.rectTransform.offsetMax = new Vector2(-14f, 0f);
val.alignment = TextAnchor.MiddleRight;
gbar.SetAsFirstSibling();
curTexts[c] = val;
}
var curHud = currency.GetComponent<CurrencyHud>();
if (curHud == null) curHud = currency.gameObject.AddComponent<CurrencyHud>();
var cso = new SerializedObject(curHud);
cso.FindProperty("goldText").objectReferenceValue = curTexts[0];
cso.FindProperty("cashText").objectReferenceValue = curTexts[1];
cso.FindProperty("specialText").objectReferenceValue = curTexts[2];
cso.ApplyModifiedPropertiesWithoutUndo();
// 우측 카메라·캐릭터 버튼은 재화 줄 아래로 내린다 (탑바 자리 확보)
if (cam != null)
{
cam.anchoredPosition = new Vector2(-Margin, -(Margin + 60f + Gap));
if (chr != null) chr.anchoredPosition = new Vector2(cam.anchoredPosition.x, cam.anchoredPosition.y - (cam.sizeDelta.y > 0f ? cam.sizeDelta.y : ButtonSize) - Gap);
}
sb.AppendLine("② 탑바: 레벨 아이콘 조합(잎+방패 배지+왕관+숫자) · 경험치 바 250×44 · 재화 3종(마름모 틴트 + 어두운 바 200) · 카메라·캐릭터 버튼 재화 줄 아래로 · 스프라이트 lv=" + (icLevelBadge != null) + " crown=" + (icLevelCrown != null) + " leaf=" + (icLeafL != null && icLeafR != null) + " resBar=" + (resBar != null) + " rhombus=" + (rhombusBg != null && rhombusIn != null));
var hud = level.GetComponent<PlayerLevelHud>();
if (hud == null) hud = level.gameObject.AddComponent<PlayerLevelHud>();
var hso = new SerializedObject(hud);
hso.FindProperty("progress").objectReferenceValue = progress;
hso.FindProperty("levelText").objectReferenceValue = levelText;
hso.FindProperty("xpText").objectReferenceValue = xpText;
hso.FindProperty("xpFill").objectReferenceValue = fillImg;
hso.ApplyModifiedPropertiesWithoutUndo();
sb.AppendLine("② HUD_Level 좌상단 배지 120 + 경험치 바 320×52");
// ③ 하단 메뉴 5종 (PD #728 스프라이트)
// PD #739 — 미사용 스프라이트 7종 삭제(Button_SkillBtn_Dark·BorderFrame_Round20_Single_Dark 등)에 맞춰 로드 제거
Sprite bgSolid = LoadSprite("Button/Button_SkillBtn_White_Bg.png", new Vector4(30f, 28f, 30f, 28f)); // 버튼 배경 = 이 한 장 + 틴트 RGB 93,93,109
Sprite dotSprite = LoadSprite("Frame/BorderFrame_Circle75_Bg.png", Vector4.zero); // 둥근 알림 점
Sprite icCrown = LoadSprite("Icons/ItemIcon_Badge_Crown.png", Vector4.zero);
Sprite icArrow = LoadSprite("Icons/Slider_Level02_Icon_Up_Green.png", Vector4.zero); // PD 지정 초록 위 화살표 (픽토 틴트 대체)
Sprite icBag = LoadSprite("Icons/ItemIcon_Bag_Brown.png", Vector4.zero);
// 제작(가운데) 조합 = PD 지정: ItemIcon_Badge_Gold + ItemIcon_Battle + ItemIcon_Gem_Star_Blue · 게이지 = BannerFrame03_Single
Sprite icBadgeGold = LoadSprite("Icons/ItemIcon_Badge_Gold.png", Vector4.zero);
Sprite icBattle = LoadSprite("Icons/ItemIcon_Battle.png", Vector4.zero);
Sprite icGemStar = LoadSprite("Icons/ItemIcon_Gem_Star_Blue.png", Vector4.zero);
Sprite banner = LoadSprite("Frame/BannerFrame03_Single.png", Vector4.zero);
Sprite icDungeon = LoadSprite("Icons/ItemIcon_Dungeon.png", Vector4.zero);
Sprite icShop = LoadSprite("Icons/ItemIcon_Shop.png", Vector4.zero);
sb.AppendLine("③ 스프라이트 로드: bgSolid=" + (bgSolid != null) + " crown=" + (icCrown != null) + " arrow=" + (icArrow != null)
+ " bag=" + (icBag != null) + " badgeGold=" + (icBadgeGold != null) + " battle=" + (icBattle != null) + " gemStar=" + (icGemStar != null)
+ " banner=" + (banner != null) + " dungeon=" + (icDungeon != null) + " shop=" + (icShop != null));
var bar = GetOrCreate(safe, "HUD_BottomMenu");
bar.anchorMin = new Vector2(RowInset, 0f); bar.anchorMax = new Vector2(1f - RowInset, 0f); bar.pivot = new Vector2(0.5f, 0f);
bar.anchoredPosition = new Vector2(0f, Gap); bar.sizeDelta = new Vector2(0f, 260f);
string[] ids = { "upgrade", "weapon", "forge", "dungeon", "shop" };
string[] labels = { "강화", "무기", "제작", "던전", "상점" };
for (int i = 0; i < 5; i++)
{
bool center = i == 2;
var btn = GetOrCreate(bar, "Btn_Menu_" + ids[i]);
// 슬롯 중심 한 점에 앵커를 둔다(앵커 폭을 벌리면 sizeDelta 가 그 폭에 더해져 버튼이 화면 1/5 로 늘어난다 — 1차 캡처 실증)
float cx = (i + 0.5f) / 5f;
btn.anchorMin = new Vector2(cx, 0f); btn.anchorMax = new Vector2(cx, 0f); btn.pivot = new Vector2(0.5f, 0f);
// 하단 정렬: 다섯 버튼 모두 바닥(y=0)에 맞춘다 (PD: 중앙 버튼도 다른 버튼과 하단 정렬)
btn.anchoredPosition = Vector2.zero;
btn.sizeDelta = center ? new Vector2(CenterW, CenterH) : new Vector2(SideW, SideH);
var img = SetImage(btn, Color.white);
if (center)
{
// PD: 중앙(제작) 버튼은 배경 프레임 없음 — 클릭 판정만 남긴 투명 이미지
img.sprite = null; img.type = Image.Type.Simple; img.color = new Color(1f, 1f, 1f, 0f); img.raycastTarget = true;
DestroyChild(btn, "Bg_Solid");
}
else
{
// PD: Dark 는 반투명이라 `Button_SkillBtn_White_Bg` 한 장으로 바꾸고 흰 부분을 RGB 93,93,109 로 틴트
if (bgSolid != null) { img.sprite = bgSolid; img.type = Image.Type.Sliced; img.pixelsPerUnitMultiplier = 1f; }
img.color = new Color32(93, 93, 109, 255);
DestroyChild(btn, "Bg_Solid");
}
var button = btn.GetComponent<Button>();
if (button == null) button = btn.gameObject.AddComponent<Button>();
button.targetGraphic = img;
var dummy = btn.GetComponent<DummyMenuButton>();
if (dummy == null) dummy = btn.gameObject.AddComponent<DummyMenuButton>();
var dso = new SerializedObject(dummy);
dso.FindProperty("menuId").stringValue = ids[i];
dso.ApplyModifiedPropertiesWithoutUndo();
// 옛 더미 자식 정리 (#727 색상 버튼 시절)
DestroyChild(btn, "MultText"); DestroyChild(btn, "CountText"); DestroyChild(btn, "Progress_BG");
if (!center)
{
// 라벨 = 아래 (PD 「강화」 예시)
var label = GetOrCreateText(btn, "Label", font, 34, FontStyle.Bold, labels[i]);
var lrt = label.rectTransform;
lrt.anchorMin = new Vector2(0f, 0f); lrt.anchorMax = new Vector2(1f, 0f); lrt.pivot = new Vector2(0.5f, 0f);
lrt.anchoredPosition = new Vector2(0f, 8f); lrt.sizeDelta = new Vector2(0f, 40f);
// 아이콘 = 위 (라벨 위 공간의 중앙)
Sprite ic = i == 0 ? icCrown : i == 1 ? icBag : i == 3 ? icDungeon : icShop;
// PD: 아이콘은 버튼 프레임을 벗어나도 되니 크게 (버튼 폭 172 에 150 · 위로 넘침)
var icon = GetOrCreateIcon(btn, "Icon", ic, new Vector2(0f, 30f), new Vector2(140f, 140f), Color.white);
if (i == 0)
{
// 강화 = 왕관 방패 배지 + PD 지정 초록 위 화살표 `Slider_Level02_Icon_Up_Green` 오른쪽 아래 겹침 (PD 예시 위치 · 원색 그대로)
var arrow = GetOrCreateIcon(btn, "Icon_Arrow", icArrow, new Vector2(52f, -12f), new Vector2(64f, 64f), Color.white);
arrow.transform.SetAsLastSibling();
}
else DestroyChild(btn, "Icon_Arrow");
}
else
{
// 제작(가운데) = PD 예시 조합: 금 월계 배지(Badge_Gold) 안에 교차 검(Battle) · 배지 아래 끝에 파란 별 보석(Gem_Star_Blue)+"5"
// · 맨 아래 배너 게이지(BannerFrame03_Single)+"22/46". 배경 프레임 없음 · 바닥은 다른 버튼과 같은 y=0.
DestroyChild(btn, "Label"); DestroyChild(btn, "Icon_Emblem"); DestroyChild(btn, "Icon_Star"); DestroyChild(btn, "Progress_Frame");
// 4차 캡처 실증: 스프라이트에 투명 여백이 있어 배지 실제 바닥이 사각형보다 높다 → 배지를 내려 배너 위에 바로 얹고, 별은 배지 끝·배너 윗선에 걸친다
// 6차 캡처 실증: 아이콘 자식은 버튼 사각형 **중앙** 기준(앵커 0.5,0.5)이라 바닥 기준 값에서 CenterH/2 를 빼야 한다
float baseY = -CenterH * 0.5f; // 버튼 바닥(= 배너 바닥)을 0 으로 보는 기준
// 7차 캡처 실증: 배지가 배너를 깊이 덮고 별이 숫자를 가림 → 배지 시각 바닥이 배너 윗선 근처, 별은 배너 위 1/3 만 걸치게
// PD 2차: 배지를 약간 줄이고(240→224) 더 내려 배너를 덮되 숫자는 가리지 않게 — 숫자는 배너 아래쪽에 붙인다
// PD 4차("좋아 · 너무 겹치니 조금만 더 위로 · Badge_Gold 위치만"): 배지만 +118 → +130 · 검·별은 그대로
var badgeGold = GetOrCreateIcon(btn, "Icon_BadgeGold", icBadgeGold, new Vector2(0f, baseY + 130f), new Vector2(224f, 224f), Color.white);
var battle = GetOrCreateIcon(btn, "Icon_Battle", icBattle, new Vector2(0f, baseY + 134f), new Vector2(138f, 138f), Color.white); // PD: 검 위치만 살짝 위로(+126→+134)
var bannerRt = GetOrCreate(btn, "Gauge_Banner");
var bannerImg = SetImage(bannerRt, Color.white);
if (banner != null) { bannerImg.sprite = banner; bannerImg.type = Image.Type.Simple; bannerImg.preserveAspect = true; }
else bannerImg.color = new Color(0.02f, 0.03f, 0.03f, 1f);
bannerImg.raycastTarget = false;
bannerRt.anchorMin = new Vector2(0.5f, 0f); bannerRt.anchorMax = new Vector2(0.5f, 0f); bannerRt.pivot = new Vector2(0.5f, 0f);
// 배너는 슬롯 폭 안(이웃 버튼과 안 겹침) · 배지는 배너 윗선에 얹히고 별 보석은 그 경계에 걸친다 (PD: 중앙 버튼이 게이지와 떨어지지 않게)
bannerRt.anchoredPosition = Vector2.zero; bannerRt.sizeDelta = new Vector2(176f, 62f);
// 숫자는 배너의 자식이 아니라 버튼 직속 · 맨 위에 그린다 — 배지가 프레임 위에 얹혀도(PD 3차) 숫자는 절대 가려지지 않게
DestroyChild(bannerRt, "ProgressText");
var progressText = GetOrCreateText(btn, "ProgressText", font, 28, FontStyle.Bold, "22/46");
var ptRt = progressText.rectTransform;
// PD #729: 숫자는 프레임 상하 길이 기준 정중앙 (프레임 176×62 와 같은 사각형 · MiddleCenter)
ptRt.anchorMin = new Vector2(0.5f, 0f); ptRt.anchorMax = new Vector2(0.5f, 0f); ptRt.pivot = new Vector2(0.5f, 0f);
ptRt.anchoredPosition = Vector2.zero; ptRt.sizeDelta = new Vector2(176f, 62f);
var gem = GetOrCreateIcon(btn, "Icon_GemStar", icGemStar, new Vector2(0f, baseY + 72f), new Vector2(72f, 72f), Color.white);
var gemText = GetOrCreateText(gem.rectTransform, "StarText", font, 30, FontStyle.Bold, "5");
gemText.rectTransform.anchoredPosition = new Vector2(0f, -4f);
// 형제 순서(그리기 순 · PD 3차): 배너(맨 뒤) → 배지 → 검 → 별 보석. 배지가 하단 프레임 **위에** 그려져 프레임 윗부분을 덮는다
bannerRt.SetSiblingIndex(0);
badgeGold.transform.SetSiblingIndex(1);
battle.transform.SetSiblingIndex(2);
gem.transform.SetAsLastSibling();
ptRt.SetAsLastSibling(); // 숫자 최상단
}
if (ids[i] == "dungeon" || ids[i] == "shop")
{
// 레퍼런스의 붉은 알림 점 (더미)
var dot = GetOrCreate(btn, "Badge_Dot");
var dotImg = SetImage(dot, new Color(0.92f, 0.20f, 0.18f, 1f));
if (dotSprite != null) { dotImg.sprite = dotSprite; dotImg.type = Image.Type.Simple; dotImg.preserveAspect = true; }
dotImg.raycastTarget = false;
dot.anchorMin = new Vector2(1f, 1f); dot.anchorMax = new Vector2(1f, 1f); dot.pivot = new Vector2(1f, 1f);
dot.anchoredPosition = new Vector2(8f, 8f); dot.sizeDelta = new Vector2(40f, 40f);
dot.SetAsLastSibling();
}
}
sb.AppendLine("③ HUD_BottomMenu 5종 배치 — 배경 Button_SkillBtn_Dark · 아이콘 조합(강화=왕관방패+화살표 · 무기=가방 · 제작=월계관 엠블럼+별+바 · 던전 · 상점)");
// ④ PD 가 직접 미세조정할 수 있도록 HUD 묶음 3개를 프리팹으로 (Assets/WL/UI/Prefabs/) — 이미 프리팹이면 빌더 변경을 프리팹에 적용
// 주의: 이 빌더를 다시 실행하면 프리팹 값이 빌더 수치로 되돌아간다. PD 미세조정 이후에는 빌더를 재실행하지 말 것.
string prefabDir = "Assets/WL/UI/Prefabs";
if (!AssetDatabase.IsValidFolder(prefabDir)) AssetDatabase.CreateFolder("Assets/WL/UI", "Prefabs");
foreach (var groupName in new[] { "HUD_Level", "HUD_Currency", "HUD_BottomMenu" })
{
var t = safe.Find(groupName);
if (t == null) continue;
string path = prefabDir + "/" + groupName + ".prefab";
if (PrefabUtility.IsPartOfPrefabInstance(t.gameObject))
{
var root = PrefabUtility.GetOutermostPrefabInstanceRoot(t.gameObject);
PrefabUtility.ApplyPrefabInstance(root, InteractionMode.AutomatedAction);
sb.AppendLine("④ 프리팹 갱신: " + path);
}
else
{
PrefabUtility.SaveAsPrefabAssetAndConnect(t.gameObject, path, InteractionMode.AutomatedAction);
sb.AppendLine("④ 프리팹 생성·연결: " + path);
}
}
EditorSceneManager.MarkSceneDirty(canvas.gameObject.scene);
EditorSceneManager.SaveScene(canvas.gameObject.scene);
AssetDatabase.SaveAssets();
sb.AppendLine("씬 저장 완료: " + canvas.gameObject.scene.name);
return sb.ToString();
}
// ── 스프라이트
/// <summary>
/// 스프라이트를 읽는다. 팩 .meta 가 없는 파일(Button·Frame)은 임포터를 Sprite(2D and UI)·Single 로 맞추고 9-슬라이스 border 를 넣어 재임포트한다.
/// </summary>
private static Sprite LoadSprite(string rel, Vector4 border)
{
string path = SpriteRoot + rel;
var ti = AssetImporter.GetAtPath(path) as TextureImporter;
if (ti == null) { Debug.LogWarning("[HudBuilder] 텍스처 없음: " + path); return null; }
bool dirty = false;
if (ti.textureType != TextureImporterType.Sprite) { ti.textureType = TextureImporterType.Sprite; dirty = true; }
if (ti.spriteImportMode != SpriteImportMode.Single) { ti.spriteImportMode = SpriteImportMode.Single; dirty = true; }
if (border != Vector4.zero && ti.spriteBorder != border) { ti.spriteBorder = border; dirty = true; }
if (ti.mipmapEnabled) { ti.mipmapEnabled = false; dirty = true; }
if (dirty) ti.SaveAndReimport();
return AssetDatabase.LoadAssetAtPath<Sprite>(path);
}
private static Image GetOrCreateIcon(RectTransform parent, string name, Sprite sprite, Vector2 pos, Vector2 size, Color color)
{
var rt = GetOrCreate(parent, name);
var img = SetImage(rt, color);
img.sprite = sprite; img.type = Image.Type.Simple; img.preserveAspect = true; img.raycastTarget = false;
rt.anchorMin = new Vector2(0.5f, 0.5f); rt.anchorMax = new Vector2(0.5f, 0.5f); rt.pivot = new Vector2(0.5f, 0.5f);
rt.anchoredPosition = pos; rt.sizeDelta = size;
return img;
}
private static void DestroyChild(Transform parent, string name)
{
var t = parent.Find(name);
if (t != null) Object.DestroyImmediate(t.gameObject);
}
// ── 도우미
private static Canvas FindCanvas(string name)
{
var all = Object.FindObjectsByType<Canvas>(FindObjectsInactive.Include, FindObjectsSortMode.None);
foreach (var c in all) if (c.name == name) return c;
return null;
}
private static Font FindFont(Transform safe)
{
var t = safe.GetComponentInChildren<Text>(true);
if (t != null && t.font != null) return t.font;
return Resources.GetBuiltinResource<Font>("LegacyRuntime.ttf");
}
private static RectTransform GetOrCreate(Transform parent, string name)
{
var t = parent.Find(name);
if (t != null) return t as RectTransform;
var go = new GameObject(name, typeof(RectTransform));
go.transform.SetParent(parent, false);
go.layer = parent.gameObject.layer;
return go.GetComponent<RectTransform>();
}
private static Image SetImage(RectTransform rt, Color color)
{
var img = rt.GetComponent<Image>();
if (img == null) img = rt.gameObject.AddComponent<Image>();
img.color = color;
return img;
}
private static void Stretch(RectTransform rt, float pad)
{
rt.anchorMin = Vector2.zero; rt.anchorMax = Vector2.one; rt.pivot = new Vector2(0.5f, 0.5f);
rt.offsetMin = new Vector2(pad, pad); rt.offsetMax = new Vector2(-pad, -pad);
}
private static Text GetOrCreateText(RectTransform parent, string name, Font font, int size, FontStyle style, string text)
{
var rt = GetOrCreate(parent, name);
var t = rt.GetComponent<Text>();
if (t == null) { t = rt.gameObject.AddComponent<Text>(); Stretch(rt, 0f); }
t.font = font; t.fontSize = size; t.fontStyle = style; t.text = text;
t.alignment = TextAnchor.MiddleCenter; t.color = Color.white; t.raycastTarget = false;
t.horizontalOverflow = HorizontalWrapMode.Overflow; t.verticalOverflow = VerticalWrapMode.Overflow;
// 레퍼런스처럼 흰 글자에 어두운 외곽선 (배경 위 가독성)
var outline = rt.GetComponent<Outline>();
if (outline == null) outline = rt.gameObject.AddComponent<Outline>();
outline.effectColor = new Color(0f, 0f, 0f, 0.6f);
outline.effectDistance = new Vector2(2f, -2f);
outline.useGraphicAlpha = true;
return t;
}
}
}