2026-11-03 19:29:16 +00:00
using System.Collections.Generic ;
2026-11-03 19:10:10 +00:00
using UnityEngine ;
using UnityEngine.EventSystems ;
using UnityEngine.UI ;
namespace TKS
{
2026-11-03 19:29:16 +00:00
/// <summary>화면 HUD(uGUI · 코드 생성): 스테이지·턴·유닛 수·안내·선택 유닛 정보·미니맵·스킬 아이콘(추후)·결과 오버레이.</summary>
2026-11-03 19:10:10 +00:00
public class BattleHUD : MonoBehaviour
{
BattleManager battle ;
2026-11-03 19:29:16 +00:00
Camera uiCam ;
CameraController camCtrl ;
2026-11-03 19:10:10 +00:00
Text stageText ;
Text turnText ;
Text countText ;
Text messageText ;
Text hintText ;
Text unitText ;
GameObject resultOverlay ;
Text resultTitle ;
Text resultSub ;
Unit selected ;
2026-11-03 19:29:16 +00:00
Image minimapBg ;
Image minimapView ;
readonly Dictionary < Unit , Image > minimapDots = new Dictionary < Unit , Image > ( ) ;
public void Build ( BattleManager b , Camera cam , CameraController cc )
2026-11-03 19:10:10 +00:00
{
battle = b ;
2026-11-03 19:29:16 +00:00
uiCam = cam ;
camCtrl = cc ;
2026-11-03 19:10:10 +00:00
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 > ( ) ;
if ( EventSystem . current = = null & & FindFirstObjectByType < EventSystem > ( ) = = null )
{
var es = new GameObject ( "EventSystem" ) ;
es . AddComponent < EventSystem > ( ) ;
es . AddComponent < StandaloneInputModule > ( ) ;
}
stageText = MakeText ( "Stage" , new Vector2 ( 0f , 1f ) , new Vector2 ( 0f , 1f ) , new Vector2 ( 0f , 1f ) , new Vector2 ( 30f , - 28f ) , new Vector2 ( 760f , 70f ) , 40 , TextAnchor . UpperLeft ) ;
countText = MakeText ( "Count" , new Vector2 ( 1f , 1f ) , new Vector2 ( 1f , 1f ) , new Vector2 ( 1f , 1f ) , new Vector2 ( - 30f , - 28f ) , new Vector2 ( 520f , 70f ) , 36 , TextAnchor . UpperRight ) ;
turnText = MakeText ( "Turn" , new Vector2 ( 0.5f , 1f ) , new Vector2 ( 0.5f , 1f ) , new Vector2 ( 0.5f , 1f ) , new Vector2 ( 0f , - 92f ) , new Vector2 ( 700f , 70f ) , 46 , TextAnchor . UpperCenter ) ;
turnText . color = new Color ( 1f , 0.95f , 0.6f ) ;
messageText = MakeText ( "Message" , new Vector2 ( 0.5f , 1f ) , new Vector2 ( 0.5f , 1f ) , new Vector2 ( 0.5f , 1f ) , new Vector2 ( 0f , - 160f ) , new Vector2 ( 1040f , 60f ) , 30 , TextAnchor . UpperCenter ) ;
messageText . color = new Color ( 0.9f , 0.95f , 1f ) ;
hintText = MakeText ( "Hint" , new Vector2 ( 0.5f , 0f ) , new Vector2 ( 0.5f , 0f ) , new Vector2 ( 0.5f , 0f ) , new Vector2 ( 0f , 36f ) , new Vector2 ( 1040f , 60f ) , 28 , TextAnchor . LowerCenter ) ;
hintText . color = new Color ( 0.85f , 0.85f , 0.85f ) ;
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 ) ;
// 스킬 아이콘(초기 단계 미구현 · 자리만)
var skill = MakeImage ( "SkillIcon" , transform , new Vector2 ( 1f , 0f ) , new Vector2 ( 1f , 0f ) , new Vector2 ( 1f , 0f ) , new Vector2 ( - 36f , 110f ) , new Vector2 ( 170f , 170f ) , new Color ( 0.3f , 0.3f , 0.33f , 0.8f ) ) ;
var skillLabel = MakeText ( "Label" , new Vector2 ( 0f , 0f ) , new Vector2 ( 1f , 1f ) , new Vector2 ( 0.5f , 0.5f ) , Vector2 . zero , Vector2 . zero , 30 , TextAnchor . MiddleCenter ) ;
skillLabel . transform . SetParent ( skill . transform , false ) ;
skillLabel . text = "스킬\n(추후)" ;
skillLabel . color = new Color ( 0.75f , 0.75f , 0.75f ) ;
2026-11-03 19:29:16 +00:00
// 미니맵 (전장 비율 · 탭하면 그 지점으로 카메라 이동)
Vector2 bs = battle . Config . boardSize ;
float mmW = 180f ;
float mmH = mmW * bs . y / Mathf . Max ( 0.01f , bs . x ) ;
minimapBg = MakeImage ( "Minimap" , transform , new Vector2 ( 1f , 1f ) , new Vector2 ( 1f , 1f ) , new Vector2 ( 1f , 1f ) , new Vector2 ( - 30f , - 215f ) , new Vector2 ( mmW , mmH ) , new Color ( 0.06f , 0.1f , 0.06f , 0.8f ) ) ;
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 ) ) ;
2026-11-03 19:10:10 +00:00
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 ;
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 ) ;
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 ) ;
}
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 ) ;
var rt = go . AddComponent < RectTransform > ( ) ;
rt . anchorMin = anchorMin ;
rt . anchorMax = anchorMax ;
rt . pivot = pivot ;
rt . anchoredPosition = anchoredPos ;
rt . sizeDelta = size ;
var t = go . AddComponent < Text > ( ) ;
t . font = TextUtil . Font ;
t . fontSize = fontSize ;
t . alignment = align ;
t . color = Color . white ;
t . horizontalOverflow = HorizontalWrapMode . Overflow ;
t . verticalOverflow = VerticalWrapMode . Overflow ;
t . raycastTarget = false ;
var sh = go . AddComponent < Shadow > ( ) ;
sh . effectDistance = new Vector2 ( 2f , - 2f ) ;
sh . effectColor = new Color ( 0f , 0f , 0f , 0.8f ) ;
return t ;
}
Image MakeImage ( string name , Transform parent , Vector2 anchorMin , Vector2 anchorMax , Vector2 pivot , Vector2 anchoredPos , Vector2 size , Color color )
{
var go = new GameObject ( name ) ;
go . transform . SetParent ( parent , false ) ;
var rt = go . AddComponent < RectTransform > ( ) ;
rt . anchorMin = anchorMin ;
rt . anchorMax = anchorMax ;
rt . pivot = pivot ;
rt . anchoredPosition = anchoredPos ;
rt . sizeDelta = size ;
var img = go . AddComponent < Image > ( ) ;
img . color = color ;
img . raycastTarget = false ;
return img ;
}
public void SetSelected ( Unit u )
{
selected = u ;
}
2026-11-03 19:29:16 +00:00
/// <summary>화면 점이 미니맵 안이면 해당 월드 좌표를 돌려준다.</summary>
public bool TryMinimapTap ( Vector2 screen , out Vector2 world )
{
world = Vector2 . zero ;
if ( minimapBg = = null | | battle = = null | | battle . Board = = null ) return false ;
var rt = minimapBg . rectTransform ;
if ( ! RectTransformUtility . RectangleContainsScreenPoint ( rt , screen , uiCam ) ) return false ;
Vector2 local ;
if ( ! RectTransformUtility . ScreenPointToLocalPointInRectangle ( rt , screen , uiCam , out local ) ) return false ;
var r = rt . rect ;
float nx = Mathf . Clamp01 ( ( local . x - r . xMin ) / r . width ) ;
float ny = Mathf . Clamp01 ( ( local . y - r . yMin ) / r . height ) ;
var b = battle . Board . Rect ;
world = new Vector2 ( b . xMin + nx * b . width , b . yMin + ny * b . height ) ;
return true ;
}
2026-11-03 19:10:10 +00:00
void LateUpdate ( )
{
if ( battle = = null | | battle . Config = = null | | battle . Config . stages . Length = = 0 ) return ;
Refresh ( ) ;
2026-11-03 19:29:16 +00:00
RefreshMinimap ( ) ;
2026-11-03 19:10:10 +00:00
}
void Refresh ( )
{
var stage = battle . Stage ;
stageText . text = "제" + ( battle . StageIndex + 1 ) + "전투 · " + stage . title + ( string . IsNullOrEmpty ( stage . subtitle ) ? "" : " " + stage . subtitle ) ;
countText . text = "아군 " + battle . AliveCount ( Faction . Player ) + " · 적 " + battle . AliveCount ( Faction . Enemy ) ;
switch ( battle . Phase )
{
case BattlePhase . PlayerTurn : turnText . text = "아군 턴 " + battle . TurnNumber ; break ;
case BattlePhase . Resolving : turnText . text = "진행 중…" ; break ;
case BattlePhase . EnemyTurn : turnText . text = "적군 턴 " + battle . TurnNumber ; break ;
case BattlePhase . StageClear : turnText . text = "승리" ; break ;
case BattlePhase . Defeat : turnText . text = "패배" ; break ;
default : turnText . text = "" ; break ;
}
messageText . text = battle . LastMessage ;
if ( battle . Phase = = BattlePhase . PlayerTurn )
{
hintText . text = battle . OnlyBonusMovesRemain ( )
? "추가 이동: 유닛을 당겨 이동 · 빈 곳을 탭하면 건너뜀"
2026-11-03 19:29:16 +00:00
: "유닛을 잡고 뒤로 당겼다 놓기 = 이동·공격 · 빈 곳 드래그 = 전장 이동" ;
2026-11-03 19:10:10 +00:00
}
else
{
hintText . text = "" ;
}
if ( selected ! = null & & selected . IsAlive )
{
var u = selected ;
string side = u . Faction = = Faction . Player ? "아군" : "적군" ;
string line1 = side + " · " + u . DisplayName + " · " + UnitTypeInfo . Korean ( u . Type ) + ( u . IsBoss ? " (장수)" : "" ) ;
string line2 = "HP " + u . Hp + "/" + u . MaxHp + " 공 " + u . Atk + " 방 " + u . Def ;
string line3 = "슬링 " + u . SlingMaxDistance . ToString ( "0.#" ) + ( u . Type = = UnitType . Archer ? " 사거리 " + u . AttackRange . ToString ( "0.#" ) : "" ) + ( u . Type = = UnitType . Spear ? " 견제 반경 " + u . AttackRange . ToString ( "0.#" ) : "" ) ;
string state = "" ;
if ( u . Faction = = Faction . Player )
{
if ( u . BonusMoveAvailable ) state = " [추가 이동]" ;
else if ( u . ActedThisTurn ) state = " [행동 완료]" ;
}
if ( u . Overwatch ) state + = " [대기 중]" ;
unitText . text = line1 + "\n" + line2 + "\n" + line3 + state ;
}
else
{
unitText . text = "유닛을 선택하세요" ;
}
bool result = battle . Phase = = BattlePhase . StageClear | | battle . Phase = = BattlePhase . Defeat ;
if ( resultOverlay . activeSelf ! = result ) resultOverlay . SetActive ( result ) ;
if ( result )
{
bool win = battle . Phase = = BattlePhase . StageClear ;
resultTitle . text = win ? "승리" : "패배" ;
resultTitle . color = win ? new Color ( 1f , 0.9f , 0.4f ) : new Color ( 1f , 0.5f , 0.5f ) ;
resultSub . text = win
? stage . title + " 제압!\n화면을 탭하면 다음 전투로"
: stage . title + " 에서 패배\n화면을 탭하면 다시 도전" ;
}
}
2026-11-03 19:29:16 +00:00
void RefreshMinimap ( )
{
if ( minimapBg = = null | | battle . Board = = null ) return ;
var r = minimapBg . rectTransform . rect ;
var b = battle . Board . Rect ;
float sx = r . width / b . width ;
float sy = r . height / b . height ;
// 카메라 시야 사각형
if ( camCtrl ! = null )
{
var v = camCtrl . ViewRect ;
float x0 = Mathf . Clamp ( ( v . xMin - b . xMin ) * sx , 0f , r . width ) ;
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 ) ) ;
}
// 유닛 점
foreach ( var u in battle . Units )
{
if ( u = = null ) continue ;
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 ,
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 ) ;
minimapDots [ u ] = dot ;
}
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 ) ;
}
// 사라진 유닛 정리
var dead = new List < Unit > ( ) ;
foreach ( var kv in minimapDots )
{
if ( kv . Key = = null ) dead . Add ( kv . Key ) ;
}
foreach ( var k in dead )
{
if ( minimapDots [ k ] ! = null ) Destroy ( minimapDots [ k ] . gameObject ) ;
minimapDots . Remove ( k ) ;
}
}
2026-11-03 19:10:10 +00:00
}
}