392 lines
12 KiB
C#
392 lines
12 KiB
C#
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}");
|
|
}
|
|
}
|
|
}
|