Godot クライアント

共有オブジェクトを Godot で表示・操作するためのノードと設定

sync と stateMachine のルームでは、ルームにあるオブジェクトを Godot のノードとして表示できます。このページは、その Godot 側の設定と API を説明します。対象は Godot 4 の GDScript で、2D と 3D の両方に対応しています。

同期の仕組み(誰が何を書き換えられるか、送信の順序、再接続など)はエンジンに共通なので、オブジェクト同期の仕組みにまとめています。このページでは、Godot での書き方を説明します。ルームのルールも GDScript で書けます。書き方は GDScript でルールを書くを参照してください。

Unity のクライアントと Godot のクライアントが、同じルームに参加することは想定していません。1 つのゲームでは、どちらか一方を使ってください。

全体像

Godot では、次のノードとリソースを使います。どれも GS2 SDK for Godot のアドオン(addons/gs2)に含まれています。

GodotUnity置く場所・役割
Gs2NetworkWorldNetworkWorldシーンに 1 つ。受信した状態からインスタンスを生成・更新・削除し、変更をまとめて送信する
Gs2NetworkSessionNetworkSessionシーンに 1 つ。ルームに接続し、World をつなぐ
Gs2NetworkEntityNetworkEntity同期するシーンのルートの子ノード。インスタンスと共有オブジェクトを対応付ける
Gs2NetworkTransform2D / Gs2NetworkTransform3DNetworkTransform位置・回転を同期するシーンのルートの子ノード
Gs2SharedObjectSharedObjectサーバーが更新するオブジェクトに操作を送る
Gs2NetworkPrefabCatalog(リソース)Prefab CatalogPrefab ID と、表示するシーン(PackedScene)の対応
Gs2NetworkSchema(リソース)同期スキーマ.realtimeschema を取り込んだもの

Unity ではコンポーネントを GameObject に追加しますが、Godot ではノードをシーンのルートの子として追加します。Gs2NetworkEntity と Transform のノードは、既定で親(target = "..")を対象にします。

GDScript には例外がないので、失敗は戻り値(false や null)で返り、理由は各ノードの last_error に入ります。使い方の誤りは push_error でも報告します。

ルームに接続する

  1. シーンに Gs2NetworkWorld と Gs2NetworkSession を置きます。
  2. World の catalog に Prefab カタログを、Session の world に World を設定します。
  3. 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 が出る
stateDISCONNECTED・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() を呼びます。

シーンを登録する

  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 を使います。

位置・回転を同期する

位置・回転には 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 を使います(独自の値)。

  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) など)。

サーバーに操作を送る

ルールが管理するオブジェクトには、シーンに 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:
    pass
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 を参照してください。
  • 引数には型を付けます。使える型は 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 を選び、プロジェクト > ツール > 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)パドルとパック、得点