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

심지태
∙∙조회 3
10/01 Photon Pun2 - 인게임 동기화

1. Room과 씬 동기화

Room은 함께 플레이하는 클라이언트들의 네트워크 공간이고, Scene은 각 클라이언트가 로드한 Unity 씬이다. 씬 이름이 LobbyScene이어도 네트워크 상태는 Room에 입장한 상태일 수 있다.

csharp
PhotonNetwork.AutomaticallySyncScene = true;
PhotonNetwork.LoadLevel("GameScene");

  • AutomaticallySyncScene: 같은 Room에서 Master Client의 씬을 다른 클라이언트가 따라가도록 하는 설정이다. 따라가는 클라이언트에서도 활성화해야 한다.
  • LoadLevel(): 씬을 전환하는 함수다. Room 밖에서도 씬 로드에 사용할 수 있지만, Room 전체의 씬 동기화를 위해서는 Master Client가 호출한다.
  • OnJoinedRoom(): 내 클라이언트가 Room에 입장했을 때 호출된다. 같은 Room에서 씬만 변경했다고 다시 호출되지는 않는다.

이미 Room에 입장한 뒤 GameScene으로 이동했다면, 그 씬에서 새로 생성된 매니저의 Start() 등에서 필요한 초기화를 수행한다.

2. 일반 Instantiate와 PhotonNetwork.Instantiate

함수생성 범위
Unity의 Instantiate()호출한 클라이언트에서만 생성한다. 다른 클라이언트에 자동으로 생성되지 않는다.
PhotonNetwork.Instantiate()같은 Room에 생성 정보를 전달하여 각 클라이언트에 대응하는 인스턴스를 생성한다.
csharp
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 밖의 프리팹도 사용할 수 있다.

각 클라이언트에 생성되는 시점은 네트워크 지연에 따라 다르므로, 정확히 같은 순간에 생성된다는 뜻은 아니다.

플레이어별 생성

플레이어 캐릭터는 각 클라이언트가 자기 캐릭터 하나를 생성하는 구조로 만들 수 있다. 세 명이 각자 하나씩 생성하면 각 화면에 세 캐릭터가 나타난다.

각 클라이언트가 플레이어 목록 전체를 반복하여 세 개씩 생성하면 총 아홉 개가 생길 수 있다. 다른 플레이어를 화면에 표시하기 위해 각 클라이언트에서 다시 생성 요청할 필요는 없다.

3. PhotonView의 역할과 식별 정보

PhotonView는 각 클라이언트의 대응 오브젝트를 네트워크에서 식별하고, 소유권·제어 권한과 상태 동기화·RPC를 연결하는 컴포넌트다.

항목의미
ViewIDRoom 안에서 PhotonView를 구분하는 ID다. 대응하는 PhotonView는 클라이언트 간 같은 ID를 사용한다.
Owner오브젝트의 소유 플레이어다. 일반적인 플레이어 생성에서는 생성한 클라이언트가 소유한다.
Controller현재 오브젝트의 상태를 제어하는 플레이어다.
IsMine현재 내 클라이언트가 이 PhotonView의 Controller인지 나타낸다.

Owner와 Controller는 보통 같지만, 소유자가 일시적으로 연결이 끊겨 비활성 상태로 남아 있으면 Master Client가 제어를 맡을 수 있다. 따라서 IsMine은 정확히는 소유자보다 현재 제어자를 기준으로 판단한다.

IsMine으로 입력 구분

csharp
if (!photonView.IsMine)
    return;

// 내가 제어하는 캐릭터의 입력 처리

각 클라이언트에는 다른 사람의 캐릭터도 존재하고 그 스크립트도 실행된다. 입력 처리를 구분하지 않으면 내 입력으로 다른 캐릭터까지 움직일 수 있다.

대상A 클라이언트B 클라이언트
A가 제어하는 캐릭터IsMine = trueIsMine = false
B가 제어하는 캐릭터IsMine = falseIsMine = true

ActorNumber와 Master Client

csharp
// 현재 Room에서 내 플레이어에게 부여된 식별 번호
int actorNumber = PhotonNetwork.LocalPlayer.ActorNumber;

ActorNumber는 Room 안에서 플레이어를 구분하는 번호이며, 계정에 영구적으로 부여되는 ID는 아니다. 이를 기준으로 스폰 위치, 캐릭터, 머티리얼 등을 선택하는 코드를 작성할 수 있다. 번호 자체가 해당 설정을 동기화하는 것은 아니다.

플레이어가 나가면 번호에 빈자리가 생길 수 있으므로 actorNumber - 1을 배열이나 자식 인덱스로 사용한다면 범위를 확인해야 한다.

csharp
bool amIMaster = PhotonNetwork.IsMasterClient;
Player master = PhotonNetwork.MasterClient;

Master Client 여부는 Room의 정보다. PhotonView 인스펙터의 Owner와 Controller만으로 마스터인지 판단할 수 없다. 일반 플레이어도 자기 캐릭터에서는 IsMine이 true다.

4. MonoBehaviourPun과 MonoBehaviourPunCallbacks

text
MonoBehaviour
    └─ MonoBehaviourPun
           └─ MonoBehaviourPunCallbacks

클래스제공하는 기능
MonoBehaviourUnity 기본 동작
MonoBehaviourPun기본 동작과 같은 GameObject의 PhotonView에 접근하는 photonView 프로퍼티
MonoBehaviourPunCallbacks위 기능과 OnJoinedRoom 등 PUN 연결·Room 콜백

MonoBehaviourPun은 PhotonView 참조를 가져와 캐싱하여 편하게 접근하게 해준다. PhotonView 컴포넌트를 자동으로 붙이거나 동기화를 설정하는 것은 아니다. 일반 MonoBehaviour에서도 GetComponent<PhotonView>()로 직접 참조할 수 있다.

5. Observed Components와 상태 동기화

Observed Components는 PhotonView가 상태 데이터 송수신을 맡길 컴포넌트 목록이다. 위치나 체력 값 자체가 아니라, 그 값을 처리하는 컴포넌트를 넣는다.

등록할 컴포넌트처리하는 내용
PhotonTransformView / PhotonTransformViewClassic설정한 위치·회전·크기 동기화
PhotonAnimatorView설정한 Animator 파라미터·레이어 가중치 동기화
직접 만든 IPunObservable 스크립트OnPhotonSerializeView에서 지정한 값 송수신

인스펙터에서 목록의 +로 슬롯을 만들고 해당 컴포넌트를 드래그하여 등록할 수 있다. Observable Search가 자동 검색 모드라면 같은 GameObject와 자식에서 IPunObservable 컴포넌트를 찾아 등록한다. Manual에서는 직접 목록을 구성한다.

일반 스크립트를 등록한다고 모든 필드가 자동 동기화되지는 않는다. 직접 구현한다면 IPunObservable과 OnPhotonSerializeView로 송수신할 값을 지정해야 한다.

PhotonView의 Synchronization 옵션

옵션의미
Off관찰 컴포넌트의 상태 동기화를 끈다. RPC를 끄는 설정은 아니다.
Reliable Delta Compressed전달을 보장하며, 이전과 같은 값은 델타 압축으로 전송량을 줄인다.
Unreliable일부 업데이트 누락을 허용하고 누락된 업데이트를 재전송하지 않는다.
Unreliable On Change일부 누락을 허용하며, 동일한 상태가 반복되면 전송을 멈추고 변경 시 다시 전송한다.

Reliable은 누락을 복구하는 과정에서 후속 데이터 전달이 지연될 수 있다. 계속 새 값이 나오는 이동 데이터는 일부 과거 값이 누락되어도 이후 값으로 갱신할 수 있다.

6. Transform 동기화와 Classic

제어하는 클라이언트는 입력으로 캐릭터를 움직이고 위치·회전을 보낸다. 다른 클라이언트는 수신한 정보를 바탕으로 화면의 대응 캐릭터를 갱신한다.

PhotonTransformView를 사용하려면 PhotonView의 Observed Components에 등록하고 동기화할 항목을 선택한다.

항목PhotonTransformViewPhotonTransformViewClassic
목적간단한 Transform 동기화세부 조절이 가능한 Transform 동기화
설정동기화 항목 등 간단한 설정보간 방식·속도, 외삽, 순간 이동 조건 등
특징내부에 구현된 처리 방식 사용상황에 맞게 따라가는 방식을 선택

일반 버전에 보간이 전혀 없다는 뜻은 아니다. Classic은 보간·외삽 등을 인스펙터에서 더 세밀하게 선택할 수 있다.

수업에서 확인한 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 동기화 컴포넌트가 동시에 제어하지 않도록 하나를 선택한다.

7. PhotonAnimatorView

PhotonAnimatorView는 설정한 Animator 파라미터와 레이어 가중치를 전달한다. 각 클라이언트의 Animator는 받은 값으로 애니메이션을 재생한다. 일반적으로 캐릭터의 모든 뼈 위치를 매 프레임 전송하는 방식은 아니다.

이 컴포넌트도 PhotonView의 Observed Components에 등록해야 한다.

동기화 항목의미
Synchronize Layer WeightsAnimator 각 레이어의 기여 가중치를 동기화한다. 가중치가 변하지 않는다면 동기화할 필요가 없을 수 있다.
Synchronize ParametersIsWalk, Jump 등 Animator Controller에 정의한 파라미터를 동기화한다.
이름 옆 (False)현재 값 표시다. 동기화 활성화 여부는 오른쪽 옵션으로 정한다.

Disabled / Discrete / Continuous

옵션데이터 처리
Disabled해당 항목을 동기화하지 않는다.
Discrete동기화 주기마다 그 시점의 값을 전송하고 수신 측 Animator에 적용한다.
Continuous매 프레임 값을 기록하고 전송 시 기록한 값을 묶어서 보낸다. 수신 측은 순서대로 적용한다.
text
전송 사이에 Speed 변화: 0 → 0.3 → 0.6 → 1
Discrete: 전송 시점의 값 전달
Continuous: 기록한 중간 값들도 함께 전달

Continuous는 중간 변화가 보존되어 부드럽게 표현할 수 있지만 전송량이 늘어난다. 매 프레임 패킷을 보내거나 네트워크 지연을 제거한다는 뜻은 아니다. 실제 동기화 주기는 SerializationRate 등의 설정과 관련된다.

8. RPC: 원격 함수 호출

RPC(Remote Procedure Call)는 같은 Room의 지정한 클라이언트에서 함수를 실행하도록 요청하는 기능이다. PhotonView가 어느 오브젝트에서 실행할지 식별한다.

csharp
// 같은 PhotonView를 가진 대응 오브젝트에서 함수 실행 요청
photonView.RPC(nameof(PlayAttack), RpcTarget.All, 1);

[PunRPC]
private void PlayAttack(int attackType)
{
    Debug.Log($"공격 종류: {attackType}");
}

RPC 함수가 있는 스크립트는 해당 PhotonView와 같은 GameObject에 붙어 있어야 한다. A 캐릭터의 PhotonView로 호출하면 다른 화면에서도 A 캐릭터에서 실행되며, 모든 캐릭터에서 실행되는 것이 아니다.

RpcTarget 비교

옵션실행 대상호출자의 실행나중에 입장한 플레이어
All나를 포함한 모두즉시 로컬 실행과거 호출 전달 안 됨
Others나를 제외한 모두실행하지 않음과거 호출 전달 안 됨
AllBuffered나를 포함한 모두즉시 로컬 실행저장된 호출 전달
OthersBuffered나를 제외한 모두실행하지 않음저장된 호출 전달
AllViaServer나를 포함한 모두서버에서 돌려받은 뒤 실행과거 호출 전달 안 됨
AllBufferedViaServer나를 포함한 모두서버에서 돌려받은 뒤 실행저장된 호출 전달
  • All / Others: 누구에게 실행시킬지 정한다.
  • Buffered: 서버가 호출 기록을 저장하여 나중에 입장한 플레이어에게도 전달한다. 최신 값 하나만 보관하는 방식이 아니다. 기록이 많으면 입장 시간이 늘어날 수 있다.
  • ViaServer: 호출자도 로컬 즉시 실행 대신 서버를 거친다. 서버에 도착한 RPC 순서로 수신 클라이언트의 실행 순서를 맞춘다. 정확히 같은 시각에 실행된다는 뜻은 아니다.

9. 대표적인 동기화 방식의 관계

방식전달하는 것예시
PhotonView의 상태 동기화주기적으로 갱신되는 값위치, 회전, 체력, Animator 파라미터
RPC함수 실행 요청과 인자공격, 스킬 사용, 효과 재생

RPC도 PhotonView를 사용한다. 따라서 두 방식을 서로 별개의 컴포넌트로 보기보다 주기적인 값 동기화와 함수 실행 요청으로 구분하면 정확하다. 이 외에도 Custom Properties와 RaiseEvent를 활용하는 방식이 있다.