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


# Godot 클라이언트

공유 오브젝트를 Godot에서 표시·조작하기 위한 노드와 설정



`sync`와 `stateMachine` 룸에서는 룸에 있는 오브젝트를 Godot의 노드로 표시할 수 있습니다. 이 페이지에서는 그 Godot 측의 설정과 API를 설명합니다. 대상은 Godot 4의 GDScript이며, 2D와 3D 모두에 대응합니다.

동기화의 구조(누가 무엇을 변경할 수 있는지, 전송 순서, 재접속 등)는 엔진에 공통이므로 [오브젝트 동기화의 구조]()에 정리되어 있습니다. 이 페이지에서는 Godot에서의 작성 방법을 설명합니다. 룸의 룰도 GDScript로 작성할 수 있습니다. 작성 방법은 [GDScript로 룰 작성하기]()를 참조해 주세요.

Unity 클라이언트와 Godot 클라이언트가 같은 룸에 참가하는 것은 상정하지 않습니다. 하나의 게임에서는 어느 한쪽만 사용해 주세요.

## 전체 구조 {#overview}

Godot에서는 다음 노드와 리소스를 사용합니다. 모두 GS2 SDK for Godot의 애드온(`addons/gs2`)에 포함되어 있습니다.

| Godot | Unity | 배치 위치·역할 |
| --- | --- | --- |
| `Gs2NetworkWorld` | `NetworkWorld` | 씬에 1개. 수신한 상태로부터 인스턴스를 생성·갱신·삭제하고, 변경을 모아서 전송한다 |
| `Gs2NetworkSession` | `NetworkSession` | 씬에 1개. 룸에 접속하고 World를 연결한다 |
| `Gs2NetworkEntity` | `NetworkEntity` | 동기화할 씬의 루트의 **자식 노드**. 인스턴스와 공유 오브젝트를 대응시킨다 |
| `Gs2NetworkTransform2D` / `Gs2NetworkTransform3D` | `NetworkTransform` | 위치·회전을 동기화할 씬의 루트의 자식 노드 |
| `Gs2SharedObject` | `SharedObject` | 서버가 갱신하는 오브젝트에 조작을 전송한다 |
| `Gs2NetworkPrefabCatalog`(리소스) | Prefab Catalog | Prefab ID와 표시할 씬(PackedScene)의 대응 |
| `Gs2NetworkSchema`(리소스) | 동기화 스키마 | `.realtimeschema`를 가져온 것 |

Unity에서는 컴포넌트를 GameObject에 추가하지만, Godot에서는 노드를 씬의 루트의 자식으로 추가합니다. `Gs2NetworkEntity`와 Transform 노드는 기본적으로 부모(`target = ".."`)를 대상으로 합니다.

GDScript에는 예외가 없으므로, 실패는 반환값(`false`나 `null`)으로 돌아오고 사유는 각 노드의 `last_error`에 들어갑니다. 사용법의 오류는 `push_error`로도 보고합니다.

## 룸에 접속하기 {#connection}

1. 씬에 `Gs2NetworkWorld`와 `Gs2NetworkSession`을 배치합니다.
2. World의 `catalog`에 Prefab 카탈로그를, Session의 `world`에 World를 설정합니다.
3. GS2-Realtime에서 받은 룸의 주소(액세스 토큰, IP 주소, 포트, 암호화 키)로 `connect_room`을 호출합니다.

```gdscript
extends Node

const PLAYER_PREFAB := 1 # 카탈로그 엔트리의 ID

@onready var world: Gs2NetworkWorld = $World
@onready var session: Gs2NetworkSession = $Session


func _ready() -> void:
    # 한 번만 연결한다. 접속할 때마다 연결하면 재접속 시 중복됨
    session.room_ready.connect(_on_room_ready)
    world.spawned.connect(_on_spawned)
    session.disconnected.connect(func(reason: String) -> void: print("Disconnected: ", reason))


# 접속·재접속할 때마다
func join(access_token: String, ip_address: String, port: int, encryption_key: String) -> void:
    if not session.connect_room(access_token, ip_address, port, encryption_key):
        push_error(session.last_error)


func _on_room_ready() -> void:
    # 스냅샷 반영 후. 생성이나 전송을 할 수 있다
    world.instantiate(PLAYER_PREFAB, Vector3.ZERO)


func _on_spawned(entity: Gs2NetworkEntity) -> void:
    if entity.is_mine():
        print("My player is in the room: ", entity.get_parent().name)
```

| 멤버 | 내용 |
| --- | --- |
| `connect_room(access_token, ip_address, port, encryption_key)` | 룸에 접속한다. 다시 호출하면 다시 접속한다 |
| `disconnect_room()` | 접속을 종료한다. 완료되면 `disconnected`가 발생한다 |
| `state` | `DISCONNECTED`·`CONNECTING`·`SYNCHRONIZING`(스냅샷 반영 중)·`READY` |
| `attached(client)` | World를 연결한 직후의 시그널 |
| `room_ready` | 스냅샷을 반영하여 생성이나 전송을 할 수 있게 되었을 때의 시그널 |
| `disconnected(reason)` | 접속이 종료되었을 때의 시그널. 상대 측의 종료, 접속 실패, 전송이나 해석 실패도 포함한다 |

Session은 노드가 비활성화되었을 때(`process_mode = DISABLED`)나 트리에서 제거되었을 때에도 접속을 종료합니다. Session과 World는 일시 정지의 영향을 받지 않으며(`PROCESS_MODE_ALWAYS`), 경과 시간은 실제 시간으로 측정합니다.

전송은 접속 후라면 언제 호출해도 됩니다([전송 순서](#ordering)).

Session을 사용하지 않고 독자적인 통신 경로를 연결하는 경우에는 `world.attach(connection_id, send)`로 연결하고, 받은 데이터를 `world.receive_at(payload, server_milliseconds)`에 전달하며, 접속이 끊기면 `world.detach()`를 호출합니다.

## 씬 등록하기 {#prefabs}

1. 표시용 씬을 만들고, 루트(`CharacterBody3D`, `Node2D` 등)의 자식으로 `Gs2NetworkEntity`를 추가합니다. 위치·회전도 동기화한다면 `Gs2NetworkTransform3D`(2D라면 `Gs2NetworkTransform2D`)도 자식으로 추가합니다.
2. 새 리소스로 `Gs2NetworkPrefabCatalog`를 만들고, `prefabs`에 `Gs2NetworkPrefabEntry`(Prefab ID와 PackedScene)를 추가합니다.
3. World의 `catalog`에 그 카탈로그를 지정합니다.

인스펙터에는 ID 중복, 씬이나 Entity의 누락, 다른 Prefab용 스키마가 경고로 표시됩니다. 서버로부터 도착할 가능성이 있는 Prefab ID는 모두 등록해 주세요.

플레이어가 자신의 오브젝트를 만들 때는 `PackedScene.instantiate()`가 아니라 [`world.instantiate`](#player-objects)를 사용합니다.

## 위치·회전 동기화하기 {#transform}

위치·회전은 SDK에 표준 필드가 있어, 동기화 스키마가 없어도 동기화할 수 있습니다. 서버 측에서 동기화를 허가한 축([위치·회전](#transform))만 선택합니다.

| 노드 | 동기화하는 값 |
| --- | --- |
| `Gs2NetworkTransform2D` | 위치 X / Y와 회전. 회전은 도로 변환하고 0~360으로 정규화하여 전송한다. `position_axes`와 `sync_rotation`으로 선택한다 |
| `Gs2NetworkTransform3D` | 위치 3축과 회전 3축. `position_axes`와 `rotation_axes`로 선택한다. 오일러 각의 순서는 YXZ로 고정하며, 3축 모두 동기화하는 경우에는 Quaternion으로 보간한다 |

2D에서는 Y축을 반전하지 않으므로, 룰에는 픽셀 좌표가 그대로 도착합니다. 표준 단위 간격(0.01)은 픽셀에는 너무 세밀하므로, 룰 측에서 거칠게 하는 것을 권장합니다(GDScript라면 `## @room_schema(..., standard=POSITION, position_precision=1.0)`).

| 항목 | 내용 |
| --- | --- |
| `target` | 움직일 노드. 기본값은 부모 |
| `interpolation_delay` | 몇 초 전의 자세를 표시할지(기본값 0.1). 전후로 수신한 갱신 사이를 보간한다 |
| `teleport_distance` | 이 거리보다 떨어진 위치로는 보간하지 않고 이동한다. 0이면 항상 보간한다 |
| `snap_to_network()` | 수신한 자세에 즉시 맞춘다 |

## 독자적인 값 표시하기 {#bindings}

HP나 개폐 상태 등의 독자적인 값은 룰에서 동기화 스키마로 선언하고, 생성된 `.realtimeschema`를 사용합니다([독자적인 값](#fields)).

1. 생성된 `.realtimeschema`를 프로젝트에 배치합니다. `Gs2NetworkSchema` 리소스로 가져와집니다.
2. 씬의 `Gs2NetworkEntity.schema`에 그 리소스를 지정합니다.
3. 값을 표시에 반영하려면 `property_bindings`(필드 이름, 대상 노드, 프로퍼티 또는 메서드)를 설정하거나 `field_changed(field_name, value)` 시그널을 사용합니다.

| 메서드 | 내용 |
| --- | --- |
| `get_int` / `get_bool` / `get_float(name)` | 값을 읽는다. 쓸 수 있는 경우에는 자신이 마지막으로 쓴 값(전송 전이라도) |
| `get_confirmed_int` / `get_confirmed_bool` / `get_confirmed_float(name)` | 룸에서 수신한 값을 읽는다 |
| `set_int` / `set_bool` / `set_float(name, value)` | 값을 전송한다. 해당 오브젝트의 쓰기 권한이 필요 |

매번 전송 직전에 값을 쓰려면 `_network_write(entity)`를, 수신했을 때 읽으려면 `_network_read(entity, changed)`를 씬 안의 노드에 구현합니다(Unity의 `INetworkObservable`에 해당합니다).

GDScript로 룰을 작성한 경우에는 컴파일러가 형식 지정 접근자도 생성합니다(`EnemyStateSchema.health(o)` 등).

## 서버에 조작 전송하기 {#interaction}

룰이 관리하는 오브젝트에는 씬에 `Gs2SharedObject`를 추가하여 조작을 전송합니다. 요청의 상태와 결과 코드의 의미는 [서버에 조작 전송하기](#interaction)를 참조해 주세요.

| 메서드 | 내용 |
| --- | --- |
| `interact(method, args)` | 룰의 `on_interact`(C#에서는 `Interact`)를 호출한다 |
| `acquire()` / `renew()` / `release()` | 리스를 취득·연장·해제한다 |

모두 `Gs2CommandRequest`를 반환합니다. `await request.wait()`로 완료를 기다리거나 `request.on_completed(callback)`로 결과를 받습니다.

```gdscript
var request := door.interact(DoorAction.OPEN) # DoorAction은 룰에서 생성된 상수
await request.wait()
if request.status != Gs2SyncTypes.RequestStatus.SUCCEEDED:
    print("Refused: ", Gs2SyncTypes.result_name(request.result))
```

## 조작 권한을 사용하여 값 전송하기 {#authority}

조작 권한의 규칙은 [조작 권한](#authority)을 참조해 주세요.

| 멤버 | 내용 |
| --- | --- |
| `request_control()` / `release_control()` | 조작 권한 취득을 요청한다 / 반환한다 |
| `acquire_control()` | 취득을 요청하고, 조작할 수 있게 될 때까지 기다릴 수 있는 `Gs2ControlRequest`를 반환한다 |
| `acquire_on_enable` | 활성화되었을 때 자동으로 취득을 요청한다 |
| `auto_renew` | 조작 권한의 기한이 만료되기 전에 자동으로 갱신한다(기본값으로 활성) |
| `controlled_nodes` | 조작할 수 있는 동안에만 움직일 노드(이동이나 입력 처리) |
| `control_changed(controlled)` | 조작 가능 상태가 바뀌었을 때의 시그널 |

`Gs2SharedObject`의 `acquire()` / `renew()` / `release()`는 조작 권한을 수동으로 관리하는 API이며, `controlled_nodes`의 전환이나 자동 갱신은 하지 않습니다.

## 플레이어 오브젝트 만들기 {#player-objects}

| 메서드 | 내용 |
| --- | --- |
| `world.instantiate(prefab_id, position, rotation)` | 인스턴스를 즉시 반환한다. 바로 움직이거나 RPC를 보낼 수 있다. 룸이 생성을 확정하면 `is_spawned()`가 true가 된다. 거부되면 `spawn_failed`가 발생하고 인스턴스는 삭제된다 |
| `world.spawn(prefab_id, position, rotation)` | 룸의 확정을 기다린 후 인스턴스를 만든다. `Gs2SpawnRequest`를 반환한다 |
| `entity.is_mine()` | 자신이 소유자이거나 리스 보유자라면 true |
| `entity.transfer_ownership(connection_id)` | 소유권을 다른 참가자에게 넘긴다 |
| `entity.destroy()` | 자신의 플레이어 오브젝트를 삭제한다 |

위치와 회전은 2D에서는 `Vector2`와 라디안, 3D에서는 `Vector3`와 `Quaternion` / `Basis` / 오일러 각(라디안)으로 전달할 수 있습니다. 소유자가 퇴장했을 때의 처리와 소유 기간의 규칙은 [플레이어 오브젝트](#player-objects)를 참조해 주세요.

## RPC {#rpc}

RPC는 메서드 이름으로부터 자동으로 등록됩니다. 대상은 Entity의 부모 아래에 있는 노드(중첩된 다른 Entity 아래는 제외)의 메서드입니다.

```gdscript
# 소유자만 호출할 수 있는 RPC
func net_rpc_fire(direction: Vector3, damage: int, info: Gs2NetworkRpcInfo) -> void:
    print("fired by ", info.sender)

# 어느 참가자든 호출할 수 있는 RPC(오브젝트 카탈로그의 publicMethods)
func net_rpc_public_cheer(info: Gs2NetworkRpcInfo) -> void:
    pass
```

```gdscript
entity.send_rpc("fire", Gs2SyncTypes.RpcTarget.OTHERS, Vector3.FORWARD, 10)
entity.rpc_to("fire", connection_id, Vector3.FORWARD, 10)
```

- RPC의 ID는 접두사를 제외한 이름(`fire`)의 FNV-1a 해시입니다. 메서드 이름을 바꾸면 ID도 바뀌므로, 오브젝트 카탈로그와 클라이언트를 맞추어 갱신해 주세요.
- 전송 대상은 `ALL`·`OTHERS`·`OWNER`·`ALL_BUFFERED`·`OTHERS_BUFFERED`입니다. 의미는 [RPC](#rpc)를 참조해 주세요.
- 인수에는 형식을 붙입니다. 사용할 수 있는 형식은 `bool`·`int`(enum 포함)·`float`·`String`·`PackedByteArray`·`Vector2` / `3` / `4`·`Vector2i` / `3i`·`Quaternion`·`Color`입니다. 마지막 `Gs2NetworkRpcInfo` 인수에는 보낸 사람 등이 들어갑니다.
- `Node.rpc()`와 충돌하지 않도록, 전송 메서드의 이름은 `send_rpc`입니다.

인스펙터에서 Entity를 선택하면 등록된 RPC와 ID를 확인할 수 있습니다.

## 룸 이벤트·커스텀 프로퍼티·마스터 클라이언트·Interest Group {#room}

의미와 상한은 [룸 이벤트](#events)·[커스텀 프로퍼티](#properties)·[마스터 클라이언트](#master)·[Interest Group](#interest-groups)을 참조해 주세요.

| 기능 | Godot에서의 작성 방법 |
| --- | --- |
| 룸 이벤트 | `net_event_<name>` 메서드를 가진 노드를 `world.add_event_target(node)`로 등록하고, `world.raise_event("round_start", Gs2SyncTypes.RpcTarget.OTHERS, 3)`으로 전송한다 |
| 커스텀 프로퍼티 | `world.set_room_property(key, value)` / `set_player_property(key, value)`로 쓰고, `room_property(key, fallback)`으로 읽는다 |
| 마스터 클라이언트 | `world.master_client_id()`로 읽고, `master_client_changed`로 변화를 받는다. `set_master_client(id)`로 다른 참가자를 지명한다 |
| Interest Group | 소유자가 `entity.set_group(group)`으로 옮기고, `world.subscribe_group(group)`한 참가자에게만 갱신이 도착한다 |

## 틱으로 움직이는 오브젝트 조작하기 {#tick}

틱을 사용하는 룸에서는 `world.set_input(slot, value)`로 입력을 전송합니다([틱과 예측](#tick)). 슬롯에는 룰에서 생성된 상수(`TickInput.MOVE_X` 등)를 사용합니다.

룰에 예측 함수가 있는 경우, 컴파일러가 `<룰 이름>Prediction`을 생성합니다. 게임 시작 시 `<룰 이름>Prediction.register(world)`를 한 번 호출하면, 자신의 오브젝트를 룸과 같은 계산으로 먼저 움직입니다. 예측이 빗나갔을 경우에는 Transform이 차이를 약 `correction_time`(기본 0.1초)에 걸쳐 좁힙니다. 지연 보상을 위해 World의 `view_delay`는 Transform의 `interpolation_delay`와 같은 값(기본 0.1초)으로 하세요.

## UDP로 상태 전송하기 {#udp}

룸이 UDP를 제공하고 Web 이외의 환경에서 동작할 때는 Session이 UDP를 사용합니다([UDP](#udp)). 사용하지 않는 경우에는 Session의 `use_udp`를 false로 합니다.

Web 버전에서는 UDP를 사용할 수 없습니다. 또한 https로 배포하는 페이지에서는 `ws://`에 접속할 수 없는 경우가 있습니다.

## 오브젝트 카탈로그 {#catalog}

인스펙터에서 `Gs2NetworkPrefabCatalog`를 선택하고 **Project > Tools > GS2 > Copy Object Catalog JSON**을 실행하면, 네임스페이스의 `syncSetting.objectCatalog`에 붙여 넣을 JSON이 클립보드에 복사됩니다. 룰이 정의하는 Prefab(`Gs2SharedObject`와, 룰이 정의하는 스키마의 것)은 제외됩니다. JSON의 형식과 항목은 [오브젝트 카탈로그](#catalog)를 참조해 주세요.

## 오브젝트의 수명·룸 종료·전송량 {#lifetime}

접속이 끊기거나 재접속했을 때의 오브젝트 처리, 룸 종료 조건과의 관계, 전송량 기준은 [오브젝트의 수명과 재접속](#lifetime)·[룸 종료 조건과의 관계](#idle)·[전송량 기준](#size)을 참조해 주세요. World의 전송 간격은 `send_rate`(기본값 20회/초)입니다.

## 잘 동작하지 않을 때 {#troubleshooting}

엔진에 공통인 확인 사항은 [오브젝트 동기화의 구조](#troubleshooting)를 참조해 주세요. Godot 고유의 확인 사항은 다음과 같습니다.

| 증상 | 확인할 사항 |
| --- | --- |
| 접속이나 전송이 실패한다 | 실패한 노드의 `last_error`와, 출력에 표시된 오류를 확인한다 |
| 독자적인 값을 읽을 수 없다 | 씬의 `Gs2NetworkEntity.schema`에 생성한 `.realtimeschema`를 지정했는지 |
| 움직여도 전송되지 않는다 | 자신의 오브젝트인지(`is_mine()`). 리스 오브젝트라면 조작 권한을 취득했는지 |
| RPC가 호출되지 않는다 | 메서드 이름이 `net_rpc_<이름>`이고 인수에 형식이 붙어 있는지. 메서드가 있는 노드가 Entity의 부모 아래에 있는지 |
| 조작이 거부된다 | World의 `command_rejected` 시그널로 거부된 커맨드, 대상 ID, 사유를 확인한다 |
| 생성한 오브젝트가 사라진다 | World의 `spawn_failed` 시그널의 결과를 확인한다 |
| 생성된 클래스를 찾을 수 없다, 이름이 겹친다 | 룰을 다시 빌드했는지. 생성되는 클래스 이름이 다른 룰이나 스크립트와 겹치지 않는지([GDScript로 룰 작성하기](#deployment)) |

## 샘플 {#samples}

SDK의 `samples/realtime/`에 Unity 샘플을 이식한 2개의 샘플이 있습니다. 둘 다 룰을 GDScript로 작성했습니다.

| 샘플 | 내용 |
| --- | --- |
| `object_sync`(3D) | 플레이어, 리스하여 움직이는 상자, 룰이 여닫는 문, 체력이 줄었다가 부활하는 적 |
| `air_hockey`(2D) | 패들과 퍽, 득점 |



