// WL793_Apply.cs — PD 지시 #793·#794·#795 타이틀 해상도 대응 집행 (에디트 모드 · Assets 수정) // // unity command run_script --file AgentScripts/WL793_Apply.cs --entry WL793_Apply.ApplyScene --args '[0.29]' // unity command run_script --file AgentScripts/WL793_Apply.cs --entry WL793_Apply.ApplyPrefab --args '[0.29]' // unity command run_script --file AgentScripts/WL793_Apply.cs --entry WL793_Apply.ApplyTexture // unity command run_script --file AgentScripts/WL793_Apply.cs --entry WL793_Apply.SetBgPivot --args '[0.5]' // 크롭 기준점만 재조정 // // 진단(실측 근거): // #794 원인 = CanvasScaler 참조 1920x1080 + 폭기준. 세로 1080x1920 에서 scaleFactor 0.5625 → // 로그인 버튼 104px 가 화면 58.5px 로 축소. (권장 터치 타깃 88~120px 대비 절반 이하) // #795 원인 = 배경 RawImage 가 앵커 스트레치라 1920x1080 원본이 1920x3413 캔버스에 세로 3.16배로 // 늘어남(종횡비 파괴) + Default 압축이 CompressedLQ(ASTC_8x8). 둘 다 "뭉개짐"으로 보인다. // // 해법: // 스케일 = 참조 1080x1920 · Expand (인게임 NewGameUI #775 와 동일 기준으로 통일). // 배경 = 스톡 AspectRatioFitter(HeightControlsWidth) 로 "세로 맞춤 · 종횡비 유지 · 중앙". // 커스텀 런타임 컴포넌트를 만들지 않는다(오버드로우·코드 추가 최소, C11). // 비는 영역은 부모 RawImage 를 검정 무지로 바꿔 백드롭으로 재사용한다(레이어 +1). // 크롭 기준점 = 배경 RectTransform 의 pivot.x 하나로 제어된다(수식: 화면 중앙에 오는 // 원본 이미지의 정규화 x = pivot.x). 아트가 이미지별로 조정 가능한 단일 숫자. // // 값의 SOT 는 이 스크립트가 아니라 씬/프리팹이다. 여기서는 "기본값 1회 주입"만 하고 이후는 인스펙터에서 튜닝한다(C45). using System.Text; using UnityEditor; using UnityEditor.SceneManagement; using UnityEngine; using UnityEngine.UI; public static class WL793_Apply { const string ScenePath = "Assets/Scenes/Title.unity"; const string PrefabPath = "Assets/ResWork/UIPrefabs/Title/TitleInfo.prefab"; const string BgTexPath = "Assets/Res_Addr/Loading/Loading4.jpg"; const string BgChild = "bg_image"; // 새로 만드는 배경 자식 이름 static readonly Vector2 Ref = new Vector2(1080f, 1920f); // 세로 기준 참조 해상도 // ---------- 공통 유틸 ---------- static void FixScaler(CanvasScaler cs, StringBuilder sb) { sb.AppendLine(" CanvasScaler: ref " + cs.referenceResolution + " / " + cs.screenMatchMode + " -> ref " + Ref + " / Expand"); cs.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize; cs.referenceResolution = Ref; cs.screenMatchMode = CanvasScaler.ScreenMatchMode.Expand; } /// 전체 스트레치 배경 RawImage 를 [검정 백드롭(부모) + 종횡비 유지 배경(자식)] 구조로 바꾼다. /// 부모의 Button/raycast 는 그대로 두어 "화면 아무 곳이나 터치" 동작을 보존한다. static void BuildBackground(RawImage parentRaw, float focusX, StringBuilder sb) { var tex = AssetDatabase.LoadAssetAtPath(BgTexPath); if (tex == null) { sb.AppendLine(" !! 배경 텍스처 로드 실패 " + BgTexPath); return; } var parentRt = parentRaw.rectTransform; // 1) 부모 = 검정 백드롭 (좌우/상하 여백을 확실히 검정으로 채운다. 이 씬엔 카메라가 없어 // 카메라 클리어에 기댈 수 없다.) parentRaw.texture = null; parentRaw.color = Color.black; // 부모는 전체 스트레치를 유지 parentRt.anchorMin = Vector2.zero; parentRt.anchorMax = Vector2.one; parentRt.pivot = new Vector2(0.5f, 0.5f); parentRt.anchoredPosition = Vector2.zero; parentRt.sizeDelta = Vector2.zero; sb.AppendLine(" " + parentRaw.name + ": RawImage -> 검정 백드롭(texture=null, color=black), 전체 스트레치 유지"); // 2) 자식 = 실제 배경 (세로 맞춤 · 종횡비 유지 · 중앙) var childTr = parentRt.Find(BgChild) as RectTransform; GameObject childGo; if (childTr == null) { childGo = new GameObject(BgChild, typeof(RectTransform), typeof(CanvasRenderer), typeof(RawImage), typeof(AspectRatioFitter)); childGo.transform.SetParent(parentRt, false); childTr = (RectTransform)childGo.transform; childTr.SetAsFirstSibling(); // 다른 UI 뒤에 깔린다 sb.AppendLine(" " + BgChild + ": 신규 생성"); } else { childGo = childTr.gameObject; sb.AppendLine(" " + BgChild + ": 기존 재사용"); } var raw = childGo.GetComponent() ?? childGo.AddComponent(); raw.texture = tex; raw.color = Color.white; raw.raycastTarget = false; // 터치는 부모가 받는다 (오버드로우/레이캐스트 비용 최소) // 세로 스트레치 + 가로 자유 => AspectRatioFitter 가 폭을 계산할 수 있는 유일한 구성 childTr.anchorMin = new Vector2(0.5f, 0f); childTr.anchorMax = new Vector2(0.5f, 1f); childTr.pivot = new Vector2(0.5f, 0.5f); // 오프셋은 WLBackgroundFit 이 clamp 해서 잡는다 childTr.anchoredPosition = Vector2.zero; childTr.sizeDelta = Vector2.zero; childTr.localScale = Vector3.one; var arf = childGo.GetComponent() ?? childGo.AddComponent(); arf.aspectMode = AspectRatioFitter.AspectMode.HeightControlsWidth; arf.aspectRatio = tex.width / (float)tex.height; // 원본에서 유도 (하드코딩 아님) // 크롭이 생길 때만 초점을 존중하고, 이미지가 화면보다 좁아지면 자동으로 정중앙 → 여백 대칭 var fit = childGo.GetComponent() ?? childGo.AddComponent(); fit.focusX = focusX; fit.applyVertical = false; fit.Apply(); sb.AppendLine(" tex=" + tex.name + " " + tex.width + "x" + tex.height + " ratio=" + arf.aspectRatio.ToString("F4") + " mode=HeightControlsWidth focusX=" + focusX.ToString("F2") + " raycast=false"); } static RectTransform Rt(Transform root, string path, StringBuilder sb) { var t = root.Find(path); if (t == null) { sb.AppendLine(" !! 경로 없음: " + path); return null; } return t as RectTransform; } static void SetRect(RectTransform rt, Vector2 size, Vector2? pos, StringBuilder sb, string label) { if (rt == null) return; var before = rt.sizeDelta; var beforePos = rt.anchoredPosition; rt.sizeDelta = size; if (pos.HasValue) rt.anchoredPosition = pos.Value; sb.AppendLine(" " + label + ": size " + before + " -> " + rt.sizeDelta + (pos.HasValue ? (" pos " + beforePos + " -> " + rt.anchoredPosition) : "")); } // ---------- 1) Title.unity (로더/팝업 씬) ---------- public static object ApplyScene(float bgPivotX) { if (EditorApplication.isPlaying) return "ABORT: Play 중"; var scene = EditorSceneManager.GetActiveScene(); if (scene.path != ScenePath) return "ABORT: 활성 씬이 " + scene.path + " 임 (기대: " + ScenePath + ")"; var sb = new StringBuilder(); sb.AppendLine("=== Title.unity ==="); var canvasGo = GameObject.Find("MainToTitle"); if (canvasGo == null) return "ABORT: MainToTitle 없음"; var root = canvasGo.transform; FixScaler(canvasGo.GetComponent(), sb); // 배경 (#795) var titleRaw = root.Find("Title")?.GetComponent(); if (titleRaw == null) sb.AppendLine(" !! Title RawImage 없음"); else BuildBackground(titleRaw, bgPivotX, sb); // 팝업 (#794) — btn_ok 를 터치 가능한 크기로 키우고 팝업이 이를 담도록 확장 SetRect(Rt(root, "PopupUI/bg", sb), new Vector2(860f, 520f), null, sb, "PopupUI/bg"); SetRect(Rt(root, "PopupUI/msg", sb), new Vector2(780f, 280f), new Vector2(0f, 70f), sb, "PopupUI/msg"); SetRect(Rt(root, "PopupUI/btn_ok", sb), new Vector2(340f, 130f), new Vector2(0f, -150f), sb, "PopupUI/btn_ok"); // 진행 슬라이더 — 하단 제스처바(약 48px)를 피해 올리고 폭에 좌우 여유를 준다 SetRect(Rt(root, "Slider_proc", sb), new Vector2(880f, 28f), new Vector2(0f, 90f), sb, "Slider_proc"); EditorSceneManager.MarkSceneDirty(scene); EditorSceneManager.SaveScene(scene); AssetDatabase.SaveAssets(); sb.AppendLine(" 저장 완료"); return sb.ToString(); } // ---------- 2) TitleInfo.prefab (실제 타이틀 화면) ---------- public static object ApplyPrefab(float bgPivotX) { if (EditorApplication.isPlaying) return "ABORT: Play 중"; var sb = new StringBuilder(); sb.AppendLine("=== TitleInfo.prefab ==="); var root = PrefabUtility.LoadPrefabContents(PrefabPath); if (root == null) return "ABORT: 프리팹 로드 실패 " + PrefabPath; try { FixScaler(root.GetComponent(), sb); var titleRaw = root.transform.Find("Title")?.GetComponent(); if (titleRaw == null) sb.AppendLine(" !! Title RawImage 없음"); else BuildBackground(titleRaw, bgPivotX, sb); // 로그인 버튼 = PD 가 말한 "게임 진입 버튼". 104 -> 140 (권장 120px 이상) SetRect(Rt(root.transform, "Btns", sb), new Vector2(1000f, 140f), null, sb, "Btns"); foreach (var n in new[] { "btn_apple", "btn_google", "btn_guest" }) SetRect(Rt(root.transform, "Btns/" + n, sb), new Vector2(140f, 140f), null, sb, "Btns/" + n); // 개발용 로그인 버튼(에디터 전용, 빌드에서는 비활성) — 최소 기준만 충족 SetRect(Rt(root.transform, "InputField (TMP)/btn_login", sb), new Vector2(88f, 88f), null, sb, "btn_login(에디터 전용)"); PrefabUtility.SaveAsPrefabAsset(root, PrefabPath); sb.AppendLine(" 저장 완료"); } finally { PrefabUtility.UnloadPrefabContents(root); } AssetDatabase.SaveAssets(); return sb.ToString(); } // ---------- 3) 배경 텍스처 임포트 (#795 화질) ---------- public static object ApplyTexture() { if (EditorApplication.isPlaying) return "ABORT: Play 중"; var ti = AssetImporter.GetAtPath(BgTexPath) as TextureImporter; if (ti == null) return "ABORT: TextureImporter 없음 " + BgTexPath; var sb = new StringBuilder(); int ow, oh; ti.GetSourceTextureWidthAndHeight(out ow, out oh); sb.AppendLine("=== " + BgTexPath + " (원본 " + ow + "x" + oh + ") ==="); var def = ti.GetDefaultPlatformTextureSettings(); sb.AppendLine(" Default 압축: " + def.textureCompression + " -> Compressed (LQ 해제)"); def.maxTextureSize = 2048; // 원본 1920 이상 · PD 상한 2048 def.textureCompression = TextureImporterCompression.Compressed; ti.SetPlatformTextureSettings(def); // Android 명시 오버라이드: ASTC 6x6 (PD 권고 "ASTC 6x6 이하"), 품질 100 var and = ti.GetPlatformTextureSettings("Android"); sb.AppendLine(" Android: override " + and.overridden + "/" + and.format + " -> true / ASTC_6x6 / quality 100 / maxSize 2048"); and.overridden = true; and.maxTextureSize = 2048; and.format = TextureImporterFormat.ASTC_6x6; and.compressionQuality = 100; and.crunchedCompression = false; ti.SetPlatformTextureSettings(and); sb.AppendLine(" mipmap " + ti.mipmapEnabled + " (UI 이므로 OFF 유지) / filter " + ti.filterMode); ti.mipmapEnabled = false; ti.filterMode = FilterMode.Bilinear; // 읽기 가능 플래그는 CPU 사본을 하나 더 들고 있어 메모리를 2배로 쓴다. UI 배경엔 불필요. if (ti.isReadable) { sb.AppendLine(" isReadable true -> false (CPU 사본 제거, 메모리 절감)"); ti.isReadable = false; } EditorUtility.SetDirty(ti); ti.SaveAndReimport(); AssetDatabase.SaveAssets(); var t = AssetDatabase.LoadAssetAtPath(BgTexPath); sb.AppendLine(" 재임포트 결과: " + t.width + "x" + t.height + " format=" + t.format + " mip=" + t.mipmapCount); return sb.ToString(); } // ---------- 4) 크롭 기준점만 재조정 (A/B 비교용) ---------- public static object SetBgPivot(float focusX) { if (EditorApplication.isPlaying) return "ABORT: Play 중"; var sb = new StringBuilder(); var scene = EditorSceneManager.GetActiveScene(); var t = GameObject.Find("MainToTitle")?.transform.Find("Title/" + BgChild); var f = t != null ? t.GetComponent() : null; if (f != null) { f.focusX = focusX; f.Apply(); EditorSceneManager.MarkSceneDirty(scene); EditorSceneManager.SaveScene(scene); sb.AppendLine("Title.unity " + BgChild + " focusX = " + focusX.ToString("F2")); } var root = PrefabUtility.LoadPrefabContents(PrefabPath); try { var pf = root.transform.Find("Title/" + BgChild)?.GetComponent(); if (pf != null) { pf.focusX = focusX; pf.Apply(); PrefabUtility.SaveAsPrefabAsset(root, PrefabPath); sb.AppendLine("TitleInfo.prefab " + BgChild + " focusX = " + focusX.ToString("F2")); } } finally { PrefabUtility.UnloadPrefabContents(root); } AssetDatabase.SaveAssets(); return sb.ToString(); } /// 배경 표시 방식 전환 (PD 결정 옵션 비교용). /// mode 0 = HeightControlsWidth (세로 맞춤 · 넘치면 크롭) /// mode 1 = FitInParent (전체 보이기 · 상하 검정 여백) /// unity command run_script --file AgentScripts/WL793_Apply.cs --entry WL793_Apply.SetBgMode --args '[1]' public static object SetBgMode(int mode) { if (EditorApplication.isPlaying) return "ABORT: Play 중"; var m = mode == 1 ? AspectRatioFitter.AspectMode.FitInParent : AspectRatioFitter.AspectMode.HeightControlsWidth; var sb = new StringBuilder(); var scene = EditorSceneManager.GetActiveScene(); var a = GameObject.Find("MainToTitle")?.transform.Find("Title/" + BgChild)?.GetComponent(); if (a != null) { a.aspectMode = m; // FitInParent 는 세로도 줄어들 수 있으므로 세로 정렬도 clamp 대상에 넣는다 var fit = a.GetComponent(); if (fit != null) { fit.applyVertical = mode == 1; fit.Apply(); } EditorSceneManager.MarkSceneDirty(scene); EditorSceneManager.SaveScene(scene); sb.AppendLine("Title.unity aspectMode = " + m); } var root = PrefabUtility.LoadPrefabContents(PrefabPath); try { var pa = root.transform.Find("Title/" + BgChild)?.GetComponent(); if (pa != null) { pa.aspectMode = m; var fit = pa.GetComponent(); if (fit != null) { fit.applyVertical = mode == 1; fit.Apply(); } PrefabUtility.SaveAsPrefabAsset(root, PrefabPath); sb.AppendLine("TitleInfo.prefab aspectMode = " + m); } } finally { PrefabUtility.UnloadPrefabContents(root); } AssetDatabase.SaveAssets(); return sb.ToString(); } }