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


# 실시간 편: 두 플레이어 동기화하기

내가 움직인 캐릭터를 상대 화면에도 표시해 봅니다.



두 게임 화면을 같은 룸에 연결합니다. WASD 키로 캐릭터를 움직이고 스페이스 키를 누르면 양쪽 화면에서 같은 캐릭터가 잠깐 커집니다.

기본편에서 준비한 GS2 학습용 프로젝트와 Unity 프로젝트를 사용합니다. 이 페이지에서는 새 `RealtimePractice` 씬을 만들고 SDK로 연결합니다.

## 1. Realtime 네임스페이스 만들기

관리 콘솔에서 학습용 프로젝트의 Tokyo 리전을 엽니다.

1. **Realtime → Namespace list → Create namespace** 를 엽니다.
2. 다음 값을 입력하고 생성합니다.

| 항목 | 값 |
| --- | --- |
| 이름 | `adventure-sync` |
| 서버 종류 | 오브젝트 동기화(`sync`) |
| 서버 사양 | `realtime1.nano` |

## 2. 로그인할 씬 만들기

1. Unity의 **File → New Scene** 에서 빈 씬을 만들고 `RealtimePractice`로 저장합니다.
2. <a href="/samples/adventure/ko/AdventureLogin.cs" download>AdventureLogin.cs</a>를 `Assets`에 저장합니다. 같은 파일이 이미 있다면 기존 파일을 사용합니다.
3. 빈 `Login` 오브젝트를 만들고 `AdventureLogin`을 추가합니다.
4. 관리 콘솔의 **Deploy → Default → Output list** 에서 `ApplicationClientId`와 `ApplicationClientSecret`을 가져와 **Client Id / Client Secret** 에 입력합니다.
5. **Account Namespace** 는 Studio에서 계정을 만들었다면 `Account`, SDK 코스라면 `adventure-account`로 설정합니다. **Account Slot** 은 `realtime-a`로 설정합니다.
6. **Edit → Project Settings → Player → Other Settings → Active Input Handling** 을 **Both** 로 설정합니다. 재시작을 요청하면 Unity를 재시작합니다.
7. 같은 Player 설정의 **Resolution and Presentation → Run In Background** 를 켭니다. 다른 창을 조작하는 동안에도 빌드한 앱이 동기화를 계속하도록 하는 설정입니다.

## 3. 동기화할 캐릭터 만들기

다음 두 파일을 `Assets`에 저장합니다.

- <a href="/samples/adventure/ko/realtime/AdventureRoomPlayer.cs" download>AdventureRoomPlayer.cs</a>: 이동과 스페이스 키 입력.
- <a href="/samples/adventure/ko/realtime/AdventureRoomEmote.cs" download>AdventureRoomEmote.cs</a>: 모든 화면에서 실행할 동작.

이동 코드는 Transform을 움직입니다. 위치를 다른 화면으로 보내는 역할은 다음 단계에서 추가할 `NetworkTransform`이 담당합니다.

```csharp
var move = new Vector3(Input.GetAxisRaw("Horizontal"), 0, Input.GetAxisRaw("Vertical"));
transform.position += move.normalized * (3f * Time.deltaTime);
if (Input.GetKeyDown(KeyCode.Space))
    GetComponent<NetworkEntity>().Rpc(nameof(AdventureRoomEmote.Wave), RpcTarget.All);
```

RPC는 지정한 처리를 다른 참가자의 화면에서도 실행하는 기능입니다. 여기서는 `[NetworkRpc]`가 붙은 `Wave`가 캐릭터를 0.3초 동안 크게 만듭니다.

```csharp
[NetworkRpc]
public IEnumerator Wave(NetworkRpcInfo info)
{
    transform.localScale = Vector3.one * 1.5f;
    yield return new WaitForSeconds(0.3f);
    transform.localScale = Vector3.one;
}
```

캐릭터마다 조작 권한을 가진 플레이어를 정합니다. 내 입력으로 상대 캐릭터까지 움직이지 않도록 이동 스크립트를 **Controlled Behaviours** 에 등록합니다. 여기에 등록한 스크립트는 조작 권한을 가진 화면에서만 동작합니다.

1. **GameObject → 3D Object → Capsule** 을 만들고 이름을 `NetworkPlayer`로 지정합니다.
2. 다음 컴포넌트를 추가합니다.

| 컴포넌트 | 설정 |
| --- | --- |
| Network Entity | Schema는 비워 둠. Controlled Behaviours에 이 오브젝트의 AdventureRoomPlayer 하나 추가 |
| Network Transform | Position Axes는 All, Rotation Axes는 None |
| Adventure Room Player | 기본 설정 사용 |
| Adventure Room Emote | 기본 설정 사용 |

3. `NetworkPlayer`를 Project 창으로 드래그해 Prefab으로 만듭니다. Hierarchy에 남은 Capsule은 삭제합니다.

## 4. 연결용 오브젝트 만들기

1. Project 창에서 **Create → GS2 → Realtime → Prefab Catalog** 를 선택해 `PracticeCatalog`를 만듭니다.
2. **Prefabs** 에 항목을 하나 추가하고 **Id** 를 `1`, **Prefab** 을 `NetworkPlayer`로 설정합니다.
3. <a href="/samples/adventure/ko/realtime/AdventureRoom.cs" download>AdventureRoom.cs</a>를 `Assets`에 저장합니다.
4. 빈 `Room` 오브젝트를 만들고 `AdventureRoom`을 추가합니다. `NetworkWorld`와 `NetworkSession`도 추가됩니다.
5. **Network World → Prefab Catalog** 에 `PracticeCatalog`, **Adventure Room → Login** 에 `Login` 오브젝트의 `AdventureLogin`을 할당합니다.
6. **GameObject → 3D Object → Plane** 으로 바닥을 만듭니다.
7. **GameObject → Camera** 로 카메라를 만들고 이름은 `Main Camera`, Tag는 `MainCamera`로 설정합니다. 위치는 `(0, 12, -10)`, 회전은 `(50, 0, 0)`으로 설정합니다.
8. **GameObject → Light → Directional Light** 로 조명을 만들고 회전을 `(50, -30, 0)`으로 설정합니다.

연결 코드는 룸 이름으로 IP 주소, 포트, 암호화 키를 가져와 로그인한 플레이어의 액세스 토큰과 함께 전달합니다.

```csharp
session.Connect(login.Session.AccessToken.Token,
    get.IpAddress, get.Port, get.EncryptionKey);
await UniTask.WaitUntil(
    () => session.State == NetworkSessionState.Ready || session.State == NetworkSessionState.Disconnected,
    cancellationToken: this.GetCancellationTokenOnDestroy());
```

연결이 완료되면 `Ready`가 호출되어 Prefab ID `1`의 캐릭터를 생성합니다.

```csharp
world.Instantiate(1,
    new Vector3(Random.Range(-3f, 3f), 1, Random.Range(-3f, 3f)),
    Quaternion.identity);
```

## 5. 두 화면에서 움직이기

1. 씬을 저장하고 **File → Build Profiles** 에서 데스크톱 빌드를 만듭니다. 씬 목록에 `RealtimePractice`를 넣고 다른 씬의 체크를 해제해 이번에는 `RealtimePractice`만 활성화합니다.
2. 빌드한 앱을 실행하고 로그인 화면의 저장 슬롯을 `realtime-a`로 지정해 **로그인** 을 누릅니다. Unity Editor에서도 재생하고 저장 슬롯을 `realtime-b`로 지정해 로그인합니다.
3. 관리 콘솔에서 `adventure-sync`의 **Room list → Create room** 을 열고 이름을 `practice`로 지정해 생성합니다. 알림 대상 사용자 ID 목록은 비워 둡니다.
4. 목록을 갱신합니다. `practice`에 IP 주소와 포트가 표시되면 앱과 Unity Editor 양쪽에서 룸 이름 `practice`의 **이 룸에 연결** 을 누릅니다.
5. 양쪽에 캐릭터 두 명이 표시되면 한쪽 게임 화면을 클릭하고 WASD 키를 누릅니다. 양쪽 화면에서 같은 캐릭터가 움직입니다.
6. 스페이스 키를 눌러 양쪽 화면에서 그 캐릭터가 잠깐 커지는지 확인합니다.

## 6. 조작하는 플레이어 바꾸기

1. B 화면의 **내 연결 ID** 를 A의 **조작 권한을 받을 상대 연결 ID** 에 입력합니다.
2. A가 소유한 오브젝트의 **이 오브젝트의 조작 권한 넘기기** 를 누릅니다. 오브젝트 번호를 기록합니다.
3. A에서 WASD를 눌러도 그 캐릭터는 움직이지 않습니다. B에서는 원래 캐릭터와 넘겨받은 캐릭터가 모두 움직입니다.
4. B의 입력란에 A의 연결 ID를 넣고 기록한 오브젝트 번호의 조작 권한을 A에게 돌려줍니다. 다시 각자 한 명씩 조작할 수 있게 됩니다.

`entity.TransferOwnership(otherConnectionId)`로 조작 권한을 넘깁니다. 소유자가 바뀌면 `Controlled Behaviours`의 이동 스크립트도 새 소유자의 화면에서 활성화됩니다.

## 7. 연결을 끊고 다시 연결하기

A에서 **연결 끊기** 를 누르면 B 화면에서 A의 캐릭터가 사라집니다. A를 같은 룸에 다시 연결하면 새 캐릭터가 나타나고 B의 캐릭터는 현재 위치에 표시됩니다.

확인이 끝나면 양쪽 화면에서 연결을 끊습니다. 관리 콘솔의 `adventure-sync` 룸 목록에서 실습용 `practice`를 삭제합니다.

서버에서 적 HP와 승패를 관리하는 구현은 <a href='' target="_blank" rel="noopener">GSL로 적을 움직이는 튜토리얼(새 탭)</a>에서 규칙 정의부터 Unity 조작까지 확인할 수 있습니다.

[튜토리얼 첫 페이지로]()




