288 lines
11 KiB
C#

using UnityEngine;
using UnityEngine.UI;
namespace Streamingle.Contents.BossRaid
{
/// <summary>
/// 보스 HP바 UI. 고전 RPG (포켓몬) 스타일.
/// 얇고 긴 게이지 + 이름판 + HP 수치.
/// </summary>
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>();
canvas.renderMode = RenderMode.ScreenSpaceOverlay;
canvas.sortingOrder = 100;
var scaler = canvasObj.AddComponent<CanvasScaler>();
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<RectTransform>();
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<Image>();
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.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>();
outline.effectColor = new Color(0f, 0f, 0f, 0.95f);
outline.effectDistance = new Vector2(1f, -1f);
return text;
}
#endregion
}
}