自分の画面では扉が開いたのに、友達の画面では閉じたまま。同じゲームに参加していても、1人用と同じようにメッシュを動かすだけでは、相手の画面まで変わるとは限りません。
マルチプレイでは、各プレイヤーの環境でゲームが動いています。その間で「開けてほしい」という要求と、「今は開いている」という状態をやり取りして、見える世界をそろえます。
この記事では、手前の範囲へ入ると上に開く扉を作ります。プレイヤーが頼む → サーバが判断する → 決まった状態を共有する。この流れを、Blueprintと2つのゲーム画面で確かめましょう。
この記事でわかること
- サーバとクライアントで、処理をどう分担するか
- RPCによる要求と、Replicationによる状態の共有の違い
- 自分のCharacterを経由して、共有の扉を開ける手順
- 2画面で試し、どこまで処理が届いたか調べる方法
同じ世界を、それぞれの環境で動かしている
サーバ は、扉の開閉や敵のHPなど、共有するゲーム状態を決める側です。クライアント は、そこへ接続して遊ぶ側です。入力を受け取り、必要な要求を送り、届いた状態を自分の画面へ反映します。
サーバとクライアントには、それぞれ扉のActorがあります。片方のActorの変数を変更しても、別の環境にあるActorまで直接書き換わるわけではありません。ネットワークを通じて状態を伝える仕組みが必要です。

クライアントも、カメラやUI、操作に応じた動きな どを処理しています。共有する扉については、誰か1人の見た目を正解にするのではなく、開けてよいかの判断をサーバに集める と考えると整理しやすくなります。
今回は Listen Server(リッスンサーバ) で試します。参加者の1人が、サーバとプレイヤーを兼ねる方式です。この人をホストと呼びます。残りの参加者はクライアントとして接続します。
まずは2画面で歩いてみる
BlueprintのThird Personテンプレートを使います。キャラクターは標準の BP_ThirdPersonCharacter のまま進めます。すでに自分のCharacterを使っている場合は、後のCast先もその名前に読み替えてください。
UEの PIE(Play In Editor) は、エディタからゲームを実行する機能です。Play横のメニューで、次を設定します。項目が見つからなければ「Advanced Settings」からMultiplayer Optionsを開きます。
| 項目 | 設定 | 意味 |
|---|---|---|
| Playの表示方法 | New Editor Window (PIE) | ゲームを別ウィンドウで開く |
| Number of Players | 2 | 2人分のゲームを起動する |
| Net Mode | Play As Listen Server | 片方をホスト、もう片方をクライアントにする |

レベルにはPlayer Startを2個、重ならないように置いておきます。Playすると、ホストとクライアントの画面が開きます。ウィンドウのタイトルにあるServer/Clientなどの表示で見分けてください。
片方をクリックしてキャラクターを動かし、もう片方でもそのキャラクターが動くことを確認します。Shift + F1 でマウス操作をエディタへ戻すと、もう一方のウィンドウを選びやすくなります。
ここまでできれば、2つのゲームがつながっています。テンプレートのキャラクター移動はUEの仕組みで同期されますが、これから作る扉の開閉は、自分で共有する状態を決めます。
要求を送るRPC、状態を送るReplication
扉の例では、2つの役割を分けます。
| 伝えるもの | 使う仕組み | 扉での例 |
|---|---|---|
| 向こう側で処理してほしい要求 | RPC | この扉を開けてほしい |
| 共有したい現在の状態 | Replication(レプリケーション) | この扉は開いている |
RPCは、ネットワークの向こう側へ処理を呼び出す仕組みです。今回は Server RPC で、クライアントからサーバへ要求します。サーバは条件を確かめ、問題なければ扉の bDoorOpen をtrueにします。
その値をクライアントへ伝えるのがReplicationです。クライアントは受け取った値を見て、自分の環境にある扉を開きます。

Actorと変数の両方を設定する
ActorのClass Defaultsで Replicates をオンにすると、そのActorを複製・同期する対象にできます。さらに、共有する変数ごとにReplicationを設定します。
| 変数の設定 | 何が起きるか |
|---|---|
| Replicated | サーバ側の値をクライアント側へ反映する |
| RepNotify | 値の反映に加え、変更を受け取ったときに通知関数を呼ぶ |
RepNotify は、「値が届いたので見た目も更新しよう」という処理の入口です。bDoorOpen をRepNotifyにすると、OnRep_bDoorOpen という関数が作られます。ここから扉の見た目を更新します。
Blueprintでは、その変数を通常のSetノードで設定した側でもRepNotifyが呼ばれます。今回の実践はこの仕組みを使い、サーバ側と受信側で同じ見た目の更新を行います。

なお、Replicationは、全プレイヤーへ全ての値を毎フレーム送る仕組みではありません。距離などで送信対象が変わり、短時間に何度も変えた値の途中経過が、そのまま全て届くとも限りません。扉には「今、開いているか」という状態を持たせます。
Server RPCは、自分のCharacterを入口にする
クライアントからサーバへRPCを送るには、そのRPCを持つActorが、送信するクライアントに所有されていること が必要です。ここでの 所有 は、通信上「どのプレイヤーにつながっているActorか」という意味です。
自分が操作しているCharacterやPlayerControllerは、このつながりを持ちます。一方、レベルへ置いた共有の扉は、通常、特定のクライアントの所有物ではありません。触れたり、変数に参照を入れたりしても、所有したことにはなりません。

実践では、扉に触れたことをきっかけに、自分のCharacterに定義したServer RPC を呼びます。そこへ「開けたい扉」の参照を渡します。参照は、その扉を後から指定するためのものです。
「扉のグラフから呼ぶこと」が問題なのではありません。RPCがどのActorに定義され、その呼び出しのTargetが誰になっているか が重要です。共有扉自身のServer RPCをクライアントから呼ぶと、所有条件を満たさず、サーバで実行されません。
Authorityと、自分が操作するキャラ は別
Authority は、そのActorの状態を決める側かどうかを表します。今回の共有扉では、サーバ側の扉がAuthorityを持ちます。Switch Has Authority は、その側だけに処理を絞る分岐です。
一方、Is Locally Controlled は「この環境で操作しているキャラクターか」を調べます。クライアントにもホストにも、自分が操作するキャラクターがいます。後の手順では、この違いを使って要求を送る側と、状態を書き換える側を分けます。
実践:上に開く共有の扉を用意する
手前の範囲へ入ると、扉が上へ移動して通れるようになります。一度開いたら、Playをやり直すまで開いたままにします。まずはこの1つの状態変化を、両画面でそろえましょう。

部品を配置する
Actorを親にした BP_SharedDoor を作ります。DefaultSceneRootの子に、次の3つを追加します。どれもDefaultSceneRootの直下に置き、ButtonZoneをDoorMeshの子にはしません。
| 名前 | 種類 | 設定 |
|---|---|---|
DoorMesh | Static Mesh | Cube、Location (0, 0, 100)、Scale (0.2, 2, 2)、Movable、BlockAll |
ButtonZone | Box Collision | Location (-150, 0, 50)、Box Extent (80, 140, 50)、OverlapOnlyPawn、Generate Overlap Eventsオン |
ButtonMarker | Static Mesh | Cube、Location (-150, 0, 5)、Scale (1.6, 2.8, 0.1)、NoCollision |

Cubeはコンテンツブラウザで「Show Engine Content」をオンにすると、/Engine/BasicShapes/Cube から選べます。ButtonMarkerは、踏む場所が分かる目印です。接触の判定はButtonZoneが担当します。
Box Extent は箱の半分の大きさです。ButtonZoneは幅160cm・奥行き280cm・高さ100cmの範囲になります。Overlap は、ぶつかって止める代わりに、範囲へ入ったことを知らせる仕組みです。
Class Defaultsの Replicatesをオン、Replicate Movementをオフ にします。今回はActor全体の移動を送らず、開閉状態から各環境で子のDoorMeshを動かします。DoorMeshの「Component Replicates」と「Simulate Physics」もオフのままにします。
変数と処理の入口を作る
BP_SharedDoorに、次を用意します。関数は「My Blueprint 」のFunctionsの+から作ります。
| 名前 | 種類 | 設定/役割 |
|---|---|---|
bDoorOpen | Boolean変数 | 初期値false、Replication = RepNotify |
ApplyDoorState | 関数 | 引数なし。状態を見た目へ反映する |
OpenDoor | 関数 | 引数なし。サーバ側で開く状態にする |
bDoorOpenをRepNotifyにすると、OnRep_bDoorOpen が自動で追加されます。同じ名前の関数を自分で作る必要はありません。Compileして保存します。
開いているかどうかを、見た目へ反映する
ApplyDoorStateでは、DoorMeshの 相対位置 を変えます。相対位置は、親であるDefaultSceneRootを基準にした位置です。扉Actorをレベルのどこへ置いても、同じ開き方にできます。
| bDoorOpen | DoorMeshの相対位置 | 結果 |
|---|---|---|
| false | (0, 0, 100) | 床から高さ200cmの板で通路をふさぐ |
| true | (0, 0, 350) | 板が250cm上へ移り、下を通れる |
状態から位置を選ぶ
ApplyDoorStateを開き、Select Vector を置きます。Aを (0, 0, 350)、Bを (0, 0, 100) にし、bDoorOpenのGetをPick Aへつなぎます。Vector は、ここではX・Y・Zの3つの数値で表した位置です。

Pick AがtrueならA、falseならBがReturn Valueから出ます。「開いていたら上の位置、閉じていたら下の位置」を選んでいるだけです。
関数の入口から Set Relative Location へ実行線をつなぎます。Components欄のDoorMeshをグラフへドラッグし、Targetへ。Select VectorのReturn ValueはNew Locationへつなぎ、SweepとTeleportはオフにします。

Targetは「動かす部品」、New Locationは「移動先」です。今回はアニメーションせず、決まった位置へ切り替えます。
通知と初期表示から、同じ関数を呼ぶ
OnRep_bDoorOpenを開き、入口から ApplyDoorState を呼びます。さらにEvent Graphの Event BeginPlay からもApplyDoorStateを呼び、開始時の見た目をそろえます。どちらのTargetもSelf、つまりこの扉自身です。

ApplyDoorStateは「今の状態に合う位置へ置く」だけなので、同じ値で呼び直しても扉が余分に持ち上がることはありません。HPバーやランプの色も、同じように現在の値から見た目を決めると扱いやすくなります。
自分のCharacterからサーバへ頼む
Character側にRPCを用意する
BP_ThirdPersonCharacterを開き、Event Graphで右クリックしてCustom Eventを追加し、Server_RequestOpenDoor と命名します。イベントを選び、Detailsで次を設定してください。
| 項目 | 設定 |
|---|---|
| Replicates | Run on Server |
| Reliable | オン |
| Inputs | Door:BP_SharedDoorのObject Reference |

Reliable は、受信を確認するまで再送する方式です。今回のような、たまに送る開放要求に使います。所有条件を無視して送れたり、切断後まで届いたりする保証ではありません。連続して大量に送ると待ちがたまるため、Tickから呼ぶような使い方は避けます。
CharacterのClass DefaultsでReplicatesがオンであることも確認し、Compileします。これで扉側からこのイベントを呼べます。
扉に入ったキャラクターを調べる
BP_SharedDoorへ戻ります。ButtonZoneを選び、Detailsのイベント欄から On Component Begin Overlap を追加します。実行出力を Cast to BP_ThirdPersonCharacter へ、Other ActorをそのObject入力へつなぎます。Cast Failedは何もしません。

ここでの Cast は、「入ってきた相手を、このCharacterとして扱えるか」の確認です。成功したときのAs BP Third Person Characterから線を出し、Is Locally Controlled を検索して置きます。キャラクターをTargetに取り、赤いBooleanを返す、実行ピンのないノードを使います。

Cast成功の実行出力をBranchへつなぎ、Is Locally ControlledのReturn ValueをConditionへ。True側から Server_RequestOpenDoor を呼びます。RPCのTargetにはCastのAs BP Third Person Characterを、Door入力には Self ノードをつなぎます。
