This document is about: V3 CLIENT SERVER
SWITCH TO

クイックスタートガイド(クライアントサーバー型)

これはクライアントサーバー型(Client-Server)のスタートガイドです。クイックスタートガイド(共有権限型)はこちらです。どちらを選択すべきか迷っているなら、トポロジーの選択をご覧ください。

はじめに

このガイドでは、ホストがゲームステートに対する完全な権限を持ち、クライアントがサーバーサイドで実行される入力を送信するクライアントサーバー型のマルチプレイヤーシーンを構築します。最終的には、入力キューと予測フローを使用して、2つのインスタンスが共有ルーム内でキャラクターを操作できるようになります。

クライアントサーバー型は、1人のプレイヤーがホスト(シミュレーションサーバーとも呼ばれる)として振る舞います。ホストはゲームロジックを実行し、何が起こるかを決定します。他のクライアントは入力をホストに送信し、応答性を高めるために結果をローカルで予測します。Photon Cloudはすべてのクライアントへの状態配信を処理します。

前提として、Godot 4.6がインストール済みで、GodotとGDScriptについての基本的な知識、Fusionアドオンのインストールが必要です。

ステップ 1 - 事前準備

  1. Photonダッシュボードでアカウントを作成してログインします。
  2. アプリの新規作成をクリックし、SDKはFusion、バージョンはFusion 3(UnrealとGodotの両方で使用可能)を選択します。
  3. アプリ作成後にAppIDをコピーします。
  4. SDKのダウンロードからFusionアドオンをダウンロードして、fusion/フォルダーをGodotプロジェクトルートのaddons/フォルダーにコピーします。
  5. プロジェクトを再読み込みし、プロジェクト設定を開きFusionを検索します。AppIDをfusion/connection/app_idに貼り付け、fusion/simulation/modeAuthorityに設定します。これによって、クライアントサーバー型(デフォルトは共有権限型)が有効になります。ゲームコードのFusion.get_simulation_mode()から、実行時にアクティブなモードを調べることができます。

ステップ 2 - メインシーンのセットアップ

Node3Dをルートとするメインシーンを作成して、Camera3D(テストにはOrthogonalが最適)・DirectionalLight3D・コリジョン形状とメッシュを設定した床のStaticBody3DFusionSpawner・スポーンしたオブジェクトを格納するためのCharactersという名前の空Node3Dを追加してください。FusionSpawnerspawn_path../Charactersに設定します。

メインシーン構成
メインシーン構成

ステップ 3 - FusionServerReplicatorを持つキャラクターシーン

CharacterBody3Dをルートにした新しいシーンを作成して、CollisionShape3D(カプセル形状)・MeshInstance3D(カプセル形状)・FusionServerReplicatorの子ノードを追加します。インスペクター上から、FusionServerReplicatorowner_modePLAYER_PREDICTEDroot_replication_modeAUTOに設定します。そして、シーンをauthority_character_3d.tscnとして保存します。

キャラクターシーン構成
キャラクターシーン構成

ステップ 4 - サーバー開始方法

クライアントサーバー型では、別で起動するサーバー実行ファイルはありません。シミュレーションサーバーは単なるFusionピアです。ルームを作成したピアはサーバーとなり、既存ルームに参加するピアはすべてクライアントとなります。

これを有効にするには、2つの設定が必要です:

  • fusion/simulation/mode = Authority(ステップ 1で設定済み):この設定が有効になっている場合のみ、Fusionはクライアントサーバー権限モデルを適用します。Sharedモードでは、ルーム内で共有権限が使用されます。
  • マスタークライアント(シミュレーションサーバー):Photonはルーム作成者をマスタークライアントとして指定し、そのピアをすべてのネットワークステートに対する権限者として扱います。

クライアントサーバー型は、同じコードで2つのトポロジーに対応できます:

  • クライアントサーバー:通常のゲームインスタンスがルームを作成してプレイも行います。1人のプレイヤーの端末が、シミュレーションと自身のキャラクター操作の両方を実行します。
  • 専用サーバー:レンダリングや入力を行わないGodotヘッドレスビルドがルームを作成します。そこに参加するすべてのピアはクライアントとなります。

以降のステップでは、Fusion.is_master_client()を使用して、実行時に「自身がシミュレーションサーバーか」「自身がクライアントか」どうかを区別します。

ステップ 5 - 接続と参加

メインシーンにスクリプトをアタッチします。プロジェクト設定からFusionを初期化し、Photon Cloudに接続し、接続に成功したらルーム参加/作成を行うのが、接続フローになります。ルームとは、プレイヤーがオブジェクトを同期したりRPCを通信したりする共有セッションのことです。

GDScript

extends Node3D

const CharacterScene = preload("res://authority_character_3d.tscn")

@onready var spawner: FusionSpawner = $FusionSpawner

func _ready():
    Fusion.room_joined.connect(_on_room_joined)
    Fusion.register_broadcast_receiver(self)  # このノードでブロードキャストRPCの受信を有効にする
    spawner.add_spawnable_scene(CharacterScene)

    Fusion.connect_to_photon("user_%d" % randi())
    Fusion.connected_to_photon.connect(func():
        Fusion.join_or_create_room()
    )

ステップ 6 - スポーンリクエストパターン

クライアントサーバー型では、マスタークライアントのみがオブジェクトをスポーンできます。マスタークライアントとは、最初にルームの作成/参加を行ったプレイヤーを指し、Photonからシミュレーションを実行するホストとして指定されます。マスター以外のクライアントが参加する際、そのクライアントはブロードキャストRPC(すべての登録された受信者に配信されるグローバルメッセージ)を送信して、キャラクターの生成をリクエストします。マスターはリクエストを受信し、キャラクターを生成して、リクエストしたプレイヤーに入力権限を割り当てます。マスターはルームに参加した際、自身のキャラクターも生成します。

GDScript

func _on_room_joined():
    if Fusion.is_master_client():
        _spawn_character(Fusion.get_local_player_id())
    else:
        Fusion.rpc(request_spawn)

@rpc("any_peer", "call_local")
func request_spawn():
    if not Fusion.is_master_client():
        return
    var sender_id = Fusion.get_rpc_sender_id()
    _spawn_character(sender_id)

func _spawn_character(player_id: int):
    var character = spawner.spawn()
    character.position = Vector3(randf_range(-4, 4), 1.0, randf_range(-4, 4))
    character.get_node("FusionServerReplicator").set_input_authority(player_id)

ステップ 7 - 入力の格納とキューイング

キャラクタースクリプトでは、入力権限を持つクライアントがフレームごとにキー入力を読み取り、それをバイトバッファに格納します。このバッファはqueue_input()を通してサーバーへ送信されます。入力を行えるのは、入力権限を持つクライアントのみです。

GDScript

extends CharacterBody3D

const SPEED := 5.0
const GRAVITY := -20.0

@onready var replicator: FusionServerReplicator = $FusionServerReplicator
var _tick: int = 0

func _ready():
    replicator.on_process_input.connect(process_input)

func _create_input() -> PackedByteArray:
    var dir = Input.get_vector("ui_left", "ui_right", "ui_up", "ui_down")
    var buf = PackedByteArray()
    buf.resize(12)
    buf.encode_float(0, dir.x)
    buf.encode_float(4, dir.y)
    buf.encode_u32(8, _tick)
    _tick += 1
    return buf

func _physics_process(delta):
    if replicator.has_input_authority():
        replicator.queue_input(delta, _create_input())
    replicator.process_input_queue(delta)  # すべてのピア上で実行:入力側は予測・サーバー側は正しい処理・リモート側は何もしない

ステップ 8 - 入力の実行と移動

process_input_queue()が呼び出されると、Fusionは、入力ペイロードを含むon_process_inputシグナルを発火します。このコールバックは、入力権限を持つクライアント(予測)と、サーバー(正しい処理)の両方が実行されます。移動コードは、どちらでも同じものが実行されます。

GDScript

# is_new は、この入力が初めて実行される際に true になります。予測リセット後の再シミュレーション中は false になります。
# is_new の後ろで、ワンショット効果(サウンド、パーティクル)をガードしてください。

```gdscript
# is_newはこの入力が初めて実行される際にtrueになり、予測リセット後の再シミュレーション中はfalseになる
# 単発の効果(サウンド・パーティクルなど)の場合は、is_newを使用してガードする
func process_input(tick: int, delta_time: float, payload: PackedByteArray, is_new: bool):
    var dir_x = payload.decode_float(0)
    var dir_z = payload.decode_float(4)
    velocity = Vector3(dir_x, 0.0, dir_z) * SPEED
    velocity.y += GRAVITY * delta_time
    move_and_slide()
What about other players?

キャラクターの入力権限を持たないプレイヤーには、サーバーから複製された位置・回転・速度が送信されます。FusionServerReplicatorはスムージングを自動的に適用するため、毎秒30回の更新でもリモートキャラクターの動きはスムーズに見えます。リモートクライアント上では、process_input_queue()で何も処理は行われません。

ステップ 9 - 複数インスタンスでテスト

デバッグ > 実行インスタンスのカスタマイズ > 複数インスタンスを有効2に設定して再生してください。最初のインスタンスはマスタークライアントとなり、自身のキャラクターを生成します。二番目のインスタンスはそれに参加し、RPC経由でスポーンリクエストを送信します。両方のインスタンス上で、それぞれのプレイヤーが操作する2体のキャラクターが表示されるでしょう。

ステップ 10 - 予測可能なカスタムプロパティ

ゲームプレイのカスタムプロパティ(体力・スコア・名前など)は、FusionServerReplicatorのエディター下部パネルから追加できます。キャラクターシーン内のFusionServerReplicatorノードを選択すると、ビューポート下のパネルにFusion Replication Editorが表示されます。プロパティを追加をクリックして、シーンツリーからプロパティを登録することで、ネットワーク上で同期できるようになります。

例えば、ルートノードから:scoreプロパティを追加すると、入力権限を持つクライアント上では、予測値を即時に設定して応答性を高めます。サーバーから正しい値を受信し、その値が異なっていた場合、クライアントは予測をリセットして値を自動的に補正します。

GDScript

var score: int = 0

# process_inputは、クライアント(予測)と、サーバー(正しい値)の両方で実行される
func process_input(tick: int, delta_time: float, payload: PackedByteArray, is_new: bool):
    # ... 移動コード ...
    if collected_coin:
        score += 1

カスタムプロパティの詳細な説明は、プロパティの同期をご覧ください。

次のステップ

完全なスクリプト

上記ステップで作成された完全なスクリプトです。

main.gd(メインシーンのルートにアタッチ):

GDScript

extends Node3D

const CharacterScene = preload("res://authority_character_3d.tscn")

@onready var spawner: FusionSpawner = $FusionSpawner

func _ready():
    Fusion.room_joined.connect(_on_room_joined)
    Fusion.register_broadcast_receiver(self)
    spawner.add_spawnable_scene(CharacterScene)

    Fusion.connect_to_photon("user_%d" % randi())
    Fusion.connected_to_photon.connect(func():
        Fusion.join_or_create_room()
    )

func _on_room_joined():
    if Fusion.is_master_client():
        _spawn_character(Fusion.get_local_player_id())
    else:
        Fusion.rpc(request_spawn)

@rpc("any_peer", "call_local")
func request_spawn():
    if not Fusion.is_master_client():
        return
    var sender_id = Fusion.get_rpc_sender_id()
    _spawn_character(sender_id)

func _spawn_character(player_id: int):
    var character = spawner.spawn()
    character.position = Vector3(randf_range(-4, 4), 1.0, randf_range(-4, 4))
    character.get_node("FusionServerReplicator").set_input_authority(player_id)

func _exit_tree():
    if Fusion:
        Fusion.unregister_broadcast_receiver(self)

authority_character_3d.gdCharacterBody3Dのルートにアタッチ):

GDScript

extends CharacterBody3D

const SPEED := 5.0
const GRAVITY := -20.0

@onready var replicator: FusionServerReplicator = $FusionServerReplicator
var _tick: int = 0

func _ready():
    replicator.on_process_input.connect(process_input)

func _create_input() -> PackedByteArray:
    var dir = Input.get_vector("ui_left", "ui_right", "ui_up", "ui_down")
    var buf = PackedByteArray()
    buf.resize(12)
    buf.encode_float(0, dir.x)
    buf.encode_float(4, dir.y)
    buf.encode_u32(8, _tick)
    _tick += 1
    return buf

func _physics_process(delta):
    if replicator.has_input_authority():
        replicator.queue_input(delta, _create_input())
    replicator.process_input_queue(delta)

func process_input(tick: int, delta_time: float, payload: PackedByteArray, is_new: bool):
    var dir_x = payload.decode_float(0)
    var dir_z = payload.decode_float(4)
    velocity = Vector3(dir_x, 0.0, dir_z) * SPEED
    velocity.y += GRAVITY * delta_time
    move_and_slide()
Back to top