팀 체력 UI

This commit is contained in:
2026-02-27 14:19:07 +09:00
parent 3aa43bfd20
commit c34b9df4f7
4 changed files with 411 additions and 0 deletions

View File

@@ -82,6 +82,7 @@
<Compile Include="Assets\Scripts\Resource.cs" />
<Compile Include="Assets\Data\Scripts\DataClasses\UpgradeData.cs" />
<Compile Include="Assets\Scripts\UpgradeDatabase.cs" />
<Compile Include="Assets\Scripts\UI\TeamHealthUI.cs" />
<Compile Include="Assets\Scripts\AutoTargetSystem.cs" />
<Compile Include="Assets\Scripts\CreepDataComponent.cs" />
<Compile Include="Assets\Scripts\BuildingSlotButton.cs" />

View File

@@ -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

View File

@@ -0,0 +1,391 @@
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using TMPro;
using Unity.Netcode;
namespace Northbound
{
/// <summary>
/// 팀원 체력 UI 관리
/// Slot 1: 로컬 플레이어 (본인)
/// Slot 2~4: 다른 플레이어 (플레이어 ID 순)
/// </summary>
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<ulong, NetworkPlayerController> _playerControllers = new Dictionary<ulong, NetworkPlayerController>();
private Dictionary<ulong, TeamCardSlot> _playerSlotMapping = new Dictionary<ulong, TeamCardSlot>();
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();
}
/// <summary>
/// 이름으로 슬롯 찾기 (자식의 자식 프리팹까지 검색)
/// </summary>
private TeamCardSlot FindSlotByName(string slotName)
{
// 직접 자식에서 찾기
Transform slotTransform = transform.Find(slotName);
if (slotTransform == null) return null;
// 자식 프리팹에서 TeamCardSlot 찾기 또는 생성
TeamCardSlot slot = slotTransform.GetComponentInChildren<TeamCardSlot>(true);
if (slot == null)
{
// TeamCardSlot 컴포넌트를 자식 프리팹의 루트에 추가
// 프리팹 인스턴스의 첫 번째 자식이 실제 카드 프리팹
if (slotTransform.childCount > 0)
{
GameObject cardObj = slotTransform.GetChild(0).gameObject;
slot = cardObj.AddComponent<TeamCardSlot>();
}
else
{
// 자식이 없으면 슬롯 자체에 추가
slot = slotTransform.gameObject.AddComponent<TeamCardSlot>();
}
}
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<NetworkPlayerController>(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<KeyValuePair<ulong, NetworkPlayerController>>();
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();
}
}
/// <summary>
/// 개별 팀 카드 슬롯
/// TeamCard_Large 또는 TeamCard_Small 프리팹에 붙여서 사용
/// </summary>
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<TextMeshProUGUI>();
}
}
// HPBarFill 찾기
if (_hpBarFill == null)
{
Transform hpFill = FindDeepChild(transform, "HPBarFill");
if (hpFill != null)
{
_hpBarFill = hpFill.GetComponent<Image>();
}
}
// HPText 찾기
if (_hpText == null)
{
Transform hpText = FindDeepChild(transform, "HPText");
if (hpText != null)
{
_hpText = hpText.GetComponent<TextMeshProUGUI>();
}
}
}
/// <summary>
/// 깊이 우선 탐색으로 자식 찾기
/// </summary>
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}";
}
}
/// <summary>
/// 에디터에서 UI 요소 자동 할당
/// </summary>
[ContextMenu("Auto Find UI Elements")]
private void AutoFindUIElements()
{
FindUIElements();
Debug.Log($"[TeamCardSlot] UI Elements Found - Name: {_playerNameText != null}, HPFill: {_hpBarFill != null}, HPText: {_hpText != null}");
}
}
}

View File

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