⭐ 포톤 PUN
PUN(Photon Unity Networking)은 멀티 플레이어 게임용 유니티 패키지입니다.
포톤 Fusion에 비해 상대적으로 오래전에 나온 버전이지만 직관적이고 유용한 기능을 다수 제공하기 때문에
아직까지도 많이 사용되는 실시간 멀티 플레이어 게임용 유니티 패키지입니다.
포톤 홈페이지
포톤을 본격적으로 사용하기 위해선 다음과 같은 준비과정이 필요합니다.
● 포톤 로그인 및 서버 생성
● 포톤 PUN2 에셋 패키지 임포트
포톤 서버 생성
1. 아래의 링크를 눌러 포톤 사이트로 이동하여 로그인 또는 회원가입을 진행합니다
https://www.photonengine.com/ko-kr

2. 관리 화면으로 이동 후 새 어플리케이션 만들기를 누릅니다.

3. 새 어플리케이션 생성에 필요한 항목을 작성합니다. 마지막으로 작성하기를 눌러줍니다.
어플리케이션 유형: 멀티플레이어 게임
Photon 종류: PUN
애플리케이션 이름과 설명은 자유롭게 작성

4. 생성된 서버의 아이디를 복사합니다.

포톤 에셋
1. 아래 링크를 눌러 에셋을 다운 후 원하는 유니티 프로젝트에 임포트합니다.
https://assetstore.unity.com/packages/tools/network/pun-2-free-119922

2. PUN Setup 창이 뜨면 아까 복사해둔 App ID를 기입 후 Setup Project를 클릭합니다.

3. App ID를 정상적으로 기입하면 인스펙터를 통해 서버 설정을 할 수 있게 됩니다.

로비, 방 만들기
포톤의 매치메이킹 시스템은 네트워크 서버에 입장 ↔ 각각의 방 리스트 등을 볼 수 있는 로비에 입장 ↔ 같이 게임을 진행할 다른 클라이언트들과 모일 방 입장과 같은 구조로 되어있습니다.

● 서버(Server):
여러 상용 게임들을 플레이 할 때 시작할 서버를 선택하는 것과 같이 플레이할 서버를 선택하는 것입니다.
이 서버 단위는 포톤 홈페이지에서 만들었던 서버(어플리케이션)입니다.
인증을 처리하고 어떤 로비나 지역으로 갈 지를 정할 수 있습니다.
PhotonNetwork.ConnectUsingSettings(); // 해당 App ID가 있는 서버에 접속
PhotonNetwork.Disconnect(); // 서버 접속 해제
● 로비(Lobby):
생성된 방들의 목록을 관리하고 매칭 또는 방 입장을 할 수 있는 공간입니다.
마스터 서버에서 로비로 들어올 수 있으며 플레이어는 방을 생성하거나 이미 생성된 방에 입장할 수 있습니다.
PhotonNetwork.JoinLobby(); // 로비 입장 요청
PhotonNetwork.LeaveLobby(); // 로비 퇴장 요청
● 방(Room):
실제로 같이 게임을 플레이할 플레이어들이 모이는 공간입니다.
동일한 방에 입장한 플레이어들끼리만 RPC와 PhotonView를 수신할 수 있습니다.
PhotonNetwork.CreateRoom("RoomName"); // 방 생성 요청
PhotonNetwork.JoinRoom("RoomName"); // 방 입장 요청
PhotonNetwork.LeaveRoom(); // 방 퇴장 요청
이 외의 유용한 코드들
PhotonNetwork.AutomaticallySyncScene = true; // true로 바꿔줘야 LoadLevel로 모든 클라이언트가 씬 이동을 함
PhotonNetwork.LoadLevel("SceneName"); // 씬 전환 요청 (현재 방의 모든 클라이언트)
bool isConnected = PhotonNetwork.IsConnected; // 접속 여부 확인
bool isInRoom = PhotonNetwork.InRoom; // 방 입장 여부 확인
bool isLobby = PhotonNetwork.InLobby; // 로비 입장 여부 확인
ClientState state = PhotonNetwork.NetworkClientState; // 클라이언트 상태 확인
Player player = PhotonNetwork.LocalPlayer; // 포톤 플레이어 정보 확인
Room players = PhotonNetwork.CurrentRoom; // 현재 방 정보 확인
▼ 방 안의 인원들을 모두 씬 이동시키는 코드, 방장(마스터 클라이언트만 가능)
// 자신 플레이어가 방장이 아닌 경우 반환하여 아래의 코드가 실행되지 않도록 함
if (PhotonNetwork.LocalPlayer.IsMasterClient == false)
return;
// 방장만이 실행할 수 있는 소스코드
PhotonNetwork.AutomaticallySyncScene = true; // 모든 방구성원이 같은 씬으로 > 이동하도록 동기화함
PhotonNetwork.LoadLevel("GameScene"); // 네트워크를 통해 씬을 이동하도록 > 신청함
Photon View (포톤 뷰)
네트워크 상에서 동기화가 필요한 오브젝트에 Photon View(포톤 뷰) 컴포넌트를 추가함으로써 관리해줄 수 있습니다.
Photon View(포톤 뷰)는 ViewID(고유 식별 아이디), 객체의 소유자, 네트워크 변화사항 등을 읽고 쓰기 위한 스크립트를 가지고 있습니다.
포톤 뷰 컴포넌트를 포함하고 있는 게임오브젝트는 포톤 뷰를 참조하기 위한 변수를 가진 MonobehaviourPun클래스를 상속받아 참조할 수 있습니다.
또한 Photon Animator View, Photon Transform View 컴포넌트 등을 추가하면 각각 애니메이터, 트랜스폼 정보 등이 동기화됩니다.
▼ MonoBehaviourPun 클래스 상속
public class PhotonController : MonoBehaviourPun // 게임오브젝트 동기화를 위한 스크립트
{
private void Awake()
{
PhotonView pv = photonView; // MonoBehaviourPun은 photonView를 참조하고 있음
Debug.Log(pv.ToString());
}
}
생성과 삭제
멀티 플레이 게임에서는 게임오브젝트의 동기화를 위해 모든 클라이언트가 동일한 게임오브젝트를 생성할 필요가 있습니다.
주의할 점은 생성할 프리팹의 경로는 Resources 폴더에 위치해야합니다.
● 게임 오브젝트 생성
PhotonNetwork.Instantiate("PrefabName", position, rotation);
- 해당 오브젝트를 생성한 사람이 소유권자가 됨
- PhotonNetwork.InstantiateRoomObject();를 사용하면 방 소유의 오브젝트 생성 (ex. 보스 몬스터)
● 게임 오브젝트 파괴
PhotonNetwork.Destroy(photonView);
소유권
포톤 뷰를 가지고 있는 객체는 소유자가 존재합니다.
소유자는 해당 객체를 생성한 클라이언트를 뜻하며, 소유자가 아닌 클라이언트들의 네트워크 객체 조작은 무시하게 됩니다.
소유권이 없는 네트워크 객체를 조작할 경우 로컬에서만 동작하고 다른 클라이언트들에게는 전달되지 않습니다.
따라서 포톤 뷰의 소유권이 없을 경우 동작하지 않도록 방어 코드를 작성할 필요가 있습니다.
if (photonView.IsMine == false)
return;
// 포톤 뷰의 소유권이 있는 클라이언트만 실행 가능한 코드
변수 동기화: IPunObservable 인터페이스
IPunObservable 인터페이스를 상속받으면 반드시 OnPhotonSerializeView() 메서드를 구현해야 합니다.
이 메서드는 포톤 설정에서 정해진 주기마다(기본 초당 20회) 자동으로 호출되며,
데이터를 서버로 보내거나 반대로 서버에서 데이터를 받아올 수 있습니다.
public class PhotonController : MonoBehaviourPun, IPunObservable
{
[SerializeField] int value1;
[SerializeField] float value2;
[SerializeField] bool value3;
public void OnPhotonSerializeView(PhotonStream stream, PhotonMessageInfo info)
{
// 변수 데이터를 보내는 경우
// stream.IsWriting → 현재 클라이언트가 이 객체의 주인일 때 참
if (stream.IsWriting)
{
stream.SendNext(value1);
stream.SendNext(value2);
stream.SendNext(value3);
}
// 변수 데이터를 받는 경우
// stream.IsReading → 현재 클라이언트가 이 객체의 주인이 아닐 때 참
else if (stream.IsReading)
{
value1 = (int)stream.ReceiveNext();
value2 = (float)stream.ReceiveNext();
value3 = (bool)stream.ReceiveNext();
}
}
}
● 반드시 데이터를 보낸 순서와 받는 순서가 동일해야합니다.
● 기본적으로 값 형식의 데이터를 전달할 수 있습니다
○ 참조 형식의 경우 주소값을 가지고 각각의 클라이언트는 상이한 메모리 위치를 가지기 때문
참조 형식 동기화
포톤 뷰를 이용하면 참조형식 데이터 또한 전달할 수 있습니다.
포톤 뷰를 통해 고유 ID를 받아온 뒤 해당 ID를 가진 오브젝트의 컴포넌트를 불러오면 되는 것입니다.
// 포톤 뷰 아이디를 확인
// 포톤 뷰는 모든 클라이언트가 동일하므로 동일한 게임오브젝트를 참조 가능
int id = photonView.ViewID;
// 포톤 뷰 아이디를 기준으로 탐색
// 포톤 뷰 아이디는 미리 캐싱되어 있으므로 빠른 탐색이 가능
PhotonView target = PhotonView.Find(id);
// 해당 아이디로 찾은 객체의 컴포넌트 받아오기
Entity entity = target.getcomponent<Entity>();
함수 동기화: PunRPC (Remote Procedure Call, 원격 함수 호출)
동일한 함수를 호출해야할 때 RPC를 사용할 수 있습니다.
아래 코드 예시를 통해 확인해보겠습니다.
// [호출하는 쪽]
// "Attack"이라는 이름의 함수를 모든 사람(All)에게 실행하라고 명령, 매개변수는 10
photonView.RPC("Attack", RpcTarget.All, 10);
// [실행되는 쪽]
[PunRPC]
void Attack(int damage)
{
Debug.Log(damage + "만큼 데미지를 입히는 이펙트와 소리를 재생합니다!");
}
● RPC를 사용할 메서드는 [PunRPC] 어트리뷰트를 붙여야됨
● PhotonView.RPC()로 호출
● RPCTarget을 통해 보낼 주체를 정할 수 있음
○ RPCTarget.All: 나를 포함한 모든 클라이언트
○ RPCTarget.Others: 나를 제외한 모든 클라이언트
○ RPCTarget.MasterClient: 현재 방의 방장(마스터 클라이언트)
○ RPCTarget.AllBuffered: 현재 있는 사람 + 나중에 이 방에 들어올 사람도 기록을 해둔 뒤 실행(바뀐 맵 상태 등)
타겟과 버퍼
RPCTarget의 타입에는 해당 메서드를 기록 해두거나, 호출 시점을 정할 수도 있습니다.
● Buffered:
→ 서버에서 RPC들을 기억해두고 있다가 새로운 플레이어가 들어왔을 때 기억해둔 RPC들을 호출해줍니다.
● ViaServer:
→ 일반적으로 RPC는 전송 클라이언트가 RPC를 실행할 때 직접 진행하게 됩니다.
이 경우 RPC를 전송하는 클라이언트는 지연시간이 없지만, 다른 클라이언트들과의 공정성을 보장해주지 않습니다.
ViaServer의 경우 클라이언트가 RPC를 실행하면 서버까지 보내고 함수를 실행한 클라이언트에게 돌아왔을 때 실행하게 됩니다.
이를 통해 모든 클라이언트가 거의 동일한 타이밍에 함수를 동작할 수 있게 되지만 함수를 실행한 클라이언트는 약간의 지연시간을 가지게 됩니다.
여러 가지 동기화 기법
● 지연 보상
네트워크를 통한 데이터 전송은 물리적으로 지연 시간이 발생할 수 밖에 없는데 이를 줄이는 방법을 "지연 보상"이라고 합니다.
대표적인 지연 보상 기법으로는 아래와 같습니다.
1. 지연 시간 계산: RPC를 보낸 시간과 현재 시간의 차이를 구해 조정하는 방법
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("게임 스타트!");
}
2. 물리 객체를 위한 지연 보상: 지연시간이 발생한 만큼 기존의 속도를 이용해 추가적인 위치 이동을 해주는 방법
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;
}
}
3. 물리 객체가 아닐 때 지연 보상: Transform 컴포넌트를 이용해서 이전 시점, 현재 시점 데이터의 차이를 이용해 계산하는 방법
private Vector3 networkPosition;
private float deltaPosition;
private Quaternion networkRotation;
private float deltaRotation;
private void Update()
{
if (photonView.IsMine == false)
{
transform.position = Vector3.MoveTowards(transform.position, networkPosition, deltaPosition * Time.deltaTime * PhotonNetwork.SerializationRate);
transform.rotation = Quaternion.RotateTowards(transform.rotation, networkRotation, deltaRotation * Time.deltaTime * PhotonNetwork.SerializationRate);
}
}
public void OnPhotonSerializeView(PhotonStream stream, PhotonMessageInfo info)
{
if (stream.IsWriting)
{
stream.SendNext(transform.position);
stream.SendNext(transform.rotation);
}
else if (stream.IsReading)
{
networkPosition = (Vector3)stream.ReceiveNext();
networkRotation = (Quaternion)stream.ReceiveNext();
deltaPosition = Vector3.Distance(transform.position, networkPosition);
deltaRotation = Quaternion.Angle(transform.rotation, networkRotation);
}
}
● 마스터 클라이언트와 호스트 마이그레이션
마이그레이션은 방장이 방에서 나가더라도 다른 클라이언트에게 호스트 권한을 승계하여 게임을 자연스럽고 끊김없이 유지하는 기법입니다.
포톤에서는 기본적으로 호스트 마이그레이션 과정이 적용되어 있으나 약간의 추가 과정이 필요합니다.
방장 승계가 되면 OnMasterClientSwitched() 콜백 함수를 자동적으로 호출합니다.
public class PhotonController : MonoBehaviourPunCallbacks
{
// 마스터 클라이언트 변경시 호출됨
public override void OnMasterClientSwitched(Player newMasterClient)
{
if (PhotonNetwork.LocalPlayer != newMasterClient)
return;
// 변경된 마스터 클라이언트가 자신인 경우
// 방장으로서 해야할 준비 작업 진행
}
}
private void Update()
{
// 방장이 아니라면 빠져나가기
if (PhotonNetwork.IsMasterClient == false)
return;
// 방장만 수행해야하는 로직
}
MonobehaviourPunCallbacks 콜백함수
마스터 서버, 로비, 방 등 여러 네트워크 작업을 한 뒤에는 그에 맞는 콜백함수를 받을 수 있습니다.
이 콜백함수를 이용해 여러 반응에 대응하는 기능을 작성할 수 있습니다.
단 해당 콜백 함수들을 사용하기 위해선 MonoBehaviourPunCallbacks 클래스를 상속받아야 합니다.
public class NetworkManager : MonoBehaviourPunCallbacks
{
// 포톤 접속시 호출됨
public override void OnConnected() { }
// 마스터 서버 접속시 호출됨
public override void OnConnectedToMaster() { }
// 접속 해제시 호출됨
public override void OnDisconnected(DisconnectCause cause) { }
// 방 생성시 호출됨
public override void OnCreatedRoom() { }
// 방 입장시 호출됨
public override void OnJoinedRoom() { }
// 방 퇴장시 호출됨
public override void OnLeftRoom() { }
// 새로운 플레이어가 방 입장시 호출됨
public override void OnPlayerEnteredRoom(Player newPlayer) { }
// 다른 플레이어가 방 퇴장시 호출됨
public override void OnPlayerLeftRoom(Player otherPlayer) { }
// 방 생성 실패시 호출됨
public override void OnCreateRoomFailed(short returnCode, string message) { }
// 방 입장 실패시 호출됨
public override void OnJoinRoomFailed(short returnCode, string message) { }
// 로비 입장시 호출됨
public override void OnJoinedLobby() { }
// 로비 퇴장시 호출됨
public override void OnLeftLobby() { }
// 방 목록 변경시 호출됨
public override void OnRoomListUpdate(List<RoomInfo> roomList) { }
}
커스텀 프로퍼티
커스텀 프로퍼티는 네트워크 전송을 위해 직렬화 처리가 추가된 해시테이블 형태의 자료구조입니다.
키 값을 이용해 데이터를 저장하고 가져올 수 있습니다.
같은 방에 있으면 다른 클라이언트의 정보 또한 가져올 수 있습니다.
Room room = PhotonNetwork.CurrentRoom; // 현재 참가한 룸을 확인
// 룸 커스텀 프로퍼티 설정
ExitGames.Client.Photon.Hashtable roomProperty = new ExitGames.Client.Photon.Hashtabl ();
roomProperty["Map"] = "Select Map";
room.SetCustomProperties(roomProperty);
// 룸 커스텀 프로퍼티 확인
string curMap = (string)room.CustomProperties["Map"];
Player player = PhotonNetwork.LocalPlayer; // 자신 플레이어를 확인
// 플레이어 커스텀 프로퍼티 설정
ExitGames.Client.Photon.Hashtable playerProperty = new ExitGames.Client.Photon Hashtable();
playerProperty["Ready"] = true;
player.SetCustomProperties(playerProperty);
// 플레이어 커스텀 프로퍼티 확인
bool ready = (bool)player.CustomProperties["Ready"];
| 방식 | PhotonView 필요 여부 | 활용 |
| IPunObservable | 필요 | 주기적으로 데이터를 갱신해야 할 때 |
| RPC | 필요 | 특정 상황에 메서드를 호출해야 할 때 |
| 커스텀 프로퍼티(Custom Properties) | 불필요 | 자주 바뀌지 않지만 언제든 정보 확인이 필요할 데이터 |
'개발, IT > 유니티' 카테고리의 다른 글
| [Unity] 게임 네트워크 (0) | 2026.01.06 |
|---|---|
| [Unity] Character Controller (캐릭터 컨트롤러) (0) | 2025.11.17 |
| [Unity] Light (빛, 광원) (0) | 2025.11.16 |
| [Unity] Particle System (파티클 시스템) (0) | 2025.11.16 |
| [Unity] Audio (오디오) / 소리 재생하기 (0) | 2025.11.16 |