クイックスタートガイド(クライアントサーバー型)
これはクライアントサーバー型(Client-Server)のスタートガイドです。クイックスタートガイド(共有権限型)はこちらです。どちらを選択すべきか迷っているなら、トポロジーの選択をご覧ください。
はじめに
このガイドでは、ホストがゲームステートに対する完全な権限を持ち、クライアントがサーバーサイドで実行される入力を送信するクライアントサーバー型のマルチプレイヤーシーンを構築します。最終的には、入力キューと予測フローを使用して、2つのインスタンスが共有ルーム内でキャラクターを操作できるようになります。
クライアントサーバー型は、1人のプレイヤーがホスト(シミュレーションサーバーとも呼ばれる)として振る舞います。ホストはゲームロジックを実行し、何が起こるかを決定します。他のクライアントは入力をホストに送信し、応答性を高めるために結果をローカルで予測します。Photon Cloudはすべてのクライアントへの状態配信を処理します。
前提として、Godot 4.6がインストール済みで、GodotとGDScriptについての基本的な知識、Fusionアドオンのインストールが必要です。
ステップ 1 - 事前準備
- Photonダッシュボードでアカウントを作成してログインします。
- アプリの新規作成をクリックし、SDKはFusion、バージョンはFusion 3(UnrealとGodotの両方で使用可能)を選択します。
- アプリ作成後にAppIDをコピーします。
- SDKのダウンロードからFusionアドオンをダウンロードして、
fusion/フォルダーをGodotプロジェクトルートのaddons/フォルダーにコピーします。 - プロジェクトを再読み込みし、プロジェクト設定を開きFusionを検索します。AppIDを
fusion/connection/app_idに貼り付け、fusion/simulation/modeをAuthorityに設定します。これによって、クライアントサーバー型(デフォルトは共有権限型)が有効になります。ゲームコードのFusion.get_simulation_mode()から、実行時にアクティブなモードを調べることができます。
ステップ 2 - メインシーンのセットアップ
Node3Dをルートとするメインシーンを作成して、Camera3D(テストにはOrthogonalが最適)・DirectionalLight3D・コリジョン形状とメッシュを設定した床のStaticBody3D・FusionSpawner・スポーンしたオブジェクトを格納するためのCharactersという名前の空Node3Dを追加してください。FusionSpawnerのspawn_pathは../Charactersに設定します。

ステップ 3 - FusionServerReplicatorを持つキャラクターシーン
CharacterBody3Dをルートにした新しいシーンを作成して、CollisionShape3D(カプセル形状)・MeshInstance3D(カプセル形状)・FusionServerReplicatorの子ノードを追加します。インスペクター上から、FusionServerReplicatorのowner_modeをPLAYER_PREDICTED、root_replication_modeをAUTOに設定します。そして、シーンを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()
キャラクターの入力権限を持たないプレイヤーには、サーバーから複製された位置・回転・速度が送信されます。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
カスタムプロパティの詳細な説明は、プロパティの同期をご覧ください。
次のステップ
- 予測と入力 - 完全な入力ループ、予測と正しい値の参照
- トポロジーの選択 - トレードオフの理解
- 接続 - 完全な接続ライフサイクルとルームオプション
- スポーン - スポーン設定とサブオブジェクト
- RPC - オブジェクト対象RPCとブロードキャストRPC
- クライアントサーバー型3Dデモ - 完全なサンプルプロジェクト
完全なスクリプト
上記ステップで作成された完全なスクリプトです。
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.gd(CharacterBody3Dのルートにアタッチ):
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