From c34b9df4f7ea048c75b74b1cab9d9dc4851c17e3 Mon Sep 17 00:00:00 2001 From: dal4segno Date: Fri, 27 Feb 2026 14:19:07 +0900 Subject: [PATCH] =?UTF-8?q?=ED=8C=80=20=EC=B2=B4=EB=A0=A5=20UI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Assembly-CSharp.csproj | 1 + Assets/Scenes/GameMain.unity | 17 ++ Assets/Scripts/UI/TeamHealthUI.cs | 391 +++++++++++++++++++++++++ Assets/Scripts/UI/TeamHealthUI.cs.meta | 2 + 4 files changed, 411 insertions(+) create mode 100644 Assets/Scripts/UI/TeamHealthUI.cs create mode 100644 Assets/Scripts/UI/TeamHealthUI.cs.meta diff --git a/Assembly-CSharp.csproj b/Assembly-CSharp.csproj index 6817af5..cb36616 100644 --- a/Assembly-CSharp.csproj +++ b/Assembly-CSharp.csproj @@ -82,6 +82,7 @@ + diff --git a/Assets/Scenes/GameMain.unity b/Assets/Scenes/GameMain.unity index 5db965f..e2e13a9 100644 --- a/Assets/Scenes/GameMain.unity +++ b/Assets/Scenes/GameMain.unity @@ -1183,6 +1183,7 @@ GameObject: - component: {fileID: 665708330} - component: {fileID: 665708331} - component: {fileID: 665708332} + - component: {fileID: 665708333} m_Layer: 5 m_Name: TeamCardsRoot m_TagString: Untagged @@ -1253,6 +1254,22 @@ MonoBehaviour: m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.ContentSizeFitter m_HorizontalFit: 2 m_VerticalFit: 0 +--- !u!114 &665708333 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 665708329} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: b5135790bbd96064aab844c2ba2aaa32, type: 3} + m_Name: + m_EditorClassIdentifier: Assembly-CSharp::Northbound.TeamHealthUI + slot1: {fileID: 0} + slot2: {fileID: 0} + slot3: {fileID: 0} + slot4: {fileID: 0} --- !u!1 &672563220 GameObject: m_ObjectHideFlags: 0 diff --git a/Assets/Scripts/UI/TeamHealthUI.cs b/Assets/Scripts/UI/TeamHealthUI.cs new file mode 100644 index 0000000..1649380 --- /dev/null +++ b/Assets/Scripts/UI/TeamHealthUI.cs @@ -0,0 +1,391 @@ +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}"); + } + } +} diff --git a/Assets/Scripts/UI/TeamHealthUI.cs.meta b/Assets/Scripts/UI/TeamHealthUI.cs.meta new file mode 100644 index 0000000..fd2cc78 --- /dev/null +++ b/Assets/Scripts/UI/TeamHealthUI.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: b5135790bbd96064aab844c2ba2aaa32 \ No newline at end of file