278 lines
18 KiB
C#
278 lines
18 KiB
C#
|
|
using UnityEngine; // 임포트할거에요 -> Unity 엔진 기본 기능을
|
||
|
|
using UnityEngine.UI; // 임포트할거에요 -> Unity UI 시스템을
|
||
|
|
using TMPro; // 임포트할거에요 -> TextMeshPro를
|
||
|
|
|
||
|
|
// ══════════════════════════════════════════════════════════════════════
|
||
|
|
// BossHealthBarUI — 보스 체력바 HUD (메인 플레이어 인터페이스용)
|
||
|
|
//
|
||
|
|
// ★ HP 바 방식: RectTransform anchorMax.x 조절 (fillAmount 미사용)
|
||
|
|
// → Image.Type에 관계없이 바의 물리적 너비가 줄어듦
|
||
|
|
// → 가장 확실하고 호환성 높은 방식
|
||
|
|
//
|
||
|
|
// 배치: 메인 인게임 Canvas의 자식으로 배치
|
||
|
|
// NorcielBoss에서 bossHealthBar에 이 오브젝트의 루트를 연결
|
||
|
|
// ══════════════════════════════════════════════════════════════════════
|
||
|
|
public class BossHealthBarUI : MonoBehaviour // 클래스를 정의할거에요 -> 보스 체력바 UI 컴포넌트를
|
||
|
|
{
|
||
|
|
// ─────────────────────────────────────────────────────────────
|
||
|
|
// Inspector 설정 — 참조
|
||
|
|
// ─────────────────────────────────────────────────────────────
|
||
|
|
|
||
|
|
[Header("보스 연결")]
|
||
|
|
[SerializeField] private MonsterClass bossMonster; // 변수를 선언할거에요 -> 보스 MonsterClass 참조를
|
||
|
|
[SerializeField] private BossPhaseManager bossPhaseManager; // 변수를 선언할거에요 -> 페이즈 매니저 참조를
|
||
|
|
|
||
|
|
// ─────────────────────────────────────────────────────────────
|
||
|
|
// Inspector 설정 — UI 요소
|
||
|
|
// ─────────────────────────────────────────────────────────────
|
||
|
|
|
||
|
|
[Header("체력바 UI 요소")]
|
||
|
|
[Tooltip("실제 HP를 표시하는 Fill Image (anchorMax.x로 너비 조절)")]
|
||
|
|
[SerializeField] private Image hpFillImage; // 변수를 선언할거에요 -> HP Fill 이미지를
|
||
|
|
|
||
|
|
[Tooltip("HP 감소 시 부드럽게 따라가는 뒤쪽 바 (데미지 연출용)")]
|
||
|
|
[SerializeField] private Image hpDamageFillImage; // 변수를 선언할거에요 -> 데미지 연출 바를
|
||
|
|
|
||
|
|
[Tooltip("보스 이름 텍스트")]
|
||
|
|
[SerializeField] private TextMeshProUGUI bossNameText; // 변수를 선언할거에요 -> 보스 이름 텍스트를
|
||
|
|
|
||
|
|
[Tooltip("HP 수치 텍스트 (선택사항)")]
|
||
|
|
[SerializeField] private TextMeshProUGUI hpValueText; // 변수를 선언할거에요 -> HP 수치 텍스트를
|
||
|
|
|
||
|
|
[Tooltip("현재 페이즈 텍스트 (선택사항)")]
|
||
|
|
[SerializeField] private TextMeshProUGUI phaseText; // 변수를 선언할거에요 -> 페이즈 표시 텍스트를
|
||
|
|
|
||
|
|
[Header("프레임 & 장식")]
|
||
|
|
[SerializeField] private Image frameImage; // 변수를 선언할거에요 -> 프레임 장식 이미지를
|
||
|
|
[SerializeField] private Image bossIconImage; // 변수를 선언할거에요 -> 보스 아이콘 이미지를
|
||
|
|
|
||
|
|
[Header("페이즈 구분선")]
|
||
|
|
[SerializeField] private RectTransform phase2Divider; // 변수를 선언할거에요 -> Phase2 구분선을
|
||
|
|
[SerializeField] private RectTransform phase3Divider; // 변수를 선언할거에요 -> Phase3 구분선을
|
||
|
|
|
||
|
|
// ─────────────────────────────────────────────────────────────
|
||
|
|
// Inspector 설정 — 연출 파라미터
|
||
|
|
// ─────────────────────────────────────────────────────────────
|
||
|
|
|
||
|
|
[Header("연출 설정")]
|
||
|
|
[SerializeField] private float damageLerpSpeed = 2f; // 변수를 선언할거에요 -> 데미지 바 따라가기 속도를
|
||
|
|
[SerializeField] private string bossDisplayName = "Norciel"; // 변수를 선언할거에요 -> 보스 표시 이름을
|
||
|
|
|
||
|
|
[Header("페이즈별 바 색상 (다크 판타지 테마)")]
|
||
|
|
[SerializeField] private Color phase1Color = new Color(0.6f, 0.12f, 0.08f, 1f); // Phase1 (어두운 핏빛)
|
||
|
|
[SerializeField] private Color phase2Color = new Color(0.55f, 0.3f, 0.08f, 1f); // Phase2 (어두운 호박색)
|
||
|
|
[SerializeField] private Color phase3Color = new Color(0.5f, 0.08f, 0.2f, 1f); // Phase3 (어두운 진홍색)
|
||
|
|
[SerializeField] private Color damageBarColor = new Color(0.7f, 0.3f, 0.1f, 0.6f); // 데미지 바 (어두운 주황)
|
||
|
|
|
||
|
|
// ─────────────────────────────────────────────────────────────
|
||
|
|
// 내부 상태
|
||
|
|
// ─────────────────────────────────────────────────────────────
|
||
|
|
private float _targetRatio = 1f; // 변수를 초기화할거에요 -> HP 바 목표 비율을 (0~1)
|
||
|
|
private float _damageRatio = 1f; // 변수를 초기화할거에요 -> 데미지 바 현재 비율을
|
||
|
|
private RectTransform _hpFillRect; // 변수를 선언할거에요 -> HP Fill의 RectTransform 캐시를
|
||
|
|
private RectTransform _damageFillRect; // 변수를 선언할거에요 -> 데미지 Fill의 RectTransform 캐시를
|
||
|
|
private int _currentPhase = 1; // 변수를 초기화할거에요 -> 현재 페이즈를
|
||
|
|
|
||
|
|
// ─────────────────────────────────────────────────────────────
|
||
|
|
// 초기화
|
||
|
|
// ─────────────────────────────────────────────────────────────
|
||
|
|
|
||
|
|
private void OnEnable() // 활성화 시 실행할거에요 -> 초기화 + 이벤트 구독을
|
||
|
|
{
|
||
|
|
// RectTransform 캐싱
|
||
|
|
if (hpFillImage != null) // 조건이 맞으면 실행할거에요 -> HP 바가 있으면
|
||
|
|
_hpFillRect = hpFillImage.rectTransform; // 캐싱할거에요 -> RectTransform을
|
||
|
|
|
||
|
|
if (hpDamageFillImage != null) // 조건이 맞으면 실행할거에요 -> 데미지 바가 있으면
|
||
|
|
_damageFillRect = hpDamageFillImage.rectTransform; // 캐싱할거에요 -> RectTransform을
|
||
|
|
|
||
|
|
// ── 앵커 초기 세팅 (스트레치 기반 → anchorMax.x로 너비 제어) ──
|
||
|
|
SetupAnchor(_hpFillRect); // 실행할거에요 -> HP 바 앵커를 스트레치로
|
||
|
|
SetupAnchor(_damageFillRect); // 실행할거에요 -> 데미지 바 앵커를 스트레치로
|
||
|
|
|
||
|
|
// 색상 초기화
|
||
|
|
if (hpFillImage != null) hpFillImage.color = phase1Color; // 설정할거에요 -> Phase1 색상을
|
||
|
|
if (hpDamageFillImage != null) hpDamageFillImage.color = damageBarColor; // 설정할거에요 -> 데미지 바 색상을
|
||
|
|
|
||
|
|
// 이름 텍스트
|
||
|
|
if (bossNameText != null) bossNameText.text = bossDisplayName; // 설정할거에요 -> 보스 이름을
|
||
|
|
|
||
|
|
// 페이즈 초기화
|
||
|
|
UpdatePhaseText(1); // 실행할거에요 -> Phase 1 텍스트를
|
||
|
|
PositionPhaseDividers(); // 실행할거에요 -> 구분선 위치를
|
||
|
|
|
||
|
|
// 이벤트 구독
|
||
|
|
SubscribeEvents(); // 실행할거에요 -> 보스 이벤트 구독을
|
||
|
|
|
||
|
|
Debug.Log("[BossHealthBarUI] OnEnable — 앵커 기반 HP 바 초기화 완료"); // 로그를 찍을거에요
|
||
|
|
}
|
||
|
|
|
||
|
|
private void OnDisable() // 비활성화 시 실행할거에요 -> 이벤트 해제를
|
||
|
|
{
|
||
|
|
UnsubscribeEvents(); // 실행할거에요 -> 이벤트 해제를
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// RectTransform을 부모 스트레치 앵커로 세팅 (anchorMax.x로 너비 제어 준비)
|
||
|
|
/// anchorMin=(0,0), anchorMax=(1,1), offset=0 → 부모에 꽉 참
|
||
|
|
/// 이후 anchorMax.x를 0~1로 조절하면 우측에서 줄어듦
|
||
|
|
/// </summary>
|
||
|
|
private void SetupAnchor(RectTransform rect) // 함수를 정의할거에요 -> 앵커 초기 세팅을
|
||
|
|
{
|
||
|
|
if (rect == null) return; // 방어할거에요 -> null이면
|
||
|
|
|
||
|
|
rect.anchorMin = Vector2.zero; // 설정할거에요 -> 좌하단 (0,0)
|
||
|
|
rect.anchorMax = Vector2.one; // 설정할거에요 -> 우상단 (1,1) → 부모에 꽉 참
|
||
|
|
rect.offsetMin = Vector2.zero; // 설정할거에요 -> 여백 없음
|
||
|
|
rect.offsetMax = Vector2.zero; // 설정할거에요 -> 여백 없음
|
||
|
|
}
|
||
|
|
|
||
|
|
// ─────────────────────────────────────────────────────────────
|
||
|
|
// 이벤트 구독/해제
|
||
|
|
// ─────────────────────────────────────────────────────────────
|
||
|
|
|
||
|
|
private void SubscribeEvents() // 함수를 정의할거에요 -> 이벤트 구독을
|
||
|
|
{
|
||
|
|
if (bossMonster != null) // 조건이 맞으면 실행할거에요 -> 보스가 있으면
|
||
|
|
bossMonster.OnHealthChanged += OnBossHealthChanged; // 구독할거에요 -> HP 변경 이벤트를
|
||
|
|
|
||
|
|
if (bossPhaseManager != null) // 조건이 맞으면 실행할거에요 -> 페이즈 매니저가 있으면
|
||
|
|
bossPhaseManager.OnPhaseChanged += OnBossPhaseChanged; // 구독할거에요 -> 페이즈 변경 이벤트를
|
||
|
|
}
|
||
|
|
|
||
|
|
private void UnsubscribeEvents() // 함수를 정의할거에요 -> 이벤트 해제를
|
||
|
|
{
|
||
|
|
if (bossMonster != null) // 조건이 맞으면 실행할거에요 -> 보스가 있으면
|
||
|
|
bossMonster.OnHealthChanged -= OnBossHealthChanged; // 해제할거에요 -> HP 변경 이벤트를
|
||
|
|
|
||
|
|
if (bossPhaseManager != null) // 조건이 맞으면 실행할거에요 -> 페이즈 매니저가 있으면
|
||
|
|
bossPhaseManager.OnPhaseChanged -= OnBossPhaseChanged; // 해제할거에요 -> 페이즈 변경 이벤트를
|
||
|
|
}
|
||
|
|
|
||
|
|
// ─────────────────────────────────────────────────────────────
|
||
|
|
// 런타임 연결
|
||
|
|
// ─────────────────────────────────────────────────────────────
|
||
|
|
|
||
|
|
public void BindBoss(MonsterClass boss, BossPhaseManager phaseManager = null) // 함수를 정의할거에요 -> 런타임 보스 연결을
|
||
|
|
{
|
||
|
|
UnsubscribeEvents(); // 실행할거에요 -> 기존 해제를
|
||
|
|
bossMonster = boss; // 할당할거에요 -> 보스를
|
||
|
|
bossPhaseManager = phaseManager; // 할당할거에요 -> 페이즈 매니저를
|
||
|
|
SubscribeEvents(); // 실행할거에요 -> 새 구독을
|
||
|
|
|
||
|
|
if (bossMonster != null) // 조건이 맞으면 실행할거에요 -> 유효하면
|
||
|
|
OnBossHealthChanged(bossMonster.GetCurrentHP(), bossMonster.GetMaxHP()); // 즉시 갱신할거에요
|
||
|
|
}
|
||
|
|
|
||
|
|
// ─────────────────────────────────────────────────────────────
|
||
|
|
// 매 프레임 — 데미지 바 천천히 따라가기
|
||
|
|
// ─────────────────────────────────────────────────────────────
|
||
|
|
|
||
|
|
private void Update() // 매 프레임 실행할거에요 -> 데미지 바 애니메이션을
|
||
|
|
{
|
||
|
|
// 데미지 바만 천천히 따라감 (HP 바는 즉시 반응)
|
||
|
|
if (_damageFillRect != null && _damageRatio > _targetRatio) // 조건이 맞으면 실행할거에요 -> 데미지 바가 HP 바보다 높으면
|
||
|
|
{
|
||
|
|
_damageRatio = Mathf.Lerp(_damageRatio, _targetRatio, Time.deltaTime * damageLerpSpeed); // 보간할거에요 -> 천천히 줄임
|
||
|
|
|
||
|
|
// anchorMax.x를 비율로 설정 → 바 너비가 물리적으로 줄어듦
|
||
|
|
Vector2 aMax = _damageFillRect.anchorMax; // 가져올거에요 -> 현재 anchorMax를
|
||
|
|
aMax.x = _damageRatio; // 설정할거에요 -> X를 데미지 비율로
|
||
|
|
_damageFillRect.anchorMax = aMax; // 적용할거에요 -> anchorMax를
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// ─────────────────────────────────────────────────────────────
|
||
|
|
// 이벤트 콜백 — HP 변경
|
||
|
|
// ─────────────────────────────────────────────────────────────
|
||
|
|
|
||
|
|
private void OnBossHealthChanged(float currentHP, float maxHP) // 콜백을 정의할거에요 -> HP 변경 처리를
|
||
|
|
{
|
||
|
|
if (maxHP <= 0f) return; // 방어할거에요 -> 0 나누기 방지
|
||
|
|
|
||
|
|
float ratio = Mathf.Clamp01(currentHP / maxHP); // 계산할거에요 -> HP 비율을 (0~1)
|
||
|
|
_targetRatio = ratio; // 저장할거에요 -> 목표 비율을
|
||
|
|
|
||
|
|
// ★ HP 바 즉시 반응: anchorMax.x를 HP 비율로 설정
|
||
|
|
// anchorMax.x = 0.7 → 바가 부모 너비의 70%만큼만 표시됨
|
||
|
|
if (_hpFillRect != null) // 조건이 맞으면 실행할거에요 -> HP 바가 있으면
|
||
|
|
{
|
||
|
|
Vector2 aMax = _hpFillRect.anchorMax; // 가져올거에요 -> 현재 anchorMax를
|
||
|
|
aMax.x = ratio; // 설정할거에요 -> X를 HP 비율로 (0.7이면 70% 너비)
|
||
|
|
_hpFillRect.anchorMax = aMax; // 적용할거에요 -> anchorMax를 → 바가 물리적으로 줄어듦
|
||
|
|
}
|
||
|
|
|
||
|
|
// HP 수치 텍스트 갱신
|
||
|
|
if (hpValueText != null) // 조건이 맞으면 실행할거에요 -> 텍스트가 있으면
|
||
|
|
hpValueText.text = $"{Mathf.Max(0f, currentHP):F0} / {maxHP:F0}"; // 갱신할거에요 -> "현재/최대" 형식으로
|
||
|
|
|
||
|
|
Debug.Log($"[BossHealthBarUI] HP={currentHP:F0}/{maxHP:F0} ratio={ratio:F3} anchorMax.x={ratio:F3}"); // 디버그 로그
|
||
|
|
}
|
||
|
|
|
||
|
|
// ─────────────────────────────────────────────────────────────
|
||
|
|
// 이벤트 콜백 — 페이즈 변경
|
||
|
|
// ─────────────────────────────────────────────────────────────
|
||
|
|
|
||
|
|
private void OnBossPhaseChanged(int newPhase) // 콜백을 정의할거에요 -> 페이즈 변경 처리를
|
||
|
|
{
|
||
|
|
_currentPhase = newPhase; // 저장할거에요 -> 현재 페이즈를
|
||
|
|
UpdateBarColor(newPhase); // 실행할거에요 -> 바 색상 변경을
|
||
|
|
UpdatePhaseText(newPhase); // 실행할거에요 -> 텍스트 변경을
|
||
|
|
HidePassedDividers(newPhase); // 실행할거에요 -> 구분선 정리를
|
||
|
|
}
|
||
|
|
|
||
|
|
// ─────────────────────────────────────────────────────────────
|
||
|
|
// 페이즈 유틸
|
||
|
|
// ─────────────────────────────────────────────────────────────
|
||
|
|
|
||
|
|
private void UpdateBarColor(int phase) // 함수를 정의할거에요 -> 바 색상 변경을
|
||
|
|
{
|
||
|
|
if (hpFillImage == null) return; // 방어할거에요
|
||
|
|
switch (phase)
|
||
|
|
{
|
||
|
|
case 1: hpFillImage.color = phase1Color; break; // Phase1 핏빛
|
||
|
|
case 2: hpFillImage.color = phase2Color; break; // Phase2 호박색
|
||
|
|
case 3: hpFillImage.color = phase3Color; break; // Phase3 진홍색
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private void UpdatePhaseText(int phase) // 함수를 정의할거에요 -> 페이즈 텍스트를
|
||
|
|
{
|
||
|
|
if (phaseText != null) phaseText.text = $"Phase {phase}"; // 설정할거에요
|
||
|
|
}
|
||
|
|
|
||
|
|
private void HidePassedDividers(int phase) // 함수를 정의할거에요 -> 지나간 구분선 숨기기를
|
||
|
|
{
|
||
|
|
if (phase >= 2 && phase2Divider != null) phase2Divider.gameObject.SetActive(false); // Phase2 구분선 숨김
|
||
|
|
if (phase >= 3 && phase3Divider != null) phase3Divider.gameObject.SetActive(false); // Phase3 구분선 숨김
|
||
|
|
}
|
||
|
|
|
||
|
|
private void PositionPhaseDividers() // 함수를 정의할거에요 -> 구분선 배치를
|
||
|
|
{
|
||
|
|
// 구분선은 앵커 기반으로 배치 (Creator에서 이미 앵커로 설정됨)
|
||
|
|
if (phase2Divider != null) phase2Divider.gameObject.SetActive(true); // 활성화
|
||
|
|
if (phase3Divider != null) phase3Divider.gameObject.SetActive(true); // 활성화
|
||
|
|
}
|
||
|
|
|
||
|
|
// ─────────────────────────────────────────────────────────────
|
||
|
|
// 공개 유틸리티
|
||
|
|
// ─────────────────────────────────────────────────────────────
|
||
|
|
|
||
|
|
public void Show() { gameObject.SetActive(true); } // 표시
|
||
|
|
public void Hide() { gameObject.SetActive(false); } // 숨김
|
||
|
|
|
||
|
|
public void ResetBar() // 함수를 정의할거에요 -> 바 초기화를
|
||
|
|
{
|
||
|
|
_targetRatio = 1f; // 리셋
|
||
|
|
_damageRatio = 1f; // 리셋
|
||
|
|
_currentPhase = 1; // 리셋
|
||
|
|
|
||
|
|
if (_hpFillRect != null) { _hpFillRect.anchorMax = Vector2.one; _hpFillRect.offsetMax = Vector2.zero; } // HP 바 100%
|
||
|
|
if (_damageFillRect != null) { _damageFillRect.anchorMax = Vector2.one; _damageFillRect.offsetMax = Vector2.zero; } // 데미지 바 100%
|
||
|
|
|
||
|
|
UpdateBarColor(1); // Phase1 색상
|
||
|
|
UpdatePhaseText(1); // Phase1 텍스트
|
||
|
|
PositionPhaseDividers(); // 구분선 복원
|
||
|
|
}
|
||
|
|
}
|