Unity Netcode - 同期の実践:RPCとNetworkVariableの使い分け
- Unity Netcode - Networking Components
- Unity Netcode - 同期の実践:RPCとNetworkVariableの使い分け
- Unity Netcode - 権限モードの完全ガイド:サーバー権限 vs クライアント権限
ネットワーク同期
ネットワーク同期は、大きく分けて2つの方法があります: RPC と NetworkVariables です。
NetworkVariables は、ゲーム実行後に遅れて参加する(途中参加)クライアントとの同期を行う際に、最も一般的に使用されます。
一方、ゲームロジックを RPC だけで処理する場合、データ消失により、遅れて参加したクライアントとの同期の信頼性が低下する可能性が高くなります。
RPC (Remote Procedure Calls)
RPCは、メッセージングやイベント通知を送る方法であるだけでなく、サーバーとクライアント間、またはクライアントと
NetworkBehaviour間の直接通信を処理する方法です。クライアントは
NetworkObject上で Server RPC を呼び出すことができます。RPCはローカルキューに配置された後、サーバーに送信され、サーバーバージョンの同一NetworkObject上で実行されます。クライアントでRPCを呼び出すと、SDKは該当するRPCのオブジェクト、コンポーネント、メソッド、およびパラメータを記録し、この情報をネットワーク経由で送信します。
- Server RPC の動作原理
- Client RPC の動作原理
RPC の使用方法
RPCは基本的に、呼び出したいメソッドの上部に属性(Attribute)として宣言します。
注意:メソッド名の末尾には必ず
Rpc、ServerRpc、ClientRpcなどを付ける必要があります。
// メソッド名にサフィックス必須
// ServerRpc / ClientRpc で個別の属性
[ServerRpc]
public void PingServerRpc(int pingCount)
{
// サーバーで実行
}
[ClientRpc]
public void PongClientRpc(
int pingCount,
string message)
{
// すべてのクライアントで実行
}// メソッド名は Rpc サフィックスのみ
// SendTo で対象を指定(柔軟)
[Rpc(SendTo.Server)]
public void PingRpc(int pingCount)
{
// サーバーで実行
}
[Rpc(SendTo.NotServer)]
void PongRpc(
int pingCount,
string message)
{
// サーバー以外で実行
}
RPC Attribute Target Table
- ターゲットの種類は多いですが、主に使用するのは
Server、NotServer、Everyone程度です。
- 上記の属性とともに、よく使用されるパラメータもあります。
1
2
3
[Rpc(SendTo.Everyone, RequireOwnership = false)]
[ServerRpc(RequireOwnership = false)]
- Legacy RPCを使用するのが最も簡単で直感的だと思いますが、Universal RPC属性に置き換わりつつあるため、可能であればUniversal形式に慣れておくのが良いでしょう。
- 両方使用可能であるため、違いについてはさらなる研究が必要です。
実践例 1
実践例 2
- 各クライアントでクライアント専用メソッド
TryAddIngredientを実行します。 - その後、ServerRpcである
AddIngredientServerRpcを呼び出します。 ServerRpcはサーバーでのみ実行されますが、メソッド内部で ClientRpc であるAddIngredientClientRpcを実行し、接続されているすべてのクライアントに変更を反映させます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public bool TryAddIngredient(KitchenObjectSO kitchenObjectSO)
{
AddIngredientServerRpc(GetKitchenObjectSOIndex(kitchenObjectSO))
}
[ServerRpc(RequireOwnership = false)]
private void AddIngredientServerRpc(int kitchenObjectSOIndex)
{
AddIngredientClientRpc(kitchenObjectSOIndex);
}
[ClientRpc]
private void AddIngredientClientRpc(int kitchenObjectSOIndex)
{
KitchenObjectSO kitchenObjectSO =
KitchenGameMultiplayer.Instance.GetKitchenObjectSOFromIndex(kitchenObjectSOIndex);
kitchenObjectSOList.Add(kitchenObjectSO);
OnIngredientAdded?.Invoke(this, new OnIngredientAddedEventArgs
{
kitchenObjectSO = kitchenObjectSO
});
}
NetworkVariables 同期
NetworkVariables はRPCとは異なり、サーバーとクライアント間のプロパティなどを継続的に同期する方法です。
RPCやメッセージとは異なり、特定の時点での一回限りの通信ではなく、接続されていないクライアント(後から接続したクライアント)とも共有されます。
NetworkVariableは指定された値型Tのラッパーであり、実際に同期される値にアクセスするにはNetworkVariable.Valueプロパティを使用する必要があります。注意:型
Tには値型(int, bool, float, FixedStringなど)のみ指定可能で、参照型は使用できません。
1
2
3
4
5
6
7
8
9
private NetworkVariable<int> testValue = new NetworkVariable<int>();
private const int initValue = 1111;
public override void OnNetworkSpawn()
{
testValue.Value = initValue;
...
}
NetworkVariable は以下のように同期されます
1. 新しいクライアントがゲームに参加(途中参加)
NetworkBehaviourにNetworkVariable属性を持つNetworkObjectが生成されると、NetworkVariableの現在の状態(Value)はクライアント側で自動的に同期されます。
2. 接続済みのクライアント
NetworkVariableの値が変更されると、NetworkVariable.OnValueChangedイベントを購読しているすべての接続済みクライアントは、値が変更される前に通知を受け取ります。OnValueChangedコールバックには、previous(変更前)とcurrent(変更後)の2つのパラメータがあります。
実践例 1
stateというNetworkVariableを生成し、OnValueChangedにState_OnValueChangedメソッドを登録します。- その後、
stateの値が変更されるたびに、OnStateChangedイベントを購読しているすべてのリスナーを Invoke します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// T型である State は enum
[SerializeField] private NetworkVariable<State> state = new NetworkVariable<State>(State.WaitingToStart);
public event EventHandler OnStateChanged;
public override void OnNetworkSpawn()
{
state.OnValueChanged += State_OnValueChanged;
isGamePaused.OnValueChanged += IsGamePaused_OnValueChanged;
if (IsServer)
{
NetworkManager.Singleton.OnClientDisconnectCallback += NetworkManager_OnClientDisconnectCallback;
NetworkManager.Singleton.SceneManager.OnLoadEventCompleted += SceneManager_OnLoadEventCompleted;
}
}
private void State_OnValueChanged(State previousValue, State newValue)
{
OnStateChanged?.Invoke(this, EventArgs.Empty);
}
実践例 2
- 各クライアントは Door を使用するたびに
ServerRpcを呼び出し、サーバー側で Door の状態であるStateをトグルします。 - その後、ラップされた
Door.State.Valueが変更されると、すべての接続済みクライアントは新しいcurrent value(ここではbool)に同期され、OnStateChangedメソッドが各クライアントで呼び出されます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public class Door : NetworkBehaviour
{
public NetworkVariable<bool> State = new NetworkVariable<bool>();
public override void OnNetworkSpawn()
{
State.OnValueChanged += OnStateChanged;
}
public override void OnNetworkDespawn()
{
State.OnValueChanged -= OnStateChanged;
}
public void OnStateChanged(bool previous, bool current)
{
// note: ここで `State.Value` は `current` と同じになります
if (State.Value)
{
// ドアが開いた:
// - ドアの回転
// - アニメーション、サウンド再生など
}
else
{
// ドアが閉じた:
// - ドアの回転
// - アニメーション、サウンド再生など
}
}
[Rpc(SendTo.Server)]
public void ToggleServerRpc()
{
// これによりネットワーク上でのレプリケーションが発生し、
// 受信側で `OnValueChanged` が呼び出されます
State.Value = !State.Value;
}
}
Custom NetworkVariables
NetworkVariableを使用して、いわゆる「パケット」形式でカスタムデータを同期することも可能です。- 主にカスタム形式の
NetworkVariableは、ジェネリック型を使用して直列化(シリアライズ)を行う必要があります。
NetworkVariable の例
- カスタム
NetworkVariableを宣言する際、値の初期化だけでなく、NetworkVariableReadPermission、NetworkVariableWritePermissionオプションを指定することもできます。 - ここでは
MyCustomDataという任意の struct、つまり 「構造体」 で直列化を行いました。 - 該当する構造体は
INetworkSerializableインターフェースを継承する必要があります。 - その後、内部的に同期したい Value 型の変数を宣言し、
1
NetworkSerialize<T>
- メソッド内部で
BufferSerializerを通じて変数の直列化を行います。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private NetworkVariable<MyCustomData> randomNumber = new NetworkVariable<MyCustomData>(
new MyCustomData
{
_int = 1,
_bool = true,
_message = "nan"
}, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Owner);
public struct MyCustomData : INetworkSerializable
{
public int _int;
public bool _bool;
public string _message;
public void NetworkSerialize<T>(BufferSerializer<T> serializer) where T : IReaderWriter
{
serializer.SerializeValue(ref _int);
serializer.SerializeValue(ref _bool);
serializer.SerializeValue(ref _message);
}
}
- その後、
OnValueChangedでrandomNumberの値をラムダ式で購読し、デバッグログに出力します。 - 「T」キーを押して
randomNumberの値を指定して出力するログですが、ここで注意すべき点は、該当するNetworkVariableの所有権を誰に付与するかです。 - 上記の Door の例では
state値がServerRpcを通じて変更され、サーバーが主体となるNetworkVariableでしたが、この例ではrandomNumberはクライアントが主体となるNetworkVariableです。 - 要約すると、
NetworkVariableの所有権がサーバー管理(各種FSM状態、ゲームロジック変数)か、クライアント管理(プレイヤーステータス)かによって役割が分かれます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public override void OnNetworkSpawn()
{
randomNumber.OnValueChanged += (MyCustomData previousValue, MyCustomData newValue) =>
{
Debug.Log(OwnerClientId + "; " + newValue._int + "; " + newValue._bool + "; " + newValue._message);
};
}
private void Update()
{
if (!IsOwner) return;
if (Input.GetKeyDown(KeyCode.T))
{
randomNumber.Value = new MyCustomData
{
_int = Random.Range(0, 100),
_bool = Random.Range(0, 1) == 0 ? true : false,
_message = "Hello, World!",
};
}
}
NetworkList の例
NetworkVariableのリスト形式であるNetworkListも存在します。IEquatableインターフェースが追加されていることが確認できます。これはNetworkVariableまたはNetworkListの構造体型が効率的に同一性比較を行えるようにするためです。- また、データの変更有無を検知してネットワークトラフィックを削減するのにも役立ちます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public struct PlayerData : IEquatable<PlayerData>, INetworkSerializable
{
public ulong clientId;
public int colorId;
public FixedString64Bytes playerName;
public FixedString64Bytes playerId;
public bool Equals(PlayerData other)
{
return clientId == other.clientId &&
colorId == other.colorId &&
playerName == other.playerName &&
playerId == other.playerId;
}
public void NetworkSerialize<T>(BufferSerializer<T> serializer) where T : IReaderWriter
{
serializer.SerializeValue(ref clientId);
serializer.SerializeValue(ref colorId);
serializer.SerializeValue(ref playerName);
serializer.SerializeValue(ref playerId);
}
}
- リスト型であるためか、
previous、currentの2つのパラメータには分かれず、NetworkListEvent内部的に - Value と PreviousValue の2つの変数が存在します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public event EventHandler OnPlayerDataNetworkListChanged;
private NetworkList<PlayerData> playerDataNetworkList;
private void Awake()
{
playerDataNetworkList = new NetworkList<PlayerData>();
playerDataNetworkList.OnListChanged += PlayerDataNetworkList_OnListChanged;
}
private void PlayerDataNetworkList_OnListChanged(NetworkListEvent<PlayerData> changeEvent)
{
OnPlayerDataNetworkListChanged?.Invoke(this, EventArgs.Empty);
}
RPC vs NetworkVariable
- RPCの使用:一時的なイベントや情報を伝達する際に使用し、その情報が受信された瞬間のみ有用です。
NetworkVariablesの使用:持続的な状態情報を管理するのに有用です。「持続的な状態情報」はゲーム内で維持され続け、すべてのプレイヤーに一貫して伝達されるべきものです。
- どちらを使うべきかの最も早い決定方法は、「途中でゲームに参加したプレイヤーがその情報を受け取る必要があるか?」という質問を投げかけることです。
NetworkVariable は現在の状態を送信するため、遅れて参加したクライアントが容易に最新の状態に追いつくことができます。
もしRPCをすべてのクライアントに送信した場合、そのRPCが送信された後に途中でゲームに参加したプレイヤーはその情報を逃し、クライアント上で誤ったビジュアルを見ることになります。
- こうなると「じゃあRPCは全く役に立たなくて、NetworkVariablesだけ使えばいいんじゃないの?」と思うかもしれませんが…
すべてに NetworkVariables を使用しない理由
- RPCの方がシンプルです。
- ゲーム内のすべての一時的なイベント(爆発、オブジェクト生成など)のために
NetworkVariableを宣言して使用する必要はありません。
- NetworkVariableを使用して2つの変数が同時に受信されるようにしたい場合、RPCの方が適しています。
NetworkVariablesの “a” と “b” を変更しても、クライアント側でその2つの変数が同時に受信されるという保証はありません。
- 以下の写真を見ると、遅延時間によりクライアントがそれぞれ異なる時間にアップデートを受信していることが確認できます。
同一ティック内で更新された異なるネットワーク変数が、同時にクライアントに伝達される保証はありません。
- 一方、同一のRPCの2つのパラメータとして送信すれば、クライアント側で2つの変数を同時に受信可能です。
複数の異なるネットワーク変数がすべて同時に同期されるようにするために、クライアントRPCを使用してこれらの値の変化を一つに結合することができます。
まとめ
- NetworkVariable は持続的な状態情報を伝達する際に使用します。
- 持続的な状態情報とは、ゲーム内で維持され続け、すべてのプレイヤーに一貫して伝達されるべき情報を意味します。
- これにより、新しいプレイヤーが接続しても最新の状態を即座に同期でき、すべてのプレイヤーが同一の状態情報を共有できます。






