using UnityEngine; using UnityEngine.UI; namespace Streamingle.Contents.BossRaid { /// /// 보스 HP바 UI. 고전 RPG (포켓몬) 스타일. /// 얇고 긴 게이지 + 이름판 + HP 수치. /// public class BossHPBar : MonoBehaviour { #region Fields [Header("UI 참조 (자동 생성 가능)")] [SerializeField] private Canvas canvas; [SerializeField] private Image hpFill; [SerializeField] private Image hpDamageFill; [SerializeField] private Text bossNameText; [SerializeField] private Text hpText; [SerializeField] private Text hpValueText; [Header("설정")] [SerializeField] private float smoothSpeed = 5f; [SerializeField] private float damageShowDelay = 0.4f; [SerializeField] private float damageSmooth = 3f; [Header("색상")] [SerializeField] private Color hpColorHigh = new Color(0.18f, 0.82f, 0.28f); [SerializeField] private Color hpColorMid = new Color(0.95f, 0.78f, 0.1f); [SerializeField] private Color hpColorLow = new Color(0.92f, 0.2f, 0.15f); [SerializeField] private Color damageTrailColor = new Color(0.9f, 0.35f, 0.1f, 0.9f); private float _targetRatio = 1f; private float _currentDisplayRatio = 1f; private float _damageDisplayRatio = 1f; private float _damageTimer; private bool _isVisible; private Font _font; private RectTransform _hpFillRect; private RectTransform _dmgFillRect; private int _maxHP; // 프레임 색상 (고전 RPG 도트 느낌) private static readonly Color FrameOuter = new Color(0.12f, 0.12f, 0.18f, 1f); private static readonly Color FrameInner = new Color(0.35f, 0.35f, 0.42f, 1f); private static readonly Color FrameHighlight = new Color(0.55f, 0.55f, 0.65f, 0.5f); private static readonly Color NameplateBG = new Color(0.08f, 0.08f, 0.15f, 0.92f); private static readonly Color GaugeBG = new Color(0.02f, 0.02f, 0.05f, 1f); #endregion #region Unity Messages private void Awake() { _font = BossRaidFontLoader.Load(); if (canvas == null || hpFill == null) CreateUI(); Hide(); } private void Update() { if (!_isVisible) return; _currentDisplayRatio = Mathf.Lerp(_currentDisplayRatio, _targetRatio, Time.deltaTime * smoothSpeed); SetFillWidth(_hpFillRect, _currentDisplayRatio); hpFill.color = GetHPColor(_currentDisplayRatio); _damageTimer -= Time.deltaTime; if (_damageTimer <= 0f) _damageDisplayRatio = Mathf.Lerp(_damageDisplayRatio, _currentDisplayRatio, Time.deltaTime * damageSmooth); SetFillWidth(_dmgFillRect, _damageDisplayRatio); if (hpText != null) { int displayPercent = Mathf.RoundToInt(_currentDisplayRatio * 100f); hpText.text = $"{displayPercent}%"; } if (hpValueText != null) { int displayHP = Mathf.RoundToInt(_currentDisplayRatio * _maxHP); hpValueText.text = $"{displayHP} / {_maxHP}"; } } #endregion #region Public Methods public void Show(string bossName, float hpRatio = 1f, int maxHP = 0) { _isVisible = true; _targetRatio = hpRatio; _currentDisplayRatio = hpRatio; _damageDisplayRatio = hpRatio; _maxHP = maxHP; if (bossNameText != null) bossNameText.text = bossName; if (canvas != null) canvas.gameObject.SetActive(true); } public void Hide() { _isVisible = false; if (canvas != null) canvas.gameObject.SetActive(false); } public void SetHP(float ratio, int currentHP = -1, int maxHP = -1) { _targetRatio = Mathf.Clamp01(ratio); _damageTimer = damageShowDelay; if (maxHP > 0) _maxHP = maxHP; } #endregion #region Private Methods private void SetFillWidth(RectTransform rect, float ratio) { if (rect == null) return; rect.anchorMax = new Vector2(Mathf.Clamp01(ratio), rect.anchorMax.y); } private Color GetHPColor(float ratio) { if (ratio > 0.5f) return Color.Lerp(hpColorMid, hpColorHigh, (ratio - 0.5f) * 2f); else return Color.Lerp(hpColorLow, hpColorMid, ratio * 2f); } private void CreateUI() { // ━━━ Canvas ━━━ var canvasObj = new GameObject("BossRaid_HPBar"); canvasObj.transform.SetParent(transform); canvas = canvasObj.AddComponent(); canvas.renderMode = RenderMode.ScreenSpaceOverlay; canvas.sortingOrder = 100; var scaler = canvasObj.AddComponent(); scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize; scaler.referenceResolution = new Vector2(1920, 1080); // ━━━ 상단 영역 전체 ━━━ var topArea = R("TopArea", canvasObj.transform, new Vector2(0.1f, 0.92f), new Vector2(0.9f, 0.98f)); // ━━━ 이름판 (왼쪽 탭 형태) ━━━ // 외곽 var nameOuter = R("NameOuter", topArea, new Vector2(0f, 0f), new Vector2(0.22f, 1f)); Img(nameOuter, FrameOuter); // 내부 var nameInner = R("NameInner", nameOuter, new Vector2(0f, 0f), new Vector2(1f, 1f), new Vector2(2f, 2f), new Vector2(-2f, -2f)); Img(nameInner, NameplateBG); // 이름 텍스트 var nameTextR = R("NameText", nameInner, new Vector2(0f, 0f), new Vector2(1f, 1f), new Vector2(8f, 0f), new Vector2(-4f, 0f)); bossNameText = AddText(nameTextR.gameObject, "BOSS", 20, Color.white, TextAnchor.MiddleLeft, FontStyle.Bold); // ━━━ HP 게이지 영역 (이름판 오른쪽 ~ 끝) ━━━ // 외곽 프레임 (3중 테두리 — 도트 RPG 느낌) var gaugeOuter = R("GaugeOuter", topArea, new Vector2(0.22f, 0f), new Vector2(1f, 1f), new Vector2(-1f, 0f), Vector2.zero); Img(gaugeOuter, FrameOuter); // 중간 프레임 var gaugeMid = R("GaugeMid", gaugeOuter, Vector2.zero, Vector2.one, new Vector2(2f, 2f), new Vector2(-2f, -2f)); Img(gaugeMid, FrameInner); // 내부 프레임 var gaugeInner = R("GaugeInner", gaugeMid, Vector2.zero, Vector2.one, new Vector2(2f, 2f), new Vector2(-2f, -2f)); Img(gaugeInner, FrameOuter); // 게이지 배경 (검정) var gaugeBG = R("GaugeBG", gaugeInner, Vector2.zero, Vector2.one, new Vector2(2f, 2f), new Vector2(-2f, -2f)); Img(gaugeBG, GaugeBG); // ━━━ 데미지 트레일 ━━━ var dmgRect = R("DmgFill", gaugeBG, Vector2.zero, Vector2.one); hpDamageFill = Img(dmgRect, damageTrailColor); _dmgFillRect = dmgRect; // ━━━ HP Fill ━━━ var fillRect = R("HPFill", gaugeBG, Vector2.zero, Vector2.one); hpFill = Img(fillRect, hpColorHigh); _hpFillRect = fillRect; // ━━━ 하이라이트 (게이지 위쪽 밝은 줄) ━━━ var hlRect = R("Highlight", fillRect, new Vector2(0f, 0.55f), new Vector2(1f, 0.9f), new Vector2(1f, 0f), new Vector2(-1f, 0f)); Img(hlRect, new Color(1f, 1f, 1f, 0.18f)); // ━━━ 하단 그림자 (게이지 아래쪽 어두운 줄) ━━━ var shRect = R("Shadow", fillRect, new Vector2(0f, 0.05f), new Vector2(1f, 0.3f), new Vector2(1f, 0f), new Vector2(-1f, 0f)); Img(shRect, new Color(0f, 0f, 0f, 0.2f)); // ━━━ 프레임 위 하이라이트 라인 (상단 엣지) ━━━ var topLineRect = R("TopLine", gaugeMid, new Vector2(0f, 0.85f), new Vector2(1f, 1f), new Vector2(2f, 0f), new Vector2(-2f, 0f)); Img(topLineRect, FrameHighlight); // ━━━ HP % 텍스트 (게이지 중앙) ━━━ var pctRect = R("HPPercent", gaugeOuter, Vector2.zero, Vector2.one, new Vector2(4f, 0f), new Vector2(-4f, 0f)); hpText = AddText(pctRect.gameObject, "100%", 18, Color.white, TextAnchor.MiddleCenter, FontStyle.Bold); // ━━━ HP 수치 (게이지 오른쪽 바깥) ━━━ var valRect = R("HPValue", topArea, new Vector2(0.82f, -0.6f), new Vector2(1f, 0f), Vector2.zero, new Vector2(-4f, 0f)); hpValueText = AddText(valRect.gameObject, "1000 / 1000", 14, new Color(0.7f, 0.7f, 0.75f), TextAnchor.MiddleRight, FontStyle.Normal); } // ── 유틸 ── private RectTransform R(string name, Transform parent, Vector2 anchorMin, Vector2 anchorMax, Vector2? offsetMin = null, Vector2? offsetMax = null) { var obj = new GameObject(name); obj.transform.SetParent(parent, false); var rt = obj.AddComponent(); rt.anchorMin = anchorMin; rt.anchorMax = anchorMax; rt.offsetMin = offsetMin ?? Vector2.zero; rt.offsetMax = offsetMax ?? Vector2.zero; return rt; } private Image Img(RectTransform rt, Color color) { var img = rt.gameObject.AddComponent(); img.color = color; img.raycastTarget = false; return img; } private Text AddText(GameObject go, string content, int fontSize, Color color, TextAnchor anchor, FontStyle style) { var text = go.AddComponent(); text.text = content; text.fontSize = fontSize; text.color = color; text.alignment = anchor; text.fontStyle = style; text.raycastTarget = false; if (_font != null) text.font = _font; var outline = go.AddComponent(); outline.effectColor = new Color(0f, 0f, 0f, 0.95f); outline.effectDistance = new Vector2(1f, -1f); return text; } #endregion } }