ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Photon - 동기화에 대한 이해 (1)
    Unity 2023. 11. 6. 23:14

    ** 로비 화면 / 룸 화면을 구성하면서 겪었던 사례를 통해 느낀 점을 서술하였습니다.

     

    I. 동기화의 범위

     

    동기화

     

     : 게임의 여러 플레이어 간 또는 클라이언트와 서버 간의 상태 및 데이터를 일관되게 유지하는 것

     => 이 일관되게 유지하는 것의 범위를 잘 이해하고, 효율적인 수단을 사용함으로써 더 좋은 동기화를 유지할 수 있다.

     

     

    AutomaticallySyncScene

     

      : 해당 옵션이 true인 경우, 룸에 참가한 클라이언트들은 모두 마스터 클라이언트(호스트)와 같은 레벨(씬)을 load하게 된다.

     

     PhotonNetwork.AutomaticallySyncScene = true;

     

     

    MonoBehaviourPun

     

        : 포톤 클라우드 서비스와 연결된 Unity 애플리케이션에서 네트워크 관련 기능을 활용할 수 있도록 지원.

        : 해당 클래스의 인스턴스화가 이루어진 시점부터, 네트워크 이벤트 및 동기화가 시작됨.

        : 기본적으로 각 클라이언트들은 해당 스크립트의 필드값이 동일하게 유지됨..

     

     

    1.. MonoBehaviorPun 클래스에서 정의된 필드는 동기화 된다.

       방에, Ready 버튼이 있고, 이 버튼을 누르면 게임을 시작할 준비상태가 되고, 해당 버튼이 누를 시에 준비 상태가 취소되는 취소 버튼으로 변한다고 생각해보자.

     

     

     

    private bool isPlayerReady;
    
    //.. 생략 ..
    
    public void SetPlayerReady(bool playerReady)
    {
        if (playerReady)
        {
            ReadyButton.GetComponentInChildren<TextMeshProUGUI>().text = "취소";
            ReadyButton.GetComponentInChildren<Image>().color = new Color(130 / 255f, 241 / 255f, 96 / 255f);
        }
        else
        {
            ReadyButton.GetComponentInChildren<TextMeshProUGUI>().text = "준비";
            ReadyButton.GetComponentInChildren<Image>().color = new Color(255 / 255f, 182 / 255f, 182 / 255f);
        }
    }
    
    public void OnReadyButtonClicked()
    {
        isPlayerReady = !(bool)isPlayerReady;
        SetPlayerReady((bool)isPlayerReady);
        var props = new Hashtable() { { "IsPlayerReady", isPlayerReady } };
        PhotonNetwork.LocalPlayer.SetCustomProperties(props);
    }

     

     

    버튼을 눌렀을 시, 버튼의 모양을 바꿀 수 있는, SetPlayerReady() 메서드와 인스펙터에서 준비 버튼에 연결해둔 OnReadyButtonClicked() 메서드를 다음과 같이 설정하고, 버튼을 누른다면 ....

     

     

     

    위의 그림과 같이, 한 쪽의 버튼을 눌렀는데 다른 클라이언트의 버튼도 같이 눌린 경험을 하였다.

     

     

     

    동기화가 진행되면서, 필드 값 중 하나인 isPlayerReady의 값이 동일하게 유지되어야 하기 때문에 일어난 일이다..

    위와 같은 상황에서는 동기화가 필요 없는데, 동기화가 적용된 상황이다.

    이런 상황을 제어하기 위해, 커스텀 프로퍼티 혹은 RPC를 사용할 수 있다.

     

    2.. 커스텀 프로퍼티

     

      : 플레이어 혹은 룸과 관련된 키-값의 hashtable로 이루어진 추가 정보.

      : 각 클라이언트 별로 따로 저장된다.

     

      커스텀 프로퍼티 (플레이어) 설정

     

    var props = new Hashtable() { { "IsPlayerReady", isPlayerReady } };
    PhotonNetwork.LocalPlayer.SetCustomProperties(props);

     

      커스텀 프로퍼티 값 확인

     

    if (PhotonNetwork.LocalPlayer.CustomProperties.TryGetValue("IsPlayerReady", out object isPlayerReady))
    {
        isPlayerReady = !(bool)isPlayerReady;

     

     

        : 필드 값은 동일하게 유지되지만, 커스텀 프로퍼티는 클라이언트별로 설정한 값을 갖기 때문에 해당 값을 통해서 클라이언트 각각의 상태를 분리할 수 있다.

     

       커스텀 프로퍼티 활용 : 직업

     

    public enum CharClass
    {
        Soldier,
        Shotgun,
        Sniper,
    }
    
    if (!playerCP.ContainsKey("Char_Class"))
    {
        PhotonNetwork.SetPlayerCustomProperties(
        new ExitGames.Client.Photon.Hashtable()
        { {"Char_Class", CharClass.Soldier} }
        );
    }

     

         : 커스텀 프로퍼티를 활용하여, 각 클라이언트가 어떤 직업을 선택하였는 지, 설정하게 하였다.

     

    public void SetClassType(int charType, GameObject playerGo = null)
    {
        PlayerStatHandler statSO;
        if (playerGo != null)
        {
            Debug.Log($"적용 오브젝트 : {playerGo.name}");
            statSO = playerGo.GetComponent<PlayerStatHandler>();
        }
        else
        {
            Debug.Log($"적용 오브젝트 : PlayerContainer");
            statSO = playerContainer.GetComponentInChildren<PlayerStatHandler>();
        }
    
        switch (charType)
        {
            case (int)LobbyPanel.CharClass.Soldier:
                statSO.CharacterChange(soldierSO);
                break;
            case (int)LobbyPanel.CharClass.Shotgun:
                statSO.CharacterChange(shotGunSO);
                break;
            case (int)LobbyPanel.CharClass.Sniper:
                statSO.CharacterChange(sniperSO);
                break;
        }
    }

     

     

     

        : 이후, 플레이어가 직업을 바꿀 때마다, 커스텀 프로퍼티 또한 반영하게 하여, 해당 커스텀 프로퍼티로 각 직업의 스크립터블 오브젝트에 접근하게 하여 직업 변경을 구현하였다.

     

     

     3.. RPC

     

       : 지정한 대상으로 하여금, 지정한 메서드를 실행시킨다.

       : 지정한 대상의 클라이언트만 실행된다.

     

       다른 클라이언트에 전달.

     

       : 별다른 처리를 하지 않는다면, 단순히 PUN2 에 연결되었다고 해서 게임오브젝트가 동기화되지 않는다.

     

       : 위에서 SetClassType()으로 각 클라이언트의 직업을 해당 클라이언트에서만 변경한 것이다. RPC 혹은, IPunObservable 인터페이스를 활용하여 변경된 사항을 동기화할 수 있다.

     

       : 로비에서 룸으로 입장하였을 때, OnJoinedRoom() 에서

         PhotonNetwork.Instantiate() 를 통하여 클라이언트의 플레이어 오브젝트를 인스턴스화 시켜주고, 

         로비에서 선택한 직업을 photonView.RPC()로 다른 클라이언트에 전달하였다.

     

    public override void OnJoinedRoom()
    {
        Debug.Log($"{PhotonNetwork.LocalPlayer.NickName} 입장");
        //... 생략 ...
    
    	// 오브젝트 인스턴스화
        instantiatedPlayer = InstantiatePlayer();
        viewID = instantiatedPlayer.GetPhotonView().ViewID;
        instantiatedPlayer.GetComponent<ClassIdentifier>().playerData = playerDataSetting.GetComponent<PlayerDataSetting>();
    
        // 로비에서 선택한 직업을 다른 클라이언트에서도 적용 (RPC)
        object classNum;
        PhotonNetwork.LocalPlayer.CustomProperties.TryGetValue("Char_Class", out classNum); 
        instantiatedPlayer.GetComponent<PhotonView>().RPC("ApplyClassChange", RpcTarget.Others, (int)classNum, viewID);
    }
    
    public GameObject InstantiatePlayer()
    {
        GameObject playerPrefab = playerContainer.transform.GetChild(0).gameObject;
        playerPrefab.name = "Pefabs/Player";
    	
        // 오브젝트 인스턴스화
        PlayerDataSetting playerData = playerDataSetting.GetComponent<PlayerDataSetting>();
        GameObject playerNet = PhotonNetwork.Instantiate(playerPrefab.name, Vector3.zero, Quaternion.identity);
    	
        // 로비에서 선택한 직업을 적용 (커스텀 프로퍼티)
        PhotonNetwork.LocalPlayer.CustomProperties.TryGetValue("Char_Class", out object classNum);
        playerData.SetClassType((int)classNum, playerNet);
    
        Destroy(playerPrefab);
        return playerNet;
    }

     

     

         : 동시에, OnPlayerEnteredRoom() 을 활용하여 기존에 방에 있던 각 클라이언트가 선택한 직업을 룸에 들어온 클라이언트에 전달하여, 해당 클라이언트에 있을 기존 클라이언트들의 각 플레이어 특성을 적용시켜 주었다.

     

    public override void OnPlayerEnteredRoom(Player newPlayer)
    {
        Debug.Log($"{newPlayer.NickName} 입장");
    
        // ... 생략 
        
        // ... 새로운 룸 가입 클라이언트에게 본인 플레이어의 정보(구현 내용) 전달. (RPC)
        object classNum;
        PhotonNetwork.LocalPlayer.CustomProperties.TryGetValue("Char_Class", out classNum);
        instantiatedPlayer.GetComponent<PhotonView>().RPC("ApplyClassChange", RpcTarget.Others, (int)classNum, viewID);
    }

     

     

         : 전달하여 실행시키게한 메서드의 파라미터로 전달하는 클라이언트가 인스턴스화한 플레이어 (= 조작하는 플레이어)의 PhotonView 컴포넌트의 ViewID와 선택한 직업 (커스텀 프로퍼티의 value) 을 추가하였다.

     

         : 파라미터로 해당 정보들을 일종의 상수로 전달하기 때문에, 다른 클라이언트에서도 정확히 원하는 플레이어 오브젝트에 접근하여 메서드에 있는 기능을 구현할 수 있었다.

     

    public class ClassIdentifier : MonoBehaviourPunCallbacks
    {    
        public PlayerDataSetting playerData;
    
        private int viewID;
        public void Initialize()
        {
            viewID = photonView.ViewID;
        }
    
        public void ClassChangeApply(int classNum)
        {
            Initialize();
            playerData.SetClassType(classNum, this.gameObject);
        }
    
        [PunRPC]
        public void ApplyClassChange(int classNum, int viewID)
        {        
            PhotonView photonView = PhotonView.Find(viewID);
            playerData.SetClassType(classNum, photonView.gameObject);
        }
    
        public int GetViewID()
        {
            return viewID;
        }
    }

     

    'Unity' 카테고리의 다른 글

    Photon - Demo 탐색 (2)  (1) 2023.10.29
    photon - 채팅 기능 구현 기획  (0) 2023.10.26
    Photon - Demo 탐색  (0) 2023.10.25
    네트워크 게임의 이해  (0) 2023.10.24
    readme 작성  (0) 2023.10.19
Designed by Tistory.