252 lines
14 KiB
C#
252 lines
14 KiB
C#
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 + ": 레이캐스터");
|
|
}
|
|
|
|
// 로비 배경 연출(깃발·불빛·새·꽃잎 · PD 2026-11-11) = 자기 Frame 층 · 화면 캔버스(Event)는 매 프레임 다시 묶이지 않음
|
|
og.ShowLobby();
|
|
yield return null;
|
|
var amb = og.Lobby.Rt.GetComponentInChildren<LobbyAmbient>(true);
|
|
Assert.IsTrue(UISkin.Icon("bg_lobby") == null || amb != null, "로비 배경 연출 층 없음");
|
|
if (amb != null)
|
|
{
|
|
AssertNested(amb.GetComponent<Canvas>(), UiRate.Frame, "로비 배경 연출");
|
|
var waves = amb.GetComponentsInChildren<UiWaveImage>(true);
|
|
Assert.Greater(waves.Length, 0, "로비 깃발 없음(표 lobby_scene.csv flag 줄 · 테마 lobby_banner_*)");
|
|
foreach (var w in waves) AssertInFrame(w.transform, "로비 깃발 " + w.name);
|
|
// 그리는 순서 = 뒤 연출(깃발·불빛) → 영웅 → 앞 연출(새·꽃잎) · PD 2026-11-11 「벚꽃잎은 유닛보다 앞 레이어」
|
|
var back = amb.transform.Find("Back");
|
|
var line = amb.transform.Find("Lineup");
|
|
var front = amb.transform.Find("Front");
|
|
Assert.IsTrue(back != null && line != null && front != null, "로비 장면 층(Back · Lineup · Front) 없음");
|
|
Assert.Less(back.GetSiblingIndex(), line.GetSiblingIndex(), "깃발·불빛은 영웅 뒤");
|
|
Assert.Less(line.GetSiblingIndex(), front.GetSiblingIndex(), "꽃잎·새는 영웅 앞");
|
|
Assert.Greater(front.childCount, 0, "앞 층(꽃잎·새) 비어 있음");
|
|
Assert.Greater(line.childCount, 0, "영웅 층 비어 있음");
|
|
foreach (var fa in line.GetComponentsInChildren<UiFrameAnim>(true)) AssertInFrame(fa.transform, "로비 영웅 대기 " + fa.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 + ")");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 대기 재생 규칙(PD 2026-11-11 「4장 + 깜빡임」) — meta 의 sequence 0→1→2→3 이 단계별 유지 초대로 돌고, 깜빡임 프레임은 쉼(첫 프레임)일 때만 every_min~max 간격으로 dur 동안.
|
|
/// 값은 유닛 meta.json 에서 읽음(C45) · 유비 = 시제품(meta 에 sequence·blink 가 없으면 건너뜀).
|
|
/// </summary>
|
|
[Test]
|
|
public void IdleSequenceAndBlink_FollowMeta()
|
|
{
|
|
var d = UnitArt.Load("liubei");
|
|
Assert.IsNotNull(d, "유비 유닛 아트");
|
|
const string clip = "front_idle";
|
|
Sprite[] fr;
|
|
Assert.IsTrue(d.Clips.TryGetValue(clip, out fr), clip);
|
|
int n = fr.Length;
|
|
var blink = d.ClipBlink(clip, n);
|
|
int[] seq;
|
|
if (blink == null || !d.Sequences.TryGetValue(clip, out seq)) Assert.Ignore("유비 대기에 sequence·blink 없음");
|
|
seq = d.Sequences[clip];
|
|
float cycle = d.CycleSeconds(clip, n);
|
|
// ① 순서: 한 바퀴를 촘촘히 훑으면 순서대로 바뀜 · 깜빡임 프레임은 순서에 없음
|
|
var seen = new List<int>();
|
|
for (float t = 0f; t < cycle; t += 0.005f)
|
|
{
|
|
int k = d.FrameAt(clip, n, t);
|
|
Assert.AreNotEqual(blink.frame, k, "깜빡임 프레임이 순서에 섞임");
|
|
if (seen.Count == 0 || seen[seen.Count - 1] != k) seen.Add(k);
|
|
}
|
|
CollectionAssert.AreEqual(seq, seen.ToArray(), "재생 순서");
|
|
// ② 깜빡임: 30 초 동안 간격·길이·쉼 자세에서만
|
|
float next = 0f, end = -1f, start = -1f, last = -1f;
|
|
int count = 0;
|
|
for (float t = 0f; t < 30f; t += 0.01f)
|
|
{
|
|
int raw = d.FrameAt(clip, n, t);
|
|
int k = d.ApplyBlink(clip, n, raw, t, ref next, ref end);
|
|
if (k == blink.frame)
|
|
{
|
|
Assert.AreEqual(d.RestFrame(clip, n), raw, "깜빡임은 쉼 자세에서만");
|
|
if (start < 0f)
|
|
{
|
|
if (last >= 0f) Assert.GreaterOrEqual(t - last, blink.every_min - 0.02f, "깜빡임 간격 하한");
|
|
start = t;
|
|
count++;
|
|
}
|
|
}
|
|
else if (start >= 0f)
|
|
{
|
|
Assert.LessOrEqual(t - start, blink.dur + 0.03f, "깜빡임 길이");
|
|
last = start;
|
|
start = -1f;
|
|
}
|
|
}
|
|
Assert.GreaterOrEqual(count, Mathf.FloorToInt(30f / (blink.every_max + cycle)), "30 초 동안 깜빡임 횟수");
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
}
|