- Assets/_Game/ 하위로 게임 에셋 통합 - External/ 패키지 벤더별 분류 (Synty, Animations, UI) - 에셋 네이밍 컨벤션 확립 및 적용 (Data_Skill_, Data_SkillEffect_, Prefab_, Anim_, Model_, BT_ 등) - pre-commit hook으로 네이밍 컨벤션 자동 검사 추가 - RESTRUCTURE_CHECKLIST.md 작성 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
249 lines
7.4 KiB
C#
249 lines
7.4 KiB
C#
using System;
|
|
using UnityEngine;
|
|
using UnityEngine.UI;
|
|
using TMPro;
|
|
|
|
using Colosseum.Enemy;
|
|
|
|
namespace Colosseum.UI
|
|
{
|
|
/// <summary>
|
|
/// 보스 체력바 UI 컴포넌트.
|
|
/// BossEnemy의 체력 변화를 자동으로 UI에 반영합니다.
|
|
/// </summary>
|
|
public class BossHealthBarUI : MonoBehaviour
|
|
{
|
|
[Header("References")]
|
|
[Tooltip("체력 슬라이더 (없으면 자동 검색)")]
|
|
[SerializeField] private Slider healthSlider;
|
|
|
|
[Tooltip("체력 텍스트 (예: '999 / 999')")]
|
|
[SerializeField] private TMP_Text healthText;
|
|
|
|
[Tooltip("보스 이름 텍스트")]
|
|
[SerializeField] private TMP_Text bossNameText;
|
|
|
|
[Header("Target")]
|
|
[Tooltip("추적할 보스 (런타임에 설정 가능)")]
|
|
[SerializeField] private BossEnemy targetBoss;
|
|
|
|
[Header("Settings")]
|
|
[Tooltip("보스 사망 시 UI 숨김 여부")]
|
|
[SerializeField] private bool hideOnDeath = true;
|
|
|
|
[Tooltip("슬라이더 값 변환 속도")]
|
|
[Min(0f)] [SerializeField] private float lerpSpeed = 5f;
|
|
|
|
private float displayHealthRatio;
|
|
private float targetHealthRatio;
|
|
private bool isSubscribed;
|
|
private bool isSubscribedToStaticEvent;
|
|
|
|
/// <summary>
|
|
/// 현재 추적 중인 보스
|
|
/// </summary>
|
|
public BossEnemy TargetBoss => targetBoss;
|
|
|
|
/// <summary>
|
|
/// 보스 수동 설정 (런타임에서 호출)
|
|
/// </summary>
|
|
public void SetBoss(BossEnemy boss)
|
|
{
|
|
// 기존 보스 이벤트 구독 해제
|
|
UnsubscribeFromBoss();
|
|
|
|
targetBoss = boss;
|
|
|
|
// 새 보스 이벤트 구독
|
|
SubscribeToBoss();
|
|
|
|
// 초기 UI 업데이트
|
|
if (targetBoss != null)
|
|
{
|
|
UpdateBossName();
|
|
UpdateHealthImmediate();
|
|
gameObject.SetActive(true);
|
|
}
|
|
else
|
|
{
|
|
gameObject.SetActive(false);
|
|
}
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// 보스 스폰 이벤트 핸들러
|
|
/// </summary>
|
|
private void OnBossSpawned(BossEnemy boss)
|
|
{
|
|
if (boss == null)
|
|
return;
|
|
|
|
SetBoss(boss);
|
|
}
|
|
|
|
private void Awake()
|
|
{
|
|
// 컴포넌트 자동 검색
|
|
if (healthSlider == null)
|
|
healthSlider = GetComponentInChildren<Slider>();
|
|
|
|
if (healthText == null)
|
|
healthText = transform.Find("SliderBox/Label_HP")?.GetComponent<TMP_Text>();
|
|
|
|
if (bossNameText == null)
|
|
bossNameText = transform.Find("SliderBox/Label_BossName")?.GetComponent<TMP_Text>();
|
|
}
|
|
|
|
private void OnEnable()
|
|
{
|
|
// 정적 이벤트 구독 (보스 스폰 자동 감지)
|
|
if (!isSubscribedToStaticEvent)
|
|
{
|
|
BossEnemy.OnBossSpawned += OnBossSpawned;
|
|
isSubscribedToStaticEvent = true;
|
|
}
|
|
}
|
|
private void OnDisable()
|
|
{
|
|
// 정적 이벤트 구독 해제
|
|
if (isSubscribedToStaticEvent)
|
|
{
|
|
BossEnemy.OnBossSpawned -= OnBossSpawned;
|
|
isSubscribedToStaticEvent = false;
|
|
}
|
|
}
|
|
private void Start()
|
|
{
|
|
// 이미 활성화된 보스가 있으면 연결
|
|
if (BossEnemy.ActiveBoss != null)
|
|
{
|
|
SetBoss(BossEnemy.ActiveBoss);
|
|
}
|
|
// 인스펙터에서 설정된 보스가 있으면 구독
|
|
else if (targetBoss != null)
|
|
{
|
|
SubscribeToBoss();
|
|
UpdateBossName();
|
|
UpdateHealthImmediate();
|
|
}
|
|
else
|
|
{
|
|
// 보스가 없으면 비활성화 (이벤트 대기)
|
|
gameObject.SetActive(false);
|
|
}
|
|
}
|
|
private void Update()
|
|
{
|
|
// 부드러운 체력바 애니메이션
|
|
if (!Mathf.Approximately(displayHealthRatio, targetHealthRatio))
|
|
{
|
|
displayHealthRatio = Mathf.Lerp(displayHealthRatio, targetHealthRatio, lerpSpeed * Time.deltaTime);
|
|
|
|
if (Mathf.Abs(displayHealthRatio - targetHealthRatio) < 0.01f)
|
|
displayHealthRatio = targetHealthRatio;
|
|
|
|
UpdateSliderVisual();
|
|
}
|
|
}
|
|
private void OnDestroy()
|
|
{
|
|
UnsubscribeFromBoss();
|
|
|
|
// 정적 이벤트 구독 해제
|
|
if (isSubscribedToStaticEvent)
|
|
{
|
|
BossEnemy.OnBossSpawned -= OnBossSpawned;
|
|
isSubscribedToStaticEvent = false;
|
|
}
|
|
}
|
|
private void SubscribeToBoss()
|
|
{
|
|
if (targetBoss == null || isSubscribed)
|
|
return;
|
|
|
|
targetBoss.OnHealthChanged += OnBossHealthChanged;
|
|
targetBoss.OnDeath += OnBossDeath;
|
|
isSubscribed = true;
|
|
}
|
|
private void UnsubscribeFromBoss()
|
|
{
|
|
if (targetBoss == null || !isSubscribed)
|
|
return;
|
|
|
|
targetBoss.OnHealthChanged -= OnBossHealthChanged;
|
|
targetBoss.OnDeath -= OnBossDeath;
|
|
isSubscribed = false;
|
|
}
|
|
private void OnBossHealthChanged(float currentHealth, float maxHealth)
|
|
{
|
|
if (maxHealth <= 0f)
|
|
return;
|
|
|
|
targetHealthRatio = Mathf.Clamp01(currentHealth / maxHealth);
|
|
UpdateHealthText(currentHealth, maxHealth);
|
|
}
|
|
private void OnBossDeath()
|
|
{
|
|
if (hideOnDeath)
|
|
{
|
|
gameObject.SetActive(false);
|
|
}
|
|
}
|
|
private void UpdateHealthImmediate()
|
|
{
|
|
if (targetBoss == null)
|
|
return;
|
|
|
|
float currentHealth = targetBoss.CurrentHealth;
|
|
float maxHealth = targetBoss.MaxHealth;
|
|
|
|
if (maxHealth <= 0f)
|
|
return;
|
|
|
|
targetHealthRatio = Mathf.Clamp01(currentHealth / maxHealth);
|
|
displayHealthRatio = targetHealthRatio;
|
|
|
|
UpdateSliderVisual();
|
|
UpdateHealthText(currentHealth, maxHealth);
|
|
}
|
|
private void UpdateSliderVisual()
|
|
{
|
|
if (healthSlider != null)
|
|
{
|
|
healthSlider.value = displayHealthRatio;
|
|
}
|
|
}
|
|
private void UpdateHealthText(float currentHealth, float maxHealth)
|
|
{
|
|
if (healthText != null)
|
|
{
|
|
healthText.text = $"{Mathf.CeilToInt(currentHealth)} / {Mathf.CeilToInt(maxHealth)}";
|
|
}
|
|
}
|
|
private void UpdateBossName()
|
|
{
|
|
if (bossNameText == null || targetBoss == null)
|
|
return;
|
|
|
|
// EnemyData에서 보스 이름 가져오기
|
|
if (targetBoss.Data != null && !string.IsNullOrEmpty(targetBoss.Data.EnemyName))
|
|
{
|
|
bossNameText.text = targetBoss.Data.EnemyName;
|
|
}
|
|
else
|
|
{
|
|
// 폴백: GameObject 이름 사용
|
|
bossNameText.text = targetBoss.name;
|
|
}
|
|
}
|
|
#if UNITY_EDITOR
|
|
private void OnValidate()
|
|
{
|
|
if (healthSlider == null)
|
|
healthSlider = GetComponentInChildren<Slider>();
|
|
}
|
|
#endif
|
|
}
|
|
}
|