🌐

NetCode for GameObjectsでの値の共有方法まとめ

2023/08/09に公開

はじめに

マルチプレイ開発初心者です。Netcode for GameObjectsでプレイヤー間で値を共有する方法、最初どれをどうやって使っていいのか分からなくなったり、使ってみたら制約があったりしたのでまとめてみました。下の動画見たものの、触ってみたらよくわからんみたいな人用です。

https://www.youtube.com/watch?v=GRUtGLL8iMQ

https://www.youtube.com/watch?v=-XQkv5Ugeq4

主に、利用した方法は3つです。

  • NetworkTransform等の既存のComponentで共有する。
  • ServerRPCとClientRPCを使って共有する。
  • ServerRPCとNetworkVariableで共有する。

NetworkTransform等の既存のComponentで共有する


普通のプレイヤーだけが共有されているゲームだとTransformの共有やAnimationの共有はこのコンポネントでの共有が簡単かもしれません。ローカル座標だけや、スケール、ポジション、ローテーションだけなどのオプションもあるので、テンプレートの共有はこれで大丈夫です。
動画にも紹介されている通り、ClientNetworkTransformという便利なクラスがあるおかげで、クライアント側のTransrofmの値を正としてTransformを共有することもできます。
ただ、作っている中で2点制約がありました。

共有されたTransformにオフセットをつけることができない。

例えば、プレイヤーAからみたプレイヤーBの座標をx座標2だけずらしたいみたいなことがあった場合はそのままではできないです。継承したクラスを作って何とかすればできるのかもしれないですが、便利クラス的には使えないので他の方法でいいような気もします。

IsOwnerではないObjectを操作できない

ClientNetworkTransformはクライアント側の値を正として処理をしてくれますが、それはあくまでもクライアント側がそのObjectのOwnerである必要があります。なので、例えば共通の一つのオブジェクトのTransformを二人が動かすみたいな場合はこれを使うことができません。

ClientNetworkTransfromの中身はNetWorkTransform内部のNetworkTransform.OnIsServerAuthoritative =falseにしたものですが、下記URLではOwner Authoritative Modeとして紹介されており、名前の通りownerが管理する必要があります。
https://docs-multiplayer.unity3d.com/netcode/current/components/networktransform/

ServerRPCとClientRPCを使って共有する

次にServerPRCとClientRPCを利用する方法です。下のコードでは、トグルボタンを押して_isSelectedが変わるのを共有しています。

private bool _isSelected
public OnClickButton()
{
    if(IsHost)
    {
   	 _isSelected = !_isSelected
   	 SelectedClientRpc(_isSelected)
    }
    else
    {
   	 SelectedServerRpc(!_isSelected)
    }
}
[Unity.Netcode.ClientRpc]
public void SelectedClientRpc(bool responseIsSelected)
{
   _isSelected = responseIsSelected
}

[Unity.Netcode.ServerRpc(RequireOwnership = false)]
public void SelectedServerRpc(bool requestIsSelected)
{
   _isSelected = requestIsSelected
   SelectedClientRpc(_isSelected)
}

ホストの場合は、自身の値を入れ替えた後、クライアントに対してSelectedClientRpcでクライアントに値の変更を依頼。
(ホストOnClick→ホスト値変更→クライアントへ、変更依頼→クライアントで変更)

クライアントの場合もホスト側に!_isSelectedへと変更を依頼し、ホスト側がそれを受け取った後にクライアント側にSelectedClientRpcで返しています。
(クライアントOnClick→ホストに値変更依頼→ホストで値変更→クライアントに変更依頼→クライアントで変更)

また、下のようにSercerRpcParamsを引数に持つことで、送ってきたクライアントIDを知ることができるので、ClietRpcで値を送るときに特定のIDのクライアントには変更を行わないなどの分岐を作ることもできます。

[Unity.Netcode.ServerRpc(RequireOwnership = false)]
public void SelectedServerRpc(bool requestIsSelected,ServerRpcParams serverRpcParams = default)
{
	// RPCを送ってきたクライアントID
	var clientId =serverRpcParams.Receive.SenderClientId
}

制約として、途中で参加してきたプレイヤーに値を共有できないという制約があります。下の図のように途中参加のプレイヤーは_isSelectedの値はdefault値になっているため、すでにtrueになっている場合には途中参加したことを検知して値を共有するなどの手段が必要になります。
これを克服しようとしたのが、次に紹介する。NetworkVariableになります。

↓公式でもNetworkVariableとRPCが比較されています。
https://docs-multiplayer.unity3d.com/netcode/current/learn/rpcvnetvar/

ServerRPCとNetworkVariableを使って共有する

まず、NetworkVariable単体では、共有機能が片手落ちです。ほぼほぼServerRpcと一緒に利用します。
NetworkVariableは、前項で話した『途中参加のプレイヤーの初期値の共有』を可能にする+『クライアント側への値の共有』がデフォルトでついているものです。なので、クライアント側でNetworkVariableの値を変更しようとしても変更できません。ホスト側でこの値を変更する必要があるため、クライアント側からはServerRpcでアクセスします。

private NetworkVariable<bool> _isSelected = new NetworkVariable<bool>();

void Start()
{
    Debug.Log(_isSelected.Value) //共有されて現在の値になっている。
    _isSelected.OnChangeValue = OnChangeSelected
}
public OnClickButton()
{
    if(IsHost)
    {
   	 _isSelected.Value = !_isSelected.Value
    }
    else
    {
   	 SelectedServerRpc(!_isSelected)
    }
}

[Unity.Netcode.ServerRpc(RequireOwnership = false)]
public void SelectedServerRpc(bool requestIsSelected)
{
   _isSelected.Value = requestIsSelected
}

private void OnChangeSelected(bool pre,bool current)
{
   // 変更の際の処理
}

先ほどのClientRpcのコードをNetworkVariableで書き換えるとこんな感じになります。ClientRpcがなくなりすっきりしました。また、値の変更があった場合はOnChangeValueイベントが呼ばれるため、Start時にイベントを登録することで、変更時の処理も簡単に書くことができます。

制約としては、RPCも同じ制約を持っているのですが、共有する値の型に制約があります。


https://docs-multiplayer.unity3d.com/netcode/current/basics/networkvariable/

もし、独自の値を共有する場合はINetworkSerializableを継承したstructを作成することで値を共有することができます。例えば、下のようなStructを作成し、NetworkVariabl<RayLinePoints> として共有することはできます。

struct RayLinePoints : INetworkSerializable
{
    public Vector3 OriginPosition;
    public Vector3 FinishPosition;

    // INetworkSerializable
    public void NetworkSerialize<T>(BufferSerializer<T> serializer) where T : IReaderWriter
    {
	serializer.SerializeValue(ref OriginPosition);
	serializer.SerializeValue(ref FinishPosition);
    }
    // ~INetworkSerializable
}

先ほどのようにClientRpcに引数を乗せることができないですが、上のようなStructにIDを乗せて共有すれば同じようなことができます。通信する値を一つ増やしてしまうので、使いすぎはよくないかもしれません。

まとめ

以上です。マルチプレイの実装が初めてなので、間違っているところもあると思いますが、参考にしていただけたらと思います。基本的には、迷ったら3つ目の方法をつかって実装するのが楽だと思います。

参考にさせていただいたサイト

https://docs-multiplayer.unity3d.com/netcode/current/about/

https://xrdnk.hateblo.jp/entry/2021/03/29/232721

https://synamon.hatenablog.com/entry/2021/11/22/190000

Discussion