using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; using TMPro; using Unity.Netcode; namespace Northbound { /// /// 팀원 체력 UI 관리 /// Slot 1: 로컬 플레이어 (본인) /// Slot 2~4: 다른 플레이어 (플레이어 ID 순) /// public class TeamHealthUI : MonoBehaviour { [Header("Slot References (자동 할당됨)")] [SerializeField] private TeamCardSlot slot1; // 로컬 플레이어 (본인) [SerializeField] private TeamCardSlot slot2; // 다른 플레이어 1 [SerializeField] private TeamCardSlot slot3; // 다른 플레이어 2 [SerializeField] private TeamCardSlot slot4; // 다른 플레이어 3 private Dictionary _playerControllers = new Dictionary(); private Dictionary _playerSlotMapping = new Dictionary(); private ulong _localPlayerId = ulong.MaxValue; private void Awake() { // 슬롯 초기화 InitializeSlots(); } private void Start() { // 네트워크 이벤트 구독 if (NetworkManager.Singleton != null) { NetworkManager.Singleton.OnClientConnectedCallback += OnClientConnected; NetworkManager.Singleton.OnClientDisconnectCallback += OnClientDisconnect; } // 이미 연결된 플레이어들 처리 RefreshAllPlayers(); } private void OnDestroy() { // 이벤트 구독 해제 if (NetworkManager.Singleton != null) { NetworkManager.Singleton.OnClientConnectedCallback -= OnClientConnected; NetworkManager.Singleton.OnClientDisconnectCallback -= OnClientDisconnect; } // 모든 플레이어 체력 변경 이벤트 구독 해제 foreach (var kvp in _playerControllers) { if (kvp.Value != null) { kvp.Value.OnOwnerChanged -= OnPlayerOwnerChanged; } } } private void InitializeSlots() { // 슬롯 자동 할당 - 이름으로 찾기 if (slot1 == null) slot1 = FindSlotByName("Slot_1"); if (slot2 == null) slot2 = FindSlotByName("Slot_2"); if (slot3 == null) slot3 = FindSlotByName("Slot_3"); if (slot4 == null) slot4 = FindSlotByName("Slot_4"); // 모든 슬롯 초기화 (비어있음) slot1?.Clear(); slot2?.Clear(); slot3?.Clear(); slot4?.Clear(); } /// /// 이름으로 슬롯 찾기 (자식의 자식 프리팹까지 검색) /// private TeamCardSlot FindSlotByName(string slotName) { // 직접 자식에서 찾기 Transform slotTransform = transform.Find(slotName); if (slotTransform == null) return null; // 자식 프리팹에서 TeamCardSlot 찾기 또는 생성 TeamCardSlot slot = slotTransform.GetComponentInChildren(true); if (slot == null) { // TeamCardSlot 컴포넌트를 자식 프리팹의 루트에 추가 // 프리팹 인스턴스의 첫 번째 자식이 실제 카드 프리팹 if (slotTransform.childCount > 0) { GameObject cardObj = slotTransform.GetChild(0).gameObject; slot = cardObj.AddComponent(); } else { // 자식이 없으면 슬롯 자체에 추가 slot = slotTransform.gameObject.AddComponent(); } } return slot; } private void OnClientConnected(ulong clientId) { // 약간의 딜레이 후 플레이어 컨트롤러가 생성될 때까지 대기 Invoke(nameof(RefreshAllPlayers), 0.5f); } private void OnClientDisconnect(ulong clientId) { // 플레이어 제거 RemovePlayer(clientId); } private void RefreshAllPlayers() { // 로컬 플레이어 ID 확인 if (NetworkManager.Singleton != null) { _localPlayerId = NetworkManager.Singleton.LocalClientId; } // 모든 NetworkPlayerController 찾기 var allPlayers = FindObjectsByType(FindObjectsSortMode.None); // 기존 매핑 클리어 _playerControllers.Clear(); // 플레이어 등록 foreach (var player in allPlayers) { ulong playerId = player.OwnerPlayerId; if (playerId != ulong.MaxValue) { _playerControllers[playerId] = player; player.OnOwnerChanged -= OnPlayerOwnerChanged; player.OnOwnerChanged += OnPlayerOwnerChanged; } } // UI 업데이트 UpdateAllSlots(); } private void OnPlayerOwnerChanged(ulong newOwnerId) { // 플레이어 소유자 변경 시 갱신 Invoke(nameof(RefreshAllPlayers), 0.1f); } private void UpdateAllSlots() { // 기존 매핑 클리어 _playerSlotMapping.Clear(); slot1?.Clear(); slot2?.Clear(); slot3?.Clear(); slot4?.Clear(); // 로컬 플레이어를 Slot 1에 배치 if (_playerControllers.TryGetValue(_localPlayerId, out var localPlayer)) { slot1?.SetPlayer(localPlayer, true); _playerSlotMapping[_localPlayerId] = slot1; } // 다른 플레이어들을 ID 순으로 Slot 2~4에 배치 var otherPlayers = new List>(); foreach (var kvp in _playerControllers) { if (kvp.Key != _localPlayerId) { otherPlayers.Add(kvp); } } // ID 순으로 정렬 otherPlayers.Sort((a, b) => a.Key.CompareTo(b.Key)); // 슬롯에 배치 var slots = new[] { slot2, slot3, slot4 }; for (int i = 0; i < otherPlayers.Count && i < slots.Length; i++) { var player = otherPlayers[i]; slots[i]?.SetPlayer(player.Value, false); _playerSlotMapping[player.Key] = slots[i]; } } private void RemovePlayer(ulong playerId) { if (_playerControllers.TryGetValue(playerId, out var player)) { player.OnOwnerChanged -= OnPlayerOwnerChanged; _playerControllers.Remove(playerId); } if (_playerSlotMapping.TryGetValue(playerId, out var slot)) { slot?.Clear(); _playerSlotMapping.Remove(playerId); } // 슬롯 재배치 UpdateAllSlots(); } } /// /// 개별 팀 카드 슬롯 /// TeamCard_Large 또는 TeamCard_Small 프리팹에 붙여서 사용 /// public class TeamCardSlot : MonoBehaviour { [Header("UI References (자동 할당됨)")] [SerializeField] private TextMeshProUGUI _playerNameText; [SerializeField] private Image _hpBarFill; [SerializeField] private TextMeshProUGUI _hpText; private NetworkPlayerController _player; private bool _isLocalPlayer; private int _lastHealth = -1; private int _lastMaxHealth = -1; private void Awake() { // 자동으로 UI 요소 찾기 FindUIElements(); } private void FindUIElements() { // PlayerNameText 찾기 - 이름으로 정확히 찾기 if (_playerNameText == null) { Transform nameText = FindDeepChild(transform, "PlayerNameText"); if (nameText != null) { _playerNameText = nameText.GetComponent(); } } // HPBarFill 찾기 if (_hpBarFill == null) { Transform hpFill = FindDeepChild(transform, "HPBarFill"); if (hpFill != null) { _hpBarFill = hpFill.GetComponent(); } } // HPText 찾기 if (_hpText == null) { Transform hpText = FindDeepChild(transform, "HPText"); if (hpText != null) { _hpText = hpText.GetComponent(); } } } /// /// 깊이 우선 탐색으로 자식 찾기 /// private Transform FindDeepChild(Transform parent, string name) { // 직접 자식에서 찾기 Transform found = parent.Find(name); if (found != null) return found; // 재귀적으로 자식의 자식에서 찾기 for (int i = 0; i < parent.childCount; i++) { found = FindDeepChild(parent.GetChild(i), name); if (found != null) return found; } return null; } public void SetPlayer(NetworkPlayerController player, bool isLocalPlayer) { _player = player; _isLocalPlayer = isLocalPlayer; _lastHealth = -1; // 강제 업데이트를 위해 리셋 _lastMaxHealth = -1; if (player == null) { Clear(); return; } // UI 요소가 없으면 다시 찾기 FindUIElements(); // 플레이어 이름 설정 if (_playerNameText != null) { string playerName = $"Player {player.OwnerPlayerId + 1}"; if (isLocalPlayer) { playerName += " (YOU)"; } _playerNameText.text = playerName; } // 초기 체력 업데이트 UpdateHealth(); // 활성화 gameObject.SetActive(true); } public void Clear() { _player = null; _lastHealth = -1; _lastMaxHealth = -1; if (_playerNameText != null) { _playerNameText.text = "Waiting..."; } if (_hpBarFill != null) { _hpBarFill.fillAmount = 0f; } if (_hpText != null) { _hpText.text = "--- / ---"; } } private void LateUpdate() { // 체력 업데이트 (변경 시에만) if (_player != null) { int currentHp = _player.GetCurrentHealth(); int maxHp = _player.GetMaxHealth(); // 체력이나 최대 체력이 변경된 경우에만 UI 업데이트 if (currentHp != _lastHealth || maxHp != _lastMaxHealth) { UpdateHealth(); _lastHealth = currentHp; _lastMaxHealth = maxHp; } } } private void UpdateHealth() { if (_player == null) return; int currentHp = _player.GetCurrentHealth(); int maxHp = _player.GetMaxHealth(); float hpPercent = maxHp > 0 ? (float)currentHp / maxHp : 0f; if (_hpBarFill != null) { _hpBarFill.fillAmount = hpPercent; } if (_hpText != null) { _hpText.text = $"{currentHp} / {maxHp}"; } } /// /// 에디터에서 UI 요소 자동 할당 /// [ContextMenu("Auto Find UI Elements")] private void AutoFindUIElements() { FindUIElements(); Debug.Log($"[TeamCardSlot] UI Elements Found - Name: {_playerNameText != null}, HPFill: {_hpBarFill != null}, HPText: {_hpText != null}"); } } }