동기화와 상태
다른 플레이어들의 상태를 최신의 상태로 유지시켜 주는 것이 게임의 전부입니다.
다른 플레이어들이 누구이며 무엇을 하고 어디에 있는지 그리고 게임이 어떻게 보이는지에 대해 알고 싶어 할 것입니다.
PUN(그리고 일반적으로 Photon)은 상태를 갱신하고 유지하는 몇개의 툴을 제공 합니다.
이 페이지에서 옵션들과 언제 사용하는지에 대해서 설명해 드립니다.
객체 동기화
PUN에서는 "네트워크에 동작하는" 게임 오브젝트들을 쉽게 생성 할 수 있습니다.
PhotonView를 할당하면 객체는 쉽게 위치와 회전 및 다른 값들을 원격 복사본에 쉽게 동기화 할 수 있습니다.
PhotonView 는 Transform 처럼 또는 스크립트 처럼 반드시 "observe" 컴포넌트를 설정해야 합니다.
데모의 대부분에서 객체 동기화를 사용 합니다.
일부 스크립트에서 OnPhotonSerializeView()
메소드를 구현하고 PhotonView 를 관찰하는 컴포넌트가 되게 됩니다.
OnPhotonSerializeView()
에서 위치 및 다른 값들은 스트림에 쓰여지고 스트림에서 읽혀 집니다.
이 함수를 이용하기 위해서, 스크립트는 IPunObservable
인터페이스를 구현해야 합니다.
원격 프로시져 호출(RPC,Remote Procedure Call)
룸 내에있는 모든 클라이언트가 메소드를 호출 할 수 있도록 표시할 수 있습니다.
속성 [PunRPC]
로 'ChangeColorToRed()' 를 구현하면 원격 플레이어는 photonView.RPC("ChangeColorToRed", RpcTarget.All);
를 호출하여 게임 오브젝트의 색상을 빨간색으로 변경 할 수 있습니다.
호출은 항상 게임오브젝트의 특정 PhotonView 를 타겟으로 합니다.
따라서 'ChangeColorToRed()'이 호출되면 PhotonView 가 있는 게임오브젝트에 대해서만 실행 합니다.
이런 방식은 특정 객체들에게만 영향을 주고 싶을 때 매우 유용합니다.
물론 메소드를 위해서 씬에 타켓을 가지고 있지 않은 "더미(dummay)"의 비어있는 게임오브젝트를 넣을 수 있습니다.
예를 들면 RPC로 채팅을 구현할 수 있는데 특정 게임오브젝트에 관련된 것이 아니라는 것 입니다.
RPC는 "버퍼링" 할 수 있습니다.
서버는 RPC가 호출된 이후 참여하는 모두에게 전송합니다.
이렇게 하면 액션을 저장하고 PhotonNetwork.Instantiate()
의 대안을 구현 할 수 있습니다.
수동 인스턴스생성을 확인하세요.
단점은 주의하지 않으면 버퍼가 점점 더 커진다는 것입니다.
커스텀 프로퍼티
Photon의 커스텀 프로퍼티는 키-값의 해시테이블로 구성되어 있어 필요시에 사용할 수 있습니다.
값은 클라이언트에서 동기화되고 캐시되기 때문에 사용하기 전에 패치(fetch)할 필요가 없습니다.
변경 사항들은 SetCustomProperties()
를 통하여 다른 플레이어들에게 푸시됩니다.
이게 왜 유용할까요? 일반적으로 룸과 플레이어들은 게임오브젝트와 관계없는 속성들을 가지고 있습니다:
현재 맵 또는 플레이어 캐릭터의 색상(2d 점프 경주를 생각해 보세요)등 입니다.
이러한 것들은 객체 동기화 또는 RPC를 통해서 전송 될 수 있습니다. 하지만 커스텀 프로퍼티를 사용하는 것이 더 편리한 경우가 있습니다.
플레이어에게 커스텀 프로퍼티 설정은 PhotonPlayer.SetCustomProperties(Hashtable propsToSet)
을 사용하여 추가 또는 변경하기 위해서 key-value 를 포함시켜 주시면 됩니다.
로컬 플레이어 숏컷(shotcut)은 PhotonNetwork.LocalPlayer
입니다.
유사하게 PhotonNetwork.CurrentRoom.SetCustomProperties(Hashtable propsToSet)
를 사용하여 참가하고 있는 룸을 업데이트 합니다.
모든 업데이트 전송에는 시간이 좀 걸리겠지만 모든 클라이언트들이 CurrentRoom.CustomProperties
와 Player.CustomProperties
을 맞게 갱신할 것입니다.
PUN은 프로퍼티가 변경되었을 때 콜백으로 OnRoomPropertiesUpdate(Hashtable propertiesThatChanged)
또는 OnPlayerPropertiesUpdate(Player targetPlayer, Hashtable changedProps)
를 각각 호출 합니다.
룸을 생성할 때 프로퍼티를 설정 할 수 도 있습니다.
매치메이킹에서 룸 프로퍼티를 사용하므로 매우 유용합니다.
프로퍼티-해시테이블을 사용하여 맞는 조건을 필터링 후 룸에 참여하는 JoinRandomRoom()
오버로드 함수가 있습니다.
룸을 생성할 때 RoomOptions.CustomRoomPropertiesForLobby
를 설정하여 로비에서 필터링 할 수 있도록 룸 프로퍼티를 정의 해 주어야 합니다.
매치메이킹에서 커스텀 프로포티 사용방법문서에 설명이 있습니다.
프로퍼티의 Check 와 Swap(CAS)
SetCustomProperties
를 사용할 때 서버는 일반적으로 모든 클라이언트로부터 새로운 값을 받아 들이긴 하지만 곤란한 상황이 발생 할 수 도 있습니다.
예를 들어, 프로퍼티는 룸에서 유일한 아이템을 획득한 플레이어가 누구인지 저장하는 데 사용 될 수 있습니다.
따라서 프로퍼티의 키가 아이템이 되고 값이 누가 아이템을 획득 했는지를 정의 하게 됩니다.
모든 클라이언트는 언제라도 프로퍼티를 자신의 actorNumber로 설정 할 수 있습니다.
모든 플레이어가 동시에 설정하게 된다면 마지막으로 SetCustomProperties
를 호출한 플레이어가 아이템을 취득 한 것으로 됩니다.
이런 상황은 원하는 것이 아닐 것 입니다.
SetCustomProperties
는 선택적인 expectedValues
파라미터를 가지고 있으며 조건에 사용 될 수 있습니다.
expectedValues
로 서버는 현재의 key-values 가 expectedValues
와 일치하는 프로퍼티만을 갱신 할 것 입니다.
오래된 expectedValues
업데이트들은 무시 될 것입니다(결과적으로 클라이언트는 에러를 받게 되고 다른 것들은 갱신 실패를 알지 못합니다).
예제에서는 expectedValues
가 유일한 아이템을 갖고 있는 현재 오너가 포함되어 있을 수 있습니다.
모든 플레이어가 아이템을 얻으려고 해도 expectedValues
안에는 쓸모없는 플레이어 정보를 가지고 업데이트 요청을 하기 때문에 첫 번째 플레이어만 성공 할 것 입니다.
SetCustomProperties
내의 조건으로 expectedValues
를 사용하는 것을 Check와 Swap(CAS)라고 부릅니다.
컨커런시(concurrency) 이슈를 해결 하는데 매우 유용한 방식이며 여러가지 창의적인 방식에서도 사용 할 수 있습니다.
SetCustomProperties
가 실패 할 수도 있기 때문에 모든 클라이언트는 서버-전송 이벤트에 의해서만 커스텀 프로퍼티를 갱신 해야 합니다. 클라이언트가 새로운 값으로 설정하려고 시도하는 것도 포함 됩니다. CAS 없이 값을 설정하는 것과 비교하면 시간 차이가 있습니다.
동기화, RPC 와 프로퍼티 최대한 활용하기
값의 동기화를 위해서 어떤 동기화 방식을 사용할 것인지는 얼마나 자주 변경되며 "변경이력"이 필요한지 여부를 체크하여 결정하는 것이 가장 좋습니다.
갱신이 많이 발생하는 경우 (위치, 캐릭터 상태)
빈번하게 업데이트 되는 것은 Object Synchronization
을 사용하세요.
불확실하면 스크립트에서 모든 변경 사항에 대해 스트림으로 전혀 Write를 하지않고 업데이트를 스킵 할 수 있습니다.
캐릭터의 위치는 매우 빈번하게 변경됩니다. 각각의 변경사항은 유용한 정보지만 빠르게 새로운 값으로 변경 될 확률이 높습니다.
PhotonView 는 "Unreliable" 또는 "Unreliable On Change" 로 설정 할 수 있습니다.
첫 번째 방식은 캐릭터가 움직이지 않아도 고정된 주기로 업데이트를 전송합니다.
두 번째 방식은 게임오브젝트(캐릭터,유닛)가 멈추었을 때 업데이트 전송을 중지 합니다.
갱신이 드문 경우 (플레이어의 행동)
캐릭터 장비 도구의 사용 또는 게임의 턴 종류는 빈번하게 발생되는 행동은 아닙니다.
사용자 입력으로 발생하는 사항으로 RPC 로 전송하는 것이 가장 좋은 방법일 것 입니다.
객체 동기화 사용을 위하여 순차적으로 전달되는 것은 명확하지 않습니다.
객체 동기화를 사용한다면 특정 행동에 대해 더 자주 업데이트 하여 "순차적인 사건"으로 인식 될 수 있습니다.
예를 들어 캐릭터의 위치를 전송하면 이어 "점프" 상태를 쉽게 전송할 수 있습니다.
그러면 분리된 RPC 로 전송할 필요가 없습니다.
RPC는 photonView.RPC("rpcName", ...)
호출 즉시 전송되지 않습니다.
객체 동기화 주기가 될 때 까지 버퍼링되고 전송 주기가 되면 변경사항을 전송 합니다.
오버헤드를 줄이기 위해서 RPC는 작은 패키지로 통합 하지만 일부 지연이 나타나게 됩니다.
로컬 지연을 없애기 위해서 RPC 업데이트 루프를 PhotonNetwork.SendAllOutgoingCommands()
를 호출하여 중지 할 수 있습니다.
턴을 전송하려고 많은 RPC에 의존하는 게임이라면 납득 할 수 있을 것입니다.
객체 동기화와 달리 RPC는 버퍼링 되어야 합니다.
모든 버퍼링된 RPC 는 나중에 참여한 플레이어에게 전송되며 차례대로 리플레이 될 수 있는 액션을 해야 하는 경우에 매우 유용합니다.
예를 들어, 참여한 클라이언트가 누가 씬에서 도구를 놓았고 누가 도구를 업그레이드 했는지 리플레이 할 수 있습니다.
나중의 것은 첫 번째 행동에 밀접한 관련이 있습니다.
신규 플레이어에 버퍼링된 RPC를 전송하는 것은 네트워크 대역이 필요하며 클라이언트들이 "라이브" 게임을 하기 전 까지 이전의 플레이를 다시 해야 하고 각 행동을 적용 해야 한다는 의미입니다.
이런 방식은 참 참기 힘든 부분이고 수많은 버퍼링된 정보로 인하여 네트워크 연결이 약한 클라이언트가 끊어 질 수 있으니 버퍼링을 주의해서 사용 하시기 바랍니다.
빈번하지 않은 업데이트와 상태 (문 개방/폐쇄, 지도, 캐릭터 장비)
아주 드물게 변경이 발생하는 경우는 Custom Properties
에 저장하는 것이 가장 좋습니다.
버퍼링된 RPC와는 달리 프로퍼티 해시테이블은 현재의 키-값만을 가지고 있습니다.
문의 상태가 열려 있는지 닫혀 있는지에 대한 상태를 관리하는 것이 좋습니다. 플레이어는 이전에 문이 열려있었는지 닫혀있었는지에 대해서 전혀 상관을 하지 않습니다.
위의 RPC 예제에서는 씬에 누군가 놓은 도구가 있고 업그레이드 되었습니다.
몇 가지 행동은 RPC 를 사용하는 것이 좋습니다. 변경이 매우 많은 경우에 대해서는 프로퍼티의 단일 값에 현재 상태를 통합하는 것이 더 쉬울 수 있습니다.
여러개의 "+10 방어력" 업그레이드는 여러개의 RPC 대신에 단일 값으로 저장하는 것이 더 쉽습니다.
다시 한번 말씀드리지만 커스텀 프로퍼티의 사용과 RPC 사용에 대한 구분은 정확 하지 않습니다.
커스텀 프로퍼티에 대한 다른 유즈케이스는 룸의 "시작 시간"을 저장하는 것 입니다.
게임이 시작 할 때 프로퍼티로 PhotonNetwork.Time
을 저장합니다.
이 값은 (대략) 모든 클라이언트가 참가한 시간과 동일하며 이 시작 시간으로 모든 클라이언트가 게임이 얼마나 오래 진행 되었는지를 계산할 수 가 있습니다.
물론 각 턴의 시작 시간을 저장 할 수 도 있습니다.
만약 게임이 멈출 수 있는 경우에 더 좋습니다.