어제 포톤 서버 연결, 리소스 로드, 게임 객체 생성을 순서대로 하기 위한 GameManager를 작성했었다.
오늘은 게임이 시작되면 1개의 공을 생성하고, 플레이어가 입장할 때마다 해당 플레이어의 캐릭터를 생성하도록 해봤다.
멀티플레이 환경에서 유저끼리의 객체 데이터 동기화를 도와주는 PhotonView
컴포넌트가 있다.
처음엔 이거 하나만 달랑 달아놓고 왜 동기화 안되지.. 하고 있었는데, 아래의 컴포넌트를 추가로 달아줘야 동작하는 것이었다 ;;
PhotonTransformView
PhotonRigidbodyView
PhotonAnimatorView
컴포넌트 이름에 써 있듯이, 해당 컴포넌트의 정보를 PhotonView
에게 넘겨줘서 동기화를 도와준다.
주의할 점은, 같은 GameObject에 붙어있는 컴포넌트의 정보를 받아서 넘겨준다!
ex) PhotonAnimatorView
와 동기화 할 Animator
는 같은 GameObject에 붙어 있어야함.
단, PhotonView
컴포넌트는 자식 오브젝트에 있는 PhotonAnimatorView
컴포넌트를 알아서 탐색할 수 있음.
현재 공은 게임에 딱 1개만 생성되게 해놨다. 따라서 필연적으로 Master Client 소유이다.
그리고, 유저가 Room에서 나가거나 연결이 끊기면 자동으로 해당 유저의 오브젝트들은 삭제된다.
문제는 Master Client가 접속을 끊었을 때 발생한다. 자동으로 다음 유저에게 Master Client는 양도되지만, 양도 이전에 기존 Master Client의 오브젝트들은 삭제된다. 이때 공도 함께 삭제된다.
이를 해결하기 위해 가장 먼저 PunCallbacks에 있는 이벤트 메서드를 싹 다 찾아봤다.
가장 해결 가능성이 높아 보이는 메서드는 OnDisconnected
, OnPlayerLeftRoom
, OnMasterClientSwitched
세 가지로 좁혀졌다.
해당 메서드 내부에 공의 소유권을 바꿔주는 메서드를 작성했다.
참고로, PhotonView
컴포넌트의 Ownership Transfer
속성을 TakeOver
로 바꿔줘야 소유권을 변경할 수 있다고 한다. Fixed
일 경우에는 소유권을 변경할 수 없다.
public override void OnMasterClientSwitched(Photon.Realtime.Player newMasterClient)
{
Debug.Log("OnMasterClientSwitched");
GameManager.Instance.ball.GetComponent<PhotonView>().TransferOwnership(newMasterClient);
}
세 개의 메서드 전부 다 테스트 해봤지만, 모두 실패했다.
이유는 위의 이벤트 모두가 이미 공 오브젝트가 삭제된 뒤에 호출되고 있었다.
이제 내가 할 수 있는 선택지는 다음과 같았다 ..
아무리 찾아봐도 전자는 없는 것 같으니, 후자의 방법을 택했다.
공을 생성하는 건 쉽다. 문제는 기존에 있던 공과 모든게 똑같은 공
을 생성해야한다.
하지만, 이벤트 호출 시점에서 공은 이미 삭제되고 없다.
구글링을 하던 도중에 이런 글을 찾았다.
문서 아래쪽 권고사항 부분에, 위의 내용이 있었다.
요약하면, 모든 유저가 공의 상태를 실시간으로 기억하고 있다가 Master Client가 접속을 끊고 공이 사라지면, 새로운 Master Client가 기억하고 있던 정보로 공을 생성하는 것이다.
기억해야할 공의 정보를 추려봤다. 그냥 굴러다닐 뿐인 공이라 기억 할 것도 별로 없다.
public GameObject ball;
private Rigidbody _ballRigid;
public struct BallStateStruct
{
public Vector3 position;
public Quaternion rotation;
public Vector3 velocity;
}
// 이 구조체를 이용해 공의 정보를 모든 유저가 기억
public BallStateStruct ballState = new()
{
position = new Vector3(0, 10, 0),
rotation = Quaternion.identity,
velocity = new Vector3(0, 0, 0),
};
GameManager
에 공의 정보를 기억할 구조체를 작성했다.
private void Update()
{
if (ball == null)
{
ball = GameObject.FindWithTag("Ball");
if (ball != null)
_ballRigid = ball.GetComponent<Rigidbody>();
return;
}
// 공 상태 추적
ballState.position = ball.transform.position;
ballState.rotation = ball.transform.rotation;
ballState.velocity = _ballRigid.velocity;
}
이 구조체는 Update 메서드에서 실시간으로 정보가 갱신된다.
public void BallSpawn()
{
// Host에서만 공 생성
if (PhotonNetwork.IsMasterClient)
{
ball = PhotonNetwork.Instantiate("Ball.prefab", ballState.position, ballState.rotation);
_ballRigid = ball.GetComponent<Rigidbody>();
_ballRigid.velocity = ballState.velocity;
}
}
이제 공을 생성할 때, 이 구조체의 정보를 이용해서 생성한다.
공의 정보를 기억하고, 생성하는 로직은 GameManager
에서 전부 구현했으니, 이제 PunCallback
에서 Master Client가 접속을 끊었을 때 새로운 공을 생성하게 해주면 된다.
public override void OnMasterClientSwitched(Photon.Realtime.Player newMasterClient)
{
Debug.Log("OnMasterClientSwitched");
// MasterClient가 바뀌면 ballState의 정보를 이용해 공 생성
if (PhotonNetwork.IsMasterClient)
GameManager.Instance.BallSpawn();
}
오른쪽에 있는 녀석이 Master Client다.
강제 종료로 접속을 끊어도 공이 마치 사라지지 않은 것 처럼 보인다.
멀티플레이는 정말 어렵다...
포톤 관련해서 구글링 하다가 언젠가 본 글귀인데, 네가 보는 나
와 내가 보는 너
가 다르다는 걸 이해하기가 정말 쉽지 않다.
포톤이 정말 구현하기 쉽게 만들어 놓았는데도 이렇게 어려우면 포톤 없이 멀티플레이 환경 구축은 얼마나 어려울까 ..