Notice
Recent Posts
Recent Comments
Link
«   2026/01   »
1 2 3
4 5 6 7 8 9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29 30 31
Archives
Today
Total
관리 메뉴

행복한 개구리

Photon 클라이언트가 마스터의 오브젝트를 로딩하지 못하는 문제 해결 본문

Unity/Unity - Solutions

Photon 클라이언트가 마스터의 오브젝트를 로딩하지 못하는 문제 해결

HappyFrog 2021. 7. 23. 19:49

 

  • 제목 그대로 Photon을 실행하면 마스터가 PhotonNetwork.Instantiate를 통해 서버에 생성하는 객체를 클라쪽에 동기화가 되지 않는 이슈가 있었다.
  • 이게 골치아팠던 점이 확률적으로 정상일 때도 있고 위 이슈가 발생할 때도 있어서 원인을 찾기 어려웠다.
  • 하지만 유심히 살펴보니 마스터의 씬이 먼저 로드되고 그 다음 클라이언트가 넘어가면 클라이언트에게 맵이 보이지 않았고,
  • 반대로 클라이언트가 먼저 씬 로드를 마치면 나중에 씬이 넘어온 마스터의 객체들을 볼 수 있었다.
    • =>따라서 같은씬에 없을 경우 먼저 들어온 클라이언트가 생성한 후에 다른 클라이언트가 들어오면 나중에 들어온 클라이언트는 먼저 들어온 클라이언트의 객체들을 동기화받지 못한다.
  • 이를 활용하여 RPC로 클라이언트가 모두 씬전환을 마쳤으면 마스터의 씬이 전환되게 바꿨다.

 

 

  • 기존 LoadScene구조를 간략하게 그림으로 그려봤다.
  • 기존 LoadScene은 마스터가 Start를 하면 방에있는 전원에게 RPC를 전달하여 LoadScene을 실행시키는 방식으로 진행됐다.
  • 하지만 해당 방법은 마스터와 클라이언트간의 로딩속도가 시시각각 변하기때문에 문제가 발생했던 것으로 추측된다.

 

 

  • 새롭게 작성한 구조이다.
  • 마스터가 시작을 하면 자신을 제외한 방의 클라이언트들을 씬전환 시킨다.
  • 클라이언트들이 씬전환을 하면 RPC로 마스터에게 씬전환을 완료했다고 전송한다.
  • 전달받은 RPC의 갯수와 클라이언트들의 수가 같다면 마스터도 씬을 로드한다.

 

 

 


RPC는 대리자형식으로 실행하며 내용은 서버스크립트에 람다식으로 작성

우선 들어가기에 앞서 나는 모든 씬마다의 RPC를 따로 스크립트화 하여 대리자형식으로 실행했다


 

 

 public void RPC_StartGame()
    {
        if (PhotonNetwork.PlayerListOthers.Length != 0)
        {
            lobbyRPC.RPC("StartGame", RpcTarget.Others);
        }
        else
        {
            SceneManager.LoadScene("InGame");
        }
    }
  • 시작 RPC부분이다.
  • 방에 마스터 혼자있다면 그냥 시작하고
  • 다른 클라이언트가 함께 있다면 RPC를 보낸다.

 

 

//RPC 스크립트
public class LobbyRPC : PhotonView
{
    public System.Action OnStartGame;
    
    [PunRPC]
    void StartGame()
    {        
        OnStartGame();
    }
}


//서버 오브젝트의 스크립트
public class GamePunCallbacks : MonoBehaviourPunCallbacks
{
    PhotonView photonView;
    LobbyRPC lobbyRPC;
    private void Awake()
    {
        lobbyRPC = photonView.GetComponent<LobbyRPC>();
        lobbyRPC.OnStartGame = () =>
        {
            AsyncOperation async = SceneManager.LoadSceneAsync("InGame");
            async.completed += (oper) =>
            {
                if (oper.isDone)
                {
                    lobbyRPC.RPC("CompleteLoadScene", RpcTarget.MasterClient);
                }
            };
        };
    }
}
  • 클라이언트가 마스터의 RPC를 받아 비동기로 씬전환을 실행하고
  • 완료되었다면 마스터에게 완료되었다는 RPC를 보낸다.

 

 

//RPC 스크립트
public class LobbyRPC : PhotonView
{    
    public System.Action OnCompleteLoadScene;

    [PunRPC]
    void CompleteLoadScene()
    {
        OnCompleteLoadScene();
    }
}

//서버 오브젝트의 스크립트
public class GamePunCallbacks : MonoBehaviourPunCallbacks
{
    PhotonView photonView;
    LobbyRPC lobbyRPC;
    int completeLoadClients = 0;
    private void Awake()
    {
        lobbyRPC = photonView.GetComponent<LobbyRPC>();
        lobbyRPC.OnCompleteLoadScene = () =>
        {
            if (PhotonNetwork.IsMasterClient)
            {
                completeLoadClients++;                
                if (PhotonNetwork.PlayerListOthers.Length == completeLoadClients)
                {
					completeLoadClients = 0;
                    SceneManager.LoadScene("InGame");
                }
            }
        };
    }
}
  • 그리고 마스터는 RPC를 전달받을 때마다 로드를 완료한 클라이언트의 수를 1씩 올리며
  • 그 숫자가 방안의 다른 클라이언트들의 수와 같을 때 씬전환을 시작한다.
  • 그리고 그 다음에 다시 실행해도 이상없게끔 로드완료 클라이인트의 수를 초기화해준다.

 

해보니 또 다른 이슈가 있었다.

씬전환을 했기 때문에 클라이언트들은 마스터의 객체를 모두 동기화 받는 반면 늦게 들어온 마스터는 클라이언트의 객체들이 전혀 보이지 않았다.

해당 이슈를 고치기 위해서 모두 비동기로 씬을 로드했다면 마스터클라이언트가 모든 클라이언트에게 캐릭터를 생성하라고 전달하는 RPC를 전달하기로 했다.

 

 

  • 모두가 씬 전환을 마친 다음 RPC로 캐릭터를 만드는 구조이다.
  • 단순하게 모두가 씬전환을 마쳤다는 함수내용에 RPC_CreateCharacter실행을 추가해주면 된다.

 

 

 //RPC_CompleteLoadScene
 lobbyRPC.OnCompleteLoadScene = () =>
        {
            if (PhotonNetwork.IsMasterClient)
            {
                completeLoadClients++;
                if (PhotonNetwork.PlayerListOthers.Length == completeLoadClients)
                {
                    AsyncOperation async = SceneManager.LoadSceneAsync("InGame");
                    async.completed += (oper) =>
                    {
                        PUN2Manager.GetInstance<IPUN2>().FindManager();
                        inGameRPC.RPC("CreateCharacter", RpcTarget.All);
                    };
                }
            }
        };
        
        
        //RPC_CraeteCharacter
        inGameRPC.OnCreateCharacter = () =>
        {
            GameObject character = PhotonNetwork.Instantiate("TestCharacter", Vector3.zero, Quaternion.identity);
            this.character = character;
            this.character.name = PhotonNetwork.NickName;
            this.character.GetComponent<CharacterController>().isLocal = true;
            this.myViewId = this.character.GetComponent<PhotonView>().ViewID;
            PUN2Manager.GetInstance<IInGamePUN2>().SetCamController();
        };
  • 따라서 OnCompleteLoadScene을 위와 같이 비동기로 수정하며 CreateCharacter라는 RPC를 전부에게 전달하도록 했다.
  • 그리고 기존에는 InGameManager의 Awake에서 생성하던 캐릭터를 RPC에서 생성하는 것으로 바꿔주고 카메라 컨트롤러도 캐릭터가 생성된 후에 할당했다.
  • 이렇게 하니 마스터와 클라이언트 모두 서로가 잘 보이고 동기화도 잘 되는 결과가 나왔다.

 

 

스크립트 통합본

//RPC 스크립트
public class LobbyRPC : PhotonView
{    
    public System.Action OnStartGame;
    public System.Action OnCompleteLoadScene;
        
    [PunRPC]
    void StartGame()
    {        
        OnStartGame();
    }
    [PunRPC]
    void CompleteLoadScene()
    {
        OnCompleteLoadScene();
    }
}

public class InGameRPC : PhotonView
{        
    public System.Action OnCreateCharacter;
   
    [PunRPC]
    void CreateCharacter()
    {
        OnCreateCharacter();
    }
}

//서버 오브젝트의 스크립트
public class GamePunCallbacks : MonoBehaviourPunCallbacks
{
    PhotonView photonView;
    LobbyRPC lobbyRPC;
    int completeLoadClients = 0;
    private void Awake()
    {
        lobbyRPC = photonView.GetComponent<LobbyRPC>();
        lobbyRPC.OnStartGame = () =>
        {
            AsyncOperation async = SceneManager.LoadSceneAsync("InGame");
            async.completed += (oper) =>
            {
                if (oper.isDone)
                {
                    lobbyRPC.RPC("CompleteLoadScene", RpcTarget.MasterClient);
                }
            };
        };
        lobbyRPC.OnCompleteLoadScene = () =>
        {
            if (PhotonNetwork.IsMasterClient)
            {
                completeLoadClients++;
                if (PhotonNetwork.PlayerListOthers.Length == completeLoadClients)
                {
                    AsyncOperation async = SceneManager.LoadSceneAsync("InGame");
                    async.completed += (oper) =>
                    {
                        PUN2Manager.GetInstance<IPUN2>().FindManager();
                        inGameRPC.RPC("CreateCharacter", RpcTarget.All);
                    };
                }
            }
        };           
        inGameRPC.OnCreateCharacter = () =>
        {
            GameObject character = PhotonNetwork.Instantiate("TestCharacter", Vector3.zero, Quaternion.identity);
            this.character = character;
            this.character.name = PhotonNetwork.NickName;
            this.character.GetComponent<CharacterController>().isLocal = true;
            this.myViewId = this.character.GetComponent<PhotonView>().ViewID;
            PUN2Manager.GetInstance<IInGamePUN2>().SetCamController();
        };
    }
}
  • 스크립트는 여기서 서술한 이슈와 관련된 부분만 게시했다.
  • 실제로는 Singleton과 각 씬의 Manager들을 찾지 못해 발생한 이슈도 있었지만 구조적인 문제였을 뿐이고 가장 큰 이슈는 해당 이슈였기 때문에 생략하도록 하겠다.

깊게 박힌 가시를 빼낸 느낌이다 ㅎㅎ