> For the complete documentation index, see [llms.txt](/ko/llms.txt)


# Photon에서의 이전 가이드

Photon(PUN2)으로 만든 멀티플레이를 GS2-Realtime의 sync / stateMachine으로 옮기는 절차와 대응표



이 페이지는 Photon PUN2로 만든 멀티플레이를 GS2-Realtime의 오브젝트 동기화(`sync` / `stateMachine`)로 옮기기 위한 절차와 대응표입니다. 각 API의 자세한 내용은 [Unity 클라이언트]()를 참조해 주세요.

## 사고방식의 차이 {#differences}

| 관점 | Photon PUN2 | GS2-Realtime |
| --- | --- | --- |
| 룸 찾기 | Photon의 로비와 매치메이킹 | GS2-Matchmaking으로 멤버를 모으고, GS2-Realtime의 룸을 만든다 |
| 룸 접속 | `PhotonNetwork.ConnectUsingSettings` / `JoinRoom` | 발행된 IP 주소·포트·암호화 키로 `RealtimeSession`을 통해 접속한다 |
| 누가 값을 결정하는가 | 각 클라이언트(마스터 클라이언트가 조정 역할) | 룸의 서버가 쓰기 권한과 RPC 허가를 검증한다. `stateMachine`에서는 게임 진행도 서버의 룰이 결정한다 |
| 동기화하는 데이터 | `OnPhotonSerializeView`로 자유로운 데이터를 전송한다 | 오브젝트마다 최대 32개의 정수 필드. 위치·회전은 단위 간격으로 반올림하여 전송한다 |
| 통신 | UDP(reliable / unreliable) | WebSocket(확실하게 전달할 것)과 UDP(정기적인 상태 갱신) |
| 과금 | 동시 접속 수 | 룸 서버의 가동 시간과 통신량 |

가장 큰 차이는 룸의 서버가 권한을 검증할 수 있다는 점입니다. 다른 플레이어 오브젝트의 값은 변경할 수 없으며, [오브젝트 카탈로그](#catalog)를 설정하면 등록한 Prefab과 RPC만 통과하도록 검사하게 할 수 있습니다. 마스터 클라이언트에 맡기던 게임 진행(적의 HP, 득점, 승패)은 `stateMachine` 룰로 옮기면 클라이언트 개조의 영향을 받지 않게 됩니다.

## 대응표 {#mapping}

### 컴포넌트 {#mapping-components}

| Photon PUN2 | GS2-Realtime |
| --- | --- |
| `PhotonView` | `NetworkEntity` |
| `PhotonTransformView` | `NetworkTransform` |
| `IPunObservable` / `OnPhotonSerializeView` | `INetworkObservable`(`OnNetworkWrite` / `OnNetworkRead`) |
| `PhotonNetwork.PrefabPool` / `Resources`의 Prefab | Prefab Catalog와 `NetworkWorld` |
| 씬에 배치한 `PhotonView`, `InstantiateRoomObject` | 룸이 만드는 공유 오브젝트(`stateMachine` 룰, 또는 조작 권한을 대여하는 오브젝트) |

### 오브젝트 {#mapping-objects}

| Photon PUN2 | GS2-Realtime |
| --- | --- |
| `PhotonNetwork.Instantiate(name, pos, rot)` | `world.Instantiate(prefabId, pos, rot)` |
| `PhotonNetwork.Destroy(go)` | `entity.Destroy()` |
| `photonView.IsMine` | `entity.IsMine` |
| `photonView.Owner` | `entity.OwnerId`(접속 ID) |
| `photonView.TransferOwnership(player)` | `entity.TransferOwnership(connectionId)` |
| `photonView.RequestOwnership()`(`OwnershipOption.Takeover`) | `entity.RequestOwnership()`(오브젝트 카탈로그를 설정하는 경우 `"ownership": "takeover"`) |
| `PhotonNetwork.CleanupCacheOnLeave = false` | 카탈로그에서 `"keepOnLeave": true`(마스터 클라이언트에게 인계) |
| `photonView.Group` / `PhotonNetwork.SetInterestGroups` | `entity.SetGroup` / `world.SetInterestGroups`, `SubscribeGroup` |

### RPC와 이벤트 {#mapping-rpc}

| Photon PUN2 | GS2-Realtime |
| --- | --- |
| `[PunRPC]` | `[NetworkRpc]` |
| `photonView.RPC(name, RpcTarget.All, args)` | `entity.Rpc(name, RpcTarget.All, args)` |
| `RpcTarget.Others` / `AllBuffered` / `OthersBuffered` | 같은 이름의 `RpcTarget` |
| `RpcTarget.AllViaServer` | `RpcTarget.All`(항상 서버를 경유한다) |
| `RpcTarget.MasterClient` | `entity.RpcTo(name, world.MasterClientId, args)` |
| `photonView.RPC(name, player, args)` | `entity.RpcTo(name, connectionId, args)` |
| `PhotonMessageInfo` | `NetworkRpcInfo`(`Sender`는 호출자의 접속 ID) |
| `PhotonNetwork.RaiseEvent` | `world.RaiseEvent` / `RaiseEventTo`(핸들러는 `[NetworkEvent]`) |
| `IOnEventCallback.OnEvent` | `world.AddEventTarget`으로 등록한 `[NetworkEvent]` 메서드, 또는 `world.EventReceived` |

### 룸과 플레이어 {#mapping-room}

| Photon PUN2 | GS2-Realtime |
| --- | --- |
| `PhotonNetwork.LocalPlayer.ActorNumber` | `session.MyConnectionId` |
| `PhotonNetwork.PlayerList`, `OnPlayerEnteredRoom` / `OnPlayerLeftRoom` | `RealtimeSession`의 `OnJoinPlayer` / `OnLeavePlayer` |
| `PhotonNetwork.MasterClient` / `IsMasterClient` | `world.MasterClientId` / `IsMasterClient` |
| `PhotonNetwork.SetMasterClient` / `OnMasterClientSwitched` | `world.SetMasterClient` / `MasterClientChanged` |
| `Room.SetCustomProperties` / `OnRoomPropertiesUpdate` | `world.SetRoomProperty` / `RoomPropertyChanged` |
| `Player.SetCustomProperties` / `OnPlayerPropertiesUpdate` | `world.SetPlayerProperty` / `PlayerPropertyChanged` |
| `PhotonNetwork.Time` | `world.ServerTime` |
| `PhotonNetwork.SendRate` / `SerializationRate` | `world.SendRate` |
| unreliable 전송 | `RealtimeUdpChannel`(정기적인 상태 갱신을 UDP로 전송한다) |

## 이전 절차 {#steps}

1. **룸 준비를 대체한다**: Photon의 로비와 매치메이킹을 GS2-Matchmaking과 GS2-Realtime의 룸 생성으로 대체합니다. Realtime 네임스페이스는 `serverType`을 `sync`(게임 진행도 서버에서 결정한다면 `stateMachine`)로 합니다. 흐름은 [GS2-Realtime 소개]()와 [튜토리얼]()을 참조해 주세요.
2. **접속을 대체한다**: `RealtimeSession`으로 접속하고, `NetworkWorld.Attach`와 `world.Receive`를 연결합니다. [룸에 접속하기](#connection)의 내용대로 작성합니다.
3. **Prefab을 대체한다**: `PhotonView`를 `NetworkEntity`로, `PhotonTransformView`를 `NetworkTransform`으로 바꿉니다. Prefab Catalog를 만들고 Prefab ID를 할당하여 등록합니다.
4. **동기화할 값을 대체한다**: `OnPhotonSerializeView`로 전송하던 값은 동기화 스키마의 필드로 만들어 `INetworkObservable`로 읽고 씁니다. 값은 32개까지의 정수(float는 단위 간격으로 반올림)이므로, 문자열이나 배열은 RPC나 커스텀 프로퍼티로 전송합니다.
5. **RPC를 대체한다**: `[PunRPC]`를 `[NetworkRpc]`로, `photonView.RPC`를 `entity.Rpc`로 바꿉니다. 다른 플레이어의 오브젝트에 대해 호출하는 RPC에는 `AnyCaller = true`를 붙입니다. 인수의 타입이 [사용할 수 있는 타입](#rpc)에 포함되는지 확인합니다.
6. **생성과 삭제를 대체한다**: `PhotonNetwork.Instantiate`를 `world.Instantiate`로, `PhotonNetwork.Destroy`를 `entity.Destroy()`로 바꿉니다. 씬에 배치했던 오브젝트는 룸이 만드는 공유 오브젝트로 합니다.
7. **검사하려면 오브젝트 카탈로그를 설정한다**: 생성이나 RPC를 룸이 검사하게 하려면 Prefab Catalog 에셋을 우클릭하여 **Copy Object Catalog JSON**으로 생성한 JSON을 Realtime 네임스페이스의 `syncSetting.objectCatalog`에 설정합니다. RPC의 이름이나 Prefab을 바꾸면 다시 생성합니다.
8. **이벤트·프로퍼티·마스터 클라이언트를 대체한다**: 대응표대로 대체합니다.
9. **UDP를 사용한다**: WebGL 이외에서는 `RealtimeUdpChannel`로 정기적인 상태 갱신을 UDP로 전송합니다.

## 코드 변경 예 {#examples}

### RPC {#example-rpc}

```csharp
// Photon PUN2
[PunRPC]
void Fire(Vector3 direction, PhotonMessageInfo info) { /* ... */ }
photonView.RPC(nameof(Fire), RpcTarget.Others, direction);

// GS2-Realtime
[NetworkRpc]
void Fire(Vector3 direction, NetworkRpcInfo info) { /* ... */ }
entity.Rpc(nameof(Fire), RpcTarget.Others, direction);
```

### 생성과 소유자 판정 {#example-instantiate}

```csharp
// Photon PUN2
var player = PhotonNetwork.Instantiate("Player", spawnPoint, Quaternion.identity);
if (player.GetComponent<PhotonView>().IsMine) { /* 입력을 활성화한다 */ }

// GS2-Realtime(Prefab ID 1을 Prefab Catalog에 등록해 둔다)
var player = world.Instantiate(1, spawnPoint, Quaternion.identity);
if (player.IsMine) { /* 입력을 활성화한다 */ }
```

입력을 받는 스크립트는 `NetworkEntity`의 **Controlled Behaviours**에 등록하면 쓸 수 있는 동안에만 자동으로 활성화됩니다.

### 값의 동기화 {#example-serialize}

```csharp
// Photon PUN2
public void OnPhotonSerializeView(PhotonStream stream, PhotonMessageInfo info)
{
    if (stream.IsWriting) stream.SendNext(health);
    else health = (float)stream.ReceiveNext();
}

// GS2-Realtime(동기화 스키마에 float의 Health 필드를 선언해 둔다)
public void OnNetworkWrite(NetworkEntity entity) => entity.SetFloat("Health", health);
public void OnNetworkRead(NetworkEntity entity, uint changed)
{
    if (entity.HasChanged("Health", changed)) health = entity.GetFloat("Health");
}
```

### 이벤트와 프로퍼티 {#example-room}

```csharp
// Photon PUN2
PhotonNetwork.RaiseEvent(GoalCode, team, new RaiseEventOptions { Receivers = ReceiverGroup.All }, SendOptions.SendReliable);
PhotonNetwork.CurrentRoom.SetCustomProperties(new Hashtable { { "stage", "forest" } });

// GS2-Realtime
world.RaiseEvent("Goal", RpcTarget.All, team);   // [NetworkEvent] void Goal(int team)를 등록해 둔다
world.SetRoomProperty("stage", "forest");
```

## 주의 사항 {#caveats}

- **확정은 비동기**: `world.Instantiate`는 즉시 사용할 수 있지만, 룸이 거부하면 `SpawnFailed`가 호출되고 삭제됩니다. 커스텀 프로퍼티는 룸을 경유하여 전원에게 도착한 시점에 반영됩니다.
- **값은 반올림된다**: 위치는 0.01, 회전은 360/4096도의 단위 간격으로 전송합니다. 정밀도를 바꾸려면 동기화 스키마에서 지정합니다.
- **권한은 서버가 검증한다**: 소유자 이외의 쓰기는 거부됩니다. 오브젝트 카탈로그를 설정하면 등록하지 않은 Prefab과 RPC, 허가하지 않은 소유권 취득도 거부됩니다.
- **RPC 인수의 상한**: 합계 4096바이트까지입니다. 큰 데이터는 나누어 전송합니다.
- **전송 순서**: UDP로 전송하는 정기적인 갱신과 WebSocket으로 전송하는 RPC나 이벤트 사이의 순서는 보장하지 않습니다. RPC 직전에 변경한 값은 RPC보다 먼저 도착합니다.
- **룸 종료 조건**: 참가자 전원으로부터 1분간 통신이 없으면 룸은 종료됩니다. [룸 종료 조건과의 관계](#idle)를 참조해 주세요.
- **WebGL**: UDP를 사용할 수 없으므로 모두 WebSocket으로 전송합니다.
- **Photon의 다른 제품**: Photon Voice나 Photon Chat에 해당하는 기능은 GS2-Realtime에 포함되어 있지 않습니다. 텍스트 채팅에는 GS2-Chat을 사용할 수 있습니다.



