chore: Assets 디렉토리 구조 정리 및 네이밍 컨벤션 적용
- 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>
This commit is contained in:
303
Assets/_Game/Scripts/UI/AbnormalityListUI.cs
Normal file
303
Assets/_Game/Scripts/UI/AbnormalityListUI.cs
Normal file
@@ -0,0 +1,303 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using Colosseum.Abnormalities;
|
||||
|
||||
namespace Colosseum.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// 이상 상태 목록 UI 관리자
|
||||
/// 버프/디버프 목록을 표시하고 관리합니다.
|
||||
/// </summary>
|
||||
public class AbnormalityListUI : MonoBehaviour
|
||||
{
|
||||
[Header("Containers")]
|
||||
[Tooltip("버프 컨테이너")]
|
||||
[SerializeField] private Transform buffContainer;
|
||||
|
||||
[Tooltip("디버프 컨테이너")]
|
||||
[SerializeField] private Transform debuffContainer;
|
||||
|
||||
[Header("Prefab")]
|
||||
[Tooltip("이상 상태 슬롯 프리팹")]
|
||||
[SerializeField] private AbnormalitySlotUI slotPrefab;
|
||||
|
||||
[Header("Settings")]
|
||||
[Tooltip("최대 표시 개수")]
|
||||
[SerializeField] private int maxSlots = 10;
|
||||
|
||||
[Tooltip("자동으로 플레이어 추적")]
|
||||
[SerializeField] private bool autoFindPlayer = true;
|
||||
|
||||
// 추적 중인 AbnormalityManager
|
||||
private AbnormalityManager targetManager;
|
||||
|
||||
// 생성된 슬롯 풀
|
||||
private readonly List<AbnormalitySlotUI> slotPool = new List<AbnormalitySlotUI>();
|
||||
|
||||
// 현재 활성화된 슬롯 목록
|
||||
private readonly List<AbnormalitySlotUI> activeSlots = new List<AbnormalitySlotUI>();
|
||||
|
||||
// 이전 프레임의 효과 수 (변경 감지용)
|
||||
private int lastAbnormalityCount = -1;
|
||||
|
||||
private void Start()
|
||||
{
|
||||
if (autoFindPlayer)
|
||||
{
|
||||
// 로컬 플레이어 찾기
|
||||
FindLocalPlayer();
|
||||
}
|
||||
|
||||
// 슬롯 풀 초기화
|
||||
InitializeSlotPool();
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
// 이벤트 구독 해제
|
||||
if (targetManager != null)
|
||||
{
|
||||
targetManager.OnAbnormalityAdded -= OnAbnormalityAdded;
|
||||
targetManager.OnAbnormalityRemoved -= OnAbnormalityRemoved;
|
||||
targetManager.OnAbnormalitiesChanged -= OnAbnormalitiesChanged;
|
||||
}
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
// 타겟이 없으면 주기적으로 플레이어 찾기
|
||||
if (targetManager == null && autoFindPlayer && Time.frameCount % 30 == 0)
|
||||
{
|
||||
FindLocalPlayer();
|
||||
}
|
||||
|
||||
// 주기적으로 UI 갱신 (성능 최적화를 위해 매 프레임이 아닌 일정 간격으로)
|
||||
if (Time.frameCount % 10 == 0) // 10프레임마다 한 번
|
||||
{
|
||||
RefreshUI();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 로컬 플레이어 찾기
|
||||
/// </summary>
|
||||
private void FindLocalPlayer()
|
||||
{
|
||||
var playerObjects = FindObjectsByType<AbnormalityManager>(FindObjectsSortMode.None);
|
||||
|
||||
foreach (var manager in playerObjects)
|
||||
{
|
||||
// 네트워크 오브젝트인 경우 로컬 플레이어 확인
|
||||
if (manager.TryGetComponent<Unity.Netcode.NetworkObject>(out var netObj))
|
||||
{
|
||||
if (netObj.IsOwner)
|
||||
{
|
||||
SetTarget(manager);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 네트워크 오브젝트가 없거나 로컬 플레이어를 찾지 못한 경우
|
||||
// 첫 번째 플레이어 사용 (싱글플레이어용)
|
||||
if (playerObjects.Length > 0)
|
||||
{
|
||||
SetTarget(playerObjects[0]);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 추적 대상 설정
|
||||
/// </summary>
|
||||
/// <param name="manager">추적할 AbnormalityManager</param>
|
||||
public void SetTarget(AbnormalityManager manager)
|
||||
{
|
||||
// 기존 구독 해제
|
||||
if (targetManager != null)
|
||||
{
|
||||
targetManager.OnAbnormalityAdded -= OnAbnormalityAdded;
|
||||
targetManager.OnAbnormalityRemoved -= OnAbnormalityRemoved;
|
||||
targetManager.OnAbnormalitiesChanged -= OnAbnormalitiesChanged;
|
||||
}
|
||||
|
||||
targetManager = manager;
|
||||
|
||||
// 새로운 대상 구독
|
||||
if (targetManager != null)
|
||||
{
|
||||
targetManager.OnAbnormalityAdded += OnAbnormalityAdded;
|
||||
targetManager.OnAbnormalityRemoved += OnAbnormalityRemoved;
|
||||
targetManager.OnAbnormalitiesChanged += OnAbnormalitiesChanged;
|
||||
}
|
||||
|
||||
// 즉시 UI 갱신
|
||||
ForceRefreshUI();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 슬롯 풀 초기화
|
||||
/// </summary>
|
||||
private void InitializeSlotPool()
|
||||
{
|
||||
if (slotPrefab == null) return;
|
||||
|
||||
// 필요한 만큼 슬롯 미리 생성
|
||||
for (int i = 0; i < maxSlots; i++)
|
||||
{
|
||||
var slot = CreateSlot();
|
||||
slot.gameObject.SetActive(false);
|
||||
slotPool.Add(slot);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 새 슬롯 생성
|
||||
/// </summary>
|
||||
private AbnormalitySlotUI CreateSlot()
|
||||
{
|
||||
var go = Instantiate(slotPrefab.gameObject, transform);
|
||||
return go.GetComponent<AbnormalitySlotUI>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 슬롯 가져오기 (풀에서 또는 새로 생성)
|
||||
/// </summary>
|
||||
private AbnormalitySlotUI GetSlot()
|
||||
{
|
||||
// 풀에서 비활성화된 슬롯 찾기
|
||||
foreach (var slot in slotPool)
|
||||
{
|
||||
if (!slot.gameObject.activeSelf)
|
||||
{
|
||||
return slot;
|
||||
}
|
||||
}
|
||||
|
||||
// 풀에 없으면 새로 생성
|
||||
if (slotPool.Count < maxSlots)
|
||||
{
|
||||
var newSlot = CreateSlot();
|
||||
slotPool.Add(newSlot);
|
||||
return newSlot;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 슬롯 반환 (비활성화)
|
||||
/// </summary>
|
||||
private void ReturnSlot(AbnormalitySlotUI slot)
|
||||
{
|
||||
slot.gameObject.SetActive(false);
|
||||
activeSlots.Remove(slot);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 이상 상태 추가 시 호출
|
||||
/// </summary>
|
||||
private void OnAbnormalityAdded(ActiveAbnormality abnormality)
|
||||
{
|
||||
ForceRefreshUI();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 이상 상태 제거 시 호출
|
||||
/// </summary>
|
||||
private void OnAbnormalityRemoved(ActiveAbnormality abnormality)
|
||||
{
|
||||
ForceRefreshUI();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 이상 상태 변경 시 호출
|
||||
/// </summary>
|
||||
private void OnAbnormalitiesChanged()
|
||||
{
|
||||
ForceRefreshUI();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// UI 강제 갱신
|
||||
/// </summary>
|
||||
public void ForceRefreshUI()
|
||||
{
|
||||
if (targetManager == null) return;
|
||||
|
||||
// 모든 슬롯 반환
|
||||
foreach (var slot in activeSlots.ToArray())
|
||||
{
|
||||
ReturnSlot(slot);
|
||||
}
|
||||
|
||||
activeSlots.Clear();
|
||||
|
||||
// 활성화된 이상 상태 표시
|
||||
var abnormalities = targetManager.ActiveAbnormalities;
|
||||
|
||||
foreach (var abnormality in abnormalities)
|
||||
{
|
||||
var slot = GetSlot();
|
||||
if (slot == null) continue;
|
||||
|
||||
// 버프/디버프에 따라 적절한 컨테이너에 배치
|
||||
Transform container = abnormality.Data.isDebuff ? debuffContainer : buffContainer;
|
||||
if (container == null) container = transform;
|
||||
|
||||
slot.transform.SetParent(container, false);
|
||||
slot.Initialize(abnormality);
|
||||
slot.gameObject.SetActive(true);
|
||||
activeSlots.Add(slot);
|
||||
}
|
||||
|
||||
lastAbnormalityCount = abnormalities.Count;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// UI 주기적 갱신 (변경 감지 시에만)
|
||||
/// </summary>
|
||||
private void RefreshUI()
|
||||
{
|
||||
if (targetManager == null) return;
|
||||
|
||||
int currentCount = targetManager.ActiveAbnormalities.Count;
|
||||
|
||||
// 이상 상태 수가 변경되었으면 갱신
|
||||
if (currentCount != lastAbnormalityCount)
|
||||
{
|
||||
ForceRefreshUI();
|
||||
}
|
||||
|
||||
// 활성 슬롯들의 지속 시간 업데이트
|
||||
UpdateSlotDurations();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 모든 활성 슬롯의 지속 시간 업데이트
|
||||
/// </summary>
|
||||
private void UpdateSlotDurations()
|
||||
{
|
||||
foreach (var slot in activeSlots)
|
||||
{
|
||||
if (slot != null && slot.TrackedAbnormality != null)
|
||||
{
|
||||
var abnormality = slot.TrackedAbnormality;
|
||||
slot.UpdateDisplay(abnormality.RemainingDuration, abnormality.Data.duration);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 모든 슬롯 숨기기
|
||||
/// </summary>
|
||||
public void HideAll()
|
||||
{
|
||||
foreach (var slot in activeSlots.ToArray())
|
||||
{
|
||||
ReturnSlot(slot);
|
||||
}
|
||||
|
||||
activeSlots.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/_Game/Scripts/UI/AbnormalityListUI.cs.meta
Normal file
2
Assets/_Game/Scripts/UI/AbnormalityListUI.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 15447f4a4d271354fb52bbdf1a526c6e
|
||||
140
Assets/_Game/Scripts/UI/AbnormalitySlotUI.cs
Normal file
140
Assets/_Game/Scripts/UI/AbnormalitySlotUI.cs
Normal file
@@ -0,0 +1,140 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using TMPro;
|
||||
using Colosseum.Abnormalities;
|
||||
|
||||
namespace Colosseum.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// 개별 이상 상태 UI 슬롯
|
||||
/// 버프/디버프 아이콘, 지속 시간 등을 표시합니다.
|
||||
/// </summary>
|
||||
public class AbnormalitySlotUI : MonoBehaviour
|
||||
{
|
||||
[Header("UI References")]
|
||||
[Tooltip("이상 상태 아이콘")]
|
||||
[SerializeField] private Image iconImage;
|
||||
|
||||
[Tooltip("지속 시간 채우기 이미지 (시계 방향)")]
|
||||
[SerializeField] private Image durationFill;
|
||||
|
||||
[Tooltip("남은 시간 텍스트")]
|
||||
[SerializeField] private TMP_Text durationText;
|
||||
|
||||
[Tooltip("효과 이름 텍스트")]
|
||||
[SerializeField] private TMP_Text effectNameText;
|
||||
|
||||
[Tooltip("배경 이미지 (버프/디버프 구분용)")]
|
||||
[SerializeField] private Image backgroundImage;
|
||||
|
||||
[Header("Colors")]
|
||||
[Tooltip("버프 배경 색상")]
|
||||
[SerializeField] private Color buffColor = new Color(0.2f, 0.6f, 0.2f, 0.8f);
|
||||
|
||||
[Tooltip("디버프 배경 색상")]
|
||||
[SerializeField] private Color debuffColor = new Color(0.6f, 0.2f, 0.2f, 0.8f);
|
||||
|
||||
private ActiveAbnormality trackedAbnormality;
|
||||
|
||||
/// <summary>
|
||||
/// 추적 중인 활성 이상 상태
|
||||
/// </summary>
|
||||
public ActiveAbnormality TrackedAbnormality => trackedAbnormality;
|
||||
|
||||
/// <summary>
|
||||
/// UI 초기화
|
||||
/// </summary>
|
||||
/// <param name="abnormality">표시할 활성 이상 상태</param>
|
||||
public void Initialize(ActiveAbnormality abnormality)
|
||||
{
|
||||
trackedAbnormality = abnormality;
|
||||
|
||||
if (abnormality?.Data == null) return;
|
||||
|
||||
// Cooltime 요소 활성화 (부모 게임오브젝트들이 비활성화되어 있을 수 있음)
|
||||
EnableDurationElements();
|
||||
|
||||
// 아이콘 설정
|
||||
if (iconImage != null)
|
||||
{
|
||||
iconImage.sprite = abnormality.Data.icon;
|
||||
iconImage.enabled = abnormality.Data.icon != null;
|
||||
}
|
||||
|
||||
// 이름 설정
|
||||
if (effectNameText != null)
|
||||
{
|
||||
effectNameText.text = abnormality.Data.abnormalityName;
|
||||
}
|
||||
|
||||
// 배경 색상 설정 (버프/디버프 구분)
|
||||
if (backgroundImage != null)
|
||||
{
|
||||
backgroundImage.color = abnormality.Data.isDebuff ? debuffColor : buffColor;
|
||||
}
|
||||
|
||||
// 초기 상태 업데이트
|
||||
UpdateDisplay(abnormality.RemainingDuration, abnormality.Data.duration);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// UI 표시 업데이트
|
||||
/// </summary>
|
||||
/// <param name="remainingDuration">남은 지속 시간</param>
|
||||
/// <param name="totalDuration">전체 지속 시간</param>
|
||||
public void UpdateDisplay(float remainingDuration, float totalDuration)
|
||||
{
|
||||
// 지속 시간 채우기 이미지 업데이트 (시간이 지날수록 채워짐)
|
||||
if (durationFill != null && totalDuration > 0)
|
||||
{
|
||||
durationFill.fillAmount = 1f - (remainingDuration / totalDuration);
|
||||
}
|
||||
|
||||
// 남은 시간 텍스트 업데이트 (정수만 표시)
|
||||
if (durationText != null)
|
||||
{
|
||||
if (remainingDuration > 60f)
|
||||
{
|
||||
durationText.text = $"{(int)(remainingDuration / 60f)}:{(int)(remainingDuration % 60f):D2}";
|
||||
}
|
||||
else if (remainingDuration > 0f)
|
||||
{
|
||||
durationText.text = $"{Mathf.CeilToInt(remainingDuration)}";
|
||||
}
|
||||
else
|
||||
{
|
||||
durationText.text = string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 지속 시간 관련 UI 요소들의 부모 게임오브젝트를 활성화합니다.
|
||||
/// </summary>
|
||||
private void EnableDurationElements()
|
||||
{
|
||||
// durationFill의 모든 부모 게임오브젝트 활성화
|
||||
if (durationFill != null)
|
||||
{
|
||||
Transform parent = durationFill.transform.parent;
|
||||
while (parent != null && parent != transform)
|
||||
{
|
||||
parent.gameObject.SetActive(true);
|
||||
parent = parent.parent;
|
||||
}
|
||||
}
|
||||
|
||||
// durationText의 모든 부모 게임오브젝트 활성화
|
||||
if (durationText != null)
|
||||
{
|
||||
Transform parent = durationText.transform.parent;
|
||||
while (parent != null && parent != transform)
|
||||
{
|
||||
parent.gameObject.SetActive(true);
|
||||
parent = parent.parent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
2
Assets/_Game/Scripts/UI/AbnormalitySlotUI.cs.meta
Normal file
2
Assets/_Game/Scripts/UI/AbnormalitySlotUI.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 287a45a81e69cbf48845f88759cf7eb4
|
||||
116
Assets/_Game/Scripts/UI/AbnormalitySystemTest.cs
Normal file
116
Assets/_Game/Scripts/UI/AbnormalitySystemTest.cs
Normal file
@@ -0,0 +1,116 @@
|
||||
using UnityEngine;
|
||||
using Colosseum.Abnormalities;
|
||||
|
||||
namespace Colosseum.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// 이상 상태 시스템 테스트 스크립트
|
||||
/// Q 키: 버프 적용, E 키: 디버프 적용
|
||||
/// 로그를 통해 OnEffect 이벤트 호출 및 이상 상태 적용 과정을 추적합니다.
|
||||
/// </summary>
|
||||
public class AbnormalitySystemTest : MonoBehaviour
|
||||
{
|
||||
private AbnormalityManager abnormalityManager;
|
||||
private AbnormalityData testBuff;
|
||||
private AbnormalityData testDebuff;
|
||||
|
||||
private float testTimer;
|
||||
|
||||
void Start()
|
||||
{
|
||||
// 플레이어에서 AbnormalityManager 찾기
|
||||
abnormalityManager = GetComponent<AbnormalityManager>();
|
||||
if (abnormalityManager == null)
|
||||
{
|
||||
Debug.LogError("[AbnormalitySystemTest] AbnormalityManager not found on player!");
|
||||
return;
|
||||
}
|
||||
|
||||
// 테스트용 이상 상태 데이터 생성 (에셋 생성)
|
||||
testBuff = ScriptableObject.CreateInstance<AbnormalityData>();
|
||||
testBuff.abnormalityName = "Test Buff";
|
||||
testBuff.duration = 5f;
|
||||
testBuff.isDebuff = false;
|
||||
|
||||
testDebuff = ScriptableObject.CreateInstance<AbnormalityData>();
|
||||
testDebuff.abnormalityName = "Test Debuff";
|
||||
testDebuff.duration = 5f;
|
||||
testDebuff.isDebuff = true;
|
||||
|
||||
// 이벤트 구독
|
||||
abnormalityManager.OnAbnormalityAdded += OnAbnormalityAdded;
|
||||
abnormalityManager.OnAbnormalityRemoved += OnAbnormalityRemoved;
|
||||
|
||||
Debug.Log("=== Abnormality System Test Started ===");
|
||||
Debug.Log("Press Q to apply buff, Press E to apply debuff");
|
||||
Debug.Log($"Initial Active Abnormalities Count: {abnormalityManager.ActiveAbnormalities.Count}");
|
||||
}
|
||||
|
||||
void Update()
|
||||
{
|
||||
testTimer += Time.deltaTime;
|
||||
|
||||
// Q 키로 버프 적용 (3초마다 1회만)
|
||||
if (Input.GetKeyDown(KeyCode.Q) && testTimer >= 3f)
|
||||
{
|
||||
testTimer = 0f;
|
||||
ApplyTestBuff();
|
||||
}
|
||||
|
||||
// E 키로 디버프 적용 (3초마다 1회만)
|
||||
if (Input.GetKeyDown(KeyCode.E) && testTimer >= 3f)
|
||||
{
|
||||
testTimer = 0f;
|
||||
ApplyTestDebuff();
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyTestBuff()
|
||||
{
|
||||
if (testBuff == null || abnormalityManager == null)
|
||||
{
|
||||
Debug.LogWarning("[AbnormalitySystemTest] Cannot apply buff - data or manager is null");
|
||||
return;
|
||||
}
|
||||
|
||||
Debug.Log($"[AbnormalitySystemTest] >>> Applying BUFF: {testBuff.abnormalityName} to {gameObject.name}");
|
||||
abnormalityManager.ApplyAbnormality(testBuff, gameObject);
|
||||
}
|
||||
|
||||
private void ApplyTestDebuff()
|
||||
{
|
||||
if (testDebuff == null || abnormalityManager == null)
|
||||
{
|
||||
Debug.LogWarning("[AbnormalitySystemTest] Cannot apply debuff - data or manager is null");
|
||||
return;
|
||||
}
|
||||
|
||||
Debug.Log($"[AbnormalitySystemTest] >>> Applying DEBUFF: {testDebuff.abnormalityName} to {gameObject.name}");
|
||||
abnormalityManager.ApplyAbnormality(testDebuff, gameObject);
|
||||
}
|
||||
|
||||
private void OnAbnormalityAdded(ActiveAbnormality abnormality)
|
||||
{
|
||||
Debug.Log($"[AbnormalitySystemTest] <<< ABNORMALITY ADDED: {abnormality.Data.abnormalityName} | isDebuff: {abnormality.Data.isDebuff} | Duration: {abnormality.Data.duration}s | Remaining: {abnormality.RemainingDuration:F1}s");
|
||||
}
|
||||
|
||||
private void OnAbnormalityRemoved(ActiveAbnormality abnormality)
|
||||
{
|
||||
Debug.Log($"[AbnormalitySystemTest] <<< ABNORMALITY REMOVED: {abnormality.Data.abnormalityName}");
|
||||
}
|
||||
|
||||
void OnDestroy()
|
||||
{
|
||||
// 이벤트 구독 해제
|
||||
if (abnormalityManager != null)
|
||||
{
|
||||
abnormalityManager.OnAbnormalityAdded -= OnAbnormalityAdded;
|
||||
abnormalityManager.OnAbnormalityRemoved -= OnAbnormalityRemoved;
|
||||
}
|
||||
|
||||
// 정리
|
||||
if (testBuff != null) Destroy(testBuff);
|
||||
if (testDebuff != null) Destroy(testDebuff);
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/_Game/Scripts/UI/AbnormalitySystemTest.cs.meta
Normal file
2
Assets/_Game/Scripts/UI/AbnormalitySystemTest.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a1b2c3d4e5f6a7b8c9a0d1e5e3
|
||||
248
Assets/_Game/Scripts/UI/BossHealthBarUI.cs
Normal file
248
Assets/_Game/Scripts/UI/BossHealthBarUI.cs
Normal file
@@ -0,0 +1,248 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
2
Assets/_Game/Scripts/UI/BossHealthBarUI.cs.meta
Normal file
2
Assets/_Game/Scripts/UI/BossHealthBarUI.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 892f9842e85256b47b24e0aab016820b
|
||||
100
Assets/_Game/Scripts/UI/ConnectionUI.cs
Normal file
100
Assets/_Game/Scripts/UI/ConnectionUI.cs
Normal file
@@ -0,0 +1,100 @@
|
||||
using UnityEngine;
|
||||
using Unity.Netcode;
|
||||
using Unity.Netcode.Transports.UTP;
|
||||
|
||||
namespace Colosseum.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// 네트워크 연결 설정 (Inspector에서 제어)
|
||||
/// </summary>
|
||||
public class ConnectionUI : MonoBehaviour
|
||||
{
|
||||
[Header("Connection Settings")]
|
||||
[SerializeField] private string ipAddress = "127.0.0.1";
|
||||
[SerializeField] private ushort port = 7777;
|
||||
|
||||
[Header("Status (Read Only)")]
|
||||
[SerializeField, Tooltip("현재 연결 상태")] private string connectionStatus = "Disconnected";
|
||||
|
||||
private UnityTransport transport;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
transport = NetworkManager.Singleton?.GetComponent<UnityTransport>();
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
UpdateTransportSettings();
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
UpdateConnectionStatus();
|
||||
}
|
||||
|
||||
private void UpdateConnectionStatus()
|
||||
{
|
||||
if (NetworkManager.Singleton == null)
|
||||
{
|
||||
connectionStatus = "No NetworkManager";
|
||||
return;
|
||||
}
|
||||
|
||||
if (NetworkManager.Singleton.IsServer && NetworkManager.Singleton.IsHost)
|
||||
connectionStatus = "Host";
|
||||
else if (NetworkManager.Singleton.IsServer)
|
||||
connectionStatus = "Server";
|
||||
else if (NetworkManager.Singleton.IsClient)
|
||||
connectionStatus = NetworkManager.Singleton.IsConnectedClient ? "Connected" : "Connecting...";
|
||||
else
|
||||
connectionStatus = "Disconnected";
|
||||
}
|
||||
|
||||
public void StartHost()
|
||||
{
|
||||
UpdateTransportSettings();
|
||||
NetworkManager.Singleton.StartHost();
|
||||
Debug.Log("[Network] Started as Host");
|
||||
}
|
||||
|
||||
public void StartClient()
|
||||
{
|
||||
UpdateTransportSettings();
|
||||
NetworkManager.Singleton.StartClient();
|
||||
Debug.Log($"[Network] Connecting to {ipAddress}:{port}...");
|
||||
}
|
||||
|
||||
public void StartServer()
|
||||
{
|
||||
UpdateTransportSettings();
|
||||
NetworkManager.Singleton.StartServer();
|
||||
Debug.Log("[Network] Started as Server");
|
||||
}
|
||||
|
||||
public void Disconnect()
|
||||
{
|
||||
if (NetworkManager.Singleton != null)
|
||||
{
|
||||
NetworkManager.Singleton.Shutdown();
|
||||
Debug.Log("[Network] Disconnected");
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateTransportSettings()
|
||||
{
|
||||
if (transport != null)
|
||||
{
|
||||
transport.SetConnectionData(ipAddress, port);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnValidate()
|
||||
{
|
||||
if (Application.isPlaying && transport != null)
|
||||
{
|
||||
UpdateTransportSettings();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/_Game/Scripts/UI/ConnectionUI.cs.meta
Normal file
2
Assets/_Game/Scripts/UI/ConnectionUI.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 11a53a74e3d983f478f37ef0c99d5847
|
||||
59
Assets/_Game/Scripts/UI/GameOverUI.cs
Normal file
59
Assets/_Game/Scripts/UI/GameOverUI.cs
Normal file
@@ -0,0 +1,59 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using TMPro;
|
||||
|
||||
namespace Colosseum.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// 게임 오버 UI 컨트롤러.
|
||||
/// GameManager에 의해 활성화/비활성화됩니다.
|
||||
/// </summary>
|
||||
public class GameOverUI : MonoBehaviour
|
||||
{
|
||||
[Header("UI References")]
|
||||
[Tooltip("게임 오버 텍스트")]
|
||||
[SerializeField] private TMP_Text gameOverText;
|
||||
|
||||
[Tooltip("재시작 카운트다운 텍스트")]
|
||||
[SerializeField] private TMP_Text countdownText;
|
||||
|
||||
[Tooltip("게임 오버 애니메이터")]
|
||||
[SerializeField] private Animator animator;
|
||||
|
||||
[Header("Settings")]
|
||||
[Tooltip("게임 오버 텍스트")]
|
||||
[SerializeField] private string gameOverMessage = "GAME OVER";
|
||||
|
||||
[Tooltip("텍스트 색상")]
|
||||
[SerializeField] private Color textColor = Color.red;
|
||||
|
||||
private void Start()
|
||||
{
|
||||
if (gameOverText != null)
|
||||
{
|
||||
gameOverText.text = gameOverMessage;
|
||||
gameOverText.color = textColor;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
// 애니메이션 재생
|
||||
if (animator != null)
|
||||
{
|
||||
animator.SetTrigger("Show");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 카운트다운 텍스트 업데이트
|
||||
/// </summary>
|
||||
public void UpdateCountdown(int seconds)
|
||||
{
|
||||
if (countdownText != null)
|
||||
{
|
||||
countdownText.text = $"Restarting in {seconds}...";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/_Game/Scripts/UI/GameOverUI.cs.meta
Normal file
2
Assets/_Game/Scripts/UI/GameOverUI.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f87daf25f7fc5c4499b66d327b6c4cf2
|
||||
121
Assets/_Game/Scripts/UI/PlayerHUD.cs
Normal file
121
Assets/_Game/Scripts/UI/PlayerHUD.cs
Normal file
@@ -0,0 +1,121 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
using Colosseum.Player;
|
||||
|
||||
namespace Colosseum.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// 플레이어 HUD - 체력/마나 바 관리
|
||||
/// </summary>
|
||||
public class PlayerHUD : MonoBehaviour
|
||||
{
|
||||
[Header("Stat Bars")]
|
||||
[SerializeField] private StatBar healthBar;
|
||||
[SerializeField] private StatBar manaBar;
|
||||
|
||||
[Header("Target")]
|
||||
[Tooltip("자동으로 로컬 플레이어 찾기")]
|
||||
[SerializeField] private bool autoFindPlayer = true;
|
||||
|
||||
private PlayerNetworkController targetPlayer;
|
||||
|
||||
private void Start()
|
||||
{
|
||||
if (autoFindPlayer)
|
||||
{
|
||||
FindLocalPlayer();
|
||||
}
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
// 플레이어가 아직 없으면 계속 찾기
|
||||
if (targetPlayer == null && autoFindPlayer)
|
||||
{
|
||||
FindLocalPlayer();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
// 이벤트 구독 해제
|
||||
UnsubscribeFromEvents();
|
||||
}
|
||||
|
||||
private void FindLocalPlayer()
|
||||
{
|
||||
foreach (var player in FindObjectsByType<PlayerNetworkController>(FindObjectsSortMode.None))
|
||||
{
|
||||
if (player.IsOwner)
|
||||
{
|
||||
SetTarget(player);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 추적할 플레이어 설정
|
||||
/// </summary>
|
||||
public void SetTarget(PlayerNetworkController player)
|
||||
{
|
||||
// 이전 타겟 구독 해제
|
||||
UnsubscribeFromEvents();
|
||||
|
||||
targetPlayer = player;
|
||||
|
||||
// 새 타겟 구독
|
||||
SubscribeToEvents();
|
||||
|
||||
// 초기 값 설정
|
||||
UpdateStatBars();
|
||||
}
|
||||
|
||||
private void SubscribeToEvents()
|
||||
{
|
||||
if (targetPlayer == null) return;
|
||||
|
||||
targetPlayer.OnHealthChanged += HandleHealthChanged;
|
||||
targetPlayer.OnManaChanged += HandleManaChanged;
|
||||
}
|
||||
|
||||
private void UnsubscribeFromEvents()
|
||||
{
|
||||
if (targetPlayer == null) return;
|
||||
|
||||
targetPlayer.OnHealthChanged -= HandleHealthChanged;
|
||||
targetPlayer.OnManaChanged -= HandleManaChanged;
|
||||
}
|
||||
|
||||
private void HandleHealthChanged(float oldValue, float newValue)
|
||||
{
|
||||
if (healthBar != null && targetPlayer != null)
|
||||
{
|
||||
healthBar.SetValue(newValue, targetPlayer.MaxHealth);
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleManaChanged(float oldValue, float newValue)
|
||||
{
|
||||
if (manaBar != null && targetPlayer != null)
|
||||
{
|
||||
manaBar.SetValue(newValue, targetPlayer.MaxMana);
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateStatBars()
|
||||
{
|
||||
if (targetPlayer == null) return;
|
||||
|
||||
if (healthBar != null)
|
||||
{
|
||||
healthBar.SetValue(targetPlayer.Health, targetPlayer.MaxHealth);
|
||||
}
|
||||
|
||||
if (manaBar != null)
|
||||
{
|
||||
manaBar.SetValue(targetPlayer.Mana, targetPlayer.MaxMana);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/_Game/Scripts/UI/PlayerHUD.cs.meta
Normal file
2
Assets/_Game/Scripts/UI/PlayerHUD.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e956d7232e6a45246ac4d8079f12f9c3
|
||||
142
Assets/_Game/Scripts/UI/SkillQuickSlotUI.cs
Normal file
142
Assets/_Game/Scripts/UI/SkillQuickSlotUI.cs
Normal file
@@ -0,0 +1,142 @@
|
||||
using UnityEngine;
|
||||
using Colosseum.Player;
|
||||
using Colosseum.Skills;
|
||||
|
||||
namespace Colosseum.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// 스킬 퀵슬롯 UI 관리자.
|
||||
/// PlayerSkillInput과 연동하여 쿨타임, 마나 등을 표시합니다.
|
||||
/// </summary>
|
||||
public class SkillQuickSlotUI : MonoBehaviour
|
||||
{
|
||||
[Header("Skill Slots")]
|
||||
[Tooltip("6개의 스킬 슬롯 UI (인덱스 순서대로)")]
|
||||
[SerializeField] private SkillSlotUI[] skillSlots = new SkillSlotUI[6];
|
||||
|
||||
[Header("Debug")]
|
||||
[SerializeField] private bool debugMode = false;
|
||||
|
||||
[Header("Keybind Labels")]
|
||||
[Tooltip("키바인딩 표시 텍스트 (기본: Q, W, E, R, A, S)")]
|
||||
[SerializeField] private string[] keyLabels = { "Q", "W", "E", "R", "A", "S" };
|
||||
|
||||
private PlayerSkillInput playerSkillInput;
|
||||
private PlayerNetworkController networkController;
|
||||
|
||||
private void Start()
|
||||
{
|
||||
// 로컬 플레이어 찾기
|
||||
FindLocalPlayer();
|
||||
}
|
||||
|
||||
private void FindLocalPlayer()
|
||||
{
|
||||
var players = FindObjectsByType<PlayerSkillInput>(FindObjectsSortMode.None);
|
||||
|
||||
if (players.Length == 0)
|
||||
{
|
||||
Debug.LogWarning("[SkillQuickSlotUI] No PlayerSkillInput found in scene");
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var player in players)
|
||||
{
|
||||
if (player.IsOwner)
|
||||
{
|
||||
playerSkillInput = player;
|
||||
networkController = player.GetComponent<PlayerNetworkController>();
|
||||
Debug.Log($"[SkillQuickSlotUI] Found local player: {player.name}");
|
||||
InitializeSlots();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Debug.LogWarning("[SkillQuickSlotUI] No local player found (IsOwner = false)");
|
||||
}
|
||||
|
||||
private void InitializeSlots()
|
||||
{
|
||||
if (playerSkillInput == null) return;
|
||||
|
||||
int initializedCount = 0;
|
||||
for (int i = 0; i < skillSlots.Length && i < keyLabels.Length; i++)
|
||||
{
|
||||
SkillData skill = playerSkillInput.GetSkill(i);
|
||||
string keyLabel = i < keyLabels.Length ? keyLabels[i] : (i + 1).ToString();
|
||||
|
||||
if (skillSlots[i] != null)
|
||||
{
|
||||
skillSlots[i].Initialize(i, skill, keyLabel);
|
||||
if (skill != null) initializedCount++;
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning($"[SkillQuickSlotUI] Slot {i} is not assigned");
|
||||
}
|
||||
}
|
||||
|
||||
Debug.Log($"[SkillQuickSlotUI] Initialized {initializedCount} skill slots");
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (playerSkillInput == null)
|
||||
{
|
||||
// 플레이어가 아직 없으면 다시 찾기 시도
|
||||
FindLocalPlayer();
|
||||
return;
|
||||
}
|
||||
|
||||
UpdateSlotStates();
|
||||
}
|
||||
|
||||
private float debugLogTimer = 0f;
|
||||
|
||||
private void UpdateSlotStates()
|
||||
{
|
||||
bool shouldLog = debugMode && Time.time > debugLogTimer;
|
||||
if (shouldLog) debugLogTimer = Time.time + 1f;
|
||||
|
||||
for (int i = 0; i < skillSlots.Length; i++)
|
||||
{
|
||||
if (skillSlots[i] == null) continue;
|
||||
|
||||
SkillData skill = playerSkillInput.GetSkill(i);
|
||||
if (skill == null) continue;
|
||||
|
||||
float remainingCooldown = playerSkillInput.GetRemainingCooldown(i);
|
||||
float totalCooldown = skill.Cooldown;
|
||||
bool hasEnoughMana = networkController == null || networkController.Mana >= skill.ManaCost;
|
||||
|
||||
if (shouldLog && remainingCooldown > 0f)
|
||||
{
|
||||
Debug.Log($"[SkillQuickSlotUI] Slot {i}: {skill.SkillName}, CD: {remainingCooldown:F1}/{totalCooldown:F1}");
|
||||
}
|
||||
|
||||
skillSlots[i].UpdateState(remainingCooldown, totalCooldown, hasEnoughMana);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 플레이어 참조 수동 설정 (씬 전환 등에서 사용)
|
||||
/// </summary>
|
||||
public void SetPlayer(PlayerSkillInput player)
|
||||
{
|
||||
playerSkillInput = player;
|
||||
networkController = player?.GetComponent<PlayerNetworkController>();
|
||||
InitializeSlots();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 특정 슬롯의 스킬 변경
|
||||
/// </summary>
|
||||
public void UpdateSkillSlot(int slotIndex, SkillData skill)
|
||||
{
|
||||
if (slotIndex < 0 || slotIndex >= skillSlots.Length) return;
|
||||
|
||||
string keyLabel = slotIndex < keyLabels.Length ? keyLabels[slotIndex] : (slotIndex + 1).ToString();
|
||||
skillSlots[slotIndex].Initialize(slotIndex, skill, keyLabel);
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/_Game/Scripts/UI/SkillQuickSlotUI.cs.meta
Normal file
2
Assets/_Game/Scripts/UI/SkillQuickSlotUI.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 637160b507db7634aba029b1624bc4a5
|
||||
173
Assets/_Game/Scripts/UI/SkillSlotUI.cs
Normal file
173
Assets/_Game/Scripts/UI/SkillSlotUI.cs
Normal file
@@ -0,0 +1,173 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using TMPro;
|
||||
using Colosseum.Skills;
|
||||
|
||||
namespace Colosseum.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// 개별 스킬 슬롯 UI
|
||||
/// </summary>
|
||||
public class SkillSlotUI : MonoBehaviour
|
||||
{
|
||||
[Header("References")]
|
||||
[SerializeField] private Image iconImage;
|
||||
[SerializeField] private Image cooldownOverlay;
|
||||
[SerializeField] private TMP_Text cooldownText;
|
||||
[SerializeField] private TMP_Text keybindText;
|
||||
|
||||
[Header("Settings")]
|
||||
[SerializeField] private Color availableColor = Color.white;
|
||||
[SerializeField] private Color cooldownColor = new Color(0.2f, 0.2f, 0.2f, 0.9f);
|
||||
[SerializeField] private Color noManaColor = new Color(0.5f, 0.2f, 0.2f, 0.8f);
|
||||
|
||||
private SkillData skill;
|
||||
private int slotIndex;
|
||||
private bool useIconForCooldown = false;
|
||||
private Animator cooldownAnimator; // 쿨다운 오버레이를 제어하는 Animator
|
||||
private GameObject cooldownContainer; // 쿨다운 오버레이의 부모 GameObject (Cooldown)
|
||||
|
||||
public int SlotIndex => slotIndex;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (cooldownOverlay == null)
|
||||
{
|
||||
useIconForCooldown = true;
|
||||
}
|
||||
else if (cooldownOverlay.type != Image.Type.Filled)
|
||||
{
|
||||
useIconForCooldown = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
// 쿨다운 오버레이의 상위 GameObject에 있는 Animator 찾기
|
||||
// 구조: CooldownItem (Animator) -> Cooldown -> SPR_Cooldown (Image)
|
||||
cooldownAnimator = cooldownOverlay.GetComponentInParent<Animator>();
|
||||
|
||||
// Animator가 Cooldown GameObject를 제어하므로
|
||||
// 쿨다운 표시를 위해 Animator를 비활성화하고 수동으로 제어
|
||||
if (cooldownAnimator != null)
|
||||
{
|
||||
cooldownAnimator.enabled = false;
|
||||
|
||||
// Animator가 제어하던 Cooldown GameObject 찾기
|
||||
// SPR_Cooldown의 부모가 Cooldown
|
||||
cooldownContainer = cooldownOverlay.transform.parent?.gameObject;
|
||||
}
|
||||
|
||||
// 쿨다운 오버레이 초기화
|
||||
cooldownOverlay.fillAmount = 0f;
|
||||
cooldownOverlay.enabled = false;
|
||||
|
||||
// Cooldown 컨테이너 비활성화
|
||||
if (cooldownContainer != null)
|
||||
{
|
||||
cooldownContainer.SetActive(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Initialize(int index, SkillData skillData, string keyLabel)
|
||||
{
|
||||
slotIndex = index;
|
||||
skill = skillData;
|
||||
|
||||
if (keybindText != null)
|
||||
keybindText.text = keyLabel;
|
||||
|
||||
if (skill != null && iconImage != null)
|
||||
{
|
||||
iconImage.sprite = skill.Icon;
|
||||
iconImage.enabled = true;
|
||||
iconImage.color = availableColor;
|
||||
}
|
||||
else if (iconImage != null)
|
||||
{
|
||||
iconImage.enabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateState(float cooldownRemaining, float cooldownTotal, bool hasEnoughMana)
|
||||
{
|
||||
if (skill == null)
|
||||
{
|
||||
if (cooldownContainer != null) cooldownContainer.SetActive(false);
|
||||
if (cooldownOverlay != null) cooldownOverlay.enabled = false;
|
||||
if (cooldownText != null) cooldownText.text = "";
|
||||
return;
|
||||
}
|
||||
|
||||
if (cooldownRemaining > 0f)
|
||||
{
|
||||
float ratio = cooldownRemaining / cooldownTotal;
|
||||
|
||||
if (useIconForCooldown && iconImage != null)
|
||||
{
|
||||
// 아이콘 색상으로 쿨다운 표시
|
||||
float brightness = Mathf.Lerp(0.3f, 1f, 1f - ratio);
|
||||
iconImage.color = new Color(brightness, brightness, brightness, 1f);
|
||||
}
|
||||
else if (cooldownOverlay != null)
|
||||
{
|
||||
// Cooldown 컨테이너 활성화
|
||||
if (cooldownContainer != null && !cooldownContainer.activeSelf)
|
||||
{
|
||||
cooldownContainer.SetActive(true);
|
||||
}
|
||||
|
||||
// Image 컴포넌트 활성화
|
||||
cooldownOverlay.enabled = true;
|
||||
cooldownOverlay.fillAmount = ratio;
|
||||
}
|
||||
|
||||
if (cooldownText != null)
|
||||
{
|
||||
cooldownText.text = cooldownRemaining < 1f
|
||||
? $"{cooldownRemaining:F1}"
|
||||
: $"{Mathf.CeilToInt(cooldownRemaining)}";
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// 쿨다운 완료
|
||||
if (useIconForCooldown && iconImage != null)
|
||||
{
|
||||
iconImage.color = hasEnoughMana ? availableColor : noManaColor;
|
||||
}
|
||||
else if (cooldownOverlay != null)
|
||||
{
|
||||
// Image 컴포넌트 비활성화
|
||||
cooldownOverlay.enabled = false;
|
||||
|
||||
// Cooldown 컨테이너 비활성화
|
||||
if (cooldownContainer != null)
|
||||
{
|
||||
cooldownContainer.SetActive(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (cooldownText != null)
|
||||
{
|
||||
cooldownText.text = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void SetSkill(SkillData skillData)
|
||||
{
|
||||
skill = skillData;
|
||||
|
||||
if (skill != null && iconImage != null)
|
||||
{
|
||||
iconImage.sprite = skill.Icon;
|
||||
iconImage.enabled = true;
|
||||
iconImage.color = availableColor;
|
||||
}
|
||||
else if (iconImage != null)
|
||||
{
|
||||
iconImage.enabled = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/_Game/Scripts/UI/SkillSlotUI.cs.meta
Normal file
2
Assets/_Game/Scripts/UI/SkillSlotUI.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bad7358e0082af244b16b0a515710fbc
|
||||
117
Assets/_Game/Scripts/UI/StatBar.cs
Normal file
117
Assets/_Game/Scripts/UI/StatBar.cs
Normal file
@@ -0,0 +1,117 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using TMPro;
|
||||
|
||||
namespace Colosseum.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// 체력/마나 바 UI 컴포넌트
|
||||
/// Slider 또는 Image.Fill 방식 지원
|
||||
/// </summary>
|
||||
public class StatBar : MonoBehaviour
|
||||
{
|
||||
[Header("References - Slider 방식")]
|
||||
[SerializeField] private Slider slider;
|
||||
|
||||
[Header("References - Fill Image 방식")]
|
||||
[SerializeField] private Image fillImage;
|
||||
|
||||
[Header("References - 텍스트")]
|
||||
[SerializeField] private TMP_Text valueText;
|
||||
|
||||
[Header("Colors")]
|
||||
[SerializeField] private Color fullColor = Color.green;
|
||||
[SerializeField] private Color lowColor = Color.red;
|
||||
[SerializeField] private float lowThreshold = 0.3f;
|
||||
[SerializeField] private bool useColorTransition = true;
|
||||
|
||||
[Header("Animation")]
|
||||
[SerializeField] private bool smoothTransition = true;
|
||||
[SerializeField] private float lerpSpeed = 5f;
|
||||
|
||||
private float currentValue;
|
||||
private float maxValue;
|
||||
private float displayValue;
|
||||
|
||||
/// <summary>
|
||||
/// 바 값 설정
|
||||
/// </summary>
|
||||
public void SetValue(float current, float max)
|
||||
{
|
||||
currentValue = current;
|
||||
maxValue = max;
|
||||
|
||||
if (!smoothTransition)
|
||||
{
|
||||
displayValue = currentValue;
|
||||
}
|
||||
|
||||
// 항상 즉시 시각적 업데이트 수행
|
||||
UpdateVisuals();
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (smoothTransition && !Mathf.Approximately(displayValue, currentValue))
|
||||
{
|
||||
displayValue = Mathf.Lerp(displayValue, currentValue, lerpSpeed * Time.deltaTime);
|
||||
|
||||
if (Mathf.Abs(displayValue - currentValue) < 0.01f)
|
||||
displayValue = currentValue;
|
||||
|
||||
UpdateVisuals();
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateVisuals()
|
||||
{
|
||||
if (maxValue <= 0f)
|
||||
{
|
||||
Debug.LogWarning($"[StatBar:{gameObject.name}] UpdateVisuals: maxValue is {maxValue}, skipping");
|
||||
return;
|
||||
}
|
||||
|
||||
float ratio = Mathf.Clamp01(displayValue / maxValue);
|
||||
|
||||
// Slider 방식
|
||||
if (slider != null)
|
||||
{
|
||||
slider.value = ratio;
|
||||
|
||||
// Slider 내부 Fill 이미지 색상 변경
|
||||
if (useColorTransition && slider.fillRect != null)
|
||||
{
|
||||
var fillImg = slider.fillRect.GetComponent<Image>();
|
||||
if (fillImg != null)
|
||||
{
|
||||
fillImg.color = ratio <= lowThreshold ? lowColor : fullColor;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Image.Fill 방식
|
||||
else if (fillImage != null)
|
||||
{
|
||||
fillImage.fillAmount = ratio;
|
||||
|
||||
if (useColorTransition)
|
||||
{
|
||||
fillImage.color = ratio <= lowThreshold ? lowColor : fullColor;
|
||||
}
|
||||
}
|
||||
|
||||
// 텍스트
|
||||
if (valueText != null)
|
||||
{
|
||||
valueText.text = $"{Mathf.CeilToInt(displayValue)} / {Mathf.CeilToInt(maxValue)}";
|
||||
}
|
||||
}
|
||||
|
||||
private void OnValidate()
|
||||
{
|
||||
if (slider == null)
|
||||
slider = GetComponentInChildren<Slider>();
|
||||
if (fillImage == null && slider == null)
|
||||
fillImage = GetComponentInChildren<Image>();
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/_Game/Scripts/UI/StatBar.cs.meta
Normal file
2
Assets/_Game/Scripts/UI/StatBar.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b5c5d0fa667f83d4399abb45ffcaea31
|
||||
55
Assets/_Game/Scripts/UI/VictoryUI.cs
Normal file
55
Assets/_Game/Scripts/UI/VictoryUI.cs
Normal file
@@ -0,0 +1,55 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using TMPro;
|
||||
using Colosseum.Enemy;
|
||||
|
||||
namespace Colosseum.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// 승리 UI 컨트롤러.
|
||||
/// GameManager에 의해 활성화/비활성화됩니다.
|
||||
/// </summary>
|
||||
public class VictoryUI : MonoBehaviour
|
||||
{
|
||||
[Header("UI References")]
|
||||
[Tooltip("승리 텍스트")]
|
||||
[SerializeField] private TMP_Text victoryText;
|
||||
|
||||
[Tooltip("보스 이름 텍스트")]
|
||||
[SerializeField] private TMP_Text bossNameText;
|
||||
|
||||
[Tooltip("승리 애니메이터")]
|
||||
[SerializeField] private Animator animator;
|
||||
|
||||
[Header("Settings")]
|
||||
[Tooltip("승리 텍스트")]
|
||||
[SerializeField] private string victoryMessage = "VICTORY!";
|
||||
|
||||
[Tooltip("텍스트 색상")]
|
||||
[SerializeField] private Color textColor = Color.yellow;
|
||||
|
||||
private void Start()
|
||||
{
|
||||
if (victoryText != null)
|
||||
{
|
||||
victoryText.text = victoryMessage;
|
||||
victoryText.color = textColor;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
// 보스 이름 표시
|
||||
if (bossNameText != null && BossEnemy.ActiveBoss != null)
|
||||
{
|
||||
bossNameText.text = $"{BossEnemy.ActiveBoss.name} Defeated!";
|
||||
}
|
||||
|
||||
// 애니메이션 재생
|
||||
if (animator != null)
|
||||
{
|
||||
animator.SetTrigger("Show");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/_Game/Scripts/UI/VictoryUI.cs.meta
Normal file
2
Assets/_Game/Scripts/UI/VictoryUI.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 514ff17abf102744faf81dbad1251d86
|
||||
Reference in New Issue
Block a user