Projext/Assets/02_Scripts/UI/Boss/BossHealthBarUI.cs
hydrozen e989d20668 카툰 쉐이더 추가 + 중복 스크립트 수정 + 전체 업데이트
- ToonPostProcess.shader: 횃불 고딕 스타일 후처리 쉐이더 (Built-in RP)
- ToonCameraEffect.cs: 카메라 자동 부착 후처리 스크립트
- 중복 UI 스크립트 제거 (MenuIntroController, ToggleCustom)
- 씬, 프리팹, 애니메이션 등 전체 업데이트

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 12:31:16 +09:00

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(); // 구분선 복원
}
}