유니티/포톤

[Photon 정리] Pun 기능 개요 및 인스턴스 생성. 보스 동기화, 내가 가한 데미지만 표시하는 방법

맛난과자 2024. 11. 26. 19:34

기능 개요

 

  • PhotonNetwork 파일 안에는 필요한 모든 함수와 변수들 있음.

 

  • Photon 연결
PhotonNetwork.ConnectUsingSettings("v1.0")

 

  • 게임 생성 및 참가
PhotonNetwork.JoinRoom(roomName);    // 방 참가

PhotonNetwork.CreatRoom(roomName);   // 방 생성

PhotonNetwork.JoinRandomRoom();      // 방 무작위 생성

 

 

  • Photon.PunBehaviour 상속을 통한 콜백 오버라이드
public override void OnJoinedRoom()   // 콜백 오버라이드
{
	Debug.Log("OnJoinedRoom() called by PUN: " + PhotonNetwork.room.name);
}

 

 

  • 게임 룸에 메시지 전송하는 방법
  • PhotonView

      게임오브젝트나 프리팹에 붙이는 스크립트 컴포넌트.

      메세지 전송과 선택적 인스턴스 생성/다른 PhotonView 들의 할당을 위해 붙임.

      변수 동기화에 쓰임.

      

      PhotonView의 Observed Components 영역에,

 

      transform을 넣으면, 플레이어들의 위치, 회전, 확대비율 등의 정보 동기화.

      스크립트를 넣으면, 스크립트 안의 변수, 함수 등 동기화. 이 경우 스크립트의 OnPhotonSerializeview 메소드 호출.

      이는 이 스크립트가 로컬 플레이어에 의해 제어되는지에 따라 객체의 상태를 쓰고 읽기 위함.

 

     일반적으로는 직접적으로 드래그해서 넣지만, 비어있어도 자동으로 Find 됨. 

     

     ex) 캐릭터 상태 동기화 추가

void OnPhotonSerializeview(PhotonStream stream, PhotonMessageInfo info)
{
	if (stream.isWriting)
    {
    	// 내가 이 플레이어 소유자라면, 다른 이에게 우리 정보 보냄
    	stream.SendNext((int)controllerScript._characterState);
        stream.SendNext(transform.position);
        stream.SendNext(transform.rotation);
    }
    else
    {
    	// 아니라면, 정보를 받음.
    	controllerScript._characterState = (CharacterState)(int)stream.ReceiveNext();
        correctPlayerPos = (Vector3)stream.ReceiveNext();
        correctPlayerRot = (Quaternion)stream.ReceiveNext();
    }
}

 

      Observe Option

      - off : RPC만 쓸 때

      - Unreliable : 항상 모두 전송(데이터가 변하지 않아도), 손실될 수 있음. 

      - Unreliable on Change : 모든 값 중 변경 사항 있으면 모두 전송.

      - Reliable Delta Compressed : 비교 후 변경된 값만 전송. 부하 큼.

     

 

  • Remote Procedure Call

       원격 절차 호출.

       함수 동기화에 쓰임.

       각 플레이어의 클론에 의해 수행됨.

       Scene에 대한 효과 트리거 쉬움.

     

       요구사항: 게임 오브젝트에 PhtonView 컴포넌트, 메소드에 [PunRPC] 속성 마킹.

[PunRPC]
void ChatMessage(string a, string b)   // 호출할 메소드
{
	Debug.Log("메세지 " + a + " " + b);
}

PhotonView photonView = PhotonView.Get(this);   // 대상 객체의 PhotonView에 접근

photonView.RPC("ChatMessage", PhotonTargets.All, "총알", "장전!");   // PhotonView.RPC() 로 메서드 호출

 

 

 

 

Gameplay

인스턴스 생성

 

  • PhotonNetwork.Instantiate

      네트워크 객체를 생성 함수.

      요구사항 : 프리팹은 PhotonNetwork의  resources 폴더에, PhotonView 컴포넌트가 있는 상태여야함.

 

  • OnPhotonInstantiate(PhotonMessageInfo info) 

      PhotonNetwork.Instantiate로 객체 생성 시 호출되는 콜백 함수.

      함수를 호출한 플레이어의 정보를 가지고 호출됨.

 

      PhotonMessageInfo info

       : 네트워크 객체를 생성한 플레이어 정보(이름, 네트워크 타임스탬프 등)

       - info.Sender: 객체를 생성한 플레이어

       - info.timestamp: 생성된 시간 정보

 

      PhotonView.InstantiationData

       : PhotonNetwork.Instantiate 호출 될 때 전달된 데이터(설정 값, 초기화 매개변수 등)

 

      ex) 플레이어 스폰 데이터 처리

void OnPhotonInstantiate(PhotonMessageInfo info)
{
    object[] instantiationData = this.photonView.InstantiationData;
    
    if (data != null && data.Length > 0)
    {
    	string playerName = (string)data[0];
        Debug.Log($"플레이어 이름: {playerName}");
    }
    
    Debug.Log($"{info.Sender.NickName}님께서 {info.timestampe}때 오브젝트를 생성했습니다");
}

 

 

  • 네트워크 객체의 생명 주기

    PhotonNetwork.Instantiate 메소드에 의해 생성된 게임오브젝트는 룸이 변경되어도 이동 안함.

    클라이언트가 룸을 떠나면 사라짐.

    이를 원치 않을 경우 PhotonNetwork.autoCleanUpPlayerObjects 값을 false로 설정.

 

  • 씬 전환

    PhotonNetwork.automaticallySyncScene 값을 true로 설정,

    PhotonNetwork.LoadLevel() 로 씬 전환.

 

 

 

동기화와 상태

 

  • 객체 동기화

    OnPhotonSerializeView() 사용

 

  • RPC 호출
  • 커스텀 프로퍼티

    현재 맵 또는 플레이어 캐릭터의 색과 같은 것들 동기화에 유용    

 

    PhotonPlayer.SetCustomProperties(Hashtable propsToSet)을 사용하여 설정.

    PhotonNetwork.room.SetCustomProperties(Hashtable propsToSet)을 사용하여 참가하고 있는 룸 업데이트

 

   

  • 동기화, RPC, 프로퍼티 최대 활용

    갱신이 많이 발생하는 경우 (위치, 캐릭터 상태)

    - Object Synchronization 사용. Unreliable 또는 Unreliable On Change로 설정.

 

    갱신이 드문 경우 (사용자 입력에 의한 이벤트)    

    - RPC 전송.

    - 약간이 지연 발생 (Photon의 전송 주기에 맞춰 버퍼링(나중에 참여한 플레이어에게 리플레이 가능)되어 전송되기 때문

    - PhotonNetwork.SendOutgoingCommands()를 호출하여 RPC 호출 후 즉시 데이터를 전송할 수 있음.

 

    아주 드문 업데이트와 상태 (문 개방/폐쇄, 지도, 캐릭터 장비)    

    - Custom Properties에 저장.

 

 


 

RPC와 객체 동기화에 대한 비교를 정리해보자면 이렇다.

항목  RPC 객체 동기화
사용 목적 행동(이벤트) 전달 상태(데이터) 전달
전송 주기 특정 시점에 호출 (이벤트 기반) 주기적으로 동기화 (자동 반복)
버퍼링 지원 가능 (나중에 참여한 플레이어에게 리플레이 가능) 버퍼링 없음
전송 방식 호출 시 버퍼링 후 전송 (약간의 지연 발생) Photon 주기에 따라 실시간 전송
예시 턴 종료, 아이템 사용 캐릭터 위치, 애니메이션 상태

 

 

 

우리 팀이 만들고 있는 게임에서 용은 RPC로, 보스는 PhotonNetwork.Instantiate로 소환해야겠다.

대신 보스의 공격 및 사망 처리는 RPC를 사용해야겠다.

 

ex) 용

[PunRPC]
void SummonDragon(Vector3 pos, Quaternion rot)
{
    GameObject dragon = Instantiate(dragonPrefab, pos, rot);
}

photonView.RPC("SummonDragon", RpcTarget.All, summonPosition);

 

ex) 보스 생성 및 동기화

// 마스터 클라이언트가 보스의 소환과 상태 관리
if (PhotonNetwork.IsMasterClient)
{
    PhotonNetwork.Instantiate("BossPrefab", spawnPos, Quaternion.identity);
}

// 적의 상태 동기화
[PunRPC]
public void TakeDamage(float damage)
{
    if (!PhotonNetwork.IsMasterClient) return;  // 마스터 클라이언트만 체력 계산
    
    currentHp -= damage;
    
    if (currentHp <= 0)
    {
        PhotonNetwork.Destroy(gameObjedct);  // 네트워크 객체 제거
        SpawnNewBoss();
    }
}

void SpawnNewBoss()
{
    if (PhotonNetwork.IsMasterClient)
    {
        PhotonNetwork.Instantiate("BossPrefab", spawnPos, Quaternion.identity);
    }
}


// 플레이어가 적 공격
void Attack(GameObject enemy)
{
    PhotonView enemyPhotonView = enemy.GetComponent<PhotonView>();
    
    if (enemyPhotonView != null)
    {
        enemyPhotonView.RPC("TakeDamage", RpcTarget.MasterClient, 10f);
    }
}

 

 

 

내가 많이 헤맸던 **플레이어가 직접 가한 데미지**만 표시하는 방법도 해결할 수 있을 것 같다.

공격은 RPC로 하되, 공격한 클라이언트가 자신이 가한 데미지 정보를 별도로 처리할 수 있도록 추가 정보를 전송한다.

 

RPC 호출 전에 자신의 화면에 데미지 정보를 직접 표시하는 것이다.

데미지를 가할 때 자신의 PhotonView ID를 함께 전달하여 보스가 로컬 클라이언트의 데미지 표시 RPC 메서드를 호출하게 한다.

// 적 스크립트
public class Boss : MonoBehaviourPunCallbacks  
{
    ...
    
    [PumRPC]
    public void TakeDamage(int damage, int attackerId)   // 해당 클라이언트의 정보 받음
    {
        if (!PhotonNetwork.IsMasterClient) return;
        
        ...
        
        // 데미지 정보를 해당 클라이언트에게 전송
        PhotonView attacker = PhotonView.Find(attackerId);
        if (attacker != null && attacker.IsMine)
        {
            attacker.RPC("ShowDamageText", RpcTarget.All, damage);
        }
    }
}


// 플레이어 스크립트
public class Player : MonoBehaviourPunCallbacks
{
    ...
    
    public void Attack(GameObject enemy)
    {
        // 적의 PhotonView 가져오기
        PhotonView enemyPhotonView = enemy.GetComponent<PhotonView>();
        
        if (enemyPhotonView != null)
        {
            // 자신의 PhotonView ID 함께 전달
            enemyPhotonView.RPC("TakeDamage", RpcTarget.MasterClient, 10, photonView.ViewID);
        }
    }
    
    [PunRPC]
    public void ShowDamageTxt(int damage)
    {
        // 화면에 텍스트 표시
        GameObject txtObj = Instantiate(damageTextPrefab, tranform.position + Vector3.up*2, Quaternion.identity);
        TextMesh txt = txtObj.GetComponent<TextMesh>();
        txt.text = damage.ToString();
        
        Destroy(txtObj, 2f);  // 2초 후 제거
    }
}

 

이때 UI 텍스트(TextMeshProUGUI 등)으로 하면 Canvas 안에 텍스트를 생성해야해서 WorldToScreenPoint로 텍스트 위치를 변환해야하므로, 그냥 3D Object에서 Text 생성해서 3D TextMesh를 사용하자.

 

 

그런데 생각해보니 PhotonView.Find를 매번 호출하다보면 과부화가 올 것 같다는 생각이 들었다.

 

그래서 처음의 생각대로 플레이어 아이디(ActorNumber)를 전달하는 방식으로 사용하고자 한다.

// 캐릭터 클래스
public void Attack(GameObject enemy)
{
    PhotonView enemyPhotonView = enemy.GetComponent<PhotonView>();
    
    if (enemyPhotonView != null)
    {
        // 적에게 ActorNum 전달
        enemyPhotonView.RPC("TakeDamage", RpcTarget.MasterClient, 10, PhotonNetwork.LocalPlayer.ActorNumber);
    }
}


// 보스 클래스
private void Start()
{
    PhotonView bossTextSystemView = FindObjectWithTag("BossText").GetComponent<PhotonView>();
}

[PunRPC]
public void TakeDamage(int damage, int attackerActorNumber)
{
    // 텍스트 표시 메서드는 모든 클라이언트에서 호출하고, 이후 마스터 클라이언트만 체력을 관리
    if (bossTextSystem != null)
    {
        bossTextSystemView.RPC("ShowDamageText", RpcTarget.All, damage, attackerActorNumber);
    }
    
    if (!PhotonNetwork.IsMasterClient) return;  
    ...
}
    

// 보스 데미지 텍스트 관리 클래스
[PunRPC]
public void ShowDamageText(int damage, int attackerActorNum)
{
    if (PhotonNetwork.LocalPlayer.ActorNumber == attackerActorNum)
    {
        // 데미지 텍스트로 표시
    }
}