10/01 Photon Pun2 - 인게임 동기화

댓글 0
댓글을 작성하려면 로그인이 필요합니다.
아직 댓글이 없습니다. 첫 번째 댓글을 작성해보세요.

댓글을 작성하려면 로그인이 필요합니다.
아직 댓글이 없습니다. 첫 번째 댓글을 작성해보세요.
Room은 함께 플레이하는 클라이언트들의 네트워크 공간이고, Scene은 각 클라이언트가 로드한 Unity 씬이다. 씬 이름이 LobbyScene이어도 네트워크 상태는 Room에 입장한 상태일 수 있다.
PhotonNetwork.AutomaticallySyncScene = true;
PhotonNetwork.LoadLevel("GameScene");
AutomaticallySyncScene: 같은 Room에서 Master Client의 씬을 다른 클라이언트가 따라가도록 하는 설정이다. 따라가는 클라이언트에서도 활성화해야 한다.LoadLevel(): 씬을 전환하는 함수다. Room 밖에서도 씬 로드에 사용할 수 있지만, Room 전체의 씬 동기화를 위해서는 Master Client가 호출한다.OnJoinedRoom(): 내 클라이언트가 Room에 입장했을 때 호출된다. 같은 Room에서 씬만 변경했다고 다시 호출되지는 않는다.이미 Room에 입장한 뒤 GameScene으로 이동했다면, 그 씬에서 새로 생성된 매니저의 Start() 등에서 필요한 초기화를 수행한다.
| 함수 | 생성 범위 |
|---|---|
Unity의 Instantiate() | 호출한 클라이언트에서만 생성한다. 다른 클라이언트에 자동으로 생성되지 않는다. |
PhotonNetwork.Instantiate() | 같은 Room에 생성 정보를 전달하여 각 클라이언트에 대응하는 인스턴스를 생성한다. |
PhotonNetwork.Instantiate("Player", spawnPosition, Quaternion.identity);
온라인 상태에서는 Room에 입장한 뒤 호출해야 한다. Lobby에만 있는 상태에서는 사용할 수 없다. OfflineMode에서는 로컬 테스트용으로 사용할 수 있다.
기본 프리팹 풀을 사용할 때는 프리팹이 Resources 폴더 안에 있어야 하며, PhotonView가 미리 붙어 있어야 한다. Instantiate()가 PhotonView를 자동으로 추가해주는 것은 아니다.
| 프리팹 위치 | 전달할 이름 |
|---|---|
Assets/Resources/Player.prefab | "Player" |
Assets/Resources/Prefabs/Player.prefab | "Prefabs/Player" |
경로는 Resources 기준이며 확장자는 생략한다. 커스텀 IPunPrefabPool을 구현하면 Resources 밖의 프리팹도 사용할 수 있다.
각 클라이언트에 생성되는 시점은 네트워크 지연에 따라 다르므로, 정확히 같은 순간에 생성된다는 뜻은 아니다.
플레이어 캐릭터는 각 클라이언트가 자기 캐릭터 하나를 생성하는 구조로 만들 수 있다. 세 명이 각자 하나씩 생성하면 각 화면에 세 캐릭터가 나타난다.
각 클라이언트가 플레이어 목록 전체를 반복하여 세 개씩 생성하면 총 아홉 개가 생길 수 있다. 다른 플레이어를 화면에 표시하기 위해 각 클라이언트에서 다시 생성 요청할 필요는 없다.
PhotonView는 각 클라이언트의 대응 오브젝트를 네트워크에서 식별하고, 소유권·제어 권한과 상태 동기화·RPC를 연결하는 컴포넌트다.
| 항목 | 의미 |
|---|---|
ViewID | Room 안에서 PhotonView를 구분하는 ID다. 대응하는 PhotonView는 클라이언트 간 같은 ID를 사용한다. |
Owner | 오브젝트의 소유 플레이어다. 일반적인 플레이어 생성에서는 생성한 클라이언트가 소유한다. |
Controller | 현재 오브젝트의 상태를 제어하는 플레이어다. |
IsMine | 현재 내 클라이언트가 이 PhotonView의 Controller인지 나타낸다. |
Owner와 Controller는 보통 같지만, 소유자가 일시적으로 연결이 끊겨 비활성 상태로 남아 있으면 Master Client가 제어를 맡을 수 있다. 따라서 IsMine은 정확히는 소유자보다 현재 제어자를 기준으로 판단한다.
if (!photonView.IsMine)
return;
// 내가 제어하는 캐릭터의 입력 처리
각 클라이언트에는 다른 사람의 캐릭터도 존재하고 그 스크립트도 실행된다. 입력 처리를 구분하지 않으면 내 입력으로 다른 캐릭터까지 움직일 수 있다.
| 대상 | A 클라이언트 | B 클라이언트 |
|---|---|---|
| A가 제어하는 캐릭터 | IsMine = true | IsMine = false |
| B가 제어하는 캐릭터 | IsMine = false | IsMine = true |
// 현재 Room에서 내 플레이어에게 부여된 식별 번호
int actorNumber = PhotonNetwork.LocalPlayer.ActorNumber;
ActorNumber는 Room 안에서 플레이어를 구분하는 번호이며, 계정에 영구적으로 부여되는 ID는 아니다. 이를 기준으로 스폰 위치, 캐릭터, 머티리얼 등을 선택하는 코드를 작성할 수 있다. 번호 자체가 해당 설정을 동기화하는 것은 아니다.
플레이어가 나가면 번호에 빈자리가 생길 수 있으므로 actorNumber - 1을 배열이나 자식 인덱스로 사용한다면 범위를 확인해야 한다.
bool amIMaster = PhotonNetwork.IsMasterClient;
Player master = PhotonNetwork.MasterClient;
Master Client 여부는 Room의 정보다. PhotonView 인스펙터의 Owner와 Controller만으로 마스터인지 판단할 수 없다. 일반 플레이어도 자기 캐릭터에서는 IsMine이 true다.
MonoBehaviour
└─ MonoBehaviourPun
└─ MonoBehaviourPunCallbacks
| 클래스 | 제공하는 기능 |
|---|---|
MonoBehaviour | Unity 기본 동작 |
MonoBehaviourPun | 기본 동작과 같은 GameObject의 PhotonView에 접근하는 photonView 프로퍼티 |
MonoBehaviourPunCallbacks | 위 기능과 OnJoinedRoom 등 PUN 연결·Room 콜백 |
MonoBehaviourPun은 PhotonView 참조를 가져와 캐싱하여 편하게 접근하게 해준다. PhotonView 컴포넌트를 자동으로 붙이거나 동기화를 설정하는 것은 아니다. 일반 MonoBehaviour에서도 GetComponent<PhotonView>()로 직접 참조할 수 있다.
Observed Components는 PhotonView가 상태 데이터 송수신을 맡길 컴포넌트 목록이다. 위치나 체력 값 자체가 아니라, 그 값을 처리하는 컴포넌트를 넣는다.
| 등록할 컴포넌트 | 처리하는 내용 |
|---|---|
PhotonTransformView / PhotonTransformViewClassic | 설정한 위치·회전·크기 동기화 |
PhotonAnimatorView | 설정한 Animator 파라미터·레이어 가중치 동기화 |
직접 만든 IPunObservable 스크립트 | OnPhotonSerializeView에서 지정한 값 송수신 |
인스펙터에서 목록의 +로 슬롯을 만들고 해당 컴포넌트를 드래그하여 등록할 수 있다. Observable Search가 자동 검색 모드라면 같은 GameObject와 자식에서 IPunObservable 컴포넌트를 찾아 등록한다. Manual에서는 직접 목록을 구성한다.
일반 스크립트를 등록한다고 모든 필드가 자동 동기화되지는 않는다. 직접 구현한다면 IPunObservable과 OnPhotonSerializeView로 송수신할 값을 지정해야 한다.
| 옵션 | 의미 |
|---|---|
Off | 관찰 컴포넌트의 상태 동기화를 끈다. RPC를 끄는 설정은 아니다. |
Reliable Delta Compressed | 전달을 보장하며, 이전과 같은 값은 델타 압축으로 전송량을 줄인다. |
Unreliable | 일부 업데이트 누락을 허용하고 누락된 업데이트를 재전송하지 않는다. |
Unreliable On Change | 일부 누락을 허용하며, 동일한 상태가 반복되면 전송을 멈추고 변경 시 다시 전송한다. |
Reliable은 누락을 복구하는 과정에서 후속 데이터 전달이 지연될 수 있다. 계속 새 값이 나오는 이동 데이터는 일부 과거 값이 누락되어도 이후 값으로 갱신할 수 있다.
제어하는 클라이언트는 입력으로 캐릭터를 움직이고 위치·회전을 보낸다. 다른 클라이언트는 수신한 정보를 바탕으로 화면의 대응 캐릭터를 갱신한다.
PhotonTransformView를 사용하려면 PhotonView의 Observed Components에 등록하고 동기화할 항목을 선택한다.
| 항목 | PhotonTransformView | PhotonTransformViewClassic |
|---|---|---|
| 목적 | 간단한 Transform 동기화 | 세부 조절이 가능한 Transform 동기화 |
| 설정 | 동기화 항목 등 간단한 설정 | 보간 방식·속도, 외삽, 순간 이동 조건 등 |
| 특징 | 내부에 구현된 처리 방식 사용 | 상황에 맞게 따라가는 방식을 선택 |
일반 버전에 보간이 전혀 없다는 뜻은 아니다. Classic은 보간·외삽 등을 인스펙터에서 더 세밀하게 선택할 수 있다.
| 항목 | 설정과 의미 |
|---|---|
| Synchronize Position | 위치 동기화 활성화 |
| Interpolate Option: Lerp | 받은 목표 위치까지 부드럽게 접근 |
| Lerp Speed: 3 | 목표를 따라가는 빠르기 조절 |
| Enable teleport for great distances | 큰 위치 차이는 즉시 이동으로 보정 |
| Teleport if distance greater than: 3 | 위치 차이가 3을 넘으면 즉시 이동 |
| Extrapolate Option: Disabled | 다음 위치를 예측하는 외삽 비활성화 |
| Synchronize Rotation / Scale | 이미지에서는 비활성화 |
보간은 통신 지연을 없애는 것이 아니라 화면상 이동을 부드럽게 만든다. 같은 위치를 두 Transform 동기화 컴포넌트가 동시에 제어하지 않도록 하나를 선택한다.
PhotonAnimatorView는 설정한 Animator 파라미터와 레이어 가중치를 전달한다. 각 클라이언트의 Animator는 받은 값으로 애니메이션을 재생한다. 일반적으로 캐릭터의 모든 뼈 위치를 매 프레임 전송하는 방식은 아니다.
이 컴포넌트도 PhotonView의 Observed Components에 등록해야 한다.
| 동기화 항목 | 의미 |
|---|---|
| Synchronize Layer Weights | Animator 각 레이어의 기여 가중치를 동기화한다. 가중치가 변하지 않는다면 동기화할 필요가 없을 수 있다. |
| Synchronize Parameters | IsWalk, Jump 등 Animator Controller에 정의한 파라미터를 동기화한다. |
이름 옆 (False) | 현재 값 표시다. 동기화 활성화 여부는 오른쪽 옵션으로 정한다. |
| 옵션 | 데이터 처리 |
|---|---|
Disabled | 해당 항목을 동기화하지 않는다. |
Discrete | 동기화 주기마다 그 시점의 값을 전송하고 수신 측 Animator에 적용한다. |
Continuous | 매 프레임 값을 기록하고 전송 시 기록한 값을 묶어서 보낸다. 수신 측은 순서대로 적용한다. |
전송 사이에 Speed 변화: 0 → 0.3 → 0.6 → 1
Discrete: 전송 시점의 값 전달
Continuous: 기록한 중간 값들도 함께 전달
Continuous는 중간 변화가 보존되어 부드럽게 표현할 수 있지만 전송량이 늘어난다. 매 프레임 패킷을 보내거나 네트워크 지연을 제거한다는 뜻은 아니다. 실제 동기화 주기는 SerializationRate 등의 설정과 관련된다.
RPC(Remote Procedure Call)는 같은 Room의 지정한 클라이언트에서 함수를 실행하도록 요청하는 기능이다. PhotonView가 어느 오브젝트에서 실행할지 식별한다.
// 같은 PhotonView를 가진 대응 오브젝트에서 함수 실행 요청
photonView.RPC(nameof(PlayAttack), RpcTarget.All, 1);
[PunRPC]
private void PlayAttack(int attackType)
{
Debug.Log($"공격 종류: {attackType}");
}
RPC 함수가 있는 스크립트는 해당 PhotonView와 같은 GameObject에 붙어 있어야 한다. A 캐릭터의 PhotonView로 호출하면 다른 화면에서도 A 캐릭터에서 실행되며, 모든 캐릭터에서 실행되는 것이 아니다.
| 옵션 | 실행 대상 | 호출자의 실행 | 나중에 입장한 플레이어 |
|---|---|---|---|
All | 나를 포함한 모두 | 즉시 로컬 실행 | 과거 호출 전달 안 됨 |
Others | 나를 제외한 모두 | 실행하지 않음 | 과거 호출 전달 안 됨 |
AllBuffered | 나를 포함한 모두 | 즉시 로컬 실행 | 저장된 호출 전달 |
OthersBuffered | 나를 제외한 모두 | 실행하지 않음 | 저장된 호출 전달 |
AllViaServer | 나를 포함한 모두 | 서버에서 돌려받은 뒤 실행 | 과거 호출 전달 안 됨 |
AllBufferedViaServer | 나를 포함한 모두 | 서버에서 돌려받은 뒤 실행 | 저장된 호출 전달 |
| 방식 | 전달하는 것 | 예시 |
|---|---|---|
| PhotonView의 상태 동기화 | 주기적으로 갱신되는 값 | 위치, 회전, 체력, Animator 파라미터 |
| RPC | 함수 실행 요청과 인자 | 공격, 스킬 사용, 효과 재생 |
RPC도 PhotonView를 사용한다. 따라서 두 방식을 서로 별개의 컴포넌트로 보기보다 주기적인 값 동기화와 함수 실행 요청으로 구분하면 정확하다. 이 외에도 Custom Properties와 RaiseEvent를 활용하는 방식이 있다.
Develog는 단순히 글을 쓰는 공간이 아닙니다. 성장의 과정을 기록하고, 그 기록으로 나를 증명하며 원하는 기회를 얻는 곳이길 바랐습니다. 누군가의 꿈으로 향하는 중간다리가 되는 것, 그것이 Develog를 만든 이유입니다.

우리들의 게임 발매 이야기

안녕하세요. 플밍 4기 입니다. 게임 개발을 배우기 전 네트워크 엔지니어 도메인에서 익히고 배웠던 네트워크 이론에 대한 기초 입니다. 학습에 도움이 되길 바라며 공유 드립니다.
Develog는 단순히 글을 쓰는 공간이 아닙니다. 성장의 과정을 기록하고, 그 기록으로 나를 증명하며 원하는 기회를 얻는 곳이길 바랐습니다. 누군가의 꿈으로 향하는 중간다리가 되는 것, 그것이 Develog를 만든 이유입니다.



안녕하세요. 플밍 4기 입니다. 게임 개발을 배우기 전 네트워크 엔지니어 도메인에서 익히고 배웠던 네트워크 이론에 대한 기초 입니다. 학습에 도움이 되길 바라며 공유 드립니다.