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

View File

@ -76,6 +76,7 @@ namespace TKS.OutGame
title.rectTransform.offsetMin = new Vector2(150f, 34f);
// 별 3 = 받침 원(Circle78 chip_dark) + 별(가운데 크게 · 위로) · 0.15 s 간격 튀김
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++)
{
bool mid = i == 1;

View File

@ -328,6 +328,7 @@ namespace TKS.OutGame
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.gameObject.name = "Ghost_" + id;
UiLayer.Nest(ghost.gameObject, UiRate.Frame); // 끄는 동안 매 프레임 이동 = 자기 캔버스(PD 2026-11-10)
MoveGhost(e);
}

View File

@ -203,9 +203,12 @@ namespace TKS.OutGame
wm.BackRequested = ShowLobby;
}
mainCanvas = MakeCanvas("OutGameCanvas", 350, 0.45f);
resultCanvas = MakeCanvas("BattleResultCanvas", 200, 0.8f);
topCanvas = MakeCanvas("OutGameTopCanvas", 450, 0.4f);
// 캔버스 층(PD 2026-11-10 · 프로젝트 CLAUDE.md 「UI 캔버스·아틀라스 구조」):
// 본 화면 루트 = 바탕 · 하단 탭(Static) + 화면마다 중첩 캔버스(Event) · 스크롤 내용 · 호흡 등 = Frame 중첩
// 위 덮개 루트 = 재화 바 · 팝업 · 토스트 · 끌기 유령 각각 중첩 · 전투 결과 루트 = 결과 패널(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;
var bg = UiKit.Fill("Bg", mainCanvas.transform);
@ -232,27 +235,18 @@ namespace TKS.OutGame
{
scr.Root = this;
scr.Rt = UiKit.Fill(typeof(T).Name, mainCanvas.transform);
UiLayer.Nest(scr.Rt.gameObject, UiRate.Event, true); // 화면 1장 = 자기 캔버스(값 갱신이 바탕·탭·다른 화면을 다시 묶지 않게)
scr.Build();
scr.Rt.gameObject.SetActive(false);
screens.Add(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);
go.transform.SetParent(transform, false);
var c = go.AddComponent<Canvas>();
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;
return UiLayer.Root(go, Cam, order, plane, Reference, rate); // Screen Space - Camera = 카메라 캡처(검증)에도 찍힘
}
// ---------- 화면 전환 ----------
@ -375,6 +369,7 @@ namespace TKS.OutGame
void BuildCurrencyBar()
{
barRoot = UiKit.TopBand("CurrencyBar", TopRoot, 0f, TopBarH);
UiLayer.Nest(barRoot.gameObject, UiRate.Event, true); // 군량 타이머 = 1초마다 · 팝업·토스트와 따로
// 테마 = 띠 없이 칩만(화면 바탕이 비침 · 목업 hero_detail) · 투명 판은 아래 클릭 막기용
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("골드 = 스테이지 보상 · 거래 > 상점 · (구매 상점 준비 중)"));
@ -547,6 +542,7 @@ namespace TKS.OutGame
{
CloseModal();
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);
dimImg.raycastTarget = true;
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.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;
UiLayer.Nest(toast, UiRate.Event, false);
toastText = UiKit.FillLabel(img.transform, "", 32, UiKit.White, TextAnchor.MiddleCenter, false, 16f);
}
toast.transform.SetAsLastSibling();

View File

@ -303,6 +303,7 @@ namespace TKS.OutGame
{
CloseResult();
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);
dimg.raycastTarget = true;
var skip = dim.gameObject.AddComponent<Button>();

View File

@ -596,6 +596,8 @@ namespace TKS.OutGame
content.anchorMax = new Vector2(1f, 1f);
content.pivot = new Vector2(0.5f, 1f);
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>();
grid.cellSize = cell;
grid.spacing = spacing;

View File

@ -2,13 +2,22 @@ using UnityEngine;
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 float delay;
public float from = 1.4f;
public float duration = 0.18f;
float t0;
bool done;
void Awake()
{
UiLayer.Isolate(gameObject);
}
void OnEnable()
{
@ -18,19 +27,18 @@ namespace TKS.OutGame
public void Play()
{
t0 = Time.unscaledTime + delay;
done = false;
transform.localScale = Vector3.zero;
}
void Update()
{
if (done) return;
float t = Time.unscaledTime - t0;
if (t < 0f)
{
transform.localScale = Vector3.zero;
return;
}
if (t < 0f) return; // 기다리는 동안 = Play 가 둔 0 그대로
float k = duration > 0f ? Mathf.Clamp01(t / duration) : 1f;
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
{
/// <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 float min = 0.6f;
@ -16,6 +19,7 @@ namespace TKS.OutGame
{
g = GetComponent<Graphic>();
if (g != null) baseA = g.color.a;
UiLayer.Isolate(gameObject);
}
void Update()

View File

@ -202,6 +202,8 @@ namespace TKS.OutGame
matched.Add(k);
}
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");
shot++;
yield return CaptureUtil.Capture(cam, w, h, file);

View File

@ -66,24 +66,24 @@ namespace TKS
Image minimapBg;
Image minimapView;
RectTransform minimapLive;
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)
{
battle = b;
uiCam = cam;
camCtrl = cc;
var canvas = gameObject.AddComponent<Canvas>();
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>();
UiLayer.Root(gameObject, cam, 100, 1f, UiLayer.Reference, UiRate.Static);
group = gameObject.AddComponent<CanvasGroup>();
info = UiLayer.Group("Info", transform, UiRate.Event, false);
if (EventSystem.current == null && FindFirstObjectByType<EventSystem>() == null)
{
@ -96,10 +96,11 @@ namespace TKS
{
// 팩 칩(PD #1100): 스테이지 · 유닛 수 · 턴 띠 · 선택 유닛 정보 판 — 글자 뒤(먼저 만든다) · 반투명 진남
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("CountChip", 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));
Chip("UnitPanel", new Vector2(0f, 0f), new Vector2(0f, 0f), new Vector2(14f, 96f), new Vector2(790f, 196f), panelCol);
Chip("StageChip", transform, new Vector2(0f, 1f), new Vector2(0f, 1f), new Vector2(14f, -14f), new Vector2(730f, 86f), panelCol);
Chip("CountChip", transform, new Vector2(1f, 1f), new Vector2(1f, 1f), new Vector2(-14f, -14f), new Vector2(330f, 86f), panelCol);
// 턴 띠 = 아군/적군 턴마다 색이 바뀜 → Info 층
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);
// 스테이지 표 제목(최대 ~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);
// 스킬 버튼(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.transform.SetParent(skillPanel.transform, false);
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));
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));
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();
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.transform.SetParent(resultOverlay.transform, false);
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.transform.SetParent(resultOverlay.transform, 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>
@ -211,7 +232,8 @@ namespace TKS
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)));
UISkin.Apply(card, "card");
unitCard = card.rectTransform;
// 판 바탕 = 루트(정적) · 내용(초상 · 이름 · 체력 · 칩) = 같은 자리의 Info 칸(선택·피해 때만 바뀜)
unitCard = Like("UnitCardInfo", info, card.rectTransform);
unitBody = OutGame.UiKit.Fill("Body", unitCard);
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);
@ -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);
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);
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.horizontalOverflow = HorizontalWrapMode.Overflow;
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);
// 위 안내 한 줄 = 어두운 칩 위(글자가 바탕 없이 화면 끝에서 잘리던 것 · 검수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");
msgChip.transform.SetSiblingIndex(messageText.transform.GetSiblingIndex());
messageText.rectTransform.anchoredPosition = new Vector2(0f, -204f);
@ -250,7 +272,9 @@ namespace TKS
messageText.alignment = TextAnchor.MiddleCenter;
// 수 칩 = 아군 · 적 아이콘 + 숫자(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)
{
countText.gameObject.SetActive(false);
@ -262,7 +286,7 @@ namespace TKS
// 스킬 버튼 뒤 빛(사용 가능 = 호흡)
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");
skillGlow.transform.SetSiblingIndex(skillPanel.transform.GetSiblingIndex());
var pulse = skillGlow.gameObject.AddComponent<OutGame.UiPulse>();
@ -295,9 +319,11 @@ namespace TKS
}
nameText.text = u.DisplayName + (u.IsBoss ? " <size=24>장수</size>" : "");
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.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;
atkText.text = u.Atk.ToString();
defText.text = u.Def.ToString();
@ -316,10 +342,11 @@ namespace TKS
stateText.text = state;
}
/// <summary>글자 = Info 층(값이 바뀌어도 정적 판을 다시 묶지 않게).</summary>
Text MakeText(string name, Vector2 anchorMin, Vector2 anchorMax, Vector2 pivot, Vector2 anchoredPos, Vector2 size, int fontSize, TextAnchor align)
{
var go = new GameObject(name);
go.transform.SetParent(transform, false);
go.transform.SetParent(info, false);
var rt = go.AddComponent<RectTransform>();
rt.anchorMin = anchorMin;
rt.anchorMax = anchorMax;
@ -357,9 +384,9 @@ namespace TKS
}
/// <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");
return img;
}
@ -579,7 +606,8 @@ namespace TKS
}
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 (result)
{
@ -609,8 +637,12 @@ namespace TKS
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 y1 = Mathf.Clamp((v.yMax - b.yMin) * sy, 0f, r.height);
minimapView.rectTransform.anchoredPosition = new Vector2(x0, y0);
minimapView.rectTransform.sizeDelta = new Vector2(Mathf.Max(2f, x1 - x0), Mathf.Max(2f, y1 - y0));
// 값이 같으면 쓰지 않는다(카메라가 멈춰 있으면 MinimapLive 층도 다시 묶지 않음)
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;
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),
u.Faction == Faction.Player ? UnitView.PlayerColor : UnitView.EnemyColor);
if (u.IsBoss) dot.color = new Color(1f, 0.85f, 0.3f);
@ -628,7 +660,11 @@ namespace TKS
}
bool show = u.IsAlive;
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>();

View File

@ -138,7 +138,9 @@ namespace TKS
b.y = Mathf.Floor(b.y * 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.hideFlags = HideFlags.DontSave;
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;
// 캔버스 층(PD 2026-11-10): 루트 = 바탕 · 챕터 띠 · 넘김 · 본영 버튼(챕터를 바꿀 때만 · Static)
// → Content(맵 · 노드 · 스크롤로 움직임 · Frame) → 커서(매 프레임 흔들림 · Frame) · Popup(Event)
root = new GameObject("WorldMapCanvas");
root.transform.SetParent(transform, false);
var canvas = root.AddComponent<Canvas>();
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>();
UiLayer.Root(root, cam, 300, 0.5f, new Vector2(data.viewSize[0], data.viewSize[1]), UiRate.Static); // 카메라 캡처(자동 플레이)에도 찍힘
group = root.AddComponent<CanvasGroup>();
// 스크롤(맵 = 콘텐츠 · 왼쪽 위 기준)
@ -411,6 +404,7 @@ namespace TKS
vpImg.color = new Color(0.1f, 0.12f, 0.1f);
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]));
UiLayer.Nest(content.gameObject, UiRate.Frame, true); // 스크롤 내용 = 자기 캔버스(노드 = 누름)
mapImage = content.gameObject.AddComponent<Image>();
mapImage.raycastTarget = true; // 맵 아무 데나 끌기
scroll = viewport.gameObject.AddComponent<ScrollRect>();
@ -581,6 +575,8 @@ namespace TKS
{
cursor = cimg.rectTransform;
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.raycastTarget = true;
popup = dim.gameObject;
UiLayer.Nest(popup, UiRate.Event, true); // 스테이지 팝업 = 자기 캔버스(열 때·소탕 때만)
// 아웃게임 줄(군량 · 보상 · 별 · #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));
panel.type = Image.Type.Sliced;
@ -850,7 +847,8 @@ namespace TKS
void Update()
{
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)

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+비율) 채워진 붉은 도넛 · 범위 안 적이 없으면 회색).
- 보고는 실측·추정·미확인을 분리한다(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 = 연출 담당)
- **스킬 테스트 씬** = `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/Gimmicks/` — 지형 기믹 레이어(위 「코드 규칙」) · `Assets/Scripts/Runtime/AI/` — 예측 시뮬레이션 적 AI(`Sim` · `TacticalAI` · `AiWeights`)
- `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`)

View File

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