[WL-802b] Safe Area 전 화면 공통(TitleInfo·SortOrder_5 캔버스 직속 WL_SafeArea) + UI 유닛 스케일 노트 (#802)
- TitleInfo.prefab · SortOrder_5.prefab 캔버스 루트 직속에 전용 루트 패널 WL_SafeArea 추가(스트레치·offset 0·Graphic 없음)하고 WL.UI.SafeAreaFitter 1개씩 부착. 기존 자식 재부모화 0 · diff 추가 전용(+51/파일 · 삭제 0). - NewGameUI.prefab 은 IngameUIs/WL_HUD 기존 부착 유지(파일당 1 · 중복 0) — 무수정. - Assets/WL/UI/UI_UnitScale_SafeArea_Note.md: 세 프리팹 참조 해상도(1080x1920 vs 1920x1080)·Expand·계수 1.0 vs 0.5625·1.7778배 환산표, Safe Area 적용 규칙, NGUI 잔재 3종 실측·대체 계획. - AgentScripts/WL802b_Apply.cs(Apply/Revert/Resave) · WL802b_Probe.cs(Dump) 신규. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
abcd038656
commit
5c0b9fc904
|
|
@ -0,0 +1,108 @@
|
|||
// WL802b_Apply.cs — #802 재단 2차: 세로 Safe Area 를 타이틀·로딩/팝업 프리팹까지 공통 적용 (에디트 모드 · 프리팹 컨텐츠 · Play 불필요)
|
||||
// unity command run_script --file AgentScripts/WL802b_Apply.cs --entry WL802b_Apply.Resave (무변경 재저장 — 재직렬화 diff 측정용)
|
||||
// unity command run_script --file AgentScripts/WL802b_Apply.cs --entry WL802b_Apply.Apply
|
||||
// unity command run_script --file AgentScripts/WL802b_Apply.cs --entry WL802b_Apply.Revert (신규 패널 제거 = 2026-09-07 원상)
|
||||
// 방식: 각 프리팹 캔버스 루트 직속에 **전용 루트 패널**(Graphic 없음 · 스트레치 · offset 0)을 1개 추가하고 WL.UI.SafeAreaFitter 를 붙인다.
|
||||
// 기존 자식은 재부모화하지 않는다(#806 팝업 값 포함 레이아웃 회귀 0). 패널은 후속 UI 가 들어갈 안전 영역 레이어다.
|
||||
// NewGameUI.prefab 은 WL_HUD 에 이미 SafeAreaFitter 가 있어 대상에서 제외한다(파일당 1개 · 중복 0).
|
||||
// 롤백: 이 스크립트의 Revert() 또는 git checkout -- <프리팹 2종>
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
public static class WL802b_Apply
|
||||
{
|
||||
// 이번 웨이브 UI 단독 소유 핫스팟 2종 (NewGameUI 는 기존 부착 유지 → 대상 아님)
|
||||
static readonly string[] Targets =
|
||||
{
|
||||
"Assets/ResWork/UIPrefabs/Title/TitleInfo.prefab",
|
||||
"Assets/ResWork/UIPrefabs/Title/SortOrder_5.prefab",
|
||||
};
|
||||
|
||||
public const string PanelName = "WL_SafeArea";
|
||||
const int UILayer = 5;
|
||||
|
||||
static RectTransform FindPanel(GameObject root)
|
||||
{
|
||||
var t = root.transform.Find(PanelName);
|
||||
return t as RectTransform;
|
||||
}
|
||||
|
||||
static string Describe(RectTransform rt)
|
||||
{
|
||||
return "aMin=" + rt.anchorMin + " aMax=" + rt.anchorMax
|
||||
+ " offMin=" + rt.offsetMin + " offMax=" + rt.offsetMax
|
||||
+ " size=" + rt.sizeDelta + " pos=" + rt.anchoredPosition
|
||||
+ " pivot=" + rt.pivot + " scale=" + rt.localScale;
|
||||
}
|
||||
|
||||
public static object Apply()
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
foreach (var path in Targets)
|
||||
{
|
||||
var root = PrefabUtility.LoadPrefabContents(path);
|
||||
try
|
||||
{
|
||||
if (FindPanel(root) != null) { sb.AppendLine(path + " : 이미 " + PanelName + " 있음 — 건너뜀"); continue; }
|
||||
|
||||
var go = new GameObject(PanelName, typeof(RectTransform));
|
||||
go.layer = UILayer;
|
||||
var rt = (RectTransform)go.transform;
|
||||
rt.SetParent(root.transform, false); // 마지막 자식 = 최상위 시블링(빈 패널이라 렌더 영향 0)
|
||||
rt.localScale = Vector3.one;
|
||||
rt.localRotation = Quaternion.identity;
|
||||
rt.pivot = new Vector2(0.5f, 0.5f);
|
||||
rt.anchorMin = Vector2.zero; // 스트레치
|
||||
rt.anchorMax = Vector2.one;
|
||||
rt.offsetMin = Vector2.zero; // offset 0 (= sizeDelta 0 · anchoredPosition 0)
|
||||
rt.offsetMax = Vector2.zero;
|
||||
go.AddComponent<WL.UI.SafeAreaFitter>(); // applyHorizontal/applyVertical 기본 true
|
||||
|
||||
PrefabUtility.SaveAsPrefabAsset(root, path);
|
||||
sb.AppendLine(path + " : " + PanelName + " 추가 · SafeAreaFitter 부착 · " + Describe(rt));
|
||||
}
|
||||
finally { PrefabUtility.UnloadPrefabContents(root); }
|
||||
}
|
||||
AssetDatabase.SaveAssets();
|
||||
AssetDatabase.Refresh();
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
public static object Revert()
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
foreach (var path in Targets)
|
||||
{
|
||||
var root = PrefabUtility.LoadPrefabContents(path);
|
||||
try
|
||||
{
|
||||
var panel = FindPanel(root);
|
||||
if (panel == null) { sb.AppendLine(path + " : " + PanelName + " 없음 — 건너뜀"); continue; }
|
||||
Object.DestroyImmediate(panel.gameObject);
|
||||
PrefabUtility.SaveAsPrefabAsset(root, path);
|
||||
sb.AppendLine(path + " : " + PanelName + " 제거");
|
||||
}
|
||||
finally { PrefabUtility.UnloadPrefabContents(root); }
|
||||
}
|
||||
AssetDatabase.SaveAssets();
|
||||
AssetDatabase.Refresh();
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>내용 변경 없이 LoadPrefabContents→SaveAsPrefabAsset 만 수행 — 재직렬화가 diff 를 만드는지 측정한다(완료 기준 ⓓ 사전 확인).</summary>
|
||||
public static object Resave()
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
var only = new List<string>(Targets);
|
||||
foreach (var path in only)
|
||||
{
|
||||
var root = PrefabUtility.LoadPrefabContents(path);
|
||||
try { PrefabUtility.SaveAsPrefabAsset(root, path); sb.AppendLine(path + " : 무변경 재저장"); }
|
||||
finally { PrefabUtility.UnloadPrefabContents(root); }
|
||||
}
|
||||
AssetDatabase.Refresh();
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,139 @@
|
|||
// WL802b_Probe.cs — #802 재단 2차 검증 덤프: CanvasScaler 유닛 스케일 + SafeAreaFitter 부착/앵커 실측 (에디트 모드 · Play 불필요)
|
||||
// unity command run_script --file AgentScripts/WL802b_Probe.cs --entry WL802b_Probe.Dump
|
||||
// 산출: ⓑ 파일당 SafeAreaFitter 개수·경로 · ⓒ 부착 RectTransform 앵커/offset + safeArea=전체화면 시뮬레이션 · ⓓ #806 팝업 값 · ⓔ 환산표.
|
||||
using System.Text;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
public static class WL802b_Probe
|
||||
{
|
||||
static readonly string[] Prefabs =
|
||||
{
|
||||
"Assets/ResWork/UIPrefabs/Title/TitleInfo.prefab",
|
||||
"Assets/ResWork/UIPrefabs/Title/SortOrder_5.prefab",
|
||||
"Assets/Res_Addr/MainUI/NewGameUI.prefab",
|
||||
};
|
||||
|
||||
// Lead 확정 QA 캡처 해상도 5종 (§3)
|
||||
static readonly Vector2Int[] QaRes =
|
||||
{
|
||||
new Vector2Int(1080, 1920), new Vector2Int(1080, 2340), new Vector2Int(1080, 2400),
|
||||
new Vector2Int(720, 1600), new Vector2Int(1440, 3200),
|
||||
};
|
||||
|
||||
// CanvasScaler.ScreenMatchMode.Expand 의 실제 계산식 (UnityEngine.UI 소스와 동일)
|
||||
static float ExpandScale(Vector2 refRes, Vector2Int screen)
|
||||
{
|
||||
return Mathf.Min(screen.x / refRes.x, screen.y / refRes.y);
|
||||
}
|
||||
|
||||
static string Path(Transform t, Transform stop)
|
||||
{
|
||||
var s = t.name;
|
||||
for (var p = t.parent; p != null && p != stop; p = p.parent) s = p.name + "/" + s;
|
||||
return s;
|
||||
}
|
||||
|
||||
static string Rt(RectTransform rt)
|
||||
{
|
||||
return "aMin=" + rt.anchorMin + " aMax=" + rt.anchorMax + " offMin=" + rt.offsetMin + " offMax=" + rt.offsetMax
|
||||
+ " size=" + rt.sizeDelta + " pos=" + rt.anchoredPosition + " pivot=" + rt.pivot;
|
||||
}
|
||||
|
||||
// SafeAreaFitter.Apply() 와 동일한 산식 — Play 없이 결과를 재현한다.
|
||||
static string SimulateFitter(Rect safe, Vector2Int screen)
|
||||
{
|
||||
var min = new Vector2(safe.xMin / screen.x, safe.yMin / screen.y);
|
||||
var max = new Vector2(safe.xMax / screen.x, safe.yMax / screen.y);
|
||||
return "screen=" + screen.x + "x" + screen.y + " safeArea=" + safe
|
||||
+ " → anchorMin=" + min + " anchorMax=" + max + " offsetMin=(0,0) offsetMax=(0,0)";
|
||||
}
|
||||
|
||||
static RectTransform FindByName(GameObject root, string name)
|
||||
{
|
||||
foreach (var rt in root.GetComponentsInChildren<RectTransform>(true)) if (rt.name == name) return rt;
|
||||
return null;
|
||||
}
|
||||
|
||||
public static object Dump()
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("== WL-802b Probe ==");
|
||||
|
||||
foreach (var path in Prefabs)
|
||||
{
|
||||
var root = PrefabUtility.LoadPrefabContents(path);
|
||||
try
|
||||
{
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("### " + path + " (root=" + root.name + ")");
|
||||
|
||||
// --- CanvasScaler ---
|
||||
foreach (var cs in root.GetComponentsInChildren<CanvasScaler>(true))
|
||||
{
|
||||
sb.AppendLine(" CanvasScaler @" + Path(cs.transform, root.transform.parent)
|
||||
+ " mode=" + cs.uiScaleMode + " ref=" + cs.referenceResolution
|
||||
+ " match=" + cs.screenMatchMode + " matchWH=" + cs.matchWidthOrHeight
|
||||
+ " refPPU=" + cs.referencePixelsPerUnit);
|
||||
foreach (var r in QaRes)
|
||||
sb.AppendLine(" scaleFactor " + r.x + "x" + r.y + " = " + ExpandScale(cs.referenceResolution, r).ToString("0.#####")
|
||||
+ " (1 px = " + (1f / ExpandScale(cs.referenceResolution, r)).ToString("0.####") + " unit)");
|
||||
}
|
||||
|
||||
// --- SafeAreaFitter ---
|
||||
var fitters = root.GetComponentsInChildren<WL.UI.SafeAreaFitter>(true);
|
||||
sb.AppendLine(" SafeAreaFitter count = " + fitters.Length);
|
||||
foreach (var f in fitters)
|
||||
{
|
||||
var so = new SerializedObject(f);
|
||||
var rt = (RectTransform)f.transform;
|
||||
bool activeChain = true;
|
||||
for (var t = f.transform; t != null; t = t.parent) if (!t.gameObject.activeSelf) { activeChain = false; break; }
|
||||
sb.AppendLine(" @" + Path(f.transform, root.transform.parent)
|
||||
+ " parent=" + (f.transform.parent != null ? f.transform.parent.name : "(none)")
|
||||
+ " canvasDirectChild=" + (f.transform.parent == root.transform)
|
||||
+ " activeInPrefab=" + activeChain
|
||||
+ " applyH=" + so.FindProperty("applyHorizontal").boolValue
|
||||
+ " applyV=" + so.FindProperty("applyVertical").boolValue);
|
||||
sb.AppendLine(" RectTransform: " + Rt(rt));
|
||||
sb.AppendLine(" graphic=" + (rt.GetComponent<Graphic>() != null) + " childCount=" + rt.childCount);
|
||||
}
|
||||
|
||||
// --- #806 팝업 값 (레이아웃 회귀 확인) ---
|
||||
if (path.EndsWith("SortOrder_5.prefab"))
|
||||
{
|
||||
foreach (var n in new[] { "PopupUI", "LoadingUI" })
|
||||
{
|
||||
var p = FindByName(root, n);
|
||||
if (p == null) { sb.AppendLine(" [" + n + "] 없음"); continue; }
|
||||
sb.AppendLine(" [" + n + "] " + Rt(p));
|
||||
foreach (var c in p.GetComponentsInChildren<RectTransform>(true))
|
||||
if (c != p && (c.name == "bg" || c.name == "msg" || c.name == "btn_ok"))
|
||||
sb.AppendLine(" " + c.name + " " + Rt(c));
|
||||
}
|
||||
}
|
||||
}
|
||||
finally { PrefabUtility.UnloadPrefabContents(root); }
|
||||
}
|
||||
|
||||
// --- ⓒ safeArea = 전체 화면일 때 회귀 0 ---
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("### SafeAreaFitter.Apply 시뮬레이션 (Play 없이 산식 재현)");
|
||||
foreach (var r in QaRes)
|
||||
sb.AppendLine(" 미적용기기: " + SimulateFitter(new Rect(0, 0, r.x, r.y), r));
|
||||
sb.AppendLine(" 노치예시(추정): " + SimulateFitter(new Rect(0, 0, 1080, 2400 - 91), new Vector2Int(1080, 2400)));
|
||||
|
||||
// --- ⓔ 환산표 ---
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("### 유닛 ↔ px 환산 (세로 1080 폭 기준 · Expand)");
|
||||
sb.AppendLine(" ref 1080x1920: scale 1.0 → 1 unit = 1 px");
|
||||
sb.AppendLine(" ref 1920x1080: scale 0.5625 → 1 px = 1.7778 unit");
|
||||
foreach (var px in new[] { 44f, 88f, 130f, 340f })
|
||||
sb.AppendLine(" " + px + " px = " + px + " unit(1080기준) = " + (px / 0.5625f).ToString("0.##") + " unit(1920기준)");
|
||||
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("Screen(batch) = " + Screen.width + "x" + Screen.height + " safeArea=" + Screen.safeArea + " (배치모드 값 — 실기 아님)");
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
|
|
@ -9127,6 +9127,7 @@ RectTransform:
|
|||
- {fileID: 5196368072863920478}
|
||||
- {fileID: 8045443696459751298}
|
||||
- {fileID: 5452868491221757929}
|
||||
- {fileID: 425477525647692469}
|
||||
m_Father: {fileID: 0}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
|
|
@ -12904,6 +12905,56 @@ MonoBehaviour:
|
|||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
m_localtextKey: 14
|
||||
--- !u!1 &8173757062782547902
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 425477525647692469}
|
||||
- component: {fileID: 2442976163919323621}
|
||||
m_Layer: 5
|
||||
m_Name: WL_SafeArea
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!224 &425477525647692469
|
||||
RectTransform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 8173757062782547902}
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 2109408569755660315}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 1, y: 1}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 0, y: 0}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &2442976163919323621
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 8173757062782547902}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 4f4cdcc41055ee140ae3a9916e8563ce, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Assembly-CSharp::WL.UI.SafeAreaFitter
|
||||
applyHorizontal: 1
|
||||
applyVertical: 1
|
||||
--- !u!1 &8185998291231727465
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
|
|
|
|||
|
|
@ -830,6 +830,56 @@ MonoBehaviour:
|
|||
m_FlexibleWidth: -1
|
||||
m_FlexibleHeight: -1
|
||||
m_LayoutPriority: 1
|
||||
--- !u!1 &2938214554690998105
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 3273655931171191069}
|
||||
- component: {fileID: 1369323462138204881}
|
||||
m_Layer: 5
|
||||
m_Name: WL_SafeArea
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!224 &3273655931171191069
|
||||
RectTransform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 2938214554690998105}
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 444483819741401241}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 1, y: 1}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 0, y: 0}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &1369323462138204881
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 2938214554690998105}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 4f4cdcc41055ee140ae3a9916e8563ce, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Assembly-CSharp::WL.UI.SafeAreaFitter
|
||||
applyHorizontal: 1
|
||||
applyVertical: 1
|
||||
--- !u!1 &3491913842085948827
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
|
|
@ -867,6 +917,7 @@ RectTransform:
|
|||
- {fileID: 8826935861239906465}
|
||||
- {fileID: 1885993367526989969}
|
||||
- {fileID: 4554408050129889465}
|
||||
- {fileID: 3273655931171191069}
|
||||
m_Father: {fileID: 0}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,121 @@
|
|||
# WL UI 기준 노트 — CanvasScaler 유닛 스케일 · Safe Area 공통 적용
|
||||
|
||||
> WL-802b (#802 재단 2차) · 2026-09-08 · 브랜치 `wl/ui/WL-802b-safearea-scale` (base `main` @ `abcd03865`)
|
||||
> 대상 = 세로 1080×1920 모바일(#782 모바일 전제 · #803 세로 전용). **후속 UI 작업은 이 문서의 계수·규칙을 기준으로 한다.**
|
||||
> 줄 번호는 **이 브랜치(WL-802b 적용 후)** 파일 기준. 실측 재현 = `unity command run_script --file AgentScripts/WL802b_Probe.cs --entry WL802b_Probe.Dump`
|
||||
|
||||
---
|
||||
|
||||
## 1. 세 프리팹의 CanvasScaler (실측)
|
||||
|
||||
| 프리팹 | 참조 해상도 | UI Scale Mode | Screen Match Mode | matchWidthOrHeight | 근거 |
|
||||
|---|---|---|---|---|---|
|
||||
| `Assets/ResWork/UIPrefabs/Title/TitleInfo.prefab` | **1080 × 1920** | ScaleWithScreenSize(`m_UiScaleMode: 1`) | **Expand**(`m_ScreenMatchMode: 1`) | 0 (Expand 에서 미사용) | `TitleInfo.prefab:983-988` |
|
||||
| `Assets/ResWork/UIPrefabs/Title/SortOrder_5.prefab` | **1920 × 1080** | ScaleWithScreenSize | Expand | 0 | `SortOrder_5.prefab:9173-9178` |
|
||||
| `Assets/Res_Addr/MainUI/NewGameUI.prefab` | **1920 × 1080** | ScaleWithScreenSize | Expand | 0 | `NewGameUI.prefab:131617-131622` |
|
||||
|
||||
- 세 프리팹 모두 캔버스가 **프리팹 루트**다(`m_Father: {fileID: 0}`) · `m_ReferencePixelsPerUnit: 100` 동일.
|
||||
- ⚠️ **matchWidthOrHeight 는 Expand 모드에서 아무 효과가 없다.** `m_MatchWidthOrHeight: 0` 은 직렬화된 잔여값일 뿐, MatchWidthOrHeight 모드로 바꾸기 전에는 무의미하다. 「match 0 이라서 폭 기준」이라는 해석은 틀렸다 — 폭 기준이 되는 이유는 아래 Expand 산식 때문이다.
|
||||
|
||||
## 2. Expand 산식과 계수
|
||||
|
||||
```
|
||||
scaleFactor = min(screenW / refW, screenH / refH) // ScreenMatchMode.Expand
|
||||
실화면 px = 프리팹 유닛 × scaleFactor
|
||||
```
|
||||
|
||||
세로(폭이 좁은) 화면에서는 두 참조 해상도 모두 **폭 항이 최솟값**이 되므로 `scaleFactor = screenW / refW` 이고,
|
||||
따라서 두 계열의 비는 화면 해상도와 무관하게 **항상 1920 / 1080 = 1.7778 배**로 고정된다.
|
||||
|
||||
| 화면(QA 5종 · #802 §3) | ref 1080×1920 (`TitleInfo`) | ref 1920×1080 (`SortOrder_5`·`NewGameUI`) | 비 |
|
||||
|---|---|---|---|
|
||||
| 1080 × 1920 | **1.0** | **0.5625** | 1.7778 |
|
||||
| 1080 × 2340 | 1.0 | 0.5625 | 1.7778 |
|
||||
| 1080 × 2400 | 1.0 | 0.5625 | 1.7778 |
|
||||
| 720 × 1600 | 0.66667 | 0.375 | 1.7778 |
|
||||
| 1440 × 3200 | 1.33333 | 0.75 | 1.7778 |
|
||||
|
||||
- 세로 화면에서 높이는 남는다(Expand = 잘림 없음). 1080×2400 이면 1920 기준 캔버스의 높이 유닛은 2400/0.5625 = 4266.7 유닛까지 늘어난다 → **화면 상·하단에 여백이 생기므로 세로 전체를 채우는 배경은 stretch 앵커로 잡아야 한다**(고정 sizeDelta 금지).
|
||||
|
||||
## 3. 유닛 ↔ px 환산표 (세로 · 화면 폭 1080 기준)
|
||||
|
||||
| 실화면 px | ref 1080 프리팹의 유닛 | ref 1920 프리팹의 유닛 (= px ÷ 0.5625 = px × 1.7778) |
|
||||
|---|---|---|
|
||||
| 1 px | 1 | **1.7778** |
|
||||
| 44 px (최소 터치 하한 참고) | 44 | 78.2 |
|
||||
| **88 px** | 88 | **156.4** |
|
||||
| 130 px | 130 | 231.1 |
|
||||
| 340 px | 340 | 604.4 |
|
||||
|
||||
- 검증 예 (#806 후속 B · `SortOrder_5.prefab` 의 `PopupUI/child` 실측):
|
||||
|
||||
| 오브젝트 | 유닛(1920 기준, 직렬화 값) | ÷ 1.7778 | 실화면 px (1080 폭) |
|
||||
|---|---|---|---|
|
||||
| `bg` | 1529 × 924 | | 860 × 520 |
|
||||
| `msg` | 1387 × 498 @ (0, 124) | | 780 × 280 @ (0, 70) |
|
||||
| `btn_ok` | 604 × 231 @ (0, −267) | | 340 × 130 @ (0, −150) |
|
||||
| 폰트 | 48 | | 27 |
|
||||
|
||||
340 ÷ 0.5625 = 604.4, 130 ÷ 0.5625 = 231.1 → 반올림 일치. 산출 근거는 `AgentScripts/WL806_Popup.cs:18-20`.
|
||||
- **작성 규칙**: `TitleInfo`(1080 기준)에 넣는 값은 세로 1080 화면에서 px 와 1:1 이다. `SortOrder_5`·`NewGameUI`(1920 기준)에 같은 실크기를 내려면 **반드시 ×1.7778** 한다. 두 계열의 수치를 그대로 복사하면 1.78배 어긋난다.
|
||||
|
||||
## 4. 인계서 §1 표기 불일치 (문서 오기)
|
||||
|
||||
인계서 §1 UI 행은 `NewGameUI.prefab` 을 **1080×1920** 으로 적고 있으나, 실제 직렬화 값은 **1920×1080** 이다(`NewGameUI.prefab:131620` `m_ReferenceResolution: {x: 1920, y: 1080}`).
|
||||
→ 인계서 쪽이 오기로 **「추정」**. 이 노트의 실측값을 SOT 로 쓴다. 인계서 수정은 Lead 판단(조직 레포는 워커가 커밋하지 않는다).
|
||||
|
||||
## 5. Safe Area 적용 규칙
|
||||
|
||||
### 컴포넌트
|
||||
`WL.UI.SafeAreaFitter` (`Assets/WL/UI/Scripts/SafeAreaFitter.cs` · guid `4f4cdcc41055ee140ae3a9916e8563ce`)
|
||||
`Screen.safeArea` 를 화면 크기로 나눠 **anchorMin/anchorMax 에 넣고 offsetMin/offsetMax 를 0 으로 덮어쓴다**(`SafeAreaFitter.cs:40-48`).
|
||||
|
||||
### 반드시 지킬 것
|
||||
1. **전용 루트 패널에만 붙인다.** 캔버스 직속에 Graphic 없는 빈 패널(`WL_SafeArea`)을 두고 거기에만 부착한다. 안전 영역을 따라야 하는 UI 는 이 패널의 자식으로 넣는다.
|
||||
2. **이미 스트레치 + offset 을 쓰는 패널에는 붙이지 않는다.** `Apply()` 가 offsetMin/Max 를 무조건 0 으로 덮어쓰므로, 여백(offset)으로 레이아웃을 잡아둔 패널에 붙이면 그 여백이 소실된다. 같은 이유로 `LayoutGroup`/`ContentSizeFitter` 가 자기 RectTransform 을 제어하는 오브젝트에도 붙이지 않는다.
|
||||
3. **프리팹당 1개.** `[DisallowMultipleComponent]` 는 같은 오브젝트의 중복만 막는다. 중첩 부착(부모·자식 양쪽)은 막지 못하고 안전 영역이 이중으로 축소되니 금지.
|
||||
4. **성능**: `Update()` 가 매 프레임 `Screen.safeArea` 를 폴링한다(`SafeAreaFitter.cs:25-29`). 값이 바뀔 때만 `Apply()` 하므로 비용은 비교 3회뿐이지만, 모바일 예산상 **부착 수는 화면당 1개로 유지**한다.
|
||||
5. **미적용 기기 회귀 0**: `Screen.safeArea` 가 전체 화면이면 min=(0,0)·max=(1,1)·offset 0 → 프리팹 저장값과 동일해 아무 변화가 없다(WL-802b ⓒ).
|
||||
|
||||
### 현재 부착 현황 (WL-802b 적용 후)
|
||||
|
||||
| 프리팹 | 경로 | 캔버스 직속 | 비고 |
|
||||
|---|---|---|---|
|
||||
| `TitleInfo.prefab` | `TitleInfo/WL_SafeArea` | O | WL-802b 신규 · 빈 패널(자식 0 · Graphic 없음) · `TitleInfo.prefab:833-882` |
|
||||
| `SortOrder_5.prefab` | `SortOrder_5/WL_SafeArea` | O | WL-802b 신규 · 빈 패널(자식 0 · Graphic 없음) · `SortOrder_5.prefab:12908-12957` |
|
||||
| `NewGameUI.prefab` | `NewGameUI/IngameUIs/WL_HUD` | **X** | 기존(#800 계열) · 이번 태스크 무수정 · Fitter 블록 `NewGameUI.prefab:107746-107759` |
|
||||
|
||||
`WL802b_Probe.Dump` 실측(2026-09-08): 세 파일 모두 `SafeAreaFitter count = 1` · `applyH=True applyV=True` · 부착 RectTransform `aMin=(0,0) aMax=(1,1) offMin=(0,0) offMax=(0,0) pivot=(0.5,0.5)`.
|
||||
|
||||
- 신규 패널은 **비어 있다**. 기존 자식을 재부모화하면 레이아웃 회귀(WL-802b ⓓ)가 나므로 이동시키지 않았다. 이 패널은 **후속 UI 가 들어갈 안전 영역 레이어**이고, 기존 자식을 옮기는 작업은 별도 발주(레이아웃 변경 승인 필요)로 분리한다.
|
||||
- `NewGameUI` 의 기존 부착은 캔버스 직속이 아니라 `IngameUIs` 아래에 있다(`NewGameUI.prefab:107739` `m_Father: {fileID: 2647977513349308266}` = `IngameUIs`). `IngameUIs` 는 프리팹 저장 상태에서 **비활성**(`m_IsActive: 0`)이므로, 런타임에 활성화되기 전에는 Fitter 도 돌지 않는다 → **「미확인」**(런타임 활성화 시점 미검증). 파일당 1개(중복 0) 기준을 지키려 이번에는 옮기거나 추가하지 않았다.
|
||||
|
||||
### ⚠️ 남은 공백 — `NewGameUI` 최상위 레이어 (후속 발주 후보)
|
||||
|
||||
`NewGameUI` 캔버스 직속 자식은 12개다(실측 · 저장 상태 활성/비활성 표기):
|
||||
|
||||
`New Image`(비활성) · `Camera` · `CamRot` · `Common` · `LobbyUIs` · **`IngameUIs`(비활성)** · `FarmUIs`(비활성) · `MessageInfo` · `Center` · **`PopupMgr`** · `TestMapUIMgr`
|
||||
|
||||
기존 `SafeAreaFitter` 는 `IngameUIs/WL_HUD` **한 갈래만** 덮는다. 즉 **`PopupMgr`·`MessageInfo`·`Common`·`LobbyUIs`·`Center` 는 안전 영역 밖에 그대로 노출된다.**
|
||||
WL-802b 는 완료 기준 ⓑ(파일당 1 · 중복 0)와 ⓓ(기존 오브젝트 무변경) 때문에 이 프리팹을 수정하지 않았다.
|
||||
→ **후속 발주 제안**: 이 5개 레이어를 덮는 방식은 (가) 각 레이어를 새 `WL_SafeArea` 패널의 자식으로 재부모화(레이아웃 승인 필요) 또는 (나) 레이어별 부착(부착 수 증가 = 매 프레임 폴링 증가) 중 **PD·Lead 결정 사항**이다. 이 노트만으로 임의 채택하지 않는다.
|
||||
|
||||
## 6. NGUI 잔재 3종 — 실측과 대체 계획 (W2 Systems 후보 · 이번 태스크 무수정)
|
||||
|
||||
`Assets/Script/Util/**` 는 Systems 소유이자 WL-802b 금지 경로다. 아래는 **읽기 전용 실측**이다.
|
||||
|
||||
| 파일 | 부착 수(프리팹·씬 grep) | 세로 전용(#803)에서의 실동작 | 대체 |
|
||||
|---|---|---|---|
|
||||
| `Assets/Script/Util/SafeArea.cs` | **0** | `switch(Screen.orientation)` 이 `LandscapeLeft`/`LandscapeRight` 만 처리(`:20-37`) → 세로에서는 **어떤 분기도 타지 않음**. 그런데 `while(true) { … yield return null; }`(`:18-38`) 은 계속 돈다 = **효과 0 · 매 프레임 코루틴 비용만 남음** | 삭제 (부착 0 → 무위험) |
|
||||
| `Assets/Script/Util/UISafeAreaTopController.cs` | **0** | NGUI `UIRoot.manualHeight` 의존. `GetComponentInParent<UIRoot>()` 가 null 이면 조기 return(`:30-31`) → uGUI 캔버스 아래에서는 no-op | 삭제 |
|
||||
| `Assets/Script/Util/UISafeAreaRightController.cs` | **1** — `Assets/ResWork/UIPrefabs/Equip/EquipUI.prefab:52-64` (루트 `EquipUI` · `BasePosisLeft: 1` · `xMul: 1`) | `MyCoroutine.Set_Repeat_for1sec` 로 **1초마다** 실행(`:17`). `m_cUIRoot` 를 받아만 두고 쓰지 않아 null 이어도 조기 return 이 없다. `localPosition.x` 를 **`Screen.safeArea.y`**(= 세로 상단 인셋)만큼 민다(`:28-30`) — **가로 화면 전제의 축 오용**. `EquipUI.prefab` 루트는 `RectTransform`(`!u!224`)이 아니라 `Transform`(`!u!4`) = NGUI 시절 프리팹 | uGUI 재작성 후 `SafeAreaFitter` 로 교체 |
|
||||
|
||||
- 세로 전용 전환 후 이 3종은 **동작하지 않거나(0·0건) 축이 틀린 상태(1건)**다. 세로에서 노치 대응은 전부 `WL.UI.SafeAreaFitter` 로 일원화한다.
|
||||
- **W2 Systems 제안**: ① `SafeArea.cs`·`UISafeAreaTopController.cs` 삭제(부착 0 실측) ② `EquipUI.prefab` 을 uGUI 로 재작성할 때 `UISafeAreaRightController` 를 제거하고 상위 `WL_SafeArea` 패널의 자식으로 넣는다 ③ NGUI `UIRoot` 를 쓰는 실사용 프리팹은 `BoxOpenUI`·`GetItemUI`·`ItemDescUI` 3종(나머지 23건은 `Assets/ThirdParty/NGUI/Examples/**`) — 재작성 범위 산정에 사용.
|
||||
|
||||
## 7. 미확인 · 리스크
|
||||
|
||||
- **「미확인」** `NewGameUI` 의 `IngameUIs` 가 런타임에 언제 활성화되는지 · 팝업/오버레이 레이어가 `WL_HUD` 밖에 따로 있는지(5.3 MB 프리팹 통째 Read 금지 · 부분 실측만 수행).
|
||||
- **「미확인」** `Title.unity` 안 PopupUI 인스턴스와 `SortOrder_5.prefab` 의 관계(씬 수정·Play 금지 범위).
|
||||
- **「미확인」** 실기 노치 인셋 값. QA 5종 캡처(#802 §3)는 시뮬레이터 프리셋이며 실제 기기 `Screen.safeArea` 는 **「추정」**.
|
||||
- **「미확인」** 세로 전용 자동회전이 실기 빌드에서 적용되는지(빌드 미수행).
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 2d09ff72d4e8bd14a8909d2a37ac571e
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Loading…
Reference in New Issue