using UnityEngine; using UnityEngine.UI; namespace TKS { /// /// UI 캔버스 갱신 등급(PD 2026-11-10 「리소스·정보가 얼마나 자주 바뀌는지에 따라 캔버스를 나누어 드로우콜·재구성 최소화」). /// Static = 화면을 열 때만 바뀜(바탕 · 틀 · 고정 글자) · Event = 플레이어 조작·게임 사건 때 바뀜(값 · 선택 · 턴 · 팝업) · /// Frame = 매 프레임 바뀜(호흡 · 튀김 · 스크롤 · 미니맵 · 끌기 · 커서). /// public enum UiRate { Static, Event, Frame } /// 캔버스 1장의 등급 표시(검증 보고 `UiLayerProbe` · 규칙 테스트가 읽는다). [DisallowMultipleComponent] public class UiLayerInfo : MonoBehaviour { public UiRate rate; } /// /// 캔버스 층 만들기 — 루트 캔버스(화면 단위 · 정렬 순서 · 평면 거리) + 중첩 캔버스(등급별 분리). /// 규칙(프로젝트 CLAUDE.md 「UI 캔버스·아틀라스 구조」): /// ① 중첩 캔버스는 overrideSorting = false(그리는 순서 · RectMask2D/Mask 자르기 유지) ② 눌리는 그래픽이 있는 층만 GraphicRaycaster /// ③ 매 프레임 바뀌는 것은 가장 작은 단위로 자기 캔버스(UiPulse · UiPop 은 스스로 분리) ④ 캔버스 1장 = 최소 1 드로우콜이므로 등급이 같은 것은 합친다. /// public static class UiLayer { public static readonly Vector2 Reference = new Vector2(1080f, 1920f); /// 화면 단위 루트 캔버스(Screen Space - Camera · 카메라 캡처에도 찍힘). public static Canvas Root(GameObject go, Camera cam, int order, float plane, Vector2 reference, UiRate rate, bool raycast = true) { var c = go.GetComponent(); if (c == null) c = go.AddComponent(); c.renderMode = RenderMode.ScreenSpaceCamera; c.worldCamera = cam; c.planeDistance = plane; c.sortingOrder = order; var sc = go.GetComponent(); if (sc == null) sc = go.AddComponent(); sc.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize; sc.referenceResolution = reference; sc.matchWidthOrHeight = 0.5f; if (raycast && go.GetComponent() == null) go.AddComponent(); Tag(go, rate); return c; } /// /// 이미 있는 UI 오브젝트를 중첩 캔버스로 분리(자식 포함 · 위치·순서 그대로). /// raycast = true/false 지정 · null = 지금 자식 중 raycastTarget 그래픽이 있으면 붙임(나중에 눌리는 자식을 넣을 층은 true 로 지정). /// public static Canvas Nest(GameObject go, UiRate rate, bool? raycast = null) { if (go == null) return null; var c = go.GetComponent(); if (c == null) c = go.AddComponent(); c.overrideSorting = false; var root = TopCanvas(go.transform.parent); if (root != null) { // 중첩 캔버스의 이벤트 카메라 = 루트와 같게(GraphicRaycaster 가 이 값을 읽음 · 꺼진 상태에서도 부모 사슬로 찾음) c.worldCamera = root.worldCamera; c.additionalShaderChannels = root.additionalShaderChannels; } bool needRay = raycast ?? HasRaycastTarget(go); var gr = go.GetComponent(); if (needRay && gr == null) go.AddComponent(); else if (!needRay && gr != null) Object.Destroy(gr); Tag(go, rate); return c; } public static Canvas Nest(Component c, UiRate rate, bool? raycast = null) { return c != null ? Nest(c.gameObject, rate, raycast) : null; } /// /// 매 프레임 바뀌는 요소를 Frame 캔버스로 — 바로 위 부모가 이미 Frame 층(움직이는 것끼리 모은 그룹)이면 그 층을 쓰고, 아니면 자기 캔버스. /// 캔버스 밖(아직 부모 없음)이면 아무것도 하지 않는다. /// public static void Isolate(GameObject go) { if (go == null || go.GetComponent() != null) return; var p = go.transform.parent; if (p == null || TopCanvas(p) == null) return; var tag = p.GetComponent(); if (tag != null && tag.rate == UiRate.Frame && p.GetComponent() != null) return; Nest(go, UiRate.Frame); } /// 부모를 꽉 채우는 빈 층(중첩 캔버스) — 같은 등급의 요소를 모아 담는다. public static RectTransform Group(string name, Transform parent, UiRate rate, bool? raycast = null) { var go = new GameObject(name, typeof(RectTransform)); go.transform.SetParent(parent, false); var rt = (RectTransform)go.transform; rt.anchorMin = Vector2.zero; rt.anchorMax = Vector2.one; rt.offsetMin = rt.offsetMax = Vector2.zero; Nest(go, rate, raycast ?? false); return rt; } /// 부모 사슬의 가장 위 캔버스(루트) — 꺼져 있어도 찾는다. static Canvas TopCanvas(Transform t) { Canvas top = null; for (; t != null; t = t.parent) { var c = t.GetComponent(); if (c != null) top = c; } return top; } static bool HasRaycastTarget(GameObject go) { foreach (var g in go.GetComponentsInChildren(true)) if (g.raycastTarget) return true; return false; } static void Tag(GameObject go, UiRate rate) { var t = go.GetComponent(); if (t == null) t = go.AddComponent(); t.rate = rate; } } }