기능 개요
- 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)
{
// 데미지 텍스트로 표시
}
}