Godot 클라이언트
sync와 stateMachine 룸에서는 룸에 있는 오브젝트를 Godot의 노드로 표시할 수 있습니다. 이 페이지에서는 그 Godot 측의 설정과 API를 설명합니다. 대상은 Godot 4의 GDScript이며, 2D와 3D 모두에 대응합니다.
동기화의 구조(누가 무엇을 변경할 수 있는지, 전송 순서, 재접속 등)는 엔진에 공통이므로 오브젝트 동기화의 구조에 정리되어 있습니다. 이 페이지에서는 Godot에서의 작성 방법을 설명합니다. 룸의 룰도 GDScript로 작성할 수 있습니다. 작성 방법은 GDScript로 룰 작성하기를 참조해 주세요.
Unity 클라이언트와 Godot 클라이언트가 같은 룸에 참가하는 것은 상정하지 않습니다. 하나의 게임에서는 어느 한쪽만 사용해 주세요.
전체 구조
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로도 보고합니다.
룸에 접속하기
- 씬에
Gs2NetworkWorld와Gs2NetworkSession을 배치합니다. - World의
catalog에 Prefab 카탈로그를, Session의world에 World를 설정합니다. - GS2-Realtime에서 받은 룸의 주소(액세스 토큰, IP 주소, 포트, 암호화 키)로
connect_room을 호출합니다.
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), 경과 시간은 실제 시간으로 측정합니다.
전송은 접속 후라면 언제 호출해도 됩니다(전송 순서).
Session을 사용하지 않고 독자적인 통신 경로를 연결하는 경우에는 world.attach(connection_id, send)로 연결하고, 받은 데이터를 world.receive_at(payload, server_milliseconds)에 전달하며, 접속이 끊기면 world.detach()를 호출합니다.
씬 등록하기
- 표시용 씬을 만들고, 루트(
CharacterBody3D,Node2D등)의 자식으로Gs2NetworkEntity를 추가합니다. 위치·회전도 동기화한다면Gs2NetworkTransform3D(2D라면Gs2NetworkTransform2D)도 자식으로 추가합니다. - 새 리소스로
Gs2NetworkPrefabCatalog를 만들고,prefabs에Gs2NetworkPrefabEntry(Prefab ID와 PackedScene)를 추가합니다. - World의
catalog에 그 카탈로그를 지정합니다.
인스펙터에는 ID 중복, 씬이나 Entity의 누락, 다른 Prefab용 스키마가 경고로 표시됩니다. 서버로부터 도착할 가능성이 있는 Prefab ID는 모두 등록해 주세요.
플레이어가 자신의 오브젝트를 만들 때는 PackedScene.instantiate()가 아니라 world.instantiate를 사용합니다.
위치·회전 동기화하기
위치·회전은 SDK에 표준 필드가 있어, 동기화 스키마가 없어도 동기화할 수 있습니다. 서버 측에서 동기화를 허가한 축(위치·회전)만 선택합니다.
| 노드 | 동기화하는 값 |
|---|---|
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() | 수신한 자세에 즉시 맞춘다 |
독자적인 값 표시하기
HP나 개폐 상태 등의 독자적인 값은 룰에서 동기화 스키마로 선언하고, 생성된 .realtimeschema를 사용합니다(독자적인 값).
- 생성된
.realtimeschema를 프로젝트에 배치합니다.Gs2NetworkSchema리소스로 가져와집니다. - 씬의
Gs2NetworkEntity.schema에 그 리소스를 지정합니다. - 값을 표시에 반영하려면
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) 등).
서버에 조작 전송하기
룰이 관리하는 오브젝트에는 씬에 Gs2SharedObject를 추가하여 조작을 전송합니다. 요청의 상태와 결과 코드의 의미는 서버에 조작 전송하기를 참조해 주세요.
| 메서드 | 내용 |
|---|---|
interact(method, args) | 룰의 on_interact(C#에서는 Interact)를 호출한다 |
acquire() / renew() / release() | 리스를 취득·연장·해제한다 |
모두 Gs2CommandRequest를 반환합니다. await request.wait()로 완료를 기다리거나 request.on_completed(callback)로 결과를 받습니다.
var request := door.interact(DoorAction.OPEN) # DoorAction은 룰에서 생성된 상수
await request.wait()
if request.status != Gs2SyncTypes.RequestStatus.SUCCEEDED:
print("Refused: ", Gs2SyncTypes.result_name(request.result))조작 권한을 사용하여 값 전송하기
조작 권한의 규칙은 조작 권한을 참조해 주세요.
| 멤버 | 내용 |
|---|---|
request_control() / release_control() | 조작 권한 취득을 요청한다 / 반환한다 |
acquire_control() | 취득을 요청하고, 조작할 수 있게 될 때까지 기다릴 수 있는 Gs2ControlRequest를 반환한다 |
acquire_on_enable | 활성화되었을 때 자동으로 취득을 요청한다 |
auto_renew | 조작 권한의 기한이 만료되기 전에 자동으로 갱신한다(기본값으로 활성) |
controlled_nodes | 조작할 수 있는 동안에만 움직일 노드(이동이나 입력 처리) |
control_changed(controlled) | 조작 가능 상태가 바뀌었을 때의 시그널 |
Gs2SharedObject의 acquire() / renew() / release()는 조작 권한을 수동으로 관리하는 API이며, controlled_nodes의 전환이나 자동 갱신은 하지 않습니다.
플레이어 오브젝트 만들기
| 메서드 | 내용 |
|---|---|
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 / 오일러 각(라디안)으로 전달할 수 있습니다. 소유자가 퇴장했을 때의 처리와 소유 기간의 규칙은 플레이어 오브젝트를 참조해 주세요.
RPC
RPC는 메서드 이름으로부터 자동으로 등록됩니다. 대상은 Entity의 부모 아래에 있는 노드(중첩된 다른 Entity 아래는 제외)의 메서드입니다.
# 소유자만 호출할 수 있는 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:
passentity.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를 참조해 주세요. - 인수에는 형식을 붙입니다. 사용할 수 있는 형식은
bool·int(enum 포함)·float·String·PackedByteArray·Vector2/3/4·Vector2i/3i·Quaternion·Color입니다. 마지막Gs2NetworkRpcInfo인수에는 보낸 사람 등이 들어갑니다. Node.rpc()와 충돌하지 않도록, 전송 메서드의 이름은send_rpc입니다.
인스펙터에서 Entity를 선택하면 등록된 RPC와 ID를 확인할 수 있습니다.
룸 이벤트·커스텀 프로퍼티·마스터 클라이언트·Interest Group
의미와 상한은 룸 이벤트·커스텀 프로퍼티·마스터 클라이언트·Interest Group을 참조해 주세요.
| 기능 | 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)한 참가자에게만 갱신이 도착한다 |
틱으로 움직이는 오브젝트 조작하기
틱을 사용하는 룸에서는 world.set_input(slot, value)로 입력을 전송합니다(틱과 예측). 슬롯에는 룰에서 생성된 상수(TickInput.MOVE_X 등)를 사용합니다.
룰에 예측 함수가 있는 경우, 컴파일러가 <룰 이름>Prediction을 생성합니다. 게임 시작 시 <룰 이름>Prediction.register(world)를 한 번 호출하면, 자신의 오브젝트를 룸과 같은 계산으로 먼저 움직입니다. 예측이 빗나갔을 경우에는 Transform이 차이를 약 correction_time(기본 0.1초)에 걸쳐 좁힙니다. 지연 보상을 위해 World의 view_delay는 Transform의 interpolation_delay와 같은 값(기본 0.1초)으로 하세요.
UDP로 상태 전송하기
룸이 UDP를 제공하고 Web 이외의 환경에서 동작할 때는 Session이 UDP를 사용합니다(UDP). 사용하지 않는 경우에는 Session의 use_udp를 false로 합니다.
Web 버전에서는 UDP를 사용할 수 없습니다. 또한 https로 배포하는 페이지에서는 ws://에 접속할 수 없는 경우가 있습니다.
오브젝트 카탈로그
인스펙터에서 Gs2NetworkPrefabCatalog를 선택하고 Project > Tools > GS2 > Copy Object Catalog JSON을 실행하면, 네임스페이스의 syncSetting.objectCatalog에 붙여 넣을 JSON이 클립보드에 복사됩니다. 룰이 정의하는 Prefab(Gs2SharedObject와, 룰이 정의하는 스키마의 것)은 제외됩니다. JSON의 형식과 항목은 오브젝트 카탈로그를 참조해 주세요.
오브젝트의 수명·룸 종료·전송량
접속이 끊기거나 재접속했을 때의 오브젝트 처리, 룸 종료 조건과의 관계, 전송량 기준은 오브젝트의 수명과 재접속·룸 종료 조건과의 관계·전송량 기준을 참조해 주세요. World의 전송 간격은 send_rate(기본값 20회/초)입니다.
잘 동작하지 않을 때
엔진에 공통인 확인 사항은 오브젝트 동기화의 구조를 참조해 주세요. Godot 고유의 확인 사항은 다음과 같습니다.
| 증상 | 확인할 사항 |
|---|---|
| 접속이나 전송이 실패한다 | 실패한 노드의 last_error와, 출력에 표시된 오류를 확인한다 |
| 독자적인 값을 읽을 수 없다 | 씬의 Gs2NetworkEntity.schema에 생성한 .realtimeschema를 지정했는지 |
| 움직여도 전송되지 않는다 | 자신의 오브젝트인지(is_mine()). 리스 오브젝트라면 조작 권한을 취득했는지 |
| RPC가 호출되지 않는다 | 메서드 이름이 net_rpc_<이름>이고 인수에 형식이 붙어 있는지. 메서드가 있는 노드가 Entity의 부모 아래에 있는지 |
| 조작이 거부된다 | World의 command_rejected 시그널로 거부된 커맨드, 대상 ID, 사유를 확인한다 |
| 생성한 오브젝트가 사라진다 | World의 spawn_failed 시그널의 결과를 확인한다 |
| 생성된 클래스를 찾을 수 없다, 이름이 겹친다 | 룰을 다시 빌드했는지. 생성되는 클래스 이름이 다른 룰이나 스크립트와 겹치지 않는지(GDScript로 룰 작성하기) |
샘플
SDK의 samples/realtime/에 Unity 샘플을 이식한 2개의 샘플이 있습니다. 둘 다 룰을 GDScript로 작성했습니다.
| 샘플 | 내용 |
|---|---|
object_sync(3D) | 플레이어, 리스하여 움직이는 상자, 룰이 여닫는 문, 체력이 줄었다가 부활하는 적 |
air_hockey(2D) | 패들과 퍽, 득점 |