288 lines
11 KiB
C#
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
|
|
}
|
|
}
|