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


# 튜토리얼: 플레이어 캐릭터 동기화하기

룰을 작성하지 않고, 각 플레이어 캐릭터의 위치와 RPC를 Unity에서 동기화하기까지의 절차



이 튜토리얼에서는 룸에 참가한 플레이어가 각자 자신의 캐릭터를 움직이고, 다른 플레이어의 화면에도 똑같이 표시되는 작은 룸을 만듭니다.

- 접속하면 자신의 캐릭터가 1체 생성됩니다.
- 방향키(WASD)로 자신의 캐릭터를 움직이면, 다른 플레이어의 화면에서도 부드럽게 움직입니다.
- 스페이스 키를 누르면 전원의 화면에서 그 캐릭터가 순간적으로 커집니다(RPC).

서버 타입은 `sync`를 사용합니다. 룸의 룰을 작성할 필요는 없습니다. 서버 타입의 차이는 [구성과 선택 방법]()을 참조해 주세요.

## 작업 흐름

| 순서 | 작업 장소 | 만드는 것 |
| --- | --- | --- |
| [1. 네임스페이스를 설정한다](#namespace) | GS2 | Realtime 네임스페이스 설정 |
| [2. 캐릭터의 Prefab을 만든다](#prefab) | Unity Editor | 이동과 RPC 스크립트를 붙인 Prefab |
| [3. 룸에 접속한다](#connect) | Unity Editor | Prefab Catalog와, 접속하여 자신의 캐릭터를 생성하는 씬 |
| [4. 동작을 확인한다](#verify) | Unity(클라이언트 2개) | — |
| [5. 공개에 대비해 제한한다](#catalog) | GS2 | 오브젝트 카탈로그 |

GS2 SDK for Unity를 도입한 Unity 프로젝트(Unity 2021.3 이상)에서 작업합니다. 동기화 컴포넌트는 SDK에 포함되어 있으므로 추가 패키지는 필요하지 않습니다.

## 1. 네임스페이스를 설정한다 {#namespace}

GS2-Realtime 네임스페이스에서 서버 타입을 지정합니다.

| 설정 내용 | 설정 항목 | 지정하는 값 |
| --- | --- | --- |
| 서버 타입 | `serverType` | `sync` |

룸 준비(GS2-Matchmaking과 `RoomWant`)는 다른 서버 타입과 같습니다.

## 2. 캐릭터의 Prefab을 만든다 {#prefab}

### 스크립트 준비하기

자신의 캐릭터를 움직이는 스크립트입니다. Transform을 움직이기만 하면 NetworkTransform이 다른 참가자에게 위치를 전송합니다. 스페이스 키로 전원에게 RPC를 전송합니다.

```csharp
using Gs2.Realtime.Sync;
using UnityEngine;

// NetworkEntity의 Controlled Behaviours에 등록하여, 자신의 캐릭터에서만 동작시킨다
public sealed class PlayerMover : MonoBehaviour
{
    [SerializeField] float speed = 3f;

    void Update()
    {
        var move = new Vector3(Input.GetAxisRaw("Horizontal"), 0, Input.GetAxisRaw("Vertical"));
        // Transform을 움직이기만 하면 NetworkTransform이 다른 참가자에게 전송한다
        transform.position += move.normalized * (speed * Time.deltaTime);

        if (Input.GetKeyDown(KeyCode.Space))
            GetComponent<NetworkEntity>().Rpc(nameof(PlayerEmote.Wave), RpcTarget.All);
    }
}
```

RPC로 호출되는 메서드에는 `[NetworkRpc]`를 붙입니다. `IEnumerator`를 반환하면 코루틴으로 실행됩니다.

```csharp
using System.Collections;
using Gs2.Realtime.Sync;
using UnityEngine;

public sealed class PlayerEmote : MonoBehaviour
{
    // 전원의 화면에서 그 캐릭터가 순간적으로 커진다
    [NetworkRpc]
    public IEnumerator Wave(NetworkRpcInfo info)
    {
        Debug.Log("Wave from connection " + info.Sender);
        transform.localScale = Vector3.one * 1.5f;
        yield return new WaitForSeconds(0.3f);
        transform.localScale = Vector3.one;
    }
}
```

`info.Sender`는 RPC를 보낸 플레이어의 접속 ID입니다. 서버가 설정하므로 보낸 쪽이 위조할 수 없습니다.

### Prefab 만들기

1. 캐릭터의 외형이 될 Prefab(Capsule 등)을 만들고, 루트에 다음 컴포넌트를 추가합니다.

   | 컴포넌트 | 설정 |
   | --- | --- |
   | **NetworkEntity** | **Schema**는 비워 둔다(위치만 동기화하므로) |
   | **NetworkTransform** | **Position Axes**는 `All`, **Rotation Axes**는 `None` |
   | **PlayerMover** | 위의 스크립트 |
   | **PlayerEmote** | 위의 스크립트 |

2. **NetworkEntity > Controlled Behaviours**에 `PlayerMover`를 등록합니다. `PlayerMover`는 자신이 소유한 캐릭터에서만 활성화됩니다. 등록하지 않으면 자신의 화면에 있는 다른 플레이어의 캐릭터에도 자신의 키 입력이 적용되어 버립니다.

위치와 회전은 SDK에 표준 필드가 있으므로, 동기화 스키마 에셋이 없어도 동기화할 수 있습니다. HP 같은 독자적인 값을 동기화할 때는 [독자적인 값 표시하기](#bindings)를 참조해 주세요.

## 3. 룸에 접속한다 {#connect}

### NetworkWorld 배치하기

1. Project 창의 **Create > GS2 > Realtime > Prefab Catalog**에서 카탈로그를 만듭니다.
2. 카탈로그의 **Prefabs**에 **Id** `1`과 순서 2의 Prefab을 등록합니다. Prefab ID는 오브젝트의 종류를 나타내는 번호이며, 다른 클라이언트는 이 번호로 표시할 Prefab을 선택합니다.
3. 씬에 빈 GameObject를 만들고 **NetworkWorld**를 추가한 후, **Prefab Catalog**에 카탈로그를 지정합니다.
4. 지면이 될 Plane과 위에서 내려다보는 Camera를 배치합니다.

### 접속 스크립트 추가하기

NetworkWorld와 같은 GameObject에 **NetworkSession**과 다음 스크립트를 추가합니다. 룸의 접속 정보(IP 주소·포트·암호화 키)와 액세스 토큰은 [게임 서버 접속 정보 취득](#게임-서버-접속-정보-취득)의 방법으로 취득하여 전달합니다.

```csharp
using Gs2.Realtime.Sync;
using UnityEngine;

[RequireComponent(typeof(NetworkWorld), typeof(NetworkSession))]
public sealed class PlayerRoomConnection : MonoBehaviour
{
    const uint PlayerPrefab = 1; // Prefab Catalog에 등록한 Id

    NetworkWorld world;
    NetworkSession session;

    // 이벤트는 한 번만 등록합니다(접속할 때마다 등록하면 재접속 시 핸들러가 중복됩니다)
    void Awake()
    {
        world = GetComponent<NetworkWorld>();
        session = GetComponent<NetworkSession>();
        // 룸의 상태를 반영하면 자신의 캐릭터를 생성한다.
        // 접속이 끊기면 캐릭터는 삭제되므로, 재접속할 때마다 다시 만든다
        session.Ready += () => world.Instantiate(
            PlayerPrefab,
            new Vector3(Random.Range(-3f, 3f), 0, Random.Range(-3f, 3f)),
            Quaternion.identity);
        session.Disconnected += reason => Debug.Log("접속이 종료되었습니다: " + reason);
    }

    public void Connect(string accessToken, string ipAddress, int port, string encryptionKey)
    {
        session.Connect(accessToken, ipAddress, port, encryptionKey);
    }
}
```

핵심은 다음 3가지입니다.

- **자신의 캐릭터는 `world.Instantiate`로 만듭니다.** Unity의 `Instantiate`로 만든 GameObject는 다른 참가자에게 공유되지 않습니다. `world.Instantiate`의 반환값은 그 자리에서 자신의 소유(`IsMine`)로 움직일 수 있습니다. 룸이 생성을 확정할 때까지의 이동이나 RPC는 보류되며, 확정 후에 전송됩니다.
- **다른 플레이어의 캐릭터는 NetworkWorld가 만듭니다.** 룸에서 도착한 생성을 보고 카탈로그의 Prefab으로부터 GameObject를 만들고, 수신한 위치로 보간하여 움직입니다.
- **접속하면 그 시점의 상태가 도착합니다.** 나중에 참가한 플레이어에게도 이미 참가한 플레이어의 캐릭터가 그 시점의 위치에 표시됩니다. 반영이 끝나면 `Ready`가 호출됩니다.

## 4. 동작을 확인한다 {#verify}

같은 룸에 2개의 클라이언트에서 접속합니다. 같은 userId로 접속하면 먼저 접속한 쪽이 대체되므로, 서로 다른 userId의 액세스 토큰을 사용해 주세요.

1. 양쪽 화면에 캐릭터 2체가 표시되는 것을 확인합니다.
2. 한쪽에서 캐릭터를 움직여, 다른 쪽 화면에서도 같은 캐릭터가 부드럽게 움직이는 것을 확인합니다. 다른 쪽의 키 입력으로는 자신의 캐릭터만 움직입니다.
3. 한쪽에서 스페이스 키를 눌러, 양쪽 화면에서 그 캐릭터가 순간적으로 커지는 것을 확인합니다.
4. 한쪽의 접속을 끊고, 다른 쪽 화면에서 그 캐릭터가 사라지는 것을 확인합니다. 다시 접속하면 새로운 캐릭터가 생성됩니다.

## 5. 공개에 대비해 제한한다 {#catalog}

지금까지의 설정에서는 룸이 플레이어가 생성하는 Prefab이나 호출하는 RPC를 검사하지 않습니다. 개조한 클라이언트로부터의 생성이나 RPC를 제한하려면 네임스페이스 설정의 `syncSetting.objectCatalog`에 [오브젝트 카탈로그](#catalog)를 설정합니다.

1. Prefab Catalog 에셋을 오른쪽 클릭하고 **Copy Object Catalog JSON**을 선택합니다. 등록한 Prefab의 동기화할 필드와 RPC의 ID가 JSON으로 클립보드에 복사됩니다.
2. 그 JSON을 `syncSetting.objectCatalog`에 설정합니다.

설정하면 등록한 Prefab만 생성할 수 있고, `methods`에 등록한 RPC만 소유자가 호출할 수 있게 됩니다. RPC의 ID는 메서드 이름으로 정해지므로, 메서드 이름을 변경했다면 JSON을 다시 만들어 주세요.

## 다음에 읽을 내용

- [Unity 클라이언트](): 조작 권한 대여, 룸 이벤트, 커스텀 프로퍼티, 마스터 클라이언트, Interest Group, UDP, 재접속 처리.
- [Photon에서의 이전 가이드](): PUN2 API와의 대응표와 이전 절차.
- [튜토리얼: 서버에서 HP를 관리하는 적 만들기](): 룰로 오브젝트를 생성·갱신하는 서버 타입 `stateMachine`의 사용 방법.
- [튜토리얼: 롤백 방식 동기화로 캐릭터 움직이기](): 위치를 서버가 계산하고, 자신의 캐릭터를 예측하여 움직이는 방법.



