[Unity] Network Synchronize

Lingtea_luv·2025년 7월 18일
0

Unity

목록 보기
30/30
post-thumbnail

Network Game play


멀티 플레이 게임에서는 게임 오브젝트들을 동기화할 필요가 있다. 하지만 게임 속 모든 오브젝트를 동기화할 경우 너무 많은 네트워크 통신량이 필요하게된다. 따라서 우리는 동기화할 필요가 있는 오브젝트를 구분하고 관리할 수 있어야한다.

Photon View

포톤에서는 동기화 필요성이 있는 오브젝트를 관리하기 위해서 포톤 뷰 컴포넌트를 활용한다. 포톤 뷰는 고유한 ViewID, 객체의 소유자, 네트워크 변화사항을 읽고 쓰기 위한 스크립트를 가지고 있으며, 이를 기준으로 포톤은 오브젝트 동기화 작업을 진행한다.

MonobehaviourPun

PhotonView를 활용하여 게임 오브젝트를 동기화할 때 편의를 위해 사용하는 클래스로, MonobehaviourPun 내부에 photonView를 참조하고 있어 이를 상속하여 구현하는 경우GetComponent<PhotonView>()와 같은 코드 없이 효율적인 코드 작성이 가능하다.

추가로 MonoBehaviourPunCallbacks 또한 MonobehaviourPun을 받아 구현되어있기에, photonView 캐싱 말고도 Photon 네트워크 콜백 기능이 필요하다면 MonoBehaviourPunCallbacks을 상속 받는 것이 좋다.

IPunObservable

게임 오브젝트의 네트워크 동기화를 할 때는 편의성을 위해서 MonoBehaviourPun와 함께IPunObservable의 상속도 고려된다. 해당 인터페이스는 다른 클라이언트와의 데이터 동기화를 직접 정의하는 인터페이스로 OnPhotonSerializeView 메서드가 매 프레임 또는 일정 주기로 호출되고, PhotonStream을 통해 데이터를 보내거나 받는 작업을 수행한다.

해당 인터페이스를 반드시 상속해야할 필요는 없다. Photon Tranform ViewPhoton Animator View등 다양한 컴포넌트를 활용하여 자동으로 데이터를 동기화하는 것이 가능하기 때문이다. 하지만 Transform 외에 커스텀 데이터나 더 정교한 동기화 로직이 필요한 경우, 혹은 최적화를 위해 최소한의 데이터의 전송만 하기 위해서는 IPunObservable을 상속받아 직접 구현하는 것이 필요하다.

구현할 때 주의사항은 데이터를 보낸 순서대로 받아서 사용해야한다는 점이다. 보낸 순서와 다르게 받을 경우 데이터가 불일치하게 되어 원하지 않는 상황이 발생하게된다.

public class PlayerController : MonoBehaviourPun, IPunObservable
{
    [SerializeField] private float _moveSpeed;
    [SerializeField] private Color _color;
    
    private Vector3 _networkPos;
    private Quaternion _networkRot;  

	// stream을 통해서 데이터가 전송되고 해당 데이터의 정보는 info
    // 값 형식의 데이터만 바로 전송이 가능
    public void OnPhotonSerializeView(PhotonStream stream, PhotonMessageInfo info)
    {
        // 해당 오브젝트의 소유자(Owner)가 자신인 경우 = 데이터를 보내는 쪽
        if (stream.IsWriting)
        {
             stream.SendNext(transform.position);
             stream.SendNext(transform.rotation);
             stream.SendNext(_moveSpeed);
             
             // * 참조 형식 데이터 전송 방법 *
             // photonView가 가지고 있는 고유 ID를 내보내는 것이 가능
             // => 컴포넌트로 추가함으로써 게임 오브젝트에 종속
             stream.SendNext(photonView.ViewID);
             
             // 동작 안함. => Color는 전송 불가
             //stream.SendNext(_color);
             
             // Color를 대체하기 위한 방안
             stream.SendNext(_color.r);
             stream.SendNext(_color.g);
             stream.SendNext(_color.b);
             stream.SendNext(_color.a);
        }
        // 해당 오브젝트의 소유자(Owner)가 다른 클라이언트인 경우 = 데이터를 받는 쪽
        else if(stream.IsReading)	// else로 써도 무방
        {
            _networkPos = (Vector3)stream.ReceiveNext();
            _networkRot = (Quaternion)stream.ReceiveNext();
            _moveSpeed = (float)stream.ReceiveNext();
            
            
            // * 참조 형식 데이터 전송 방법 *
            // id를 통해서 해당 photonView와 게임 오브젝트를 가져오는 것이 가능
            int id = (int)stream.ReceiveNext();
            PhotonView pv = PhotonView.Find(id);
            pv.GetComponent<Collider>();

            // 동작 안한다. => Color는 전송 불가
            //_renderer.material.color = (Color)stream.ReceiveNext();
            
            // Color를 대체하기 위한 방안
            _color.r = (float)stream.ReceiveNext();
            _color.g = (float)stream.ReceiveNext();
            _color.b = (float)stream.ReceiveNext();
            _color.a = (float)stream.ReceiveNext();
        }
    }
}

생성과 제거

PhotonNetwork.Instantiate

멀티 게임에서 동기화가 필요한 게임 오브젝트(= 네트워크 객체)의 동기화를 위해서는 모든 클라이언트가 동일한 게임 오브젝트를 생성할 필요가 있다. 다만 네트워크 객체를 생성할 때는 단순히 Instantiate가 아닌 PhotonNetwork.Instantiate를 사용해야한다.

포톤은 Resources 폴더에서 이름이 일치하는 프리팹을 찾아 생성하게 되는데, 이를 위해 네트워크 객체로 사용하고자 하는 게임 오브젝트를 프리팹화시켜 Resources 폴더에 넣어둘 필요가 있다.

PhotonNetwork.Instantiate("PrefabName", position, rotation);	// 생성

PhotonNetwork.Destroy

생성과 마찬가지로 네트워크 객체를 제거할 때도 Destroy가 아닌 PhotonNetwork.Destroy를 사용하게 되는데, 이는 룸 안에 있는 모든 클라이언트가 해당 오브젝트를 동시에 삭제하기 위해 포톤 뷰를 기준으로 삭제를 하기 위함이다.

PhotonNetwork.Destroy(this.gameObject);

PhotonNetwork.InstantiateRoomObject

PhotonNetwork.Instantiate 를 사용하여 오브젝트를 생성할 경우 해당 오브젝트의 PhotonView.Owner는 마스터 클라이언트가 된다. 이때 마스터 클라이언트가 룸에서 나가는 경우 이전까지 마스터 클라이언트가 생성한(소유한) 오브젝트들은 자동으로 삭제되는데 이는 메모리 누수 방지와 유령 오브젝트 방지를 위한 기본 정책이다.

따라서 마스터 클라이언트가 나가더라도 유지시켜야하는 오브젝트는 PhotonNetwork.InstantiateRoomObject 를 통해 룸 오브젝트로 생성해야한다. 룸 오브젝트는 마스터 클라이언트가 컨트롤 권한을 가지고 동작시키지만, 마스터 클라이언트가 교체될 경우 해당 권한은 승계되기 때문에 마스터 클라이언트가 방에서 나가더라도 해당 메서드로 생성한 오브젝트는 사라지지 않고 남아있게 된다.

private IEnumerator MonsterSpawn()
    {
        while (true)
        {
            yield return new WaitForSeconds(2f);
            Vector3 spawnPos = new Vector3(Random.Range(-5, 5), 1, Random.Range(-5, 5));
            // 몬스터가 사라지지 않도록 하기 위해서 RoomObject로 생성
            PhotonNetwork.InstantiateRoomObject("Monster", spawnPos, Quaternion.identity);
        }
    }

Remote Procedure Call

게임 오브젝트를 동기화 하기 위해서는 동일한 메서드를 호출할 필요가 있는데, Photon에서는 해당 메서드 호출을 RPC를 통해 수행한다. 이름 그대로 원격 클라이언트에 있는 메서드를 호출하는 것으로, RPC로 호출하기 위해 해당 메서드를 [PunRPC] Attribute로 지정하는 것이 필요하다.

public class PhotonController : MonoBehaviourPun
{
    public void RequestSendChat()
    {
        // RPC 호출 신청
        photonView.RPC("SendChat", RpcTarget.All, "userName", "message");
    }

    // RPC 호출 신청시 클라이언트들이 반응하는 함수
    [PunRPC]
    public void SendChat(string userName, string message)
    {
        Debug.Log($"{userName} : {message}");
    }
}

매개변수

RPC로 호출하는 메서드는 여러 매개변수를 가질 수 있어 호출 시 동일한 순서로 매개변수를 전달한다. 추가로 PhotonMessageInfo를 포함하는 경우 원격 함수 호출에 대한 정보를 전달하는 것이 가능하다.

public class PhotonController : MonoBehaviourPun
{
   public void Request()
   {
       photonView.RPC("MessageInfo", RpcTarget.All, "text", 2, 3.14f);
   }

   [PunRPC]
   public void MessageInfo(string param1, int param2, float param3, PhotonMessageInfo info)
   {
       Debug.Log(param1);      // text
       Debug.Log(param2);      // 2
       Debug.Log(param2);      // 3.14f

       Debug.Log(info.Sender);         // 보낸 플레이어
       Debug.Log(info.photonView);     // 보낸 포톤 뷰
       Debug.Log(info.SentServerTime); // 보낸 서버 시간
   }
}

RPCTarget

RPC 함수에서 원하는 클라이언트에게만 전달을 진행하기 위해 RpcTarget을 선정할 수 있다.

  • Buffered : 서버에서 RPC를 기억하도록 하여, 새로운 플레이어가 참여했을 때 기억한 RPC를 호출시켜준다.

  • ViaServer : 일반적으로 RPC는 전송 클라이언트가 RPC를 호출할 때 지연 시간 없이 즉시 실행한다.
    다만 이 경우 다른 클라이언트와의 실행 시점에 대해 공정성이 보장되지 않기 때문에, ViaServer는 서버를 거쳐서 동일하게 실행하여 공정성을 보장해준다.

RpcTarget 대상 서버 기억 여부 실행 시점
All 모든 클라이언트 X 즉시 실행
Others 다른 클라이언트 X 즉시 실행
MasterClient 마스터 클라이언트 X 즉시 실행
AllViaServer 모든 클라이언트 X 서버를 거쳐 동시에 실행
AllBuffered 모든 클라이언트 O 즉시 실행
OthersBuffered 다른 클라이언트 O 즉시 실행
AllBufferedViaServer 모든 클라이언트 O 서버를 거쳐 동시에 실행

Synchronize


지연 보상

멀티 게임에서는 동기화에 대한 차이가 클수록 플레이어의 경험을 저하시키며 심한 경우 치명적인 오류를 발생시킬 수 있다. 하지만 네트워크를 통한 데이터 전송에서 지연 시간은 필연적으로 발생하며, 이를 없애는 것은 불가능하기에 이를 완화하는 방법을 지연 보상이라 한다.

지연 시간(Lag) 보정

public void RequestGameStart()
{
  photonView.RPC("GameStart", RpcTarget.AllViaServer, PhotonNetwork.Time);
}

[PunRPC]
public void GameStart(PhotonMessageInfo info)
{
  // 서버에서 RPC를 보낸 시간과 현재 시간의 격차를 계산
  float lag = Mathf.Abs((float)(PhotonNetwork.Time - info.SentServerTime));

  // 시간의 격차만큼 감소된 시간만큼 카운트를 진행
  // ex. 지연 시간이 0.1초 발생한 경우 카운트 다운을 4.9초 진행
  StartCoroutine(GameTimer(5f- lag));
}

IEnumerator GameTimer(float timer)
{
  yield return new WaitForSeconds(timer);
  Debug.Log("게임 스타트!");
}

지연 보상 예시

public void OnPhotonSerializeView(PhotonStream stream, PhotonMessageInfo info)
{
    if (stream.IsWriting)
    {
        stream.SendNext(rigidbody.position);
        stream.SendNext(rigidbody.rotation);
        stream.SendNext(rigidbody.velocity);
    }
    else if (stream.IsReading)
    {
        rigidbody.position = (Vector3) stream.ReceiveNext();
        rigidbody.rotation = (Quaternion) stream.ReceiveNext();
        rigidbody.velocity = (Vector3) stream.ReceiveNext();

        float lag = Mathf.Abs((float) (PhotonNetwork.Time - info.timestamp));
        rigidbody.position += rigidbody.velocity * lag;
    }
}

마이그레이션

마이그레이션은 여러 의미로 폭넓게 쓰이지만, 멀티 게임에서의 마이그레이션은 방장이 연결이 해제되는 경우에도 또 다른 클라이언트가 호스트 권한을 승계받고 게임이 끊김없이 유지하는 기법이다.

포톤에는 마스터 클라이언트라는 개념이 있으며, 해당 클라이언트만이 게임 내 중요한 로직을 처리하는 것이 가능하다.

private void Update()
{
    if (PhotonNetwork.IsMasterClient == false)
        return;

    // 방장만 수행해야하는 로직
}

하지만 만약 마스터 클라이언트의 접속이 해제되거나, 끊기는 경우 이를 대신할 클라이언트가 필요한데, 포톤에서는 기본적으로 호스트 마이스레이션이 적용되어 있어 기존의 클라이언트 중 하나를 마스터 클라이언트로 전환하며, 이벤트 콜백이 호출된다.

public class PhotonController : MonoBehaviourPunCallbacks
{
    // 마스터 클라이언트 변경시 호출됨
    public override void OnMasterClientSwitched(Player newMasterClient)
    {
        if (PhotonNetwork.LocalPlayer != newMasterClient)
            return;

        // 변경된 마스터 클라이언트가 자신인 경우
        // 방장으로서 해야할 준비 작업 진행
    }
}
profile
뚠뚠뚠뚠

0개의 댓글