
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using TMPro;
using Unity.Netcode;
using UnityEngine;
public class Scoreboard : NetworkBehaviour
{
[SerializeField]
Transform scoreboardParent; //갖다 붙일 녀석의 부모
[SerializeField]
ScoreboardDisplayer scoreboardDisplayer;
//데이터 공유. 리스트에 타입을 지정해야할 스크립트가 필요.
NetworkList<ScoreboardData> scoreboardDatas = new NetworkList<ScoreboardData>();
List<ScoreboardDisplayer> scoreboardDisplayers = new List<ScoreboardDisplayer>(); //
public override void OnNetworkSpawn()
{
if (IsClient)
{
StartCoroutine(WaitAndBuildRankUI());
scoreboardDatas.OnListChanged += HandleScoreboardsChanged; //
foreach (ScoreboardData data in scoreboardDatas)
{
HandleScoreboardsChanged(new NetworkListEvent<ScoreboardData>
{
Type = NetworkListEvent<ScoreboardData>.EventType.Add,
Value = data
});
}
}
if (IsServer)
{
StartCoroutine(WaitAndBuildRankUI());
BuildRankUIClientRpc(HostSingleTon.Instance.MaxPlayers.Value);
//플레이어가 이미 존재할 수도 있다. -> 뭔말임?
PlayerController[] players = FindObjectsByType<PlayerController>(FindObjectsSortMode.None);
foreach (PlayerController player in players)
{
HandlePlayerSpawned(player);
}
PlayerController.OnPlayerSpawn += HandlePlayerSpawned;
PlayerController.OnPlayerDespawn += HandlePlayerDeSpawned;
HealthSystem.OnScored += HandleScoreGained; //
}
}
private IEnumerator WaitAndBuildRankUI()
{
// 값이 들어올 때까지 대기
yield return new WaitUntil(() => HostSingleTon.Instance.MaxPlayers.Value > 0);
BuildRankUI(HostSingleTon.Instance.MaxPlayers.Value);
}
private void BuildRankUI(int maxPlayers)
{
for (int i = 0; i < maxPlayers; i++)
{
GameObject ranked = Instantiate(RankedItem, RankedParent);
ranked.GetComponentInChildren<TextMeshProUGUI>().text = (i + 1).ToString();
}
}
yield return new WaitUntil(() => HostSingleTon.Instance.MaxPlayers.Value > 0); 을 통해서 클라이언트가 HostSingleTon.Instance.MaxPlayers.Value값을 받아올 때 까지 대기하고 BuildRankUI 호출하게 했는데 되질 않는다.
클라이언트가 처음 값을 읽었을 때(첫 프레임 떄) null이라면 코루틴이 바로 중단이 된고, Unity에서 WaitUntill의 조건식이 예외를 던지면 그 코루틴은 더 이상실행되지 않으로 BulidRankUI()까지 절대 가지 않는다.


using System.Collections;
using System.Collections.Generic;
using System.Linq;
using TMPro;
using Unity.Netcode;
using UnityEngine;
public class Scoreboard : NetworkBehaviour
{
[SerializeField]
Transform scoreboardParent; //갖다 붙일 녀석의 부모
[SerializeField]
ScoreboardDisplayer scoreboardDisplayer;
//데이터 공유. 리스트에 타입을 지정해야할 스크립트가 필요.
NetworkList<ScoreboardData> scoreboardDatas = new NetworkList<ScoreboardData>();
List<ScoreboardDisplayer> scoreboardDisplayers = new List<ScoreboardDisplayer>(); //
public override void OnNetworkSpawn()
{
if (IsClient)
{
scoreboardDatas.OnListChanged += HandleScoreboardsChanged; //
foreach (ScoreboardData data in scoreboardDatas)
{
HandleScoreboardsChanged(new NetworkListEvent<ScoreboardData>
{
Type = NetworkListEvent<ScoreboardData>.EventType.Add,
Value = data
});
}
}
if (IsServer)
{
StartCoroutine(WaitAndBuildRankUI()); //⭐ 추가
BuildRankUIClientRpc(HostSingleTon.Instance.MaxPlayers.Value); //⭐ 추가
//플레이어가 이미 존재할 수도 있다. -> 뭔말임?
PlayerController[] players = FindObjectsByType<PlayerController>(FindObjectsSortMode.None);
foreach (PlayerController player in players)
{
HandlePlayerSpawned(player);
}
PlayerController.OnPlayerSpawn += HandlePlayerSpawned;
PlayerController.OnPlayerDespawn += HandlePlayerDeSpawned;
HealthSystem.OnScored += HandleScoreGained; //
}
}
#region 순위표 - 순위 지정
[ClientRpc] //⭐ 추가
private void BuildRankUIClientRpc(int maxPlayers) //⭐ 추가
{
if (IsServer) return; //⭐ 추가서버는 이미 만들었음
BuildRankUI(maxPlayers); //⭐ 추가
}
private IEnumerator WaitAndBuildRankUI()
{
// 값이 들어올 때까지 대기
yield return new WaitUntil(() => HostSingleTon.Instance.MaxPlayers.Value > 0);
BuildRankUI(HostSingleTon.Instance.MaxPlayers.Value);
}
private void BuildRankUI(int maxPlayers)
{
Debug.Log("ㄴ");
for (int i = 0; i < maxPlayers - 1; i++) //최대 인원 수 - 1 = 접속 인원 수
{
GameObject ranked = Instantiate(RankedItem, RankedParent);
ranked.GetComponentInChildren<TextMeshProUGUI>().text = (i + 1).ToString();
}
}
}
using System.Collections;
using System.Threading.Tasks;
using TMPro;
using Unity.Collections;
using Unity.Netcode;
using Unity.Services.Lobbies;
using Unity.Services.Lobbies.Models;
using UnityEngine;
using UnityEngine.SceneManagement;
public class BattleSceneManager : NetworkBehaviour
{
private NetworkVariable<FixedString128Bytes> newPlayCount = new NetworkVariable<FixedString128Bytes>(
"", NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Server);
[SerializeField]
TextMeshProUGUI PlayCount;
[SerializeField]
TextMeshProUGUI CurrentLobbyName;
[SerializeField]
TextMeshProUGUI CurrentLobbyType;
[SerializeField]
TextMeshProUGUI CurrentLobbyCode;
[SerializeField]
TextMeshProUGUI CurrentMaxTime;
private void Start()
{
if (GameObject.FindFirstObjectByType<NetworkManager>() == null) //BattleSceneManager 스크립트가 있는 씬에 NetworkManager가 없다면
{
SceneManager.LoadScene("NetConnectScene"); //NetConnectScene 씬으로 이동
return; //반환
}
}
public override void OnNetworkSpawn() //각 클라이언트에서 실행
{
//IsHost : 서버이면서 클라이언트 일 때 (호스트만 포함)
//IsServer : 서버일 때(호스트 포함)
if (IsClient) //클라이언트 일 경우(Host,Client 포함)
{
newPlayCount.OnValueChanged += OnPlayCountChanged;
}
if (IsServer)
{
NetworkManager.Singleton.OnClientConnectedCallback += OnClientConnected;
}
}
string lobbyId;
private void OnClientConnected(ulong clientId) //클라이언트가 접속될 때 호출됨
{
if (NetworkManager.Singleton.IsHost)
{
lobbyId = HostSingleTon.Instance.LobbyId;
PlayeSceneMove();
LobbyInfoSet(); //❗ 호출
}
else //Host가 아닐 경우, 즉, Host방에 참가한 클라이언트일 경우
{
lobbyId = HostSingleTon.Instance.LobbyId; // 클라이언트도 lobbyId 받아오게 수정
LobbyInfoSet(); //❗ 호출
}
}
public async void LobbyInfoSet() // ⭐
{
Debug.Log("LobbyInfoSet()");
try
{
string lobbyId = HostSingleTon.Instance.LobbyId;
Lobby CurrentLobby = await LobbyService.Instance.GetLobbyAsync(lobbyId);
bool isLobbyType = false; //true - 비공개 방 , LobbyCode 없음 // false - 공개 방 , LobbyCoe 있음
//1. 방 이름
CurrentLobbyName.text = HostSingleTon.Instance.RoomName;
//2. 방 타입
if (CurrentLobby.Data.TryGetValue("IsPrivate", out var lobbyType))
{
if (lobbyType.Value == "true") //공개 방
{
isLobbyType = false;
CurrentLobbyType.text = "공개";
}
else
{
isLobbyType = true;
CurrentLobbyType.text = "비공개";
}
}
//3. 방 코드
if (isLobbyType)
{
CurrentLobbyCode.text = "없음";
}
else
{
if (CurrentLobby.Data.TryGetValue("Password", out var password))
{
CurrentLobbyCode.text = password.ToString();
}
}
}
catch(LobbyServiceException e)
{
}
}
//..생략
}
UI가 출력되지 않음

OnClientConnected() 에서 LobbyInfoSet() 호출 하면 UI 텍스트가 업데이트 되지 않는다.
OnClientConnected() 을 호출하는 NetworkManager.Singleton.OnClientConnectedCallback은 서버(또는 호스트) 입장에서, 어떤 클라이언트가 접속하면 호출되는 콜백이다. 순수 클라이언트는 이 콜백을 등록하지도 않고 실행되지도 않는다.
using System.Collections;
using System.Threading.Tasks;
using TMPro;
using Unity.Collections;
using Unity.Netcode;
using Unity.Services.Lobbies;
using Unity.Services.Lobbies.Models;
using UnityEngine;
using UnityEngine.SceneManagement;
public class BattleSceneManager : NetworkBehaviour
{
private NetworkVariable<FixedString128Bytes> newPlayCount = new NetworkVariable<FixedString128Bytes>(
"", NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Server);
[SerializeField]
TextMeshProUGUI PlayCount;
[SerializeField]
TextMeshProUGUI CurrentLobbyName;
[SerializeField]
TextMeshProUGUI CurrentLobbyType;
[SerializeField]
TextMeshProUGUI CurrentLobbyCode;
[SerializeField]
TextMeshProUGUI CurrentMaxTime;
private void Start()
{
if (GameObject.FindFirstObjectByType<NetworkManager>() == null) //BattleSceneManager 스크립트가 있는 씬에 NetworkManager가 없다면
{
SceneManager.LoadScene("NetConnectScene"); //NetConnectScene 씬으로 이동
return; //반환
}
}
public override void OnNetworkSpawn() //각 클라이언트에서 실행
{
//IsHost : 서버이면서 클라이언트 일 때 (호스트만 포함)
//IsServer : 서버일 때(호스트 포함)
if (IsClient) //클라이언트 일 경우(Host,Client 포함)
{
newPlayCount.OnValueChanged += OnPlayCountChanged;
LobbyInfoSet(); //⭐ 각 클라이언트에게 실행될 수 있게 함.
}
if (IsServer)
{
NetworkManager.Singleton.OnClientConnectedCallback += OnClientConnected;
}
}
string lobbyId;
private void OnClientConnected(ulong clientId) //클라이언트가 접속될 때 호출됨
{
if (NetworkManager.Singleton.IsHost)
{
lobbyId = HostSingleTon.Instance.LobbyId;
PlayeSceneMove();
// LobbyInfoSet();
}
else //Host가 아닐 경우, 즉, Host방에 참가한 클라이언트일 경우
{
lobbyId = HostSingleTon.Instance.LobbyId; // 클라이언트도 lobbyId 받아오게 수정
// LobbyInfoSet();
}
}
public async void LobbyInfoSet() // ⭐
{
Debug.Log("LobbyInfoSet()");
try
{
string lobbyId = HostSingleTon.Instance.LobbyId;
Lobby CurrentLobby = await LobbyService.Instance.GetLobbyAsync(lobbyId);
bool isLobbyType = false; //true - 비공개 방 , LobbyCode 없음 // false - 공개 방 , LobbyCoe 있음
//1. 방 이름
CurrentLobbyName.text = HostSingleTon.Instance.RoomName;
//2. 방 타입
if (CurrentLobby.Data.TryGetValue("IsPrivate", out var lobbyType))
{
if (lobbyType.Value == "true") //공개 방
{
isLobbyType = false;
CurrentLobbyType.text = "공개";
}
else
{
isLobbyType = true;
CurrentLobbyType.text = "비공개";
}
}
//3. 방 코드
if (isLobbyType)
{
CurrentLobbyCode.text = "없음";
}
else
{
if (CurrentLobby.Data.TryGetValue("Password", out var password))
{
CurrentLobbyCode.text = password.ToString();
}
}
}
catch(LobbyServiceException e)
{
}
}
//..생략
}

public override void OnNetworkSpawn()
{
if (IsClient) // 클라이언트 역할이라면 (Host 포함)
{
LobbyInfoSet(); // 이게 UI 갱신용으로 적절한 위치
}
if (IsServer) // 서버 역할이라면 (Host 포함)
{
NetworkManager.Singleton.OnClientConnectedCallback += OnClientConnected;
}
}
using System;
using Unity.Netcode;
using UnityEditor.PackageManager;
using UnityEngine;
public class HealthSystem : NetworkBehaviour
{
public BaseCharacterData _baseCharacterData;
public NetworkVariable<int> _currentHP = new NetworkVariable<int>(0, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Server);
public int CurrentHP => _currentHP.Value;
public int MaxHP => _baseCharacterData.MaxHp;
public NetworkVariable<bool> isDead = new NetworkVariable<bool>(false);
public Action<int> OnHpChanged;
public Action<HealthSystem> OnDie;
public static Action<ulong, int> OnScored;
public void Init(BaseCharacterData baseCharacterData)
{
//..생략
}
public override void OnNetworkSpawn()
{
//..생략
}
public void TakeDamageAsServer(int amount, ulong clientId)
{
//..생략
}
[ServerRpc(RequireOwnership = false)]
public void TakeDamageServerRpc(int amount, ulong clientId, bool isPlayerAttack)
{
// if (!IsServer) return; //플레이어 -> ai 공격 시, 이 줄에서 Null => 주석처리, 필요 시 해제
if (isDead.Value) return;
int damage = Mathf.Max(0, amount);
// Debug.Log($"[HealthSystem] Server: TakeDamageServerRpc called for {NetworkObjectId}. Damage: {damage}. Current HP: {_currentHP.Value}");
_currentHP.Value -= damage;
OnHpChanged?.Invoke(_currentHP.Value);
Debug.Log("TakeDamageServerRpc 메서드 진입");
if (_currentHP.Value <= 0)
{
_currentHP.Value = 0;
isDead.Value = true;
DieClientRpc();
//⭐ 문제 부분
if (TryGetComponent<StatDisPlayer>(out var statDisplay))
{
statDisplay.gameObject.layer = 10;
this.gameObject.name = "DiePlayer";
UIManager.Instance.SetStatCanvasActive(GetComponent<NetworkObject>().OwnerClientId, true);
Debug.Log("죽은 플레이어 레이어로 변경 및 생존한 플레이어 닉네임 활성화");
}
else
{
Debug.Log("StatDisPlayer 없음");
}
//⭐
Debug.Log($"[HealthSystem] Server: {NetworkObjectId} has died.");
if (IsServer && isPlayerAttack) //플레이어 공격시에만 점수 증가.
{
OnScored?.Invoke(clientId, 1);
}
}
}
[ServerRpc(RequireOwnership = false)]
public void TakeDeadServerRpc()
{
if (!IsServer) return;
TakeDead();
}
private void TakeDead()
{
isDead.Value = true;
OnDie?.Invoke(this);
}
[ClientRpc]
private void DieClientRpc()
{
OnDie?.Invoke(this);
}
}


원인 : StatDisPlayer가 자식 오브젝트에 붙어 있다면 TryGetComponent는 찾지 못합니다.
현재 이 스크립트는 부모인 Player에 부착되어 있었다. 그래서 자식에 붙어 있었으므로 TryGetComponent는 찾지 못하므로 Get을 사용

추가로 필요한 수정은 죽은 플레이어에게만 저 if문 안에 있는 기능을 실행해야 한다. 죽었을 때 DieClientRpc()가 호출 되는데 이 메서드 안에 isOwner로 체크하여 죽은자에게만 실행 할 수 있게 수정해야 한다.
최종 HealthSystem.cs 코드
using System;
using Unity.Netcode;
using UnityEditor.PackageManager;
using UnityEngine;
public class HealthSystem : NetworkBehaviour
{
public BaseCharacterData _baseCharacterData;
public NetworkVariable<int> _currentHP = new NetworkVariable<int>(0, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Server);
public int CurrentHP => _currentHP.Value;
public int MaxHP => _baseCharacterData.MaxHp;
public NetworkVariable<bool> isDead = new NetworkVariable<bool>(false);
public Action<int> OnHpChanged;
public Action<HealthSystem> OnDie;
public static Action<ulong, int> OnScored;
public void Init(BaseCharacterData baseCharacterData)
{
_baseCharacterData = baseCharacterData;
}
public override void OnNetworkSpawn()
{
_currentHP.OnValueChanged += (int previousValue, int newValue) =>
{
// Debug.Log($"[HealthSystem] HP changed on {NetworkObjectId}: {previousValue} -> {newValue}");
OnHpChanged?.Invoke(newValue);
};
if (!IsServer) return;
_currentHP.Value = MaxHP;
Debug.Log($"[HealthSystem] Initialized HP for {NetworkObjectId} to {MaxHP}");
}
public void TakeDamageAsServer(int amount, ulong clientId)
{
if (!IsServer) return;
int damage = Mathf.Max(0, amount);
Debug.Log($"[HealthSystem] Server: TakeDamageAsServer called for {NetworkObjectId}. Damage: {damage}. Current HP: {_currentHP.Value}");
_currentHP.Value -= damage;
OnHpChanged?.Invoke(_currentHP.Value);
if (_currentHP.Value <= 0)
{
_currentHP.Value = 0;
isDead.Value = true;
DieClientRpc();
Debug.Log($"[HealthSystem] Server: {NetworkObjectId} has died.");
}
}
[ServerRpc(RequireOwnership = false)]
public void TakeDamageServerRpc(int amount, ulong clientId, bool isPlayerAttack)
{
// if (!IsServer) return; //플레이어 -> ai 공격 시, 이 줄에서 Null => 주석처리, 필요 시 해제
if (isDead.Value) return;
int damage = Mathf.Max(0, amount);
// Debug.Log($"[HealthSystem] Server: TakeDamageServerRpc called for {NetworkObjectId}. Damage: {damage}. Current HP: {_currentHP.Value}");
_currentHP.Value -= damage;
OnHpChanged?.Invoke(_currentHP.Value);
Debug.Log("TakeDamageServerRpc 메서드 진입");
if (_currentHP.Value <= 0)
{
_currentHP.Value = 0;
isDead.Value = true;
DieClientRpc();
Debug.Log($"[HealthSystem] Server: {NetworkObjectId} has died.");
if (IsServer && isPlayerAttack) //플레이어 공격시에만 점수 증가.
{
OnScored?.Invoke(clientId, 1);
}
}
}
[ServerRpc(RequireOwnership = false)]
public void TakeDeadServerRpc()
{
if (!IsServer) return;
TakeDead();
}
private void TakeDead()
{
isDead.Value = true;
OnDie?.Invoke(this);
}
[ClientRpc]
private void DieClientRpc()
{
OnDie?.Invoke(this);
if (!IsOwner) return; //⭐ 추가
StatDisPlayer statDisplay = GetComponentInChildren<StatDisPlayer>(); //⭐ 추가
if (statDisplay != null) //⭐ 추가
{
statDisplay.gameObject.layer = 10; //⭐ 추가
UIManager.Instance.SetStatCanvasActive(GetComponent<NetworkObject>().OwnerClientId, true); //⭐ 추가
Debug.Log("죽은 플레이어 레이어로 변경 및 생존한 플레이어 닉네임 활성화"); //⭐ 추가
}
else
{
Debug.Log("StatDisPlayer 없음");
}
}
}
using System.Collections;
using System.Collections.Generic;
using TMPro;
using Unity.Netcode;
using Unity.Netcode.Components;
using Unity.VisualScripting;
using UnityEditor.PackageManager;
using UnityEngine;
using UnityEngine.SceneManagement;
public class GameResultManager : NetworkBehaviour //게임 승패 , 생존 , 사망 , 최종 승리자 정보 UI
{
private static GameResultManager instance;
public static GameResultManager Instance
{
get
{
return instance;
}
}
[SerializeField]
private NetworkVariable<int> AlivePlayerCount = new NetworkVariable<int>(0, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Server);
private void Awake()
{
if(instance == null)
{
instance = this;
}
else
{
Destroy(this.gameObject);
}
}
private void Start()
{
if (IsServer && !NetworkObject.IsSpawned)
{
SpawnClientRpc();
Debug.Log("GameResultManager Spawn 완료");
}
}
[ClientRpc]
private void SpawnClientRpc()
{
NetworkObject.Spawn();
}
public override void OnNetworkSpawn()
{
if(IsServer)
{
int connectedPlayerCount = 0;
foreach (var clientPair in NetworkManager.Singleton.ConnectedClients)
{
NetworkObject playerObj = clientPair.Value.PlayerObject;
if(playerObj != null)
{
connectedPlayerCount++;
}
}
AlivePlayerCount.Value = connectedPlayerCount;
}
}
//1. AlivePlayerCount가 1이 되면 => 현재 죽은 사람이 2등
//2. deadPlayerList는 3등부터 저장되어 있음(이전 사망자)
//3. 마지막 생존자를 찾아서 1등
List<ulong> deadPlayerList = new List<ulong>();
[ServerRpc(RequireOwnership = false)]
public void DeathPlayerCountServerRpc(ulong clientId, int deathCount)
{
if (!deadPlayerList.Contains(clientId)) //죽은 플레이어 저장
{
deadPlayerList.Add(clientId);
ulong firstBloodId = deadPlayerList[0];
FirstBloodPlayer(firstBloodId);
}
//⭐ 문제
AlivePlayerCount.Value -= deathCount;
if (AlivePlayerCount.Value <= 1) //최후 1인 생존
{
ulong winnerId = GetLastSurvivor();
rankingList = new List<ulong> { winnerId, clientId };
for (int i = deadPlayerList.Count - 2; i >= 0; i--) //죽은 플레이어들의 마지막 플레이어를 제외하고 역순으로
{
rankingList.Add(deadPlayerList[i]);
}
for (int i = 0; i < rankingList.Count; i++)
{
ulong rankedClientId = rankingList[i]; // 올바르게 ID 참조
Debug.Log($"{i + 1}등: ClientID {rankingList[i]}");
if (ServerSingleTon.Instance.clientIdToUserData.TryGetValue(rankingList[i], out var userData))
{
userData.Rank = i + 1;
Debug.Log($"[GameResultManager ClientID : {rankingList[i]} 의 >>>>>>>>>> 순위 : {userData.Rank} >>>>>>>> 유저 이름 : {userData.userName}");
}
}
RankingSetClientRpc(clientId);
}
//⭐
}
public List<ulong> rankingList;
[ClientRpc]
private void RankingSetClientRpc(ulong clientId)
{
ServerSingleTon.Instance.currentMap = MapManager.MapList.GameEnd;
if(IsServer)
{
string playSceneName = MapManager.Instance.WinnerMapSelected((int)HostSingleTon.Instance.MapIndex.Value);
LoadSceneServerRpc(playSceneName); //⭐
}
}
[ServerRpc(RequireOwnership = false)]
private void LoadSceneServerRpc(string playSceneName)
{
StartCoroutine(LoadSceneAfterFadeOut(playSceneName));
if (IsHost == false && IsClient)
{
LoadSceneClientRpc(playSceneName);
}
}
[ClientRpc]
private void LoadSceneClientRpc(string playSceneName)
{
if (IsHost == false)
{
StartCoroutine(LoadSceneAfterFadeOut(playSceneName));
}
}
IEnumerator LoadSceneAfterFadeOut(string scene) //시상식 씬 이동
{
yield return FadeInOut.Instance.FadeOutAndLoad();
SetStatCanvasUIClientRpc();
NetworkManager.Singleton.SceneManager.LoadScene(scene, UnityEngine.SceneManagement.LoadSceneMode.Single);
}
private ulong GetLastSurvivor()
{
foreach (var kvp in NetworkManager.Singleton.ConnectedClients) //연결된 클라이언트들 중에서
{
ulong id = kvp.Key;
if (!deadPlayerList.Contains(id)) //클라이언트 id가 deadPlayerList가 포함 안됐다면 -> 그 플레이어가 최후 1인임
{
return id;
}
}
Debug.LogWarning("최후 생존자 없음");
return 0; // default
}
[ClientRpc]
private void SetStatCanvasUIClientRpc() //시상식 씬 전환하기 전, 유저들 닉네임 전부 활성화.
{
foreach (var kvp in NetworkManager.Singleton.ConnectedClients)
{
UIManager.Instance.SetStatCanvasActive(kvp.Key, true);
}
UIManager.Instance.SaveFinalScoresToServerSingleTon();
//UIManager.Instance.SetStatCanvasActive(GetComponent<NetworkObject>().OwnerClientId, true);
}
public void FirstBloodPlayer(ulong firstBloodId)
{
if (IsServer)
{
var userData = ServerSingleTon.Instance.clientIdToUserData;
if (userData.ContainsKey(firstBloodId))
{
userData[firstBloodId].isFirstBloodPlayer = true;
bool isFirstDiePlayer = userData[firstBloodId].isFirstBloodPlayer;
}
}
}
//public void AIScoreSave(ulong clientId)
//{
// if(IsServer)
// {
// var userData = ServerSingleTon.Instance.clientIdToUserData;
// if (userData.ContainsKey(clientId))
// {
// int aiKillScore = userData[clientId].AiKiillFinalScore;
// userData[clientId].AiKiillFinalScore += 1; // 점수 증가
// Debug.Log($"클라이언트 {clientId}의 AI 킬 수는 {aiKillScore} 입니다. ");
// }
// }
//}
}

using System.Collections;
using System.Collections.Generic;
using TMPro;
using Unity.Netcode;
using Unity.Netcode.Components;
using Unity.VisualScripting;
using UnityEditor.PackageManager;
using UnityEngine;
using UnityEngine.SceneManagement;
public class GameResultManager : NetworkBehaviour //게임 승패 , 생존 , 사망 , 최종 승리자 정보 UI
{
private static GameResultManager instance;
public static GameResultManager Instance
{
get
{
return instance;
}
}
[SerializeField]
private NetworkVariable<int> AlivePlayerCount = new NetworkVariable<int>(0, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Server);
private void Awake()
{
if(instance == null)
{
instance = this;
}
else
{
Destroy(this.gameObject);
}
}
private void Start()
{
if (IsServer && !NetworkObject.IsSpawned)
{
SpawnClientRpc();
Debug.Log("GameResultManager Spawn 완료");
}
}
[ClientRpc]
private void SpawnClientRpc()
{
NetworkObject.Spawn();
}
public override void OnNetworkSpawn()
{
if(IsServer)
{
int connectedPlayerCount = 0;
foreach (var clientPair in NetworkManager.Singleton.ConnectedClients)
{
NetworkObject playerObj = clientPair.Value.PlayerObject;
if(playerObj != null)
{
connectedPlayerCount++;
}
}
AlivePlayerCount.Value = connectedPlayerCount;
}
}
private int GetAliveCount() //⭐ 생존 수를 반환하는 메서드
{
int alive = 0;
foreach (var kvp in NetworkManager.Singleton.ConnectedClients)
{
if (!deadPlayerList.Contains(kvp.Key))
{
alive++;
}
}
return alive;
}
//1. AlivePlayerCount가 1이 되면 => 현재 죽은 사람이 2등
//2. deadPlayerList는 3등부터 저장되어 있음(이전 사망자)
//3. 마지막 생존자를 찾아서 1등
List<ulong> deadPlayerList = new List<ulong>();
[ServerRpc(RequireOwnership = false)]
public void DeathPlayerCountServerRpc(ulong clientId, int deathCount)
{
if (!deadPlayerList.Contains(clientId)) //죽은 플레이어 저장
{
deadPlayerList.Add(clientId);
ulong firstBloodId = deadPlayerList[0];
FirstBloodPlayer(firstBloodId);
}
int aliveCount = GetAliveCount(); //⭐ 추가
Debug.Log("AliveCount" + aliveCount); //⭐ 추가
if (aliveCount <= 1) //⭐ 조건식 변경, 최후 1인 생존
{
ulong winnerId = GetLastSurvivor();
rankingList = new List<ulong> { winnerId, clientId };
for (int i = deadPlayerList.Count - 2; i >= 0; i--) //죽은 플레이어들의 마지막 플레이어를 제외하고 역순으로
{
rankingList.Add(deadPlayerList[i]);
}
for (int i = 0; i < rankingList.Count; i++)
{
ulong rankedClientId = rankingList[i]; // 올바르게 ID 참조
Debug.Log($"{i + 1}등: ClientID {rankingList[i]}");
if (ServerSingleTon.Instance.clientIdToUserData.TryGetValue(rankingList[i], out var userData))
{
userData.Rank = i + 1;
Debug.Log($"[GameResultManager ClientID : {rankingList[i]} 의 >>>>>>>>>> 순위 : {userData.Rank} >>>>>>>> 유저 이름 : {userData.userName}");
}
}
RankingSetClientRpc(clientId);
}
}
public List<ulong> rankingList;
[ClientRpc]
private void RankingSetClientRpc(ulong clientId)
{
ServerSingleTon.Instance.currentMap = MapManager.MapList.GameEnd;
if(IsServer)
{
string playSceneName = MapManager.Instance.WinnerMapSelected((int)HostSingleTon.Instance.MapIndex.Value);
LoadSceneServerRpc(playSceneName);
}
}
[ServerRpc(RequireOwnership = false)]
private void LoadSceneServerRpc(string playSceneName)
{
StartCoroutine(LoadSceneAfterFadeOut(playSceneName));
if (IsHost == false && IsClient)
{
LoadSceneClientRpc(playSceneName);
}
}
[ClientRpc]
private void LoadSceneClientRpc(string playSceneName)
{
if (IsHost == false)
{
StartCoroutine(LoadSceneAfterFadeOut(playSceneName));
}
}
IEnumerator LoadSceneAfterFadeOut(string scene) //시상식 씬 이동
{
yield return FadeInOut.Instance.FadeOutAndLoad();
SetStatCanvasUIClientRpc();
NetworkManager.Singleton.SceneManager.LoadScene(scene, UnityEngine.SceneManagement.LoadSceneMode.Single);
}
private ulong GetLastSurvivor()
{
foreach (var kvp in NetworkManager.Singleton.ConnectedClients) //연결된 클라이언트들 중에서
{
ulong id = kvp.Key;
if (!deadPlayerList.Contains(id)) //클라이언트 id가 deadPlayerList가 포함 안됐다면 -> 그 플레이어가 최후 1인임
{
return id;
}
}
Debug.LogWarning("최후 생존자 없음");
return 0; // default
}
[ClientRpc]
private void SetStatCanvasUIClientRpc() //시상식 씬 전환하기 전, 유저들 닉네임 전부 활성화.
{
foreach (var kvp in NetworkManager.Singleton.ConnectedClients)
{
UIManager.Instance.SetStatCanvasActive(kvp.Key, true);
}
UIManager.Instance.SaveFinalScoresToServerSingleTon();
//UIManager.Instance.SetStatCanvasActive(GetComponent<NetworkObject>().OwnerClientId, true);
}
public void FirstBloodPlayer(ulong firstBloodId)
{
if (IsServer)
{
var userData = ServerSingleTon.Instance.clientIdToUserData;
if (userData.ContainsKey(firstBloodId))
{
userData[firstBloodId].isFirstBloodPlayer = true;
bool isFirstDiePlayer = userData[firstBloodId].isFirstBloodPlayer;
}
}
}
//public void AIScoreSave(ulong clientId)
//{
// if(IsServer)
// {
// var userData = ServerSingleTon.Instance.clientIdToUserData;
// if (userData.ContainsKey(clientId))
// {
// int aiKillScore = userData[clientId].AiKiillFinalScore;
// userData[clientId].AiKiillFinalScore += 1; // 점수 증가
// Debug.Log($"클라이언트 {clientId}의 AI 킬 수는 {aiKillScore} 입니다. ");
// }
// }
//}
}
변경 사항은 네트워크 연결 끊고 나서 씬 전환 하기 전 딜레이를 줘서 씬이 너무 빨리 바뀌는 현상을 없애줌. 빨리 바뀌면 네트워크 연결이 안 끊겨질 수 있음
또한, 방을 나갈 때 호스트 혼자 남았을 수도 있으므로 생존한 플레이어 수를 체크하는 메서드를 호출 한다.

using System.Collections;
using System.Collections.Generic;
using TMPro;
using Unity.Netcode;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.UI;
public class InGameExitRoom : NetworkBehaviour //방 나가기 기능
{
[SerializeField]
Button ExitButton;
public void DisconnectPressed() //방나가기 버튼
{
// 1. 호스트인 경우 - 방 폭파
if (NetworkManager.Singleton.IsHost)
{
Debug.Log("호스트 방 종료");
// NetworkManager.Singleton.Shutdown();
HostSingleTon.Instance.ShutDown();
StartCoroutine(LoadSceneAfterFadeOut("TitleScene",1.5f)); //⭐
}
// 2. 클라이언트인 경우 - 본인만 나가기
else if (NetworkManager.Singleton.IsConnectedClient)
{
var playerObj = NetworkManager.Singleton.SpawnManager.GetLocalPlayerObject(); //현재 로컬 플레이어를 가져옴
if (playerObj != null)
{
PlayerController controller = playerObj.GetComponent<PlayerController>();
if (controller != null)
{
ulong ExitClientId = controller.GetComponent<NetworkObject>().OwnerClientId;
if (GameResultManager.Instance != null)
{
GameResultManager.Instance.ExitPlayer(ExitClientId); //⭐ 변경
}
}
}
NetworkManager.Singleton.Shutdown(); //⭐
StartCoroutine(LoadSceneAfterFadeOut("TitleScene",1.5f)); //⭐
}
}
IEnumerator LoadSceneAfterFadeOut(string scene, float delay) //⭐
{
yield return new WaitForSeconds(delay);
yield return FadeInOut.Instance.FadeOutAndLoad();
}
}
using System.Collections;
using System.Collections.Generic;
using TMPro;
using Unity.Netcode;
using Unity.Netcode.Components;
using Unity.VisualScripting;
using UnityEditor.PackageManager;
using UnityEngine;
using UnityEngine.SceneManagement;
public class GameResultManager : NetworkBehaviour //게임 승패 , 생존 , 사망 , 최종 승리자 정보 UI
{
private static GameResultManager instance;
public static GameResultManager Instance
{
get
{
return instance;
}
}
private void Awake()
{
if (instance == null)
{
instance = this;
}
else
{
Destroy(this.gameObject);
}
}
private void Start()
{
if (IsServer && !NetworkObject.IsSpawned)
{
SpawnClientRpc();
Debug.Log("GameResultManager Spawn 완료");
}
}
[ClientRpc]
private void SpawnClientRpc()
{
NetworkObject.Spawn();
}
public override void OnNetworkSpawn()
{
}
private int GetAliveCount()
{
int alive = 0;
foreach (var kvp in NetworkManager.Singleton.ConnectedClients)
{
if (!deadPlayerList.Contains(kvp.Key))
{
alive++;
}
}
return alive;
}
//1. AlivePlayerCount가 1이 되면 => 현재 죽은 사람이 2등
//2. deadPlayerList는 3등부터 저장되어 있음(이전 사망자)
//3. 마지막 생존자를 찾아서 1등
List<ulong> deadPlayerList = new List<ulong>();
[ServerRpc(RequireOwnership = false)]
public void DeathPlayerCountServerRpc(ulong clientId)
{
if (!deadPlayerList.Contains(clientId)) //죽은 플레이어 저장
{
deadPlayerList.Add(clientId);
ulong firstBloodId = deadPlayerList[0];
FirstBloodPlayer(firstBloodId);
}
int aliveCount = GetAliveCount();
Debug.Log("AliveCount" + aliveCount);
if (aliveCount <= 1) //최후 1인 생존
{
WinnerPlayer(clientId); //⭐
}
}
public void ExitPlayer(ulong clientId) //⭐ 호스트가 아닌 클라이언트가 게임 도중 나갔을 때 처리하는 메서드 추가
{
int aliveCount = GetAliveCount();
if (aliveCount <= 1)
{
WinnerPlayer(clientId);
}
else
{
Debug.Log("너가 나가도 최후의 1인은 아직 선정되지 않았어");
}
}
private void WinnerPlayer(ulong clientId) //⭐ 최후의 1인 메서드 따로 분리
{
ulong winnerId = GetLastSurvivor();
rankingList = new List<ulong> { winnerId, clientId };
for (int i = deadPlayerList.Count - 2; i >= 0; i--) //죽은 플레이어들의 마지막 플레이어를 제외하고 역순으로
{
rankingList.Add(deadPlayerList[i]);
}
for (int i = 0; i < rankingList.Count; i++)
{
ulong rankedClientId = rankingList[i]; // 올바르게 ID 참조
Debug.Log($"{i + 1}등: ClientID {rankingList[i]}");
if (ServerSingleTon.Instance.clientIdToUserData.TryGetValue(rankingList[i], out var userData))
{
userData.Rank = i + 1;
Debug.Log($"[GameResultManager ClientID : {rankingList[i]} 의 >>>>>>>>>> 순위 : {userData.Rank} >>>>>>>> 유저 이름 : {userData.userName}");
}
}
RankingSetClientRpc(clientId);
}
public List<ulong> rankingList;
[ClientRpc]
private void RankingSetClientRpc(ulong clientId)
{
//..생략
}
[ServerRpc(RequireOwnership = false)]
private void LoadSceneServerRpc(string playSceneName)
{
//..생략
}
[ClientRpc]
private void LoadSceneClientRpc(string playSceneName)
{
//..생략
}
IEnumerator LoadSceneAfterFadeOut(string scene) //시상식 씬 이동
{
//..생략
}
private ulong GetLastSurvivor()
{
//..생략
}
[ClientRpc]
private void SetStatCanvasUIClientRpc() //시상식 씬 전환하기 전, 유저들 닉네임 전부 활성화.
{
//..생략
}
public void FirstBloodPlayer(ulong firstBloodId)
{
//..생략
}
}


using System.Collections;
using System.Collections.Generic;
using TMPro;
using Unity.Netcode;
using Unity.Netcode.Components;
using Unity.VisualScripting;
using UnityEditor.PackageManager;
using UnityEngine;
using UnityEngine.SceneManagement;
public class GameResultManager : NetworkBehaviour //게임 승패 , 생존 , 사망 , 최종 승리자 정보 UI
{
private static GameResultManager instance;
public static GameResultManager Instance
{
get
{
return instance;
}
}
private void Awake()
{
if (instance == null)
{
instance = this;
}
else
{
Destroy(this.gameObject);
}
}
private void Start()
{
if (IsServer && !NetworkObject.IsSpawned)
{
SpawnClientRpc();
Debug.Log("GameResultManager Spawn 완료");
}
}
[ClientRpc]
private void SpawnClientRpc()
{
NetworkObject.Spawn();
}
public override void OnNetworkSpawn()
{
}
//..생략
#region FadeInOut 동기화 및 씬 전환
//⭐
[ServerRpc(RequireOwnership = false)]
private void WinnerSceneServerRpc()
{
ServerSingleTon.Instance.currentMap = MapManager.MapList.GameEnd;
string playSceneName = MapManager.Instance.WinnerMapSelected((int)HostSingleTon.Instance.MapIndex.Value);
WinnerScene(playSceneName);
}
private void WinnerScene(string sceneName)
{
WinnerSceneLoadServerRpc(sceneName);
}
[ServerRpc(RequireOwnership = false)]
private void WinnerSceneLoadServerRpc(string sceneName)
{
StartCoroutine(LoadSceneAfterFadeOut(sceneName));
LoadSceneClientRpc(sceneName);
}
[ClientRpc]
private void LoadSceneClientRpc(string playSceneName)
{
if (IsHost == false)
{
StartCoroutine(LoadSceneAfterFadeOut(playSceneName));
}
}
IEnumerator LoadSceneAfterFadeOut(string scene) //시상식 씬 이동
{
yield return FadeInOut.Instance.FadeOutAndLoad();
SetStatCanvasUIClientRpc();
NetworkManager.Singleton.SceneManager.LoadScene(scene, UnityEngine.SceneManagement.LoadSceneMode.Single);
}
//⭐
#endregion
//..생략
}