UI 캔버스 갱신 등급 분할 + UI 스프라이트 아틀라스(#1107 · PD 2026-11-10 「리소스·정보가 얼마나 자주 바뀌는지에 따라 캔버스를 나누어 최적화 · 드로우콜 최소화 구조」)

- 갱신 등급 3단(Static/Event/Frame · Runtime/UI/UiLayer): 아웃게임 화면마다 중첩 캔버스 · 스크롤 내용 · 재화 바 · 팝업 · 토스트 · 뽑기 결과 · 끌기 유령 분리 · UiPulse/UiPop 자동 Frame 분리
- 전투 HUD = 정적 판 루트 → Info → MinimapLive(Frame) · SkillGlow → Result · 월드맵 = 루트 → Content(스크롤) → 커서 · 팝업
- 매 프레임 갱신 코드 = 값이 같으면 대입 안 함 · UiPop 끝나면 정지
- 아틀라스 SOT Assets/Data/UI/ui_atlas.csv → Editor/UIAtlasBuilder(UI 테마 임포트 때 자동) → UI_Common 4096x2048 · UI_Content 2048x2048 · WorldMap_UI 1024x1024 · Sprite Atlas V2
- UISkin.WorldSprite = 아틀라스 자리(textureRect)로 자름
- 검증: UiLayerProbe(캡처 로그 layers 줄 + ui_layers.csv) · UILayerTests · 테스트 119/119 · 13화면 배치 합계 1124 → 439

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
깃 관리자 2026-11-10 01:05:47 +09:00
parent 182353f84a
commit 8aa804168f
31 changed files with 1231 additions and 68 deletions

View File

@ -0,0 +1,6 @@
kind,name,match,value,note
theme,UI_Content,/gear/|/chest/|/logo_skill/|/gimmick/|ItemIcon|Icon_ShopItem,2048,화면별 콘텐츠 아이콘(장비·상자·스킬·기믹 타일·상점 상품) — 그 화면에서만 쓰임
theme,UI_Common,*,4096,모든 화면 공통 크롬(틀·버튼·바·탭·재화·병과·작은 아이콘) — 위 줄에 안 걸린 테마 스프라이트 전부
folder,WorldMap_UI,Assets/Resources/ChapterMap/UI,2048,월드맵 부품(깃발·띠·판·별·커서) — 폴더 전체
option,single_area,,262144,이 넓이(px) 넘는 큰 그림(로비 바탕·키아트·로고·패턴)은 아틀라스에 넣지 않음 — 한 장 = 드로우콜 1 · 아틀라스 공간 낭비 방지
option,padding,,4,스프라이트 간격(px)
1 kind name match value note
2 theme UI_Content /gear/|/chest/|/logo_skill/|/gimmick/|ItemIcon|Icon_ShopItem 2048 화면별 콘텐츠 아이콘(장비·상자·스킬·기믹 타일·상점 상품) — 그 화면에서만 쓰임
3 theme UI_Common * 4096 모든 화면 공통 크롬(틀·버튼·바·탭·재화·병과·작은 아이콘) — 위 줄에 안 걸린 테마 스프라이트 전부
4 folder WorldMap_UI Assets/Resources/ChapterMap/UI 2048 월드맵 부품(깃발·띠·판·별·커서) — 폴더 전체
5 option single_area 262144 이 넓이(px) 넘는 큰 그림(로비 바탕·키아트·로고·패턴)은 아틀라스에 넣지 않음 — 한 장 = 드로우콜 1 · 아틀라스 공간 낭비 방지
6 option padding 4 스프라이트 간격(px)

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: dbc9f67710bed05499c84db6137a1454
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,215 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using UnityEditor;
using UnityEditor.U2D;
using UnityEngine;
using UnityEngine.U2D;
namespace TKS.EditorTools
{
/// <summary>
/// UI 스프라이트 아틀라스(PD 2026-11-10 「드로우콜 최소화 구조」) — 구성 SOT = Assets/Data/UI/ui_atlas.csv → Assets/UI/Atlases/&lt;이름&gt;.spriteatlasv2.
/// theme 줄 = UI 테마(UITheme.asset)가 쓰는 스프라이트를 match(경로 포함 · | = 또는 · * = 나머지 전부)로 나눔 · folder 줄 = 폴더 전체 ·
/// option single_area = 그보다 넓은 그림은 묶지 않음 · padding.
/// UI 테마 임포트가 끝날 때마다 자동으로 다시 맞춘다(새 납품 스프라이트 = 코드 변경 없이 아틀라스에 들어감) · 구성이 같으면 파일을 건드리지 않는다.
/// 패커 = Sprite Atlas V2(에디터 Play 에서도 묶음 · 프로젝트 설정 EditorSettings).
/// </summary>
public static class UIAtlasBuilder
{
public const string CsvPath = "Assets/Data/UI/ui_atlas.csv";
public const string OutDir = "Assets/UI/Atlases";
class Group
{
public string kind;
public string name;
public string[] match;
public int maxSize;
public readonly List<string> paths = new List<string>();
}
[MenuItem("TKS/Build UI Atlases (UI 아틀라스 갱신)")]
public static void Menu()
{
Build(AssetDatabase.LoadAssetAtPath<UITheme>(UIThemeImporter.AssetPath), true);
}
/// <summary>표대로 아틀라스를 맞춘다 · 반환 = 오류 없이 끝났는가(오류면 아틀라스 미변경).</summary>
public static bool Build(UITheme theme, bool log = false)
{
if (!File.Exists(CsvPath))
{
Debug.LogError("[TKS] UI 아틀라스 표 없음: " + CsvPath);
return false;
}
var groups = new List<Group>();
long singleArea = long.MaxValue;
int padding = 4;
var errors = new List<string>();
var lines = File.ReadAllText(CsvPath, Encoding.UTF8).TrimStart('').Replace("\r", "").Split('\n');
for (int n = 1; n < lines.Length; n++)
{
if (string.IsNullOrWhiteSpace(lines[n])) continue;
var f = lines[n].Split(',');
if (f.Length < 4)
{
errors.Add((n + 1) + "행: 열 부족");
continue;
}
string kind = f[0].Trim(), name = f[1].Trim(), match = f[2].Trim(), value = f[3].Trim();
int v;
switch (kind)
{
case "theme":
case "folder":
if (!int.TryParse(value, out v) || v < 128) errors.Add((n + 1) + "행 " + name + ": 최대 크기 " + value);
groups.Add(new Group { kind = kind, name = name, match = match.Split('|'), maxSize = v });
break;
case "option":
if (!int.TryParse(value, out v)) errors.Add((n + 1) + "행 " + name + ": 값 " + value);
else if (name == "single_area") singleArea = v;
else if (name == "padding") padding = v;
else errors.Add((n + 1) + "행: 모르는 옵션 " + name);
break;
default:
errors.Add((n + 1) + "행: 모르는 종류 " + kind);
break;
}
}
// theme 스프라이트 → 첫 번째로 맞는 theme 줄(* = 나머지)
var singles = new List<string>();
foreach (var path in ThemeTextures(theme))
{
var tex = AssetDatabase.LoadAssetAtPath<Texture2D>(path);
if (tex == null) continue;
if ((long)tex.width * tex.height > singleArea)
{
singles.Add(path);
continue;
}
Group hit = null;
foreach (var g in groups)
if (g.kind == "theme" && g.match.Any(m => m != "*" && path.Contains(m)))
{
hit = g;
break;
}
if (hit == null) hit = groups.FirstOrDefault(g => g.kind == "theme" && g.match.Contains("*"));
if (hit != null) hit.paths.Add(path);
}
foreach (var g in groups)
if (g.kind == "folder")
{
if (!AssetDatabase.IsValidFolder(g.match[0])) errors.Add(g.name + ": 폴더 없음 " + g.match[0]);
else g.paths.Add(g.match[0]);
}
if (errors.Count > 0)
{
Debug.LogError("[TKS] UI 아틀라스 표 오류 " + errors.Count + "건(아틀라스 미변경):\n" + string.Join("\n", errors));
return false;
}
// 패커 = Sprite Atlas V2(에디터 Play 에서도 묶음)
if (EditorSettings.spritePackerMode != SpritePackerMode.SpriteAtlasV2)
{
EditorSettings.spritePackerMode = SpritePackerMode.SpriteAtlasV2;
AssetDatabase.SaveAssets();
}
if (!AssetDatabase.IsValidFolder(OutDir)) AssetDatabase.CreateFolder(Path.GetDirectoryName(OutDir).Replace('\\', '/'), Path.GetFileName(OutDir));
int changed = 0;
var report = new StringBuilder();
foreach (var g in groups)
{
g.paths.Sort(System.StringComparer.Ordinal);
if (Write(g, padding)) changed++;
report.Append(" · ").Append(g.name).Append(' ').Append(g.paths.Count);
}
if (changed > 0) SpriteAtlasUtility.PackAllAtlases(EditorUserBuildSettings.activeBuildTarget);
if (log || changed > 0)
Debug.Log("[TKS] UI 아틀라스" + report + " · 큰 그림(묶지 않음) " + singles.Count + " · 갱신 " + changed + "장");
return true;
}
/// <summary>아틀라스 1장을 표 구성대로 · 구성(목록·크기·간격) 서명이 같으면 건드리지 않는다.</summary>
static bool Write(Group g, int padding)
{
string path = OutDir + "/" + g.name + ".spriteatlasv2";
string sig = Hash128.Compute(g.maxSize + ";" + padding + ";" + string.Join("|", g.paths.Select(AssetDatabase.AssetPathToGUID))).ToString();
var imp = AssetImporter.GetAtPath(path) as SpriteAtlasImporter;
if (imp != null && imp.userData == sig) return false;
var asset = new SpriteAtlasAsset();
var objs = new List<Object>();
foreach (var p in g.paths)
{
var o = AssetDatabase.LoadMainAssetAtPath(p);
if (o != null) objs.Add(o);
}
asset.Add(objs.ToArray());
SpriteAtlasAsset.Save(asset, path);
AssetDatabase.ImportAsset(path, ImportAssetOptions.ForceSynchronousImport);
imp = (SpriteAtlasImporter)AssetImporter.GetAtPath(path);
imp.includeInBuild = true;
// UI = 사각 묶음(9-slice · Filled 가 그대로) · 회전 없음 · 밉맵 없음
imp.packingSettings = new SpriteAtlasPackingSettings
{
blockOffset = 1,
enableRotation = false,
enableTightPacking = false,
enableAlphaDilation = true,
padding = padding
};
imp.textureSettings = new SpriteAtlasTextureSettings
{
readable = false,
generateMipMaps = false,
sRGB = true,
filterMode = FilterMode.Bilinear,
anisoLevel = 0
};
var ps = imp.GetPlatformSettings("DefaultTexturePlatform");
ps.maxTextureSize = g.maxSize;
ps.format = TextureImporterFormat.Automatic;
ps.textureCompression = TextureImporterCompression.Compressed; // 원본 UI 텍스처와 같은 압축(Normal)
imp.SetPlatformSettings(ps);
imp.userData = sig;
imp.SaveAndReimport();
return true;
}
/// <summary>테마가 쓰는 스프라이트의 텍스처 경로(팩 · 신규 제작 · 덮개) — 중복 없음.</summary>
public static List<string> ThemeTextures(UITheme theme)
{
var set = new HashSet<string>();
if (theme != null)
{
foreach (var s in theme.skins)
{
Add(set, s.pack);
Add(set, s.samguk);
Add(set, s.overlay);
}
foreach (var i in theme.icons)
{
Add(set, i.pack);
Add(set, i.samguk);
}
}
var list = set.ToList();
list.Sort(System.StringComparer.Ordinal);
return list;
}
static void Add(HashSet<string> set, Sprite sp)
{
if (sp == null) return;
var p = AssetDatabase.GetAssetPath(sp);
// 팩 일부 = 확장자 대문자(.Png) → 확장자 대신 본 자산이 텍스처인지로 판정
if (!string.IsNullOrEmpty(p) && AssetDatabase.GetMainAssetTypeAtPath(p) == typeof(Texture2D)) set.Add(p);
}
}
}

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 355884257a3577a4a9ea4e1bfe979ab7

View File

@ -142,6 +142,8 @@ namespace TKS.EditorTools
UITheme.Use(asset); UITheme.Use(asset);
Debug.Log("[TKS] UI 테마 임포트 · 색 " + asset.colors.Count + " · 스킨 " + asset.skins.Count + " · 아이콘 " + asset.icons.Count + Debug.Log("[TKS] UI 테마 임포트 · 색 " + asset.colors.Count + " · 스킨 " + asset.skins.Count + " · 아이콘 " + asset.icons.Count +
" · 신규 제작 치환 " + replaced + " · 추가 " + added + " → " + AssetPath); " · 신규 제작 치환 " + replaced + " · 추가 " + added + " → " + AssetPath);
// 테마가 쓰는 스프라이트 = UI 아틀라스 구성(PD 2026-11-10 · 새 스프라이트도 자동으로 묶임 · 구성이 같으면 그대로)
UIAtlasBuilder.Build(asset);
return true; return true;
} }

View File

@ -76,6 +76,7 @@ namespace TKS.OutGame
title.rectTransform.offsetMin = new Vector2(150f, 34f); title.rectTransform.offsetMin = new Vector2(150f, 34f);
// 별 3 = 받침 원(Circle78 chip_dark) + 별(가운데 크게 · 위로) · 0.15 s 간격 튀김 // 별 3 = 받침 원(Circle78 chip_dark) + 별(가운데 크게 · 위로) · 0.15 s 간격 튀김
starRow = UiKit.At(pt, "StarRow", 0f, 120f, PW, 190f); starRow = UiKit.At(pt, "StarRow", 0f, 120f, PW, 190f);
UiLayer.Nest(starRow.gameObject, UiRate.Frame, false); // 별 3 튀김 = 한 Frame 층(별마다 캔버스를 만들지 않음 · PD 2026-11-10)
for (int i = 0; i < 3; i++) for (int i = 0; i < 3; i++)
{ {
bool mid = i == 1; bool mid = i == 1;

View File

@ -328,6 +328,7 @@ namespace TKS.OutGame
if (ghost != null) Object.Destroy(ghost.gameObject); if (ghost != null) Object.Destroy(ghost.gameObject);
ghost = UiKit.Portrait("Ghost", Root.TopRoot, S.Def(id), new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), Vector2.zero, new Vector2(170f, 170f), true, 2.1f, UiKit.Red); ghost = UiKit.Portrait("Ghost", Root.TopRoot, S.Def(id), new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), Vector2.zero, new Vector2(170f, 170f), true, 2.1f, UiKit.Red);
ghost.gameObject.name = "Ghost_" + id; ghost.gameObject.name = "Ghost_" + id;
UiLayer.Nest(ghost.gameObject, UiRate.Frame); // 끄는 동안 매 프레임 이동 = 자기 캔버스(PD 2026-11-10)
MoveGhost(e); MoveGhost(e);
} }

View File

@ -203,9 +203,12 @@ namespace TKS.OutGame
wm.BackRequested = ShowLobby; wm.BackRequested = ShowLobby;
} }
mainCanvas = MakeCanvas("OutGameCanvas", 350, 0.45f); // 캔버스 층(PD 2026-11-10 · 프로젝트 CLAUDE.md 「UI 캔버스·아틀라스 구조」):
resultCanvas = MakeCanvas("BattleResultCanvas", 200, 0.8f); // 본 화면 루트 = 바탕 · 하단 탭(Static) + 화면마다 중첩 캔버스(Event) · 스크롤 내용 · 호흡 등 = Frame 중첩
topCanvas = MakeCanvas("OutGameTopCanvas", 450, 0.4f); // 위 덮개 루트 = 재화 바 · 팝업 · 토스트 · 끌기 유령 각각 중첩 · 전투 결과 루트 = 결과 패널(Event)
mainCanvas = MakeCanvas("OutGameCanvas", 350, 0.45f, UiRate.Static);
resultCanvas = MakeCanvas("BattleResultCanvas", 200, 0.8f, UiRate.Event);
topCanvas = MakeCanvas("OutGameTopCanvas", 450, 0.4f, UiRate.Event);
TopRoot = (RectTransform)topCanvas.transform; TopRoot = (RectTransform)topCanvas.transform;
var bg = UiKit.Fill("Bg", mainCanvas.transform); var bg = UiKit.Fill("Bg", mainCanvas.transform);
@ -232,27 +235,18 @@ namespace TKS.OutGame
{ {
scr.Root = this; scr.Root = this;
scr.Rt = UiKit.Fill(typeof(T).Name, mainCanvas.transform); scr.Rt = UiKit.Fill(typeof(T).Name, mainCanvas.transform);
UiLayer.Nest(scr.Rt.gameObject, UiRate.Event, true); // 화면 1장 = 자기 캔버스(값 갱신이 바탕·탭·다른 화면을 다시 묶지 않게)
scr.Build(); scr.Build();
scr.Rt.gameObject.SetActive(false); scr.Rt.gameObject.SetActive(false);
screens.Add(scr); screens.Add(scr);
return scr; return scr;
} }
Canvas MakeCanvas(string name, int order, float plane) Canvas MakeCanvas(string name, int order, float plane, UiRate rate)
{ {
var go = new GameObject(name); var go = new GameObject(name);
go.transform.SetParent(transform, false); go.transform.SetParent(transform, false);
var c = go.AddComponent<Canvas>(); return UiLayer.Root(go, Cam, order, plane, Reference, rate); // Screen Space - Camera = 카메라 캡처(검증)에도 찍힘
c.renderMode = RenderMode.ScreenSpaceCamera; // 카메라 캡처(검증)에도 찍히게
c.worldCamera = Cam;
c.planeDistance = plane;
c.sortingOrder = order;
var sc = go.AddComponent<CanvasScaler>();
sc.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
sc.referenceResolution = Reference;
sc.matchWidthOrHeight = 0.5f;
go.AddComponent<GraphicRaycaster>();
return c;
} }
// ---------- 화면 전환 ---------- // ---------- 화면 전환 ----------
@ -375,6 +369,7 @@ namespace TKS.OutGame
void BuildCurrencyBar() void BuildCurrencyBar()
{ {
barRoot = UiKit.TopBand("CurrencyBar", TopRoot, 0f, TopBarH); barRoot = UiKit.TopBand("CurrencyBar", TopRoot, 0f, TopBarH);
UiLayer.Nest(barRoot.gameObject, UiRate.Event, true); // 군량 타이머 = 1초마다 · 팝업·토스트와 따로
// 테마 = 띠 없이 칩만(화면 바탕이 비침 · 목업 hero_detail) · 투명 판은 아래 클릭 막기용 // 테마 = 띠 없이 칩만(화면 바탕이 비침 · 목업 hero_detail) · 투명 판은 아래 클릭 막기용
UiKit.Paint(barRoot, UISkin.On ? Color.clear : new Color(0.15f, 0.11f, 0.09f), false, true); UiKit.Paint(barRoot, UISkin.On ? Color.clear : new Color(0.15f, 0.11f, 0.09f), false, true);
goldText = Pill(0, "cur_gold", "plus", "골", "골드", UiKit.Coin, () => Toast("골드 = 스테이지 보상 · 거래 > 상점 · (구매 상점 준비 중)")); goldText = Pill(0, "cur_gold", "plus", "골", "골드", UiKit.Coin, () => Toast("골드 = 스테이지 보상 · 거래 > 상점 · (구매 상점 준비 중)"));
@ -547,6 +542,7 @@ namespace TKS.OutGame
{ {
CloseModal(); CloseModal();
var dim = UiKit.Fill("Modal", TopRoot); var dim = UiKit.Fill("Modal", TopRoot);
UiLayer.Nest(dim.gameObject, UiRate.Event, true);
var dimImg = UiKit.Paint(dim, UISkin.Col("dim", new Color(0f, 0f, 0f, 0.6f)), false, true); var dimImg = UiKit.Paint(dim, UISkin.Col("dim", new Color(0f, 0f, 0f, 0.6f)), false, true);
dimImg.raycastTarget = true; dimImg.raycastTarget = true;
modal = dim.gameObject; modal = dim.gameObject;
@ -608,6 +604,7 @@ namespace TKS.OutGame
? UiKit.Skin(UiKit.Rect("Toast", TopRoot, new Vector2(0.5f, 0f), new Vector2(0.5f, 0f), new Vector2(0f, TabBarH + 40f), new Vector2(940f, 110f)), "toast", UiKit.BarColor) ? UiKit.Skin(UiKit.Rect("Toast", TopRoot, new Vector2(0.5f, 0f), new Vector2(0.5f, 0f), new Vector2(0f, TabBarH + 40f), new Vector2(940f, 110f)), "toast", UiKit.BarColor)
: UiKit.Box("Toast", TopRoot, new Vector2(0.5f, 0f), new Vector2(0.5f, 0f), new Vector2(0f, TabBarH + 40f), new Vector2(940f, 110f), new Color(0.1f, 0.08f, 0.07f, 0.92f)); : UiKit.Box("Toast", TopRoot, new Vector2(0.5f, 0f), new Vector2(0.5f, 0f), new Vector2(0f, TabBarH + 40f), new Vector2(940f, 110f), new Color(0.1f, 0.08f, 0.07f, 0.92f));
toast = img.gameObject; toast = img.gameObject;
UiLayer.Nest(toast, UiRate.Event, false);
toastText = UiKit.FillLabel(img.transform, "", 32, UiKit.White, TextAnchor.MiddleCenter, false, 16f); toastText = UiKit.FillLabel(img.transform, "", 32, UiKit.White, TextAnchor.MiddleCenter, false, 16f);
} }
toast.transform.SetAsLastSibling(); toast.transform.SetAsLastSibling();

View File

@ -303,6 +303,7 @@ namespace TKS.OutGame
{ {
CloseResult(); CloseResult();
var dim = UiKit.Fill("GachaResult", Root.TopRoot); var dim = UiKit.Fill("GachaResult", Root.TopRoot);
UiLayer.Nest(dim.gameObject, UiRate.Event, true); // 위 덮개 = 자기 캔버스(재화 바와 따로 · PD 2026-11-10)
var dimg = UiKit.Paint(dim, new Color(0.08f, 0.05f, 0.04f, 0.9f), false, true); var dimg = UiKit.Paint(dim, new Color(0.08f, 0.05f, 0.04f, 0.9f), false, true);
dimg.raycastTarget = true; dimg.raycastTarget = true;
var skip = dim.gameObject.AddComponent<Button>(); var skip = dim.gameObject.AddComponent<Button>();

View File

@ -596,6 +596,8 @@ namespace TKS.OutGame
content.anchorMax = new Vector2(1f, 1f); content.anchorMax = new Vector2(1f, 1f);
content.pivot = new Vector2(0.5f, 1f); content.pivot = new Vector2(0.5f, 1f);
content.offsetMin = content.offsetMax = Vector2.zero; content.offsetMin = content.offsetMax = Vector2.zero;
// 스크롤 내용 = 자기 캔버스(Frame) — 끄는 동안 매 프레임 움직여도 화면 나머지를 다시 묶지 않게(PD 2026-11-10 · RectMask2D 자르기 유지)
UiLayer.Nest(content.gameObject, UiRate.Frame, true);
var grid = content.gameObject.AddComponent<GridLayoutGroup>(); var grid = content.gameObject.AddComponent<GridLayoutGroup>();
grid.cellSize = cell; grid.cellSize = cell;
grid.spacing = spacing; grid.spacing = spacing;

View File

@ -2,13 +2,22 @@ using UnityEngine;
namespace TKS.OutGame namespace TKS.OutGame
{ {
/// <summary>등장 튀김(Fable 제안서 §4-2 「별 0.15 s 간격 scale 1.4→1」) — 켜질 때마다 delay 뒤 from → 1 · 시간 = 실시간(일시정지 무관).</summary> /// <summary>
/// 등장 튀김(Fable 제안서 §4-2 「별 0.15 s 간격 scale 1.4→1」) — 켜질 때마다 delay 뒤 from → 1 · 시간 = 실시간(일시정지 무관).
/// 움직이는 동안 매 프레임 바뀌므로 Frame 캔버스로 분리(부모가 이미 Frame 층이면 그 층 · PD 2026-11-10 캔버스 분할) · 끝나면 쓰기를 멈춘다.
/// </summary>
public class UiPop : MonoBehaviour public class UiPop : MonoBehaviour
{ {
public float delay; public float delay;
public float from = 1.4f; public float from = 1.4f;
public float duration = 0.18f; public float duration = 0.18f;
float t0; float t0;
bool done;
void Awake()
{
UiLayer.Isolate(gameObject);
}
void OnEnable() void OnEnable()
{ {
@ -18,19 +27,18 @@ namespace TKS.OutGame
public void Play() public void Play()
{ {
t0 = Time.unscaledTime + delay; t0 = Time.unscaledTime + delay;
done = false;
transform.localScale = Vector3.zero; transform.localScale = Vector3.zero;
} }
void Update() void Update()
{ {
if (done) return;
float t = Time.unscaledTime - t0; float t = Time.unscaledTime - t0;
if (t < 0f) if (t < 0f) return; // 기다리는 동안 = Play 가 둔 0 그대로
{
transform.localScale = Vector3.zero;
return;
}
float k = duration > 0f ? Mathf.Clamp01(t / duration) : 1f; float k = duration > 0f ? Mathf.Clamp01(t / duration) : 1f;
transform.localScale = Vector3.one * Mathf.Lerp(from, 1f, 1f - (1f - k) * (1f - k)); transform.localScale = Vector3.one * Mathf.Lerp(from, 1f, 1f - (1f - k) * (1f - k));
done = k >= 1f;
} }
} }
} }

View File

@ -3,7 +3,10 @@ using UnityEngine.UI;
namespace TKS.OutGame namespace TKS.OutGame
{ {
/// <summary>그래픽 알파 핑퐁(선택 테두리 등 · Fable 제안서 §3-3 「α 0.6↔1.0 0.8 s」) — 시간 = 실시간(일시정지 무관).</summary> /// <summary>
/// 그래픽 알파 핑퐁(선택 테두리 등 · Fable 제안서 §3-3 「α 0.6↔1.0 0.8 s」) — 시간 = 실시간(일시정지 무관).
/// 매 프레임 바뀌므로 자기 캔버스(Frame)로 분리 — 화면 전체가 매 프레임 다시 묶이지 않게(PD 2026-11-10 캔버스 분할).
/// </summary>
public class UiPulse : MonoBehaviour public class UiPulse : MonoBehaviour
{ {
public float min = 0.6f; public float min = 0.6f;
@ -16,6 +19,7 @@ namespace TKS.OutGame
{ {
g = GetComponent<Graphic>(); g = GetComponent<Graphic>();
if (g != null) baseA = g.color.a; if (g != null) baseA = g.color.a;
UiLayer.Isolate(gameObject);
} }
void Update() void Update()

View File

@ -202,6 +202,8 @@ namespace TKS.OutGame
matched.Add(k); matched.Add(k);
} }
yield return Wait(); yield return Wait();
// 캔버스 층 · 드로우콜 실측(PD 2026-11-10 UI 구조 개선 전·후 비교 · ui_layers.csv)
yield return UiLayerProbe.Measure(name, Log, OutputDir);
string file = Path.Combine(OutputDir, shot.ToString("00") + "_" + name + ".png"); string file = Path.Combine(OutputDir, shot.ToString("00") + "_" + name + ".png");
shot++; shot++;
yield return CaptureUtil.Capture(cam, w, h, file); yield return CaptureUtil.Capture(cam, w, h, file);

View File

@ -66,24 +66,24 @@ namespace TKS
Image minimapBg; Image minimapBg;
Image minimapView; Image minimapView;
RectTransform minimapLive;
readonly Dictionary<Unit, Image> minimapDots = new Dictionary<Unit, Image>(); readonly Dictionary<Unit, Image> minimapDots = new Dictionary<Unit, Image>();
// 캔버스 층(PD 2026-11-10 「얼마나 자주 바뀌는지에 따라 캔버스 분할」 · 프로젝트 CLAUDE.md 「UI 캔버스·아틀라스 구조」):
// 루트 = 정적 판(칩 바탕 · 유닛 판 바탕 · 미니맵 틀 · Static) → Info(글자·값·스킬 버튼 · 턴·선택·피해 때만 · Event)
// → MinimapLive(시야 사각형·유닛 점 · 매 프레임 · Frame) · SkillGlow(호흡 · Frame) → Result(결과 덮개 · Event)
RectTransform info;
RectTransform resultLayer;
Image hpFillImg;
public void Build(BattleManager b, Camera cam, CameraController cc) public void Build(BattleManager b, Camera cam, CameraController cc)
{ {
battle = b; battle = b;
uiCam = cam; uiCam = cam;
camCtrl = cc; camCtrl = cc;
var canvas = gameObject.AddComponent<Canvas>(); UiLayer.Root(gameObject, cam, 100, 1f, UiLayer.Reference, UiRate.Static);
canvas.renderMode = RenderMode.ScreenSpaceCamera;
canvas.worldCamera = cam;
canvas.planeDistance = 1f;
canvas.sortingOrder = 100;
var scaler = gameObject.AddComponent<CanvasScaler>();
scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
scaler.referenceResolution = new Vector2(1080f, 1920f);
scaler.matchWidthOrHeight = 0.5f;
gameObject.AddComponent<GraphicRaycaster>();
group = gameObject.AddComponent<CanvasGroup>(); group = gameObject.AddComponent<CanvasGroup>();
info = UiLayer.Group("Info", transform, UiRate.Event, false);
if (EventSystem.current == null && FindFirstObjectByType<EventSystem>() == null) if (EventSystem.current == null && FindFirstObjectByType<EventSystem>() == null)
{ {
@ -96,10 +96,11 @@ namespace TKS
{ {
// 팩 칩(PD #1100): 스테이지 · 유닛 수 · 턴 띠 · 선택 유닛 정보 판 — 글자 뒤(먼저 만든다) · 반투명 진남 // 팩 칩(PD #1100): 스테이지 · 유닛 수 · 턴 띠 · 선택 유닛 정보 판 — 글자 뒤(먼저 만든다) · 반투명 진남
Color panelCol = UISkin.Col("hud_panel", new Color(0.12f, 0.15f, 0.21f, 0.85f)); Color panelCol = UISkin.Col("hud_panel", new Color(0.12f, 0.15f, 0.21f, 0.85f));
Chip("StageChip", new Vector2(0f, 1f), new Vector2(0f, 1f), new Vector2(14f, -14f), new Vector2(730f, 86f), panelCol); Chip("StageChip", transform, new Vector2(0f, 1f), new Vector2(0f, 1f), new Vector2(14f, -14f), new Vector2(730f, 86f), panelCol);
Chip("CountChip", new Vector2(1f, 1f), new Vector2(1f, 1f), new Vector2(-14f, -14f), new Vector2(330f, 86f), panelCol); Chip("CountChip", transform, new Vector2(1f, 1f), new Vector2(1f, 1f), new Vector2(-14f, -14f), new Vector2(330f, 86f), panelCol);
turnBadge = Chip("TurnBadge", new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(0f, -106f), new Vector2(440f, 84f), UISkin.Col("hud_turn_player", Color.blue)); // 턴 띠 = 아군/적군 턴마다 색이 바뀜 → Info 층
Chip("UnitPanel", new Vector2(0f, 0f), new Vector2(0f, 0f), new Vector2(14f, 96f), new Vector2(790f, 196f), panelCol); turnBadge = Chip("TurnBadge", info, new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(0f, -106f), new Vector2(440f, 84f), UISkin.Col("hud_turn_player", Color.blue));
Chip("UnitPanel", transform, new Vector2(0f, 0f), new Vector2(0f, 0f), new Vector2(14f, 96f), new Vector2(790f, 196f), panelCol);
} }
stageText = MakeText("Stage", new Vector2(0f, 1f), new Vector2(0f, 1f), new Vector2(0f, 1f), new Vector2(30f, -28f), new Vector2(700f, 70f), 40, TextAnchor.UpperLeft); stageText = MakeText("Stage", new Vector2(0f, 1f), new Vector2(0f, 1f), new Vector2(0f, 1f), new Vector2(30f, -28f), new Vector2(700f, 70f), 40, TextAnchor.UpperLeft);
// 스테이지 표 제목(최대 ~20자)이 오른쪽 위 적 수와 겹치지 않게 칸 안에서 글자 크기를 줄인다 // 스테이지 표 제목(최대 ~20자)이 오른쪽 위 적 수와 겹치지 않게 칸 안에서 글자 크기를 줄인다
@ -126,7 +127,7 @@ namespace TKS
unitText = MakeText("UnitInfo", new Vector2(0f, 0f), new Vector2(0f, 0f), new Vector2(0f, 0f), new Vector2(30f, 110f), new Vector2(760f, 170f), 32, TextAnchor.LowerLeft); unitText = MakeText("UnitInfo", new Vector2(0f, 0f), new Vector2(0f, 0f), new Vector2(0f, 0f), new Vector2(30f, 110f), new Vector2(760f, 170f), 32, TextAnchor.LowerLeft);
// 스킬 버튼(PD #1093 B · 유닛 선택 시 · 탭 = 예약/취소 · 즉시 발동) — 이름 · 쿨 · 사용 가능/불가 · 패시브 한 줄 // 스킬 버튼(PD #1093 B · 유닛 선택 시 · 탭 = 예약/취소 · 즉시 발동) — 이름 · 쿨 · 사용 가능/불가 · 패시브 한 줄
skillPanel = MakeImage("SkillButton", transform, new Vector2(1f, 0f), new Vector2(1f, 0f), new Vector2(1f, 0f), new Vector2(-30f, 110f), new Vector2(270f, 230f), SkillOffColor); skillPanel = MakeImage("SkillButton", info, new Vector2(1f, 0f), new Vector2(1f, 0f), new Vector2(1f, 0f), new Vector2(-30f, 110f), new Vector2(270f, 230f), SkillOffColor);
skillName = MakeText("Name", new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(0f, -14f), new Vector2(256f, 60f), 38, TextAnchor.UpperCenter); skillName = MakeText("Name", new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(0f, -14f), new Vector2(256f, 60f), 38, TextAnchor.UpperCenter);
skillName.transform.SetParent(skillPanel.transform, false); skillName.transform.SetParent(skillPanel.transform, false);
skillName.horizontalOverflow = HorizontalWrapMode.Wrap; skillName.horizontalOverflow = HorizontalWrapMode.Wrap;
@ -185,17 +186,37 @@ namespace TKS
var mmBorder = MakeImage("Border", minimapBg.transform, Vector2.zero, Vector2.one, new Vector2(0.5f, 0.5f), Vector2.zero, new Vector2(6f, 6f), new Color(0.85f, 0.75f, 0.45f, 0.9f)); var mmBorder = MakeImage("Border", minimapBg.transform, Vector2.zero, Vector2.one, new Vector2(0.5f, 0.5f), Vector2.zero, new Vector2(6f, 6f), new Color(0.85f, 0.75f, 0.45f, 0.9f));
mmBorder.transform.SetAsFirstSibling(); mmBorder.transform.SetAsFirstSibling();
var mmMid = MakeImage("Mid", minimapBg.transform, new Vector2(0f, 0.5f), new Vector2(1f, 0.5f), new Vector2(0.5f, 0.5f), Vector2.zero, new Vector2(0f, 2f), new Color(1f, 1f, 1f, 0.25f)); var mmMid = MakeImage("Mid", minimapBg.transform, new Vector2(0f, 0.5f), new Vector2(1f, 0.5f), new Vector2(0.5f, 0.5f), Vector2.zero, new Vector2(0f, 2f), new Color(1f, 1f, 1f, 0.25f));
minimapView = MakeImage("View", minimapBg.transform, Vector2.zero, Vector2.zero, Vector2.zero, Vector2.zero, new Vector2(40f, 60f), new Color(1f, 1f, 1f, 0.2f)); // 미니맵 틀(정적) 위 = 시야 사각형 · 유닛 점만 담는 Frame 층(카메라·유닛이 움직일 때마다 이 층만 다시 묶음)
minimapLive = UiLayer.Group("MinimapLive", minimapBg.transform, UiRate.Frame, false);
minimapView = MakeImage("View", minimapLive, Vector2.zero, Vector2.zero, Vector2.zero, Vector2.zero, new Vector2(40f, 60f), new Color(1f, 1f, 1f, 0.2f));
if (UISkin.Has("hp_bg") && UISkin.Has("chip") && UISkin.Has("circle_frame")) BuildKit(); if (UISkin.Has("hp_bg") && UISkin.Has("chip") && UISkin.Has("circle_frame")) BuildKit();
resultOverlay = MakeImage("ResultOverlay", transform, Vector2.zero, Vector2.one, new Vector2(0.5f, 0.5f), Vector2.zero, Vector2.zero, new Color(0f, 0f, 0f, 0.65f)).gameObject; resultLayer = UiLayer.Group("Result", transform, UiRate.Event, false);
resultOverlay = MakeImage("ResultOverlay", resultLayer, Vector2.zero, Vector2.one, new Vector2(0.5f, 0.5f), Vector2.zero, Vector2.zero, new Color(0f, 0f, 0f, 0.65f)).gameObject;
resultTitle = MakeText("Title", new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(0f, 60f), new Vector2(1000f, 160f), 110, TextAnchor.MiddleCenter); resultTitle = MakeText("Title", new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(0f, 60f), new Vector2(1000f, 160f), 110, TextAnchor.MiddleCenter);
resultTitle.transform.SetParent(resultOverlay.transform, false); resultTitle.transform.SetParent(resultOverlay.transform, false);
if (UISkin.On) resultTitle.font = UISkin.TitleFont; if (UISkin.On) resultTitle.font = UISkin.TitleFont;
resultSub = MakeText("Sub", new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(0f, -80f), new Vector2(1000f, 120f), 40, TextAnchor.MiddleCenter); resultSub = MakeText("Sub", new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(0f, -80f), new Vector2(1000f, 120f), 40, TextAnchor.MiddleCenter);
resultSub.transform.SetParent(resultOverlay.transform, false); resultSub.transform.SetParent(resultOverlay.transform, false);
resultOverlay.SetActive(false); resultOverlay.SetActive(false);
// 그리는 순서 = 정적 판(루트 자식들) → Info → Result
info.SetAsLastSibling();
resultLayer.SetAsLastSibling();
}
/// <summary>같은 부모 좌표계에서 src 와 같은 자리·크기의 빈 칸(정적 판 위에 얹는 Info 층 내용 칸).</summary>
static RectTransform Like(string name, Transform parent, RectTransform src)
{
var go = new GameObject(name, typeof(RectTransform));
go.transform.SetParent(parent, false);
var rt = (RectTransform)go.transform;
rt.anchorMin = src.anchorMin;
rt.anchorMax = src.anchorMax;
rt.pivot = src.pivot;
rt.anchoredPosition = src.anchoredPosition;
rt.sizeDelta = src.sizeDelta;
return rt;
} }
/// <summary> /// <summary>
@ -211,7 +232,8 @@ namespace TKS
unitText.gameObject.SetActive(false); unitText.gameObject.SetActive(false);
var card = MakeImage("UnitCard", transform, Vector2.zero, Vector2.zero, Vector2.zero, new Vector2(24f, 150f), new Vector2(720f, 204f), UISkin.Col("hud_panel", new Color(0.12f, 0.15f, 0.21f, 0.85f))); var card = MakeImage("UnitCard", transform, Vector2.zero, Vector2.zero, Vector2.zero, new Vector2(24f, 150f), new Vector2(720f, 204f), UISkin.Col("hud_panel", new Color(0.12f, 0.15f, 0.21f, 0.85f)));
UISkin.Apply(card, "card"); UISkin.Apply(card, "card");
unitCard = card.rectTransform; // 판 바탕 = 루트(정적) · 내용(초상 · 이름 · 체력 · 칩) = 같은 자리의 Info 칸(선택·피해 때만 바뀜)
unitCard = Like("UnitCardInfo", info, card.rectTransform);
unitBody = OutGame.UiKit.Fill("Body", unitCard); unitBody = OutGame.UiKit.Fill("Body", unitCard);
portraitRing = OutGame.UiKit.Skin(OutGame.UiKit.At(unitBody, "Ring", 18f, 22f, 152f, 152f), "circle_frame", Color.white); portraitRing = OutGame.UiKit.Skin(OutGame.UiKit.At(unitBody, "Ring", 18f, 22f, 152f, 152f), "circle_frame", Color.white);
var mask = MakeImage("Mask", portraitRing.transform, new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), Vector2.zero, new Vector2(132f, 132f), Color.white); var mask = MakeImage("Mask", portraitRing.transform, new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), Vector2.zero, new Vector2(132f, 132f), Color.white);
@ -228,7 +250,7 @@ namespace TKS
stateText = OutGame.UiKit.Label(unitBody, "", 24, soft, TextAnchor.MiddleRight, new Vector2(1f, 1f), new Vector2(1f, 1f), new Vector2(-20f, -22f), new Vector2(220f, 48f), false); stateText = OutGame.UiKit.Label(unitBody, "", 24, soft, TextAnchor.MiddleRight, new Vector2(1f, 1f), new Vector2(1f, 1f), new Vector2(-20f, -22f), new Vector2(220f, 48f), false);
var hpBg = OutGame.UiKit.Skin(OutGame.UiKit.At(unitBody, "Hp", 188f, 86f, HpBarW, 30f), "hp_bg", UISkin.Col("hp_bg", Color.white)); var hpBg = OutGame.UiKit.Skin(OutGame.UiKit.At(unitBody, "Hp", 188f, 86f, HpBarW, 30f), "hp_bg", UISkin.Col("hp_bg", Color.white));
hpFill = OutGame.UiKit.At(hpBg.transform, "Fill", 4f, 4f, HpBarW - 8f, 22f); hpFill = OutGame.UiKit.At(hpBg.transform, "Fill", 4f, 4f, HpBarW - 8f, 22f);
OutGame.UiKit.Skin(hpFill, "hp_fill", UISkin.Col("hp_hi", Color.green)); hpFillImg = OutGame.UiKit.Skin(hpFill, "hp_fill", UISkin.Col("hp_hi", Color.green));
hpText = OutGame.UiKit.Title(OutGame.UiKit.Label(unitBody, "", 28, Color.white, TextAnchor.MiddleLeft, new Vector2(0f, 1f), new Vector2(0f, 1f), new Vector2(188f + HpBarW + 14f, -80f), new Vector2(160f, 44f)), true); hpText = OutGame.UiKit.Title(OutGame.UiKit.Label(unitBody, "", 28, Color.white, TextAnchor.MiddleLeft, new Vector2(0f, 1f), new Vector2(0f, 1f), new Vector2(188f + HpBarW + 14f, -80f), new Vector2(160f, 44f)), true);
hpText.horizontalOverflow = HorizontalWrapMode.Overflow; hpText.horizontalOverflow = HorizontalWrapMode.Overflow;
atkText = OutGame.UiKit.InfoChip(unitBody, "Atk", 188f, 130f, 156f, 56f, "sword", "", 30); atkText = OutGame.UiKit.InfoChip(unitBody, "Atk", 188f, 130f, 156f, 56f, "sword", "", 30);
@ -237,7 +259,7 @@ namespace TKS
emptyText = OutGame.UiKit.Label(unitCard, "유닛을 선택하세요", 30, soft, TextAnchor.MiddleCenter, new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), Vector2.zero, new Vector2(680f, 60f), false); emptyText = OutGame.UiKit.Label(unitCard, "유닛을 선택하세요", 30, soft, TextAnchor.MiddleCenter, new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), Vector2.zero, new Vector2(680f, 60f), false);
// 위 안내 한 줄 = 어두운 칩 위(글자가 바탕 없이 화면 끝에서 잘리던 것 · 검수2 결함 3) · 칸 안 맞춤 축소 // 위 안내 한 줄 = 어두운 칩 위(글자가 바탕 없이 화면 끝에서 잘리던 것 · 검수2 결함 3) · 칸 안 맞춤 축소
msgChip = MakeImage("MsgChip", transform, new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(0f, -200f), new Vector2(1032f, 58f), UISkin.Col("chip_dark", new Color(0f, 0f, 0f, 0.8f))); msgChip = MakeImage("MsgChip", info, new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(0f, -200f), new Vector2(1032f, 58f), UISkin.Col("chip_dark", new Color(0f, 0f, 0f, 0.8f)));
UISkin.Apply(msgChip, "chip"); UISkin.Apply(msgChip, "chip");
msgChip.transform.SetSiblingIndex(messageText.transform.GetSiblingIndex()); msgChip.transform.SetSiblingIndex(messageText.transform.GetSiblingIndex());
messageText.rectTransform.anchoredPosition = new Vector2(0f, -204f); messageText.rectTransform.anchoredPosition = new Vector2(0f, -204f);
@ -250,7 +272,9 @@ namespace TKS
messageText.alignment = TextAnchor.MiddleCenter; messageText.alignment = TextAnchor.MiddleCenter;
// 수 칩 = 아군 · 적 아이콘 + 숫자(Sen) // 수 칩 = 아군 · 적 아이콘 + 숫자(Sen)
var cc = transform.Find("CountChip"); // 칩 바탕 = 루트(정적) · 아이콘·숫자 = 같은 자리의 Info 칸(유닛이 죽을 때만 바뀜)
var ccPlate = transform.Find("CountChip") as RectTransform;
var cc = ccPlate != null ? Like("CountInfo", info, ccPlate) : null;
if (cc != null && OutGame.UiKit.Icon(cc, "hud_ally", new Vector2(0f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(48f, 0f), 56f) != null) if (cc != null && OutGame.UiKit.Icon(cc, "hud_ally", new Vector2(0f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(48f, 0f), 56f) != null)
{ {
countText.gameObject.SetActive(false); countText.gameObject.SetActive(false);
@ -262,7 +286,7 @@ namespace TKS
// 스킬 버튼 뒤 빛(사용 가능 = 호흡) // 스킬 버튼 뒤 빛(사용 가능 = 호흡)
if (UISkin.Has("glow_circle")) if (UISkin.Has("glow_circle"))
{ {
skillGlow = MakeImage("SkillGlow", transform, new Vector2(1f, 0f), new Vector2(1f, 0f), new Vector2(0.5f, 0.5f), new Vector2(-24f - 125f, 100f + 125f), new Vector2(560f, 560f), UISkin.Col("skill_glow", new Color(0.85f, 0.25f, 0.18f, 0.4f))); skillGlow = MakeImage("SkillGlow", info, new Vector2(1f, 0f), new Vector2(1f, 0f), new Vector2(0.5f, 0.5f), new Vector2(-24f - 125f, 100f + 125f), new Vector2(560f, 560f), UISkin.Col("skill_glow", new Color(0.85f, 0.25f, 0.18f, 0.4f)));
UISkin.Apply(skillGlow, "glow_circle"); UISkin.Apply(skillGlow, "glow_circle");
skillGlow.transform.SetSiblingIndex(skillPanel.transform.GetSiblingIndex()); skillGlow.transform.SetSiblingIndex(skillPanel.transform.GetSiblingIndex());
var pulse = skillGlow.gameObject.AddComponent<OutGame.UiPulse>(); var pulse = skillGlow.gameObject.AddComponent<OutGame.UiPulse>();
@ -295,9 +319,11 @@ namespace TKS
} }
nameText.text = u.DisplayName + (u.IsBoss ? " <size=24>장수</size>" : ""); nameText.text = u.DisplayName + (u.IsBoss ? " <size=24>장수</size>" : "");
float r = u.MaxHp > 0 ? Mathf.Clamp01(u.Hp / (float)u.MaxHp) : 0f; float r = u.MaxHp > 0 ? Mathf.Clamp01(u.Hp / (float)u.MaxHp) : 0f;
hpFill.sizeDelta = new Vector2(Mathf.Max(12f, (HpBarW - 8f) * r), hpFill.sizeDelta.y); // 값이 같으면 쓰지 않는다(매 프레임 호출 · 바뀔 때만 Info 층 재구성)
var hs = new Vector2(Mathf.Max(12f, (HpBarW - 8f) * r), hpFill.sizeDelta.y);
if (hpFill.sizeDelta != hs) hpFill.sizeDelta = hs;
hpFill.gameObject.SetActive(r > 0f); hpFill.gameObject.SetActive(r > 0f);
hpFill.GetComponent<Image>().color = r > 0.5f ? UISkin.Col("hp_hi", Color.green) : r > 0.25f ? UISkin.Col("hp_mid", Color.yellow) : UISkin.Col("hp_low", Color.red); hpFillImg.color = r > 0.5f ? UISkin.Col("hp_hi", Color.green) : r > 0.25f ? UISkin.Col("hp_mid", Color.yellow) : UISkin.Col("hp_low", Color.red);
hpText.text = u.Hp + "/" + u.MaxHp; hpText.text = u.Hp + "/" + u.MaxHp;
atkText.text = u.Atk.ToString(); atkText.text = u.Atk.ToString();
defText.text = u.Def.ToString(); defText.text = u.Def.ToString();
@ -316,10 +342,11 @@ namespace TKS
stateText.text = state; stateText.text = state;
} }
/// <summary>글자 = Info 층(값이 바뀌어도 정적 판을 다시 묶지 않게).</summary>
Text MakeText(string name, Vector2 anchorMin, Vector2 anchorMax, Vector2 pivot, Vector2 anchoredPos, Vector2 size, int fontSize, TextAnchor align) Text MakeText(string name, Vector2 anchorMin, Vector2 anchorMax, Vector2 pivot, Vector2 anchoredPos, Vector2 size, int fontSize, TextAnchor align)
{ {
var go = new GameObject(name); var go = new GameObject(name);
go.transform.SetParent(transform, false); go.transform.SetParent(info, false);
var rt = go.AddComponent<RectTransform>(); var rt = go.AddComponent<RectTransform>();
rt.anchorMin = anchorMin; rt.anchorMin = anchorMin;
rt.anchorMax = anchorMax; rt.anchorMax = anchorMax;
@ -357,9 +384,9 @@ namespace TKS
} }
/// <summary>테마 칩(팩 카드 틀 · 색 틴트) — 글자보다 먼저 만들어 뒤에 깔린다.</summary> /// <summary>테마 칩(팩 카드 틀 · 색 틴트) — 글자보다 먼저 만들어 뒤에 깔린다.</summary>
Image Chip(string name, Vector2 anchor, Vector2 pivot, Vector2 pos, Vector2 size, Color color) Image Chip(string name, Transform parent, Vector2 anchor, Vector2 pivot, Vector2 pos, Vector2 size, Color color)
{ {
var img = MakeImage(name, transform, anchor, anchor, pivot, pos, size, color); var img = MakeImage(name, parent, anchor, anchor, pivot, pos, size, color);
UISkin.Apply(img, "card"); UISkin.Apply(img, "card");
return img; return img;
} }
@ -579,7 +606,8 @@ namespace TKS
} }
bool result = battle.Phase == BattlePhase.StageClear || battle.Phase == BattlePhase.Defeat; bool result = battle.Phase == BattlePhase.StageClear || battle.Phase == BattlePhase.Defeat;
if (group != null) group.alpha = HideOnResult && result ? 0f : 1f; float alpha = HideOnResult && result ? 0f : 1f;
if (group != null && group.alpha != alpha) group.alpha = alpha;
if (resultOverlay.activeSelf != result) resultOverlay.SetActive(result); if (resultOverlay.activeSelf != result) resultOverlay.SetActive(result);
if (result) if (result)
{ {
@ -609,8 +637,12 @@ namespace TKS
float y0 = Mathf.Clamp((v.yMin - b.yMin) * sy, 0f, r.height); float y0 = Mathf.Clamp((v.yMin - b.yMin) * sy, 0f, r.height);
float x1 = Mathf.Clamp((v.xMax - b.xMin) * sx, 0f, r.width); float x1 = Mathf.Clamp((v.xMax - b.xMin) * sx, 0f, r.width);
float y1 = Mathf.Clamp((v.yMax - b.yMin) * sy, 0f, r.height); float y1 = Mathf.Clamp((v.yMax - b.yMin) * sy, 0f, r.height);
minimapView.rectTransform.anchoredPosition = new Vector2(x0, y0); // 값이 같으면 쓰지 않는다(카메라가 멈춰 있으면 MinimapLive 층도 다시 묶지 않음)
minimapView.rectTransform.sizeDelta = new Vector2(Mathf.Max(2f, x1 - x0), Mathf.Max(2f, y1 - y0)); var vr = minimapView.rectTransform;
var vp = new Vector2(x0, y0);
var vs = new Vector2(Mathf.Max(2f, x1 - x0), Mathf.Max(2f, y1 - y0));
if (vr.anchoredPosition != vp) vr.anchoredPosition = vp;
if (vr.sizeDelta != vs) vr.sizeDelta = vs;
} }
// 유닛 점 // 유닛 점
@ -620,7 +652,7 @@ namespace TKS
Image dot; Image dot;
if (!minimapDots.TryGetValue(u, out dot)) if (!minimapDots.TryGetValue(u, out dot))
{ {
dot = MakeImage("Dot", minimapBg.transform, Vector2.zero, Vector2.zero, new Vector2(0.5f, 0.5f), Vector2.zero, dot = MakeImage("Dot", minimapLive, Vector2.zero, Vector2.zero, new Vector2(0.5f, 0.5f), Vector2.zero,
new Vector2(u.IsBoss ? 14f : 10f, u.IsBoss ? 14f : 10f), new Vector2(u.IsBoss ? 14f : 10f, u.IsBoss ? 14f : 10f),
u.Faction == Faction.Player ? UnitView.PlayerColor : UnitView.EnemyColor); u.Faction == Faction.Player ? UnitView.PlayerColor : UnitView.EnemyColor);
if (u.IsBoss) dot.color = new Color(1f, 0.85f, 0.3f); if (u.IsBoss) dot.color = new Color(1f, 0.85f, 0.3f);
@ -628,7 +660,11 @@ namespace TKS
} }
bool show = u.IsAlive; bool show = u.IsAlive;
if (dot.gameObject.activeSelf != show) dot.gameObject.SetActive(show); if (dot.gameObject.activeSelf != show) dot.gameObject.SetActive(show);
if (show) dot.rectTransform.anchoredPosition = new Vector2((u.Position.x - b.xMin) * sx, (u.Position.y - b.yMin) * sy); if (show)
{
var dp = new Vector2((u.Position.x - b.xMin) * sx, (u.Position.y - b.yMin) * sy);
if (dot.rectTransform.anchoredPosition != dp) dot.rectTransform.anchoredPosition = dp;
}
} }
// 사라진 유닛 정리 // 사라진 유닛 정리
var dead = new List<Unit>(); var dead = new List<Unit>();

View File

@ -138,7 +138,9 @@ namespace TKS
b.y = Mathf.Floor(b.y * k); b.y = Mathf.Floor(b.y * k);
b.w = Mathf.Floor(b.w * k); b.w = Mathf.Floor(b.w * k);
} }
sp = Sprite.Create(src.texture, src.rect, new Vector2(0.5f, 0.5f), ppu, 0, SpriteMeshType.FullRect, b); // 아틀라스에 묶인 스프라이트(PD 2026-11-10 UI 아틀라스) = texture 가 아틀라스 장 → 그 장 안의 자리(textureRect)로 잘라야 한다(rect = 원본 기준)
var area = src.packed ? src.textureRect : src.rect;
sp = Sprite.Create(src.texture, area, new Vector2(0.5f, 0.5f), ppu, 0, SpriteMeshType.FullRect, b);
sp.name = key; sp.name = key;
sp.hideFlags = HideFlags.DontSave; sp.hideFlags = HideFlags.DontSave;
worldCache[key] = sp; worldCache[key] = sp;

View File

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

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 42c5936beacb9554882a525c3d0d7c2f

View File

@ -0,0 +1,101 @@
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Text;
using Unity.Profiling;
using UnityEngine;
using UnityEngine.UI;
namespace TKS
{
/// <summary>
/// UI 층 구조 실측(PD 2026-11-10 캔버스 분할 검증) — 지금 화면의 렌더 통계(배치 · 드로우콜 · SetPass)와
/// 켜진 캔버스마다 그래픽 수 · 등급 · 레이캐스터 · 매 프레임 움직이는 요소(UiPulse · UiPop · 스크롤 내용 · 미니맵 · 커서)를 기록한다.
/// 「매 프레임 재구성 그래픽」 = 움직이는 요소가 든 캔버스들의 그래픽 수 합(캔버스는 자식 하나만 바뀌어도 전체를 다시 묶음).
/// </summary>
public static class UiLayerProbe
{
static ProfilerRecorder batches;
static ProfilerRecorder draws;
static ProfilerRecorder setPass;
static void Begin()
{
if (!batches.Valid) batches = ProfilerRecorder.StartNew(ProfilerCategory.Render, "Batches Count");
if (!draws.Valid) draws = ProfilerRecorder.StartNew(ProfilerCategory.Render, "Draw Calls Count");
if (!setPass.Valid) setPass = ProfilerRecorder.StartNew(ProfilerCategory.Render, "SetPass Calls Count");
}
/// <summary>몇 프레임 렌더 뒤 측정 → 로그 한 줄 + `ui_layers.csv`(캔버스별 행) 추가.</summary>
public static IEnumerator Measure(string shot, System.Action<string> log, string dir)
{
Begin();
for (int i = 0; i < 4; i++) yield return null;
long b = batches.Valid ? batches.LastValue : -1;
long d = draws.Valid ? draws.LastValue : -1;
long sp = setPass.Valid ? setPass.LastValue : -1;
#if UNITY_EDITOR
if (b <= 0) b = UnityEditor.UnityStats.batches;
if (d <= 0) d = UnityEditor.UnityStats.drawCalls;
if (sp <= 0) sp = UnityEditor.UnityStats.setPassCalls;
#endif
var movers = Movers();
var frameCanvases = new HashSet<Canvas>();
foreach (var m in movers)
{
var c = m.GetComponentInParent<Canvas>();
if (c != null) frameCanvases.Add(c);
}
var rows = new StringBuilder();
int roots = 0, nested = 0, graphics = 0, frameGraphics = 0;
foreach (var c in Object.FindObjectsByType<Canvas>(FindObjectsInactive.Exclude, FindObjectsSortMode.None))
{
if (!c.isActiveAndEnabled) continue;
int n = GraphicRegistry.GetGraphicsForCanvas(c).Count;
if (c.isRootCanvas) roots++;
else nested++;
graphics += n;
bool frame = frameCanvases.Contains(c);
if (frame) frameGraphics += n;
var tag = c.GetComponent<UiLayerInfo>();
rows.Append(shot).Append(',').Append(Path(c.transform)).Append(',').Append(c.isRootCanvas ? "root" : "nested").Append(',')
.Append(tag != null ? tag.rate.ToString() : "-").Append(',').Append(n).Append(',')
.Append(c.GetComponent<GraphicRaycaster>() != null ? "ray" : "-").Append(',').Append(frame ? "frame" : "-").Append('\n');
}
log("layers " + shot + " batches=" + b + " drawcalls=" + d + " setpass=" + sp + " canvases=" + roots + "+" + nested + " graphics=" + graphics + " frameRebuildGraphics=" + frameGraphics);
try
{
string csv = System.IO.Path.Combine(dir, "ui_layers.csv");
if (!File.Exists(csv)) File.WriteAllText(csv, "shot,canvas,kind,rate,graphics,raycaster,frame_mover\n", new UTF8Encoding(true));
File.AppendAllText(csv, rows.ToString(), new UTF8Encoding(false));
}
catch
{
}
}
/// <summary>매 프레임 바뀌는 요소(켜진 것만).</summary>
static List<Transform> Movers()
{
var list = new List<Transform>();
foreach (var p in Object.FindObjectsByType<OutGame.UiPulse>(FindObjectsInactive.Exclude, FindObjectsSortMode.None)) list.Add(p.transform);
foreach (var p in Object.FindObjectsByType<OutGame.UiPop>(FindObjectsInactive.Exclude, FindObjectsSortMode.None)) list.Add(p.transform);
foreach (var s in Object.FindObjectsByType<ScrollRect>(FindObjectsInactive.Exclude, FindObjectsSortMode.None))
if (s.content != null) list.Add(s.content);
foreach (var t in Object.FindObjectsByType<RectTransform>(FindObjectsInactive.Exclude, FindObjectsSortMode.None))
{
// 월드맵 커서(위아래 흔들림) · 전투 미니맵 시야 사각형(카메라 이동) · 유닛 점(이동)
if (t.name == "cursor") list.Add(t);
else if (t.name == "View" && t.parent != null && (t.parent.name == "Minimap" || t.parent.name == "MinimapLive")) list.Add(t);
}
return list;
}
static string Path(Transform t)
{
var s = t.name;
for (var p = t.parent; p != null; p = p.parent) s = p.name + "/" + s;
return s.Replace(',', ' ');
}
}
}

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: f9b7962726fbfdd4f91a918214ea088a

View File

@ -391,18 +391,11 @@ namespace TKS
} }
battle.ResultHandler = OnBattleResult; battle.ResultHandler = OnBattleResult;
// 캔버스 층(PD 2026-11-10): 루트 = 바탕 · 챕터 띠 · 넘김 · 본영 버튼(챕터를 바꿀 때만 · Static)
// → Content(맵 · 노드 · 스크롤로 움직임 · Frame) → 커서(매 프레임 흔들림 · Frame) · Popup(Event)
root = new GameObject("WorldMapCanvas"); root = new GameObject("WorldMapCanvas");
root.transform.SetParent(transform, false); root.transform.SetParent(transform, false);
var canvas = root.AddComponent<Canvas>(); UiLayer.Root(root, cam, 300, 0.5f, new Vector2(data.viewSize[0], data.viewSize[1]), UiRate.Static); // 카메라 캡처(자동 플레이)에도 찍힘
canvas.renderMode = RenderMode.ScreenSpaceCamera; // 카메라 캡처(자동 플레이)에도 찍히게
canvas.worldCamera = cam;
canvas.planeDistance = 0.5f;
canvas.sortingOrder = 300;
var scaler = root.AddComponent<CanvasScaler>();
scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
scaler.referenceResolution = new Vector2(data.viewSize[0], data.viewSize[1]);
scaler.matchWidthOrHeight = 0.5f;
root.AddComponent<GraphicRaycaster>();
group = root.AddComponent<CanvasGroup>(); group = root.AddComponent<CanvasGroup>();
// 스크롤(맵 = 콘텐츠 · 왼쪽 위 기준) // 스크롤(맵 = 콘텐츠 · 왼쪽 위 기준)
@ -411,6 +404,7 @@ namespace TKS
vpImg.color = new Color(0.1f, 0.12f, 0.1f); vpImg.color = new Color(0.1f, 0.12f, 0.1f);
viewport.gameObject.AddComponent<RectMask2D>(); viewport.gameObject.AddComponent<RectMask2D>();
content = Rect("Content", viewport, new Vector2(0f, 1f), new Vector2(0f, 1f), new Vector2(0f, 1f), Vector2.zero, new Vector2(data.mapSize[0], data.mapSize[1])); content = Rect("Content", viewport, new Vector2(0f, 1f), new Vector2(0f, 1f), new Vector2(0f, 1f), Vector2.zero, new Vector2(data.mapSize[0], data.mapSize[1]));
UiLayer.Nest(content.gameObject, UiRate.Frame, true); // 스크롤 내용 = 자기 캔버스(노드 = 누름)
mapImage = content.gameObject.AddComponent<Image>(); mapImage = content.gameObject.AddComponent<Image>();
mapImage.raycastTarget = true; // 맵 아무 데나 끌기 mapImage.raycastTarget = true; // 맵 아무 데나 끌기
scroll = viewport.gameObject.AddComponent<ScrollRect>(); scroll = viewport.gameObject.AddComponent<ScrollRect>();
@ -581,6 +575,8 @@ namespace TKS
{ {
cursor = cimg.rectTransform; cursor = cimg.rectTransform;
cursorBase = cursor.anchoredPosition; cursorBase = cursor.anchoredPosition;
// 커서 = 매 프레임 위아래 흔들림 → 자기 캔버스(맵·노드 전체를 매 프레임 다시 묶지 않게 · PD 2026-11-10)
UiLayer.Nest(cimg.gameObject, UiRate.Frame, false);
} }
} }
} }
@ -776,6 +772,7 @@ namespace TKS
dim.color = new Color(0f, 0f, 0f, 0.6f); dim.color = new Color(0f, 0f, 0f, 0.6f);
dim.raycastTarget = true; dim.raycastTarget = true;
popup = dim.gameObject; popup = dim.gameObject;
UiLayer.Nest(popup, UiRate.Event, true); // 스테이지 팝업 = 자기 캔버스(열 때·소탕 때만)
// 아웃게임 줄(군량 · 보상 · 별 · #1093)이 들어가도록 2026-11-05 패널을 키움(880×760 → 900×1080) // 아웃게임 줄(군량 · 보상 · 별 · #1093)이 들어가도록 2026-11-05 패널을 키움(880×760 → 900×1080)
var panel = Img("Panel", popup.transform, Sprite("plate"), new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(0f, 40f), new Vector2(900f, 1080f)); var panel = Img("Panel", popup.transform, Sprite("plate"), new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(0f, 40f), new Vector2(900f, 1080f));
panel.type = Image.Type.Sliced; panel.type = Image.Type.Sliced;
@ -850,7 +847,8 @@ namespace TKS
void Update() void Update()
{ {
if (cursor != null) cursor.anchoredPosition = cursorBase + new Vector2(0f, 10f * Mathf.Abs(Mathf.Sin(Time.unscaledTime * 4f))); if (cursor != null) cursor.anchoredPosition = cursorBase + new Vector2(0f, 10f * Mathf.Abs(Mathf.Sin(Time.unscaledTime * 4f)));
if (group != null) group.interactable = Time.unscaledTime >= inputBlockUntil; bool interact = Time.unscaledTime >= inputBlockUntil;
if (group != null && group.interactable != interact) group.interactable = interact; // 바뀔 때만(버튼 색 전환 → 재구성 방지)
} }
static Vector2 V(float[] a) static Vector2 V(float[] a)

View File

@ -0,0 +1,174 @@
using System.Collections;
using System.Collections.Generic;
using System.IO;
using NUnit.Framework;
using TKS.OutGame;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.SceneManagement;
using UnityEngine.TestTools;
using UnityEngine.UI;
namespace TKS.Tests
{
/// <summary>
/// UI 캔버스 층 · 아틀라스 구조(PD 2026-11-10 「리소스·정보가 얼마나 자주 바뀌는지에 따라 캔버스를 나누어 최적화 · 드로우콜 최소화 구조 필수」) —
/// ① 중첩 캔버스 = overrideSorting 끔 + 등급 표시 ② 아웃게임 화면마다 자기 캔버스 · 스크롤 내용 = Frame ③ 매 프레임 요소(호흡·튀김·커서·미니맵)는 Frame 캔버스 안
/// ④ 중첩 캔버스 위 누름이 그대로 동작하고 팝업이 아래 화면을 가림 ⑤ 테마 스프라이트(큰 그림 제외)는 아틀라스로 묶임 · 월드 9-slice 사본이 아틀라스 자리에서 잘림.
/// 기준값(큰 그림 넓이)은 표 Assets/Data/UI/ui_atlas.csv 에서 읽는다(C45).
/// </summary>
public class UILayerTests
{
[TearDown]
public void TearDown()
{
SaveStore.UseMemory(false);
}
static void AssertNested(Canvas c, UiRate rate, string what)
{
Assert.IsNotNull(c, what + ": 캔버스 없음");
// 꺼진 캔버스는 isRootCanvas = true 로 보고되므로 부모 사슬로 판정
Assert.IsNotNull(c.transform.parent != null ? c.transform.parent.GetComponentInParent<Canvas>(true) : null, what + ": 중첩이어야 함");
Assert.IsFalse(c.overrideSorting, what + ": overrideSorting 은 끈다(순서·자르기 유지)");
var tag = c.GetComponent<UiLayerInfo>();
Assert.IsNotNull(tag, what + ": 등급 표시 없음");
Assert.AreEqual(rate, tag.rate, what + ": 등급");
}
/// <summary>t 가 그리는 캔버스(가장 가까운 것)가 Frame 등급인가.</summary>
static void AssertInFrame(Transform t, string what)
{
var c = t.GetComponentInParent<Canvas>(true);
Assert.IsNotNull(c, what);
var tag = c.GetComponent<UiLayerInfo>();
Assert.IsTrue(tag != null && tag.rate == UiRate.Frame, what + ": 매 프레임 요소가 Frame 캔버스 밖(" + c.name + ")");
}
static GameObject TopHit(Vector2 screen)
{
var hits = new List<RaycastResult>();
EventSystem.current.RaycastAll(new PointerEventData(EventSystem.current) { position = screen }, hits);
return hits.Count > 0 ? hits[0].gameObject : null;
}
[UnityTest]
public IEnumerator Canvases_SplitByUpdateRate_InputAndAtlas()
{
WorldProgress.Reset();
SaveStore.UseMemory(true);
SceneManager.LoadScene("Battle");
yield return null;
yield return null;
var og = OutGameRoot.Instance;
Assert.IsNotNull(og);
// ② 화면마다 자기 캔버스(Event · 누름 있음)
foreach (var scr in new OutGameScreen[] { og.Lobby, og.HeroList, og.HeroDetail, og.Formation, og.Trade, og.Domestic, og.Treasures, og.Arena })
{
AssertNested(scr.Rt.GetComponent<Canvas>(), UiRate.Event, scr.GetType().Name);
Assert.IsNotNull(scr.Rt.GetComponent<GraphicRaycaster>(), scr.GetType().Name + ": 레이캐스터");
}
og.Show(og.HeroList);
yield return null;
yield return null;
var scrolls = og.HeroList.Rt.GetComponentsInChildren<ScrollRect>(true);
Assert.Greater(scrolls.Length, 0, "영웅 목록 스크롤");
foreach (var sr in scrolls) AssertNested(sr.content.GetComponent<Canvas>(), UiRate.Frame, "스크롤 내용 " + sr.name);
// ④ 중첩 캔버스(스크롤 내용) 위 카드 누름이 맞고, 팝업을 띄우면 같은 점이 팝업에 맞는다
var content = scrolls[0].content;
Selectable card = null;
foreach (var s in content.GetComponentsInChildren<Selectable>()) { card = s; break; }
Component handler = card;
if (handler == null) handler = content.GetComponentInChildren<TapDrag>();
Assert.IsNotNull(handler, "영웅 카드(누름) 없음");
var crt = (RectTransform)handler.transform;
var screen = RectTransformUtility.WorldToScreenPoint(og.Cam, crt.TransformPoint(crt.rect.center));
var hit = TopHit(screen);
Assert.IsNotNull(hit, "카드 위 레이캐스트 결과 없음");
Assert.IsTrue(hit.transform.IsChildOf(content), "카드 위 첫 결과가 스크롤 내용 밖: " + hit.name);
og.Modal("검사", "팝업", new OutGameRoot.ModalButton("확인", Color.gray, null));
yield return null;
hit = TopHit(screen);
Assert.IsNotNull(hit);
Assert.IsTrue(hit.transform.IsChildOf(og.TopRoot), "팝업이 아래 화면 누름을 가려야 함: " + hit.name);
og.CloseModal();
// ③ 아웃게임의 매 프레임 요소
og.Show(og.Treasures);
og.Treasures.SetPage(2);
yield return null;
foreach (var p in Object.FindObjectsByType<UiPulse>(FindObjectsInactive.Exclude, FindObjectsSortMode.None)) AssertInFrame(p.transform, "호흡 " + p.name);
// 월드맵: 스크롤 내용 = Frame · 커서 = 자기 Frame 캔버스 · 팝업 = Event
var wm = og.Map;
og.OpenWorldMap();
yield return null;
yield return null;
var cursor = GameObject.Find("cursor");
if (cursor != null)
{
AssertNested(cursor.GetComponent<Canvas>(), UiRate.Frame, "월드맵 커서");
AssertNested(cursor.transform.parent.GetComponent<Canvas>(), UiRate.Frame, "월드맵 스크롤 내용");
}
wm.OpenPopup(0);
yield return null;
AssertNested(GameObject.Find("WorldMapCanvas/Popup").GetComponent<Canvas>(), UiRate.Event, "월드맵 팝업");
// 전투 HUD: 루트 = Static · Info = Event · 미니맵 실시간 = Frame · 결과 = Event
wm.ConfirmPopup();
var bm = BattleManager.Instance;
float t0 = Time.realtimeSinceStartup;
while (bm.Phase != BattlePhase.PlayerTurn && Time.realtimeSinceStartup - t0 < 10f) yield return null;
var hud = bm.Hud.transform;
Assert.AreEqual(UiRate.Static, hud.GetComponent<UiLayerInfo>().rate, "HUD 루트 = 정적 판");
AssertNested(hud.Find("Info").GetComponent<Canvas>(), UiRate.Event, "HUD Info");
AssertNested(hud.Find("Result").GetComponent<Canvas>(), UiRate.Event, "HUD 결과");
var live = hud.Find("Minimap/MinimapLive");
Assert.IsNotNull(live, "미니맵 실시간 층");
AssertNested(live.GetComponent<Canvas>(), UiRate.Frame, "미니맵 실시간");
AssertInFrame(live.Find("View"), "미니맵 시야");
foreach (var p in hud.GetComponentsInChildren<UiPulse>(true)) AssertInFrame(p.transform, "HUD 호흡 " + p.name);
Assert.AreEqual(hud.childCount - 1, hud.Find("Result").GetSiblingIndex(), "결과 층 = 맨 위");
Assert.AreEqual(hud.childCount - 2, hud.Find("Info").GetSiblingIndex(), "Info 층 = 정적 판 위");
// ⑤ 아틀라스 — 테마 스프라이트(큰 그림 제외)는 묶임 · 월드 9-slice 사본 = 같은 크기
long single = SingleArea();
int checkedN = 0;
var theme = UITheme.Current;
Assert.IsNotNull(theme);
var sprites = new List<Sprite>();
foreach (var s in theme.skins) sprites.Add(s.Sprite);
foreach (var i in theme.icons) sprites.Add(i.Sprite);
foreach (var sp in sprites)
{
if (sp == null || (long)(sp.rect.width * sp.rect.height) > single) continue;
Assert.IsTrue(sp.packed, "아틀라스에 안 묶임: " + sp.name + " — 메뉴 TKS/Build UI Atlases");
checkedN++;
}
Assert.Greater(checkedN, 0);
foreach (var id in new[] { "hp_bg", "hp_fill", "pip" })
{
var src = UISkin.Theme.Skin(id);
if (src == null) continue;
var ws = UISkin.WorldSprite(id, 100f);
Assert.AreEqual(src.Sprite.rect.size, ws.rect.size, "월드 9-slice 사본 크기(" + id + ")");
}
}
static long SingleArea()
{
var path = Path.Combine(Application.dataPath, "Data/UI/ui_atlas.csv");
Assert.IsTrue(File.Exists(path), path);
foreach (var line in File.ReadAllLines(path))
{
var f = line.Split(',');
long v;
if (f.Length >= 4 && f[0].Trim() == "option" && f[1].Trim() == "single_area" && long.TryParse(f[3].Trim(), out v)) return v;
}
Assert.Fail("ui_atlas.csv 에 single_area 없음");
return 0;
}
}
}

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: f7d9ae2b442dcd74b8fe04a0bd78d256

8
Assets/UI/Atlases.meta Normal file
View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: e3d6a5b700e048e4fb083275b97f278f
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,216 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!612988286 &1
SpriteAtlasAsset:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name:
serializedVersion: 2
m_MasterAtlas: {fileID: 0}
m_ImporterData:
packables:
- {fileID: 2800000, guid: 33800ea627b19439fa59fbd2667c8eda, type: 3}
- {fileID: 2800000, guid: 3eba92f7629b145ae9366b196000a3f3, type: 3}
- {fileID: 2800000, guid: b802653e90c694dd0966490b86866a22, type: 3}
- {fileID: 2800000, guid: 6cdadcc1a39954748abf7d3d4351e1e6, type: 3}
- {fileID: 2800000, guid: 5dc9d06d37e614ecdbcaa9ec360e5100, type: 3}
- {fileID: 2800000, guid: 2d734a17e89824d30b1f014a9c1f5f60, type: 3}
- {fileID: 2800000, guid: 6ccda849190f74a499129a46716088b3, type: 3}
- {fileID: 2800000, guid: 9f1de124832454aaa89e2816cf5bd1e9, type: 3}
- {fileID: 2800000, guid: 615459ca724384003a91b93533033ab1, type: 3}
- {fileID: 2800000, guid: 1047c0bcbda25440faf8332b872b3529, type: 3}
- {fileID: 2800000, guid: c8638447d0c4a40fd9a7e32c25783d96, type: 3}
- {fileID: 2800000, guid: 7c7fecbb8a1f449a3aac1543a0fef8ef, type: 3}
- {fileID: 2800000, guid: 13bb59864f8ee4bd0948e2f4bebb30dd, type: 3}
- {fileID: 2800000, guid: ac729c6a63c5c46578bef27ddfc3c0fe, type: 3}
- {fileID: 2800000, guid: 55518d36418274014bfad80756376cc9, type: 3}
- {fileID: 2800000, guid: ee30b302c44c546158237019ae729a1b, type: 3}
- {fileID: 2800000, guid: 1890af01f2abc49128fb21bde6734a57, type: 3}
- {fileID: 2800000, guid: 7c529ef2aff4e4be290f9a9fc303572d, type: 3}
- {fileID: 2800000, guid: 061d65a86429c4eafa1b8edcca19d6f0, type: 3}
- {fileID: 2800000, guid: 461f4fe6d783646e993c8a7f39d054eb, type: 3}
- {fileID: 2800000, guid: 03cc62a06f5fc44639b0a7ec241ee65f, type: 3}
- {fileID: 2800000, guid: 419b7c003126040348a9fb62c5e2964c, type: 3}
- {fileID: 2800000, guid: 8b2a2f8e287cc41e4860d17e32352dbc, type: 3}
- {fileID: 2800000, guid: b75949b3a23a049fb814d9cb21aa2c39, type: 3}
- {fileID: 2800000, guid: 0386c61d06bee46feb8e6bca9882cb40, type: 3}
- {fileID: 2800000, guid: 0311dcd69620e47f4b965eb345ef9b35, type: 3}
- {fileID: 2800000, guid: d8a3e7288e03247ec8f56796382ec1a1, type: 3}
- {fileID: 2800000, guid: ae11cdefb6cd4436393f83b724ceb984, type: 3}
- {fileID: 2800000, guid: 6c9211bc1d90b4e35a3a080121e93d05, type: 3}
- {fileID: 2800000, guid: b31cc26b044a54db88302afadc39ed11, type: 3}
- {fileID: 2800000, guid: 5a25a392cf93544beb9c3d76420b92ef, type: 3}
- {fileID: 2800000, guid: 555b142732e774962897f672b89e14a9, type: 3}
- {fileID: 2800000, guid: ca6ab62179a66442588aacd94b454ffe, type: 3}
- {fileID: 2800000, guid: b625cd3838cc0477ca073199439588b5, type: 3}
- {fileID: 2800000, guid: 0f8c944da644b42e09c7342aa5735ff9, type: 3}
- {fileID: 2800000, guid: 6f23dee49868b4ee4bb927e2eebe0f5a, type: 3}
- {fileID: 2800000, guid: 710870570ec23411badc9e568d71671a, type: 3}
- {fileID: 2800000, guid: fc03017da3bbc4b8aa7267af8a565159, type: 3}
- {fileID: 2800000, guid: 3070a6f23e6ca4273897acf1b7e8c5ce, type: 3}
- {fileID: 2800000, guid: d332162b7f97a44c8968e3570e42e325, type: 3}
- {fileID: 2800000, guid: b982a0ca466374cd6a05c60234ce9d64, type: 3}
- {fileID: 2800000, guid: a263718a226e54b009c0cfc63e646872, type: 3}
- {fileID: 2800000, guid: eebd5d88ad8c94308a78cffcd517b736, type: 3}
- {fileID: 2800000, guid: ac1a5e19ad4b94781b52ca72c7602efe, type: 3}
- {fileID: 2800000, guid: 5e915d0bf336547b99658208e2770e34, type: 3}
- {fileID: 2800000, guid: 269983f7991d743e8bdec3212c98d9d7, type: 3}
- {fileID: 2800000, guid: 7a68a4faa81064e6c8ca27a8f6673baf, type: 3}
- {fileID: 2800000, guid: 12e2f59e45c9e4f6280b38d49ef68605, type: 3}
- {fileID: 2800000, guid: 74ae39be6091048c7a16b3cd8712ce2c, type: 3}
- {fileID: 2800000, guid: 6cd33b9fcf759447fa159757f293591b, type: 3}
- {fileID: 2800000, guid: 02d16dbf98f24414b9a2de2e2eb827c4, type: 3}
- {fileID: 2800000, guid: e2c03cb5e0e9b42f398207ee0e3bf743, type: 3}
- {fileID: 2800000, guid: c43b3cadd09cc4cfb831f398553411b9, type: 3}
- {fileID: 2800000, guid: 81b7d5aee59e5420b9baa91a05eb781f, type: 3}
- {fileID: 2800000, guid: d64c8ca24c87d427a91c4267854cb81c, type: 3}
- {fileID: 2800000, guid: 7d6c9feeaf8654469bb396af2537650a, type: 3}
- {fileID: 2800000, guid: eed4e49fc1a0344219abdd134bcbfe69, type: 3}
- {fileID: 2800000, guid: d7ee2d029b1dc4b78bfcf9b62318c3f3, type: 3}
- {fileID: 2800000, guid: d3ed2cc3ba43a449ca414493c0c2b8ae, type: 3}
- {fileID: 2800000, guid: 63128e5af0cdc46bfbd4a90aac012629, type: 3}
- {fileID: 2800000, guid: 9f5976efc00a04bbf9d3a01ac0219c63, type: 3}
- {fileID: 2800000, guid: f0bdcf4700e3143dd941fff695e8c391, type: 3}
- {fileID: 2800000, guid: 7878a4afed4e6465c9785efcd828aaec, type: 3}
- {fileID: 2800000, guid: e628dc11848744b1c8b2cf768e50aaeb, type: 3}
- {fileID: 2800000, guid: bf3e36f58b7084484bf9b2cf2fb6f142, type: 3}
- {fileID: 2800000, guid: 33f66db08268747a79612facacdfc2e9, type: 3}
- {fileID: 2800000, guid: ab3a1764a033d4c09a5892a64ddc8c78, type: 3}
- {fileID: 2800000, guid: 5cf538fcf439a49d48fcf2995f950403, type: 3}
- {fileID: 2800000, guid: 39afaf3c0b490422ea5f07aac1dcfbe1, type: 3}
- {fileID: 2800000, guid: 26459d86a13194dc1ab0997e73d7c594, type: 3}
- {fileID: 2800000, guid: 9987152c36a3b4365ba46f7be2a44777, type: 3}
- {fileID: 2800000, guid: 84c3adc90c0da4f009a1cfa76fa9363c, type: 3}
- {fileID: 2800000, guid: a9d09d9a3d0d442079a130341db6be90, type: 3}
- {fileID: 2800000, guid: a3b88d8ffc0ed43a9877519fd3d30d6d, type: 3}
- {fileID: 2800000, guid: f71c132174347405a9d3d63db9df3c0f, type: 3}
- {fileID: 2800000, guid: 30dce27e256a74aa797da5e70d1c9a08, type: 3}
- {fileID: 2800000, guid: 5368592eacfda44c191265e7475562df, type: 3}
- {fileID: 2800000, guid: 598c081bd6eaf40bfba9db8c9e2862a9, type: 3}
- {fileID: 2800000, guid: c7c98a2b50d944d2d933ad3c1f60ad39, type: 3}
- {fileID: 2800000, guid: f1a76f95358d94ca080a3e48691f8e5f, type: 3}
- {fileID: 2800000, guid: 64a42696c813049fd9c00923ae5842df, type: 3}
- {fileID: 2800000, guid: 3089bc9c6e35e4d94a510e57248dd925, type: 3}
- {fileID: 2800000, guid: 23684c39a5232480abc423f4c39c550d, type: 3}
- {fileID: 2800000, guid: a52e1372e66954bcf8dfac5f52044251, type: 3}
- {fileID: 2800000, guid: e90cf50716ce544d2bfd86da7ed7eb08, type: 3}
- {fileID: 2800000, guid: 7a496f5f80b1c41d59f7bd0fc631cdb7, type: 3}
- {fileID: 2800000, guid: 899da40fa9ae1462c8e7e40a89da5ca1, type: 3}
- {fileID: 2800000, guid: 4864baeee50764eea909c546760b7c64, type: 3}
- {fileID: 2800000, guid: 09135c198f8744fcd89493b169d11bd8, type: 3}
- {fileID: 2800000, guid: d228604ab6373411fac2dbf1e0e215d1, type: 3}
- {fileID: 2800000, guid: 9fdf2423376c44500a69de40dd83ef49, type: 3}
- {fileID: 2800000, guid: 578b76dca0c9c42f4ad7ee4a79575197, type: 3}
- {fileID: 2800000, guid: 29285b88e4def463f86ebf34e5eab9c4, type: 3}
- {fileID: 2800000, guid: 8ae43bb338f6c4486b21b6220a1cac9d, type: 3}
- {fileID: 2800000, guid: 4b3a9b4d9f2ed4a92996c71af3ba0c42, type: 3}
- {fileID: 2800000, guid: 65172c8f38641400dbb8dea2f06a5fcd, type: 3}
- {fileID: 2800000, guid: 7f4622e1fadad4b7680182e7309efca3, type: 3}
- {fileID: 2800000, guid: cbe57c0243cf5440e9934cffa4681019, type: 3}
- {fileID: 2800000, guid: 1054306ffb8884ea8a2e142d780c85cb, type: 3}
- {fileID: 2800000, guid: 0e3e88c80644941acab7b5bfc3d02d78, type: 3}
- {fileID: 2800000, guid: 7f272d61d71ea41479651113f8ec2456, type: 3}
- {fileID: 2800000, guid: 0127e486ba6e6408ba9139509d699e4f, type: 3}
- {fileID: 2800000, guid: 21efa6791000e47f2b2b3d493c5d34f0, type: 3}
- {fileID: 2800000, guid: c0c390b014f3f4b0781cfca1e2ebe6f0, type: 3}
- {fileID: 2800000, guid: ad90f1e81df3442d3b8fc40036d24cd3, type: 3}
- {fileID: 2800000, guid: 3654b5c84fc0440f786a4918b576d3bb, type: 3}
- {fileID: 2800000, guid: 816d14bb0d767435585a0bb55886aaef, type: 3}
- {fileID: 2800000, guid: b24f4bd4b10df44d486a526496d3215b, type: 3}
- {fileID: 2800000, guid: 6fcf7bcb2b29e4b589e82ab74f31c79e, type: 3}
- {fileID: 2800000, guid: 1eb0217bf1f784956bbb96cf1663c557, type: 3}
- {fileID: 2800000, guid: add005f288b2e43b7a4772c8f316c598, type: 3}
- {fileID: 2800000, guid: 924e36c4781e44055a9d539c1160fce2, type: 3}
- {fileID: 2800000, guid: 6200d1a117bb34c21ae7b0265c0027c3, type: 3}
- {fileID: 2800000, guid: 244985ea9313545a38c1aec6679972a9, type: 3}
- {fileID: 2800000, guid: 16edef63d3f7646d9bcf5f86b0e476ec, type: 3}
- {fileID: 2800000, guid: 6add04477e44d48508df9a7dabad0134, type: 3}
- {fileID: 2800000, guid: 7ac6a27f0a991414f99328df090ad855, type: 3}
- {fileID: 2800000, guid: 3d059bda0f1cb4181a080c8258995348, type: 3}
- {fileID: 2800000, guid: d3112ce909bbc4504bdadf79eb35726d, type: 3}
- {fileID: 2800000, guid: e0df45813c53b41d386a33c49657561d, type: 3}
- {fileID: 2800000, guid: cd6dc5dd342ab486b96c3d3e666c6bb9, type: 3}
- {fileID: 2800000, guid: e63f49ada5fda4cc59b8b5080feeb0b7, type: 3}
- {fileID: 2800000, guid: 304fa5e62cade424d9ae15c73cd20a12, type: 3}
- {fileID: 2800000, guid: acfcced52c2a44ec78ff53f2b51971b2, type: 3}
- {fileID: 2800000, guid: e9be1374ca9ae47fdb7208668217021c, type: 3}
- {fileID: 2800000, guid: 77ef9d3b0b12a4a4d954bfce8b142152, type: 3}
- {fileID: 2800000, guid: e0387aa3821274a35b00020e1848abc5, type: 3}
- {fileID: 2800000, guid: 0eaf6715a248844b3b94a9d322d282c4, type: 3}
- {fileID: 2800000, guid: dad47e9b3f72d4253bd4959b57289345, type: 3}
- {fileID: 2800000, guid: fb3e8ff77824f466aa94f46ebe089001, type: 3}
- {fileID: 2800000, guid: 8440d003b569841998e16e31f70f9b28, type: 3}
- {fileID: 2800000, guid: d96f1c6be6b8d42e7a8e1d30ba2f963f, type: 3}
- {fileID: 2800000, guid: f1b870f1b460a4adf85d57ecc60ba7ca, type: 3}
- {fileID: 2800000, guid: 878668f672fc94e3b92df41daa6fb966, type: 3}
- {fileID: 2800000, guid: 6c3ea799aad7c48919b36b752f77ead4, type: 3}
- {fileID: 2800000, guid: 0c18e637bc71d4fc887d62f484000575, type: 3}
- {fileID: 2800000, guid: 4705dc388c8e646b08b96ac581e8e1ae, type: 3}
- {fileID: 2800000, guid: 46d72fa068f1c473cacf5009c97a9b9c, type: 3}
- {fileID: 2800000, guid: cb5f2999723b840f1899e5e01cbba819, type: 3}
- {fileID: 2800000, guid: 205b81059899941c9a9596378eea8d34, type: 3}
- {fileID: 2800000, guid: 2495840c8944f4f05b1d8aa2a409ad7c, type: 3}
- {fileID: 2800000, guid: f902adce59d3b4e8f993d11ba251e7d2, type: 3}
- {fileID: 2800000, guid: 2bd06dd44f28c41ebac87b908313e284, type: 3}
- {fileID: 2800000, guid: 620879c9ab62c447b82f0c54e1e88fee, type: 3}
- {fileID: 2800000, guid: ab01a2531c35a4aafb31413b44dd1bcb, type: 3}
- {fileID: 2800000, guid: 6332433fb34294d58bfc835736ee8ac3, type: 3}
- {fileID: 2800000, guid: 3fceb536486e040c99fac1bb7f353cf6, type: 3}
- {fileID: 2800000, guid: 1dfa4588cbf5d4c8eb9ecaf11c55c88b, type: 3}
- {fileID: 2800000, guid: 3c166fa4e3ee74616b471b7c2332ab9b, type: 3}
- {fileID: 2800000, guid: 26d89dd2d7ae148e4acefd7fb973eb82, type: 3}
- {fileID: 2800000, guid: 4a78cd68f25904d5caedf86f6b56f88b, type: 3}
- {fileID: 2800000, guid: b636bad4f3b654c089e14addd5b292d2, type: 3}
- {fileID: 2800000, guid: e370be6ad4a764625bf14603bdf952bf, type: 3}
- {fileID: 2800000, guid: 9fa652a2cd39b4cfcaf06285cfff1483, type: 3}
- {fileID: 2800000, guid: 9dea6512a45784ff7b2a5bbdfa996d16, type: 3}
- {fileID: 2800000, guid: fe62dbba2502e4766a8150113c51bef0, type: 3}
- {fileID: 2800000, guid: 9209a152c6b45453e9b98b6a1f1f2084, type: 3}
- {fileID: 2800000, guid: 83aedc4dd182a4b859ab60b44189cda6, type: 3}
- {fileID: 2800000, guid: 16129dca57af842019e4005a4b102930, type: 3}
- {fileID: 2800000, guid: ceb74eb7e05304fea85ed30d56e78b43, type: 3}
- {fileID: 2800000, guid: e451938e636764f5884b742b961d9ba8, type: 3}
- {fileID: 2800000, guid: 02dc52ac5e8b84189b58ae5a14baa3b5, type: 3}
- {fileID: 2800000, guid: 77db17572c3694c38812470f80f4009b, type: 3}
- {fileID: 2800000, guid: a707b1e1252f042c58519d2882ee2651, type: 3}
- {fileID: 2800000, guid: c75fde6c7cc1847fcbe1fac91002ce1f, type: 3}
- {fileID: 2800000, guid: 405e1d64d87794698b9e8e1f484bfa2c, type: 3}
- {fileID: 2800000, guid: 50cd2b7880d1c4d1db66bd1a8f2e0d63, type: 3}
- {fileID: 2800000, guid: 57cd551b771421f4789c56a45c1aae46, type: 3}
- {fileID: 2800000, guid: c9b3f5b0e3b32fb45a9da936effb6167, type: 3}
- {fileID: 2800000, guid: 15298a0a76f52b947a09f6599309ad4b, type: 3}
- {fileID: 2800000, guid: 11de9e7c5a4cc0e4b84ab9cf91289e82, type: 3}
- {fileID: 2800000, guid: 85b9bd6df9b5b3848b056923e4c23336, type: 3}
- {fileID: 2800000, guid: e22457f3b92a44b40b6a7eeaf429903f, type: 3}
- {fileID: 2800000, guid: 942148d29807dcc40962b4f8fa1d4bec, type: 3}
- {fileID: 2800000, guid: 4adb5f83a450f1e4fbb8ca18784e4667, type: 3}
- {fileID: 2800000, guid: c1651f66d7ebc1a41800dadf075b6ed9, type: 3}
- {fileID: 2800000, guid: 32f85565e8b7a1344aabba239537fc17, type: 3}
- {fileID: 2800000, guid: 61c36c09211a0b34ca06fdbadaf1f964, type: 3}
- {fileID: 2800000, guid: 3bb26b9e5fda01a4f82f328bc0093baa, type: 3}
- {fileID: 2800000, guid: eb396bdf579952f49be3d6144ebf7a16, type: 3}
- {fileID: 2800000, guid: 13f7d098c72b722409320e5e6631bfe2, type: 3}
- {fileID: 2800000, guid: 7ea66fd4d34772748875511032d773e3, type: 3}
- {fileID: 2800000, guid: 2ff8381ec78b5ae4b9f9158111fe6fdc, type: 3}
- {fileID: 2800000, guid: 7703f9c4343c53843b43fca9b982bfaf, type: 3}
- {fileID: 2800000, guid: aea4d32da9d6a6b43b631243c524840c, type: 3}
- {fileID: 2800000, guid: def91387a43563446bec42d3bb438eec, type: 3}
- {fileID: 2800000, guid: 31aa975020e0e474da35b078de813def, type: 3}
- {fileID: 2800000, guid: 1f01d9a0db8614e4e8a3989c563a8be8, type: 3}
- {fileID: 2800000, guid: fec84f890be1ae147a6a5d53137864cf, type: 3}
- {fileID: 2800000, guid: 3ff8dfd59098d134a91324c1d12ca940, type: 3}
- {fileID: 2800000, guid: 01b2bde420ac75544ba02cc15338bf2b, type: 3}
- {fileID: 2800000, guid: 07a3e2313e15f07419355ff190ae17a4, type: 3}
- {fileID: 2800000, guid: ac72c3f7f752f5f4ea6a1e1d66059371, type: 3}
- {fileID: 2800000, guid: 84d9b1e4766bab54caca17b6472e83d6, type: 3}
- {fileID: 2800000, guid: 2bf1928eef531944fae5ecbec0dd0889, type: 3}
- {fileID: 2800000, guid: ac90ec49867d6f248966a239bbba65d8, type: 3}
- {fileID: 2800000, guid: 9238470c5d76e8c499fd2ebf8a526054, type: 3}
- {fileID: 2800000, guid: 3ca6b69da9089eb4aabb4aa5fae10ecf, type: 3}
- {fileID: 2800000, guid: 45255881d251469429f5a85c7d135441, type: 3}
- {fileID: 2800000, guid: 63a264aa26e829749a8114cd7e10a22f, type: 3}
- {fileID: 2800000, guid: 63fd1be34f67f92488e14e322a34df12, type: 3}
m_IsVariant: 0
m_ScriptablePacker: {fileID: 0}

View File

@ -0,0 +1,43 @@
fileFormatVersion: 2
guid: 34850ecd203f4ca4da803a893e7579c1
SpriteAtlasImporter:
externalObjects: {}
textureSettings:
serializedVersion: 2
anisoLevel: 0
compressionQuality: 0
maxTextureSize: 0
textureCompression: 0
filterMode: 1
generateMipMaps: 0
readable: 0
crunchedCompression: 0
sRGB: 1
platformSettings:
- serializedVersion: 4
buildTarget: DefaultTexturePlatform
maxTextureSize: 4096
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
packingSettings:
serializedVersion: 2
padding: 4
blockOffset: 1
allowAlphaSplitting: 0
enableRotation: 0
enableTightPacking: 0
enableAlphaDilation: 1
secondaryTextureSettings: {}
variantMultiplier: 1
bindAsDefault: 1
userData: 3276345e8a3575e0937012acc03ac512
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,78 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!612988286 &1
SpriteAtlasAsset:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name:
serializedVersion: 2
m_MasterAtlas: {fileID: 0}
m_ImporterData:
packables:
- {fileID: 2800000, guid: b23fb0df2c4474124bcec7b152ff27d9, type: 3}
- {fileID: 2800000, guid: a6832029756c14e05a79241b3ab8bfd5, type: 3}
- {fileID: 2800000, guid: d031f4b21493c4841914d4257119a4e3, type: 3}
- {fileID: 2800000, guid: 4ea16497cec35499e8fcb6e48452aa29, type: 3}
- {fileID: 2800000, guid: c16591869b2474a649dac6761e3e4b7e, type: 3}
- {fileID: 2800000, guid: 05b4c15e805fa4156812361f1797f526, type: 3}
- {fileID: 2800000, guid: 145874fd002594d928ac4f1cc9046b3d, type: 3}
- {fileID: 2800000, guid: 9de9bb779c7694b08bf5aabc148b1dd3, type: 3}
- {fileID: 2800000, guid: 52bd6c820cae24fbbbb577594c4cb085, type: 3}
- {fileID: 2800000, guid: fd7a2020060844292b286576606000ab, type: 3}
- {fileID: 2800000, guid: 5d49c0aac30764cde93957570569f245, type: 3}
- {fileID: 2800000, guid: 5864b32a142be4809ab2ccc225cae6bf, type: 3}
- {fileID: 2800000, guid: d3aad013ac9a74286a3cf57df76f510f, type: 3}
- {fileID: 2800000, guid: 3a3a5d970e2d94998818421e4e0e384b, type: 3}
- {fileID: 2800000, guid: e07c16cced13542f59c7400aff52d8a7, type: 3}
- {fileID: 2800000, guid: 41bc376691e7a4aef8398c3f84ee249f, type: 3}
- {fileID: 2800000, guid: 42854c0d5a9654da395ed40f0e7443ed, type: 3}
- {fileID: 2800000, guid: ae898e04b74ec47d8bb33582fcf24b12, type: 3}
- {fileID: 2800000, guid: 36c378f3ccf8c40fe8c6a3ae69624bae, type: 3}
- {fileID: 2800000, guid: 3cadefdd2c8ef4523bd35390bab0359e, type: 3}
- {fileID: 2800000, guid: 6d5687fb0dd3a428d9cd26a6f36c6d78, type: 3}
- {fileID: 2800000, guid: 67f5e0fdd0f6b4952a22b99269db6096, type: 3}
- {fileID: 2800000, guid: 99225861569984f349a535e76bfe9902, type: 3}
- {fileID: 2800000, guid: a480ffcca13b444cc828ffb178e72a88, type: 3}
- {fileID: 2800000, guid: 1ca4b50341717411f9ad901950edfc2b, type: 3}
- {fileID: 2800000, guid: 33f163c49b488460ea0182825ae1600e, type: 3}
- {fileID: 2800000, guid: 430d11e34d7ee5d479e424256b0165a9, type: 3}
- {fileID: 2800000, guid: 2a532d03a4dd3fa4bb6b63906ce49d53, type: 3}
- {fileID: 2800000, guid: e8b0a1f06e4a358448aeb6ac70971a0b, type: 3}
- {fileID: 2800000, guid: cb230e43ffd8c61468a2a5d1ec8f5151, type: 3}
- {fileID: 2800000, guid: 4dbbde299a6531549bf27381e422b96f, type: 3}
- {fileID: 2800000, guid: 58a20249710eb2a4c8442195dc7d9414, type: 3}
- {fileID: 2800000, guid: 2ad7dd51bc9f78146ba545574d60415d, type: 3}
- {fileID: 2800000, guid: e9a0cb40a9e14eb43aaf127c5fed984d, type: 3}
- {fileID: 2800000, guid: 4cdabc30ba63e3a469af26e60948c3cf, type: 3}
- {fileID: 2800000, guid: 3b2cf83d106f14d48ab44097bf590bec, type: 3}
- {fileID: 2800000, guid: f7ac87372208c4b46a3dc2b88f58f014, type: 3}
- {fileID: 2800000, guid: 6b7327a0591d3e942ad0c960f574efa7, type: 3}
- {fileID: 2800000, guid: 424391add0732cd47ab55e20eecc7606, type: 3}
- {fileID: 2800000, guid: cce0e14fea438294ea1c862ec1d64428, type: 3}
- {fileID: 2800000, guid: b1d84d8f12e10a6428eb751b9347f552, type: 3}
- {fileID: 2800000, guid: 8e6922f1a1452f0418950cd2fa74f7ae, type: 3}
- {fileID: 2800000, guid: 9400588e732bdf948a9b1031fe25f0bb, type: 3}
- {fileID: 2800000, guid: e98e4d7469e4f354cb134be52733e5d3, type: 3}
- {fileID: 2800000, guid: 8e5bf7eb1e300474785ff2781eeb5093, type: 3}
- {fileID: 2800000, guid: 0da5111d9d1d0f747b33c24f5d8ca17b, type: 3}
- {fileID: 2800000, guid: c38aad50dddf3f346ac3b671b0021953, type: 3}
- {fileID: 2800000, guid: 4d49173384dd7a147b789981a43f04c8, type: 3}
- {fileID: 2800000, guid: 4d7e25c02de23b5428ad10bd9ecec4b4, type: 3}
- {fileID: 2800000, guid: 492c89150f4884f4f95c8e4f6d97a509, type: 3}
- {fileID: 2800000, guid: a253cd46d78d9fb42a64355d249b1302, type: 3}
- {fileID: 2800000, guid: b094dcc1ba5700243ba2ff34d0a9e8b8, type: 3}
- {fileID: 2800000, guid: a5a5746389108a548846a71feb16b31b, type: 3}
- {fileID: 2800000, guid: 172a55e973b980447b2bcb592c63d2d5, type: 3}
- {fileID: 2800000, guid: ca26ada810375114ebfede22244a754d, type: 3}
- {fileID: 2800000, guid: 304684569f7a5a94893cd173591d3166, type: 3}
- {fileID: 2800000, guid: 71dc477cd3fb2e5459929ccee9dc6526, type: 3}
- {fileID: 2800000, guid: 9b1c4dba90060f44996354b306a504b0, type: 3}
- {fileID: 2800000, guid: 2d69b49a5c4140c4f991d0a1d9722aee, type: 3}
- {fileID: 2800000, guid: 33d3e53bbfb5e18408ea4b03b967f374, type: 3}
- {fileID: 2800000, guid: 22272573e60008b47bc31493110ea3ad, type: 3}
- {fileID: 2800000, guid: daa9b91ca95c3df44aa03bfaae118308, type: 3}
- {fileID: 2800000, guid: c7d5c5fdb0f26c34db40248bc372ddfb, type: 3}
m_IsVariant: 0
m_ScriptablePacker: {fileID: 0}

View File

@ -0,0 +1,43 @@
fileFormatVersion: 2
guid: 467c64cdcea8ca44b93274ce0a7763fe
SpriteAtlasImporter:
externalObjects: {}
textureSettings:
serializedVersion: 2
anisoLevel: 0
compressionQuality: 0
maxTextureSize: 0
textureCompression: 0
filterMode: 1
generateMipMaps: 0
readable: 0
crunchedCompression: 0
sRGB: 1
platformSettings:
- serializedVersion: 4
buildTarget: DefaultTexturePlatform
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
packingSettings:
serializedVersion: 2
padding: 4
blockOffset: 1
allowAlphaSplitting: 0
enableRotation: 0
enableTightPacking: 0
enableAlphaDilation: 1
secondaryTextureSettings: {}
variantMultiplier: 1
bindAsDefault: 1
userData: 4dbf55295db50cdfa18e4172abb1240b
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,16 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!612988286 &1
SpriteAtlasAsset:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name:
serializedVersion: 2
m_MasterAtlas: {fileID: 0}
m_ImporterData:
packables:
- {fileID: 102900000, guid: 6a98e20a534f9ba468053c7c7c969883, type: 3}
m_IsVariant: 0
m_ScriptablePacker: {fileID: 0}

View File

@ -0,0 +1,43 @@
fileFormatVersion: 2
guid: 3b9e68f9e94907443984dfa78de10ccd
SpriteAtlasImporter:
externalObjects: {}
textureSettings:
serializedVersion: 2
anisoLevel: 0
compressionQuality: 0
maxTextureSize: 0
textureCompression: 0
filterMode: 1
generateMipMaps: 0
readable: 0
crunchedCompression: 0
sRGB: 1
platformSettings:
- serializedVersion: 4
buildTarget: DefaultTexturePlatform
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
packingSettings:
serializedVersion: 2
padding: 4
blockOffset: 1
allowAlphaSplitting: 0
enableRotation: 0
enableTightPacking: 0
enableAlphaDilation: 1
secondaryTextureSettings: {}
variantMultiplier: 1
bindAsDefault: 1
userData: 4ff456e91362a4f3c06e9aa4c0f2a0ae
assetBundleName:
assetBundleVariant:

View File

@ -59,6 +59,19 @@ Unity.exe -projectPath <proj> -executeMethod TKS.EditorTools.ProjectSetup.AutoPl
- **사거리 표시(PD 2026-11-04)**: 유닛 탭(아군·적군) → `RangeIndicator`(슬링샷 최대 거리 = 얇은 반투명 원 · 궁병 = 슬링샷 범위 ~ ×(1+비율) 채워진 붉은 도넛 · 범위 안 적이 없으면 회색). - **사거리 표시(PD 2026-11-04)**: 유닛 탭(아군·적군) → `RangeIndicator`(슬링샷 최대 거리 = 얇은 반투명 원 · 궁병 = 슬링샷 범위 ~ ×(1+비율) 채워진 붉은 도넛 · 범위 안 적이 없으면 회색).
- 보고는 실측·추정·미확인을 분리한다(C23 · C5). 커밋은 의도 변경 파일만. - 보고는 실측·추정·미확인을 분리한다(C23 · C5). 커밋은 의도 변경 파일만.
## UI 캔버스·아틀라스 구조 (PD 2026-11-10 「리소스·정보가 얼마나 자주 바뀌는지에 따라 캔버스를 나누어 최적화 · 드로우콜 최소화 구조 필수」)
UI 를 새로 만들거나 고칠 때 **반드시** 이 구조를 따른다(어기면 `Tests/PlayMode/UILayerTests` 가 실패).
- **갱신 등급 3단**(`Runtime/UI/UiLayer.cs` `UiRate`): **Static** = 화면을 열 때만(바탕 · 틀 · 고정 글자) · **Event** = 조작·게임 사건 때(값 · 선택 · 턴 · 팝업) · **Frame** = 매 프레임(호흡 · 튀김 · 스크롤 내용 · 끌기 · 미니맵 · 커서). 등급이 다른 것은 같은 캔버스에 두지 않는다 — 캔버스는 자식 하나만 바뀌어도 그 캔버스 전체를 다시 묶는다.
- **만드는 법**: 루트 = `UiLayer.Root(go, cam, order, plane, reference, rate)` · 기존 오브젝트를 층으로 = `UiLayer.Nest(go, rate, raycast)` · 빈 층 = `UiLayer.Group(name, parent, rate)` · 매 프레임 요소 = `UiLayer.Isolate(go)`(`UiPulse`·`UiPop` 은 스스로 호출 · 바로 위 부모가 Frame 층이면 그 층에 합침). 중첩 캔버스는 **overrideSorting 끔**(그리는 순서 · `RectMask2D`/`Mask` 자르기 유지) · **누르는 그래픽이 있는 층에만 GraphicRaycaster**(중첩 캔버스의 그래픽은 부모 캔버스 레이캐스터가 못 봄 → 누름 층은 `raycast: true`).
- **현재 배치**: 아웃게임 루트(Static · 바탕 + 하단 탭) → 화면마다 중첩(Event) → `UiKit.ScrollGrid` 내용(Frame · 자동) · 위 덮개 루트 → 재화 바 · 팝업(`ModalCustom`) · 토스트 · 뽑기 결과 · 편성 끌기 유령 각각 중첩 · 전투 결과 루트(Event · 별 줄 = Frame) · 전투 HUD 루트 = 정적 판(칩 바탕 · 유닛 판 바탕 · 미니맵 틀) → `Info`(글자·값·스킬 버튼 · Event) → `Minimap/MinimapLive`(시야·점 · Frame) · `SkillGlow`(Frame) → `Result`(Event) · 월드맵 루트(Static · 띠·넘김·본영) → `Content`(맵·노드 · 스크롤 · Frame) → `cursor`(Frame) · `Popup`(Event).
- **매 프레임 호출되는 갱신 코드**는 값이 같으면 쓰지 않는다(`sizeDelta`·`anchoredPosition`·`CanvasGroup.alpha/interactable` 비교 후 대입 · `Text.text`·`Graphic.color` 는 Unity 가 이미 비교).
- **아틀라스**: 구성 SOT = `Assets/Data/UI/ui_atlas.csv`(theme 줄 = UI 테마가 쓰는 스프라이트를 경로 match 로 · folder 줄 = 폴더 통째 · `single_area` 보다 큰 그림 = 묶지 않음) → `Editor/UIAtlasBuilder` → `Assets/UI/Atlases/*.spriteatlasv2`(UI_Common 4096×2048 · UI_Content 2048×2048 · WorldMap_UI 1024×1024 · 각 1장). **UI 테마 임포트가 끝날 때마다 자동 갱신**(새 납품 스프라이트 = 코드·표 변경 없이 묶임) · 수동 = 메뉴 `TKS/Build UI Atlases (UI 아틀라스 갱신)`. 패커 = Sprite Atlas V2(에디터 Play 에서도 묶임 · `EditorSettings` m_SpritePackerMode 5). 사각 묶음(Tight 끔 · 회전 끔 — 9-slice·Filled 보존). **스프라이트를 잘라 새로 만드는 코드는 `sprite.packed ? sprite.textureRect : sprite.rect`**(`UISkin.WorldSprite` 참고 · `rect` 는 원본 기준).
- **검증**: `-executeMethod TKS.EditorTools.DataImporter.CaptureUI` 캡처 로그에 화면마다 `layers <화면> batches=… canvases=루트+중첩 … frameRebuildGraphics=…` 줄 + `ui_layers.csv`(캔버스별 그래픽 수 · 등급 · 레이캐스터 · 매 프레임 요소 여부 · `Runtime/UI/UiLayerProbe`). 2026-11-10 개선 전→후(13화면 · 1080×1920) = 배치 합계 1124 → 439 · 평상시 매 프레임 재구성 그래픽(전투 HUD 49 → 10 · 월드맵 241 → 1 · 보물 상자 115 → 2 · 전투 결과 115 → 14).
- **남은 드로우콜 원인**(2026-11-10 실측): 영웅 목록(72)·편성(120) = 유닛 초상(유닛 아트 프레임 797×826 · 유닛마다 다른 텍스처라 카드마다 끊김) — 목록용 축소 초상 아틀라스는 PD 결정 대기.
- 같은 PC 의 다른 세션도 `E:\Nerdnavis\_tks_verify` 를 캡처에 쓴다 → 사본은 세션마다 따로(예 `Tools/mirror_verify.py <본체> E:/Nerdnavis/_tks_verify_ui`) · 남이 돌리는 사본에 미러하지 않는다(실행 중 코드가 바뀜).
## 스킬 이펙트 작업 (2026-11-09 PD 「WL 에서 스킬을 구현하던 방식과 동일하게 유닛별 스킬 구현」 · 이 PC = 연출 담당) ## 스킬 이펙트 작업 (2026-11-09 PD 「WL 에서 스킬을 구현하던 방식과 동일하게 유닛별 스킬 구현」 · 이 PC = 연출 담당)
- **스킬 테스트 씬** = `Assets/Scenes/SkillTest.unity`(메뉴 `TKS/Skill Test/Build Scene` 로 다시 만듦 · 씬 = `SkillTestBootstrap` 하나) — 전투 코드 그대로 + 월드맵·아웃게임·세이브 없음 · 왼쪽 위 「유닛 선택」(heroes.csv 영웅 56 · 진영별) · 「이펙트 보기」(에디터 전용 · `Assets/VFX` 프리팹 검색 → 고른 영웅 몸에 띄움 · 크기 ×0.25~4 · 기울기 0/−45/−90 · 방향 · 경로 복사) · 왼쪽 아래 「다시 배치」(표 다시 읽기 + 처음 자리) · 쿨 무시 · 더미 행동 · 영웅 무적. 더미 = 죽지 않음(`Unit.Invincible` · 아군 턴마다 체력 회복) · 배치 표 `Resources/Skills/skill_test.csv`(hero 행 n_slot + 상대 좌표) · 더미 행동 끔 = `BattleManager.EnemyHold`. 패널 위 누름은 슬링샷이 무시(`SlingshotInput.BlockPress`). - **스킬 테스트 씬** = `Assets/Scenes/SkillTest.unity`(메뉴 `TKS/Skill Test/Build Scene` 로 다시 만듦 · 씬 = `SkillTestBootstrap` 하나) — 전투 코드 그대로 + 월드맵·아웃게임·세이브 없음 · 왼쪽 위 「유닛 선택」(heroes.csv 영웅 56 · 진영별) · 「이펙트 보기」(에디터 전용 · `Assets/VFX` 프리팹 검색 → 고른 영웅 몸에 띄움 · 크기 ×0.25~4 · 기울기 0/−45/−90 · 방향 · 경로 복사) · 왼쪽 아래 「다시 배치」(표 다시 읽기 + 처음 자리) · 쿨 무시 · 더미 행동 · 영웅 무적. 더미 = 죽지 않음(`Unit.Invincible` · 아군 턴마다 체력 회복) · 배치 표 `Resources/Skills/skill_test.csv`(hero 행 n_slot + 상대 좌표) · 더미 행동 끔 = `BattleManager.EnemyHold`. 패널 위 누름은 슬링샷이 무시(`SlingshotInput.BlockPress`).
@ -87,5 +100,5 @@ Unity.exe -projectPath <proj> -executeMethod TKS.EditorTools.ProjectSetup.AutoPl
- `Assets/Scripts/Runtime/Skills/` — 스킬 처리기(위 「코드 규칙」 스킬) - `Assets/Scripts/Runtime/Skills/` — 스킬 처리기(위 「코드 규칙」 스킬)
- `Assets/Scripts/Runtime/Gimmicks/` — 지형 기믹 레이어(위 「코드 규칙」) · `Assets/Scripts/Runtime/AI/` — 예측 시뮬레이션 적 AI(`Sim` · `TacticalAI` · `AiWeights`) - `Assets/Scripts/Runtime/Gimmicks/` — 지형 기믹 레이어(위 「코드 규칙」) · `Assets/Scripts/Runtime/AI/` — 예측 시뮬레이션 적 AI(`Sim` · `TacticalAI` · `AiWeights`)
- `Assets/Data/Gimmicks/` · `Assets/Data/AI/` — 기믹 표 3장 · AI 가중치 표 - `Assets/Data/Gimmicks/` · `Assets/Data/AI/` — 기믹 표 3장 · AI 가중치 표
- `Assets/Tests/PlayMode/` — `BattleFlowTests` · `OutGameTests` · `SkillTests` · `OutGameCTests` · `GimmickTests` · `TacticalAITests` - `Assets/Tests/PlayMode/` — `BattleFlowTests` · `OutGameTests` · `SkillTests` · `OutGameCTests` · `GimmickTests` · `TacticalAITests` · `UILayerTests`(캔버스 층·아틀라스 구조 · 2026-11-10 · 전체 119)
- `.claude/skills/` — Unity CLI 에이전트 스킬(`unity skill install claude-code --local` 산출물 · 재설치는 `unity skill refresh`) - `.claude/skills/` — Unity CLI 에이전트 스킬(`unity skill install claude-code --local` 산출물 · 재설치는 `unity skill refresh`)

View File

@ -9,7 +9,7 @@ EditorSettings:
m_DefaultBehaviorMode: 1 m_DefaultBehaviorMode: 1
m_PrefabRegularEnvironment: {fileID: 0} m_PrefabRegularEnvironment: {fileID: 0}
m_PrefabUIEnvironment: {fileID: 0} m_PrefabUIEnvironment: {fileID: 0}
m_SpritePackerMode: 0 m_SpritePackerMode: 5
m_SpritePackerCacheSize: 10 m_SpritePackerCacheSize: 10
m_SpritePackerPaddingPower: 1 m_SpritePackerPaddingPower: 1
m_Bc7TextureCompressor: 0 m_Bc7TextureCompressor: 0