-
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