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:
2026-03-16 19:08:27 +09:00
parent 309bf5f48b
commit c265f980db
17251 changed files with 2630777 additions and 206 deletions

View 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();
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 15447f4a4d271354fb52bbdf1a526c6e

View 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;
}
}
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 287a45a81e69cbf48845f88759cf7eb4

View 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);
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: a1b2c3d4e5f6a7b8c9a0d1e5e3

View 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
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 892f9842e85256b47b24e0aab016820b

View 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();
}
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 11a53a74e3d983f478f37ef0c99d5847

View 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}...";
}
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: f87daf25f7fc5c4499b66d327b6c4cf2

View 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);
}
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: e956d7232e6a45246ac4d8079f12f9c3

View 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);
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 637160b507db7634aba029b1624bc4a5

View 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;
}
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: bad7358e0082af244b16b0a515710fbc

View 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>();
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: b5c5d0fa667f83d4399abb45ffcaea31

View 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");
}
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 514ff17abf102744faf81dbad1251d86