418 lines
29 KiB
C#
418 lines
29 KiB
C#
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;
|
||
}
|
||
}
|
||
}
|