🤔 1. 랭크 설정 ( 호스트에서는 순위 프리펩 생성이 되지만, 클라이언트에서는 순위 프리펩이 생성이 안됨. 동기화 문제)

    1. OnNetworkSpawn () 에서 if(IsClient)을 통해 StartCoroutine(WaitAndBuildRankUI()); 호출 -> Client가 MaxPlayers.Value 값이 완전히 동기화 되지 않을 수 있음.
  • 동기화 되지 않으면 기본값인 0이 되므로, 랭크 설정이 제대로 되지 않음.
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()까지 절대 가지 않는다.

⭐ 문제 해결 방법

    1. ClientRpc를 통해 서버가 직접 알려주는 방식이 안정적임
    1. 서버는 MaxPlayers 값을 이미 알고 있으므로 정확한 값을 줄 수 있음.

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();
        }
    }
}

🤔 2. UI는 자기 자신의 로컬 화면에서만 존재하는 컴포넌트이므로, 클라이언트 본인의 인스턴스에서 호출되어야 UI가 갱신된다.

문제의 코드

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은 서버(또는 호스트) 입장에서, 어떤 클라이언트가 접속하면 호출되는 콜백이다. 순수 클라이언트는 이 콜백을 등록하지도 않고 실행되지도 않는다.

Host는 같은 물리적 인스턴스지만, 논리적으로 Client와 Server가 분리된 상태이다.

  • 그래서 서버 역할로 UI를 건드리면 안 됨. 서버 역할이라는 것은 OnClientConnected() 호출자를 말한다. 호출자는 서버이다. UI는 각 클라이언트에서 업데이트 할 수 있도록 해야 한다.
  • 서버에서도 UI에 영향을 줄 수는 있지만, 직접 UI를 업데이트하는 역할은 클라이언트가 담당하는 게 원칙이다.

문제 해결 코드

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)
        {

        }

    }

	//..생략

}

실행 화면 - 문제 해결 영상

  • 정상적으로 UI가 호출 됨.

서버에 접속한(OnClientConnected()) 클라이언트가 호출하는(OnNetworkSpawn()) 메서드에서 호출하게 한다.

public override void OnNetworkSpawn()
{
    if (IsClient) // 클라이언트 역할이라면 (Host 포함)
    {
        LobbyInfoSet(); // 이게 UI 갱신용으로 적절한 위치
    }

    if (IsServer) // 서버 역할이라면 (Host 포함)
    {
        NetworkManager.Singleton.OnClientConnectedCallback += OnClientConnected;
    }
}

죽은 플레이어 레이어 변경 및 생존한 플레이어 닉네임 활성화 부분

  • 기존 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)
    {
      //..생략
    }

    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 없음");
        }
    }
}

호스트가 죽으면 바로 시상식씬으로 넘어가는 오류

  • 문제는 GameResultManager.cs에 있다.
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} 입니다. ");
    //        }
    //    }
    //}

}

  • 원인 : GameResultManager는 서버에서 AlivePlayerCount를 관리 했었다. 서버 = 호스트가 죽는 순간 AlivePlayerCount값 관계 없이 시상식씬으로 진입하는 것임.
  • 해결 : AlivePlayerCount 삭제 및 , ConnectedClients에서 살아 있는 클라이언트들을 직접 카운트하여 비교한다.
  • 최종 GameResultManager.cs 코드
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} 입니다. ");
    //        }
    //    }
    //}

}

호스트가 아닌 클라이언트가 방을 나갔을 때 코드 수정

  • 변경 사항은 네트워크 연결 끊고 나서 씬 전환 하기 전 딜레이를 줘서 씬이 너무 빨리 바뀌는 현상을 없애줌. 빨리 바뀌면 네트워크 연결이 안 끊겨질 수 있음

  • 또한, 방을 나갈 때 호스트 혼자 남았을 수도 있으므로 생존한 플레이어 수를 체크하는 메서드를 호출 한다.

    1. 방 나가는 스크립트 수정
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();
    }
}
    1. GameResultManager (게임 결과)에서 코드 수정
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)
    {
       //..생략
    }


}
    1. 또한, 이젠 죽어서도 방을 나갔을 수 있으므로 Scoreboard.cs에서 다음 코드로 수정
  • Remove하게 되면 해당 클라이언트의 순위표는 삭제됨

호스트측에서 Fade 여러번 되는 오류

  • GameResultManager에서 시상식씬으로 넘어갈 때 발견했다.
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

  	//..생략

}

0개의 댓글