ThreeKingdomsSlingshot/Assets/Scripts/Runtime/WorldMapScreen.cs

634 lines
27 KiB
C#
Raw Normal View History

시작 화면 = 챕터 월드맵(스크롤 2160×3840 · 10장) → 스테이지 팝업(임시) → 전투 → 결과 탭 = 맵 복귀·다음 노드 열림 + 아군 뒷모습·황건적·동탁군 포즈 반영 (PD 2026-11-05) - WorldMapScreen(신규): ScrollRect 맵 · 길 따라 깃발 노드(open/clear/lock · 보스) · 길 점 · 현재 위치 핀 · 챕터 띠 ◀▶ · 스테이지 팝업(적 구성·보스 · 출전/닫기) · 임시 진행도(메모리) - 데이터 = Resources/ChapterMap/Data/world.json(원화 세션 chap_install.py 생성 · 맵 크기·실제 길·부품 크기/피벗/번호 자리·임시 스테이지 연결 = 챕터당 10노드 · 전투 = 기존 스테이지 0/1/2) · 지명판 = 끔(생성 그림과 위치 어긋남) - GameBootstrap: StartOnWorldMap(기본 true) · BattleManager: ResultHandler · GoIdle · BattleHUD 결과 문구 · AutoPlayDriver: 월드맵·팝업 캡처 후 1-1 출전 - UnitArtImporter: ChapterMap/Maps(4096 · 압축) · ChapterMap/UI(import.json 피벗·9-slice·PPU 133.33) - 유닛: 아군 5인 앞·뒤 × idle/ready/action(키아트 세션 · md5 30/30) · 황건적 5 = 인박스 20261105_0312_hgj(QA PASS · md5 15/15) · 동탁군 5(여포 포함 · 원화 세션 poses md5 15/15 · PD 지시로 직접 반영) - 테스트 27/27(+ 월드맵 시작 · 팝업→전투→맵 복귀 · 맵/UI 리소스 · 포즈 세트·동작별 스프라이트 교체 · 앞뒤 클립 고정 자료 hgj_infantry → 아군 아트) · console error 0 - 남은 것(QA r2 B 2 · PD 결정): ch04 보스 초상 틀이 위 HUD 줄 안 · ch08 경로가 다리 없는 큰길을 피해 풀밭·강둑으로 우회 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 18:38:47 +00:00
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
namespace TKS
{
// ---------- 데이터(Resources/ChapterMap/Data/world.json · 원화 세션 chap_install.py 생성 · 좌표 = 맵 px · 왼쪽 위 원점 · y 아래로) ----------
[Serializable]
public class WorldKitItem
{
public string name;
public float[] pivot; // 이미지 좌표(위 → 아래) 비율
public float[] refSize; // UI 기준 px
public float[] numberCenter; // 번호 자리(이미지 좌표 비율)
}
[Serializable]
public class WorldLandmark
{
public string ko;
public float x;
public float y;
public string role;
}
[Serializable]
public class WorldChapter
{
public string id;
public int no;
public string ko;
public string years;
public string map;
public float[] roadX;
public float[] roadY;
public WorldLandmark[] landmarks;
public int nodes; // 스테이지 수(임시 · 기획 세션이 확정)
public int stageLink; // 이 챕터 노드가 쓰는 BalanceConfig.stages 인덱스(임시)
}
[Serializable]
public class WorldData
{
public float[] mapSize;
public float[] viewSize;
public float dotSpacing;
public float[] flagStars; // x0,y0,x1,y1,… (노드 기준 · y 아래로)
public float[] flagBossStars;
public float[] flagCursor;
public float[] flagBossCursor;
public float[] flagFrame;
public bool showLabels; // 장소 이름판 표시(그림 속 장소 위치와 맞춘 뒤 켬)
public WorldKitItem[] kit;
public WorldChapter[] chapters;
public const string Path = "ChapterMap/Data/world";
public static WorldData Load()
{
var t = Resources.Load<TextAsset>(Path);
if (t == null) return null;
try
{
return JsonUtility.FromJson<WorldData>(t.text);
}
catch (Exception e)
{
Debug.LogError("[TKS] world.json 읽기 실패: " + e.Message);
return null;
}
}
public WorldKitItem Kit(string name)
{
if (kit != null)
foreach (var k in kit)
if (k.name == name) return k;
return null;
}
/// <summary>길(road) 을 호 길이로 n 등분한 노드 자리(맵 px). 첫 칸 = 출발 · 마지막 = 보스.</summary>
public static Vector2[] Slots(WorldChapter c, int n)
{
var acc = Arc(c);
var res = new Vector2[Mathf.Max(0, n)];
float total = acc[acc.Length - 1];
for (int i = 0; i < res.Length; i++) res[i] = At(c, acc, n <= 1 ? 0f : total * i / (n - 1));
return res;
}
public static float[] Arc(WorldChapter c)
{
int m = Mathf.Min(c.roadX.Length, c.roadY.Length);
var acc = new float[Mathf.Max(1, m)];
for (int i = 1; i < m; i++)
acc[i] = acc[i - 1] + Vector2.Distance(new Vector2(c.roadX[i - 1], c.roadY[i - 1]), new Vector2(c.roadX[i], c.roadY[i]));
return acc;
}
public static Vector2 At(WorldChapter c, float[] acc, float len)
{
int m = acc.Length;
if (m == 1) return new Vector2(c.roadX[0], c.roadY[0]);
for (int i = 1; i < m; i++)
{
if (acc[i] >= len)
{
float t = (len - acc[i - 1]) / Mathf.Max(acc[i] - acc[i - 1], 1e-4f);
return Vector2.Lerp(new Vector2(c.roadX[i - 1], c.roadY[i - 1]), new Vector2(c.roadX[i], c.roadY[i]), t);
}
}
return new Vector2(c.roadX[m - 1], c.roadY[m - 1]);
}
}
public enum NodeState { Lock, Open, Clear }
/// <summary>임시 진행도(실행 중 메모리 · 저장 없음). 챕터별 깬 노드 수.</summary>
public static class WorldProgress
{
static readonly Dictionary<int, int> cleared = new Dictionary<int, int>();
public static int Cleared(int chapter)
{
int v;
return cleared.TryGetValue(chapter, out v) ? v : 0;
}
public static void MarkCleared(int chapter, int node)
{
if (node + 1 > Cleared(chapter)) cleared[chapter] = node + 1;
}
public static void Reset()
{
cleared.Clear();
}
}
/// <summary>
/// 게임 시작 화면 = 챕터 월드맵(스크롤 · 상하좌우로 끌기). 길 위 깃발 노드를 누르면 스테이지 팝업 → 출전 = 전투 시작 ·
/// 전투 결과를 탭하면 월드맵으로 돌아와 다음 노드가 열린다. 그림·좌표·부품 배치 = world.json · 전투 데이터 = BalanceConfig(C45).
/// </summary>
public class WorldMapScreen : MonoBehaviour
{
public static WorldMapScreen Instance { get; private set; }
const string UiRoot = "ChapterMap/UI/";
const string MapRoot = "ChapterMap/Maps/";
static readonly Color Ink = new Color(0.14f, 0.09f, 0.07f);
static readonly Color Paper = new Color(1f, 0.98f, 0.93f);
BattleManager battle;
BattleHUD hud;
SlingshotInput sling;
WorldData data;
GameObject root;
CanvasGroup group;
ScrollRect scroll;
RectTransform viewport;
RectTransform content;
Image mapImage;
Text bannerText;
Button prevButton;
Button nextButton;
RectTransform cursor;
Vector2 cursorBase;
readonly List<GameObject> spawned = new List<GameObject>();
readonly List<Button> nodeButtons = new List<Button>();
GameObject popup;
Text popupTitle;
Text popupBody;
Button popupGo;
Text popupGoLabel;
int popupNode = -1;
float inputBlockUntil;
int chapter;
int entryChapter = -1;
int entryNode = -1;
public bool HasData { get { return data != null && data.chapters != null && data.chapters.Length > 0; } }
public bool IsVisible { get { return root != null && root.activeSelf; } }
public bool PopupVisible { get { return popup != null && popup.activeSelf; } }
public int Chapter { get { return chapter; } }
public int ChapterCount { get { return HasData ? data.chapters.Length : 0; } }
public WorldData Data { get { return data; } }
public Sprite MapSprite { get { return mapImage != null ? mapImage.sprite : null; } }
public int NodeCount { get { return nodeButtons.Count; } }
void Awake()
{
Instance = this;
}
void OnDestroy()
{
if (Instance == this) Instance = null;
}
public void Build(BattleManager b, BattleHUD h, SlingshotInput s, Camera cam)
{
battle = b;
hud = h;
sling = s;
data = WorldData.Load();
if (!HasData)
{
Debug.LogWarning("[TKS] 월드맵 데이터 없음(" + WorldData.Path + ") → 바로 전투");
return;
}
battle.ResultHandler = OnBattleResult;
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>();
group = root.AddComponent<CanvasGroup>();
// 스크롤(맵 = 콘텐츠 · 왼쪽 위 기준)
viewport = Rect("Viewport", root.transform, Vector2.zero, Vector2.one, new Vector2(0.5f, 0.5f), Vector2.zero, Vector2.zero);
var vpImg = viewport.gameObject.AddComponent<Image>();
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]));
mapImage = content.gameObject.AddComponent<Image>();
mapImage.raycastTarget = true; // 맵 아무 데나 끌기
scroll = viewport.gameObject.AddComponent<ScrollRect>();
scroll.viewport = viewport;
scroll.content = content;
scroll.horizontal = true;
scroll.vertical = true;
scroll.movementType = ScrollRect.MovementType.Clamped;
scroll.inertia = true;
scroll.decelerationRate = 0.12f;
scroll.scrollSensitivity = 60f;
// 위 챕터 띠 + 챕터 넘김
var banner = Img("Banner", root.transform, Sprite("banner"), new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(0f, -40f), new Vector2(640f, 112f));
banner.type = Image.Type.Sliced;
var bk = data.Kit("banner");
float tcy = bk != null && bk.numberCenter != null ? bk.numberCenter[1] : 0.4f;
bannerText = Label(banner.transform, "", 40, Paper, new Vector2(0.5f, 1f - tcy));
prevButton = SmallButton("Prev", root.transform, "◀", new Vector2(0f, 1f), new Vector2(70f, -96f), () => ShowChapter(chapter - 1));
nextButton = SmallButton("Next", root.transform, "▶", new Vector2(1f, 1f), new Vector2(-70f, -96f), () => ShowChapter(chapter + 1));
var hint = Label(root.transform, "맵을 끌어 둘러보고 깃발을 눌러 출전", 30, Paper, new Vector2(0.5f, 0f));
var hrt = hint.rectTransform;
hrt.anchorMin = hrt.anchorMax = new Vector2(0.5f, 0f);
hrt.anchoredPosition = new Vector2(0f, 90f);
BuildPopup();
root.SetActive(false);
}
// ---------- 화면 전환 ----------
/// <summary>월드맵을 연다(전투 정리 · 전투 HUD·조작 끔).</summary>
public void Show(int ch)
{
if (!HasData) return;
if (battle != null && battle.Phase != BattlePhase.Setup) battle.GoIdle();
if (hud != null) hud.gameObject.SetActive(false);
if (sling != null) sling.enabled = false;
root.SetActive(true);
inputBlockUntil = Time.unscaledTime + 0.25f; // 결과 화면 탭이 노드 클릭으로 이어지지 않게
ShowChapter(ch);
}
public void ShowChapter(int ch)
{
if (!HasData) return;
chapter = Mathf.Clamp(ch, 0, data.chapters.Length - 1);
ClosePopup();
var c = data.chapters[chapter];
foreach (var go in spawned)
if (go != null) Destroy(go);
spawned.Clear();
nodeButtons.Clear();
cursor = null;
var sp = Resources.Load<Sprite>(MapRoot + c.map);
if (sp == null) Debug.LogWarning("[TKS] 맵 그림 없음: " + MapRoot + c.map);
mapImage.sprite = sp;
mapImage.color = sp != null ? Color.white : new Color(0.55f, 0.7f, 0.45f);
bannerText.text = "제" + c.no + "장 " + c.ko;
prevButton.interactable = chapter > 0;
nextButton.interactable = chapter < data.chapters.Length - 1;
var slots = WorldData.Slots(c, c.nodes);
var acc = WorldData.Arc(c);
float total = acc[acc.Length - 1];
int cleared = ChapterUnlocked(chapter) ? WorldProgress.Cleared(chapter) : 0;
// 길 점(노드 사이)
float spacing = Mathf.Max(8f, data.dotSpacing);
for (int i = 0; i + 1 < slots.Length; i++)
{
float l0 = total * i / (slots.Length - 1) + 44f;
float l1 = total * (i + 1) / (slots.Length - 1) - 44f;
int m = Mathf.Max(0, Mathf.FloorToInt((l1 - l0) / spacing));
for (int j = 0; j <= m; j++)
{
var p = WorldData.At(c, acc, Mathf.Lerp(l0, l1, m == 0 ? 0.5f : (float)j / m));
Piece(i < cleared ? "dot_on" : "dot_off", p, false);
}
}
// 장소 이름판
if (data.showLabels && c.landmarks != null)
foreach (var L in c.landmarks)
{
if (string.IsNullOrEmpty(L.ko)) continue;
var plate = Piece("plate", new Vector2(L.x, L.y), false);
if (plate == null) continue;
plate.type = Image.Type.Sliced;
var t = Label(plate.transform, L.ko, 26, new Color(0.28f, 0.17f, 0.11f), new Vector2(0.5f, 0.5f));
t.GetComponent<Outline>().enabled = false;
plate.rectTransform.sizeDelta = new Vector2(t.preferredWidth + 60f, plate.rectTransform.sizeDelta.y);
}
// 노드
for (int i = 0; i < slots.Length; i++)
{
bool boss = i == slots.Length - 1;
var st = StateOf(chapter, i);
string key = (boss ? "flag_boss_" : "flag_") + (st == NodeState.Clear ? "clear" : st == NodeState.Open ? "open" : "lock");
if (boss) Piece("frame", slots[i] + V(data.flagFrame), false);
var img = Piece(key, slots[i], true);
if (img == null) continue;
int node = i;
var btn = img.gameObject.AddComponent<Button>();
btn.transition = Selectable.Transition.ColorTint;
btn.onClick.AddListener(() => OnNodeClicked(node));
nodeButtons.Add(btn);
var kit = data.Kit(key);
if (st != NodeState.Lock && kit != null)
{
var t = Label(img.transform, c.no + "-" + (i + 1), boss ? 38 : (i + 1 >= 10 ? 26 : 32), Paper, new Vector2(kit.numberCenter[0], 1f - kit.numberCenter[1]));
t.raycastTarget = false;
}
if (st == NodeState.Clear)
{
var so = boss ? data.flagBossStars : data.flagStars;
for (int k = 0; k + 1 < so.Length; k += 2) Piece("star_on", slots[i] + new Vector2(so[k], so[k + 1]), false);
}
if (st == NodeState.Open)
{
var co = boss ? data.flagBossCursor : data.flagCursor;
var cimg = Piece("cursor", slots[i] + V(co), false);
if (cimg != null)
{
cursor = cimg.rectTransform;
cursorBase = cursor.anchoredPosition;
}
}
}
// 처음 위치 = 도전할 노드(없으면 출발)
int focus = Mathf.Clamp(Mathf.Min(cleared, slots.Length - 1), 0, Mathf.Max(0, slots.Length - 1));
if (slots.Length > 0) ScrollTo(slots[focus]);
}
/// <summary>맵 px 지점을 화면 가운데로(맵 끝에서 멈춤).</summary>
public void ScrollTo(Vector2 p)
{
Canvas.ForceUpdateCanvases();
var vr = viewport.rect;
float w = data.mapSize[0], h = data.mapSize[1];
float nx = w > vr.width ? Mathf.Clamp01((p.x - vr.width / 2f) / (w - vr.width)) : 0f;
float ny = h > vr.height ? Mathf.Clamp01((p.y - vr.height / 2f) / (h - vr.height)) : 0f;
scroll.StopMovement();
scroll.normalizedPosition = new Vector2(nx, 1f - ny);
}
public NodeState StateOf(int ch, int node)
{
if (!HasData || !ChapterUnlocked(ch)) return NodeState.Lock;
int cleared = WorldProgress.Cleared(ch);
if (node < cleared) return NodeState.Clear;
return node == cleared ? NodeState.Open : NodeState.Lock;
}
public bool ChapterUnlocked(int ch)
{
if (!HasData || ch < 0 || ch >= data.chapters.Length) return false;
return ch == 0 || WorldProgress.Cleared(ch - 1) >= data.chapters[ch - 1].nodes;
}
// ---------- 팝업 ----------
void OnNodeClicked(int node)
{
if (Time.unscaledTime < inputBlockUntil) return;
OpenPopup(node);
}
public void OpenPopup(int node)
{
if (!HasData) return;
var c = data.chapters[chapter];
if (node < 0 || node >= c.nodes) return;
popupNode = node;
var st = StateOf(chapter, node);
var stage = StageFor(c);
bool boss = node == c.nodes - 1;
popupTitle.text = c.no + "-" + (node + 1) + (boss ? " 보스" : "") + "\n" + c.ko;
var body = new System.Text.StringBuilder();
if (stage != null)
{
body.Append(stage.title);
if (!string.IsNullOrEmpty(stage.subtitle)) body.Append(" · ").Append(stage.subtitle);
body.Append("\n\n").Append(EnemySummary(stage));
}
body.Append("\n\n<size=24>(임시 구성 · 전투 = 스테이지 ").Append(c.stageLink + 1).Append(" · 기획 확정 후 교체)</size>");
if (st == NodeState.Lock) body.Append("\n\n잠긴 스테이지 — 앞 스테이지를 먼저 깨세요");
popupBody.text = body.ToString();
popupGo.interactable = st != NodeState.Lock;
popupGoLabel.text = st == NodeState.Clear ? "다시 출전" : "출전";
popup.SetActive(true);
}
public void ClosePopup()
{
popupNode = -1;
if (popup != null) popup.SetActive(false);
}
public void ConfirmPopup()
{
if (popupNode < 0) return;
if (StateOf(chapter, popupNode) == NodeState.Lock) return;
EnterStage(chapter, popupNode);
}
/// <summary>노드의 전투로 들어간다(월드맵 닫음 · HUD·조작 켬).</summary>
public void EnterStage(int ch, int node)
{
if (!HasData)
{
battle.StartStage(0);
return;
}
chapter = Mathf.Clamp(ch, 0, data.chapters.Length - 1);
entryChapter = chapter;
entryNode = node;
ClosePopup();
root.SetActive(false);
if (hud != null) hud.gameObject.SetActive(true);
if (sling != null) sling.enabled = true;
battle.StartStage(data.chapters[chapter].stageLink);
}
void OnBattleResult(bool win)
{
int ch = entryChapter >= 0 ? entryChapter : chapter;
if (win && entryNode >= 0)
{
WorldProgress.MarkCleared(ch, entryNode);
// 챕터 보스를 깨면 다음 챕터 맵으로
if (entryNode == data.chapters[ch].nodes - 1 && ch + 1 < data.chapters.Length) ch++;
}
entryChapter = entryNode = -1;
Show(ch);
}
BalanceConfig.StageDef StageFor(WorldChapter c)
{
if (battle == null || battle.Config == null || battle.Config.stages.Length == 0) return null;
return battle.Config.stages[Mathf.Clamp(c.stageLink, 0, battle.Config.stages.Length - 1)];
}
static string EnemySummary(BalanceConfig.StageDef stage)
{
var counts = new int[UnitTypeInfo.Count];
string bossName = null;
foreach (var e in stage.enemies)
{
counts[(int)e.type]++;
if (e.isBoss) bossName = e.name;
}
var sb = new System.Text.StringBuilder("적 " + stage.enemies.Count + "기 · ");
bool first = true;
for (int t = 0; t < counts.Length; t++)
{
if (counts[t] == 0) continue;
if (!first) sb.Append(" ");
sb.Append(UnitTypeInfo.Korean((UnitType)t)).Append(" ").Append(counts[t]);
first = false;
}
if (bossName != null) sb.Append("\n보스 ").Append(bossName);
return sb.ToString();
}
void BuildPopup()
{
var dim = Img("Popup", root.transform, null, Vector2.zero, Vector2.one, new Vector2(0.5f, 0.5f), Vector2.zero, Vector2.zero);
dim.color = new Color(0f, 0f, 0f, 0.6f);
dim.raycastTarget = true;
popup = dim.gameObject;
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(880f, 760f));
panel.type = Image.Type.Sliced;
panel.raycastTarget = true;
var head = Img("Head", panel.transform, Sprite("banner"), new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(0.5f, 0.5f), new Vector2(0f, 0f), new Vector2(760f, 112f));
head.type = Image.Type.Sliced;
var bk = data.Kit("banner");
popupTitle = Label(head.transform, "", 34, Paper, new Vector2(0.5f, 1f - (bk != null ? bk.numberCenter[1] : 0.4f)));
popupTitle.lineSpacing = 0.9f;
popupBody = Label(panel.transform, "", 34, new Color(0.25f, 0.15f, 0.1f), new Vector2(0.5f, 0.55f));
popupBody.GetComponent<Outline>().enabled = false;
popupBody.rectTransform.sizeDelta = new Vector2(780f, 420f);
popupBody.horizontalOverflow = HorizontalWrapMode.Wrap;
popupBody.supportRichText = true;
popupGo = WideButton("Go", panel.transform, "출전", new Vector2(-180f, 80f), new Color(0.89f, 0.33f, 0.23f), ConfirmPopup, out popupGoLabel);
Text closeLabel;
WideButton("Close", panel.transform, "닫기", new Vector2(180f, 80f), new Color(0.6f, 0.55f, 0.5f), ClosePopup, out closeLabel);
popup.SetActive(false);
}
// ---------- 그리기 도우미 ----------
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;
}
static Vector2 V(float[] a)
{
return a != null && a.Length >= 2 ? new Vector2(a[0], a[1]) : Vector2.zero;
}
static Sprite Sprite(string name)
{
return Resources.Load<Sprite>(UiRoot + name);
}
/// <summary>맵 px 위치(y 아래로)에 부품 1개. 크기 = kit refSize · 피벗 = kit pivot.</summary>
Image Piece(string name, Vector2 mapPos, bool raycast)
{
var sp = Sprite(name);
var kit = data.Kit(name);
if (sp == null || kit == null) return null;
var go = new GameObject(name);
go.transform.SetParent(content, false);
var rt = go.AddComponent<RectTransform>();
rt.anchorMin = rt.anchorMax = new Vector2(0f, 1f);
rt.pivot = new Vector2(kit.pivot[0], 1f - kit.pivot[1]);
rt.sizeDelta = new Vector2(kit.refSize[0], kit.refSize[1]);
rt.anchoredPosition = new Vector2(mapPos.x, -mapPos.y);
var img = go.AddComponent<Image>();
img.sprite = sp;
img.raycastTarget = raycast;
spawned.Add(go);
return img;
}
static RectTransform Rect(string name, Transform parent, Vector2 aMin, Vector2 aMax, Vector2 pivot, Vector2 pos, Vector2 size)
{
var go = new GameObject(name);
go.transform.SetParent(parent, false);
var rt = go.AddComponent<RectTransform>();
rt.anchorMin = aMin;
rt.anchorMax = aMax;
rt.pivot = pivot;
rt.anchoredPosition = pos;
rt.sizeDelta = size;
return rt;
}
static Image Img(string name, Transform parent, Sprite sp, Vector2 aMin, Vector2 aMax, Vector2 pivot, Vector2 pos, Vector2 size)
{
var rt = Rect(name, parent, aMin, aMax, pivot, pos, size);
var img = rt.gameObject.AddComponent<Image>();
img.sprite = sp;
img.raycastTarget = false;
return img;
}
/// <summary>부모 안 anchor 지점(비율 · 아래 원점)에 가운데 맞춤 글자(흰 글자 + 검정 테).</summary>
static Text Label(Transform parent, string text, int size, Color color, Vector2 anchor)
{
var rt = Rect("Text", parent, anchor, anchor, new Vector2(0.5f, 0.5f), Vector2.zero, new Vector2(600f, size * 2.6f));
var t = rt.gameObject.AddComponent<Text>();
t.font = TextUtil.Font;
t.fontSize = size;
t.fontStyle = FontStyle.Bold;
t.alignment = TextAnchor.MiddleCenter;
t.color = color;
t.horizontalOverflow = HorizontalWrapMode.Overflow;
t.verticalOverflow = VerticalWrapMode.Overflow;
t.raycastTarget = false;
t.text = text;
var o = rt.gameObject.AddComponent<Outline>();
o.effectColor = Ink;
o.effectDistance = new Vector2(3f, -3f);
return t;
}
Button SmallButton(string name, Transform parent, string label, Vector2 anchor, Vector2 pos, Action onClick)
{
var img = Img(name, parent, Sprite("plate"), anchor, anchor, new Vector2(0.5f, 0.5f), pos, new Vector2(110f, 80f));
img.type = Image.Type.Sliced;
img.raycastTarget = true;
var b = img.gameObject.AddComponent<Button>();
b.onClick.AddListener(() => onClick());
var t = Label(img.transform, label, 40, new Color(0.45f, 0.22f, 0.12f), new Vector2(0.5f, 0.55f));
t.GetComponent<Outline>().enabled = false;
return b;
}
Button WideButton(string name, Transform parent, string label, Vector2 pos, Color tint, Action onClick, out Text labelText)
{
var img = Img(name, parent, Sprite("plate"), new Vector2(0.5f, 0f), new Vector2(0.5f, 0f), new Vector2(0.5f, 0.5f), pos, new Vector2(300f, 100f));
img.type = Image.Type.Sliced;
img.color = tint;
img.raycastTarget = true;
var b = img.gameObject.AddComponent<Button>();
b.onClick.AddListener(() => onClick());
labelText = Label(img.transform, label, 42, Paper, new Vector2(0.5f, 0.55f));
return b;
}
}
}