📖

Netcode for GameObjectsでプレイヤー間共通のObjectのtransformを操作する。

2023/08/25に公開

以前の、Netcode for GameObjectsでの値の共有方法まとめ という記事でNetworkTransformでtransformを操作する場合は、オーナー権限が必要であり、共通のオブジェクトをオーナー以外のプレイヤーも操作する場合は利用できないという制約がありました。

全ユーザーが操作できるオブジェクトを作りたかったので、NetworkVariableとUniRxを利用してtransformのlocalPositionを共有するコードを作成しました。
(このコード自体を実際に、動かしてはいないのでエラーはいたらすいません。。)

using UnityEngine;
using UniRx;
using Unity.Netcode;
using Cysharp.Threading.Tasks;

public class TransformAsync : Unity.Netcode.NetworkBehaviour
{
   // 監視対象のtransform
   [SerializeField] private Transform _transform;
   
   // transformのlocalPositionを監視するobservable
   private IReadOnlyReactiveProperty<Vector3> _localPositionObservable;
   
   // localPositionを共有するためのnetworkVariable
   NetworkVariable<Vector3> LocalPos = new NetworkVariable<Vector3>();
   
   // 現在transformを操作しているClientIDを共有する
   private NetworkVariable<ulong> OperatingClientId = new NetworkVariable<ulong>();

   // ClientIdの初期値、netcodeではHostのClientIdが0であるためそれと区別する。
   private readonly ulong defaultClientId = 99999;
   
   // 自分含めて誰かが操作しているフラグ 
   private bool someOneOperating;
   
   // 自分が操作しているフラグ
   private bool imOperating;
  
  // 現在自分が操作可能かのフラグ
   public bool CanOperate;
   private void Start()
   {
       // transformのObservableを作成
       _localPositionObservable = _transform
           .ObserveEveryValueChanged(x => x.localPosition)
           .ToReactiveProperty<Vector3>();

       // RPCでサーバー共有するためのSubscribe
       //自分に操作権がある時だけRPCする。
       _localPositionObservable 
           .Where(x => isImOperating)
           .Subscribe(x=>
           {
               if((NetworkManager.Singleton.IsConnectedClient || NetworkManager.Singleton.IsListening))
                   SetLocalPositionServerRPC(x);
           }).AddTo(this);
   
    // Hostとしてサーバーを立ち上げたときのイベント
       NetworkManager.Singleton.OnServerStarted += OnServerStarted;

       // Clientとしてネットワークに入った際のイベント
       NetworkManager.Singleton.OnServerStarted += OnStartClient;

       // ローカルポジションのnetworkVariable変更をを受けるイベント
       LocalPos.OnValueChanged += OnChangeLocalPos;
   
    // ClientIdのnetworkVariableの変更を受け取るイベント
    OperatingClientId.OnValueChanged += OnOperatingClientChange
   
   }
   private void OnServerStarted()
   {
   // サーバが立ち上がった時にClientIdの初期値をいれる。 
       OperatingClientId.Value = defaultClientId;
   }
   
   private void OnStartClient()
   {
   // ネットワークに入った際に共有されているポジションで初期化
   transform.localPosition = LocalPos.Value;
   // 現在操作しているIDを自分IDの比較
   imOperating = OperatingClientId.Value == NetworkManager.Singleton.LocalClientId;
   // 現在操作しているIDとデフォルト値を比較
       someoneOperating = OperatingClientId.Value != defaultClientId;
   }

   // 自身が操作したローカルポジションを共有するRPC
   [ServerRpc(RequireOwnership = false)]
   private void SetLocalPositionServerRPC(Vector3 localPos)
   {
       LocalPos.Value = localPos;
   }
  
  
   private void OnChangeLocalPos(Vector3 pre, Vector3 current)
   {
     // 自分の操作はすでに反映されているので、
       // 自分が操作してない場合は共有された値を入れる。
   
       if(!isImOperating)
       {
           _transform.localPosition = current;
       }
   }
   
   // 操作権限をリクエストするRPC
       [Unity.Netcode.ServerRpc(RequireOwnership = false)]
   public void RequestClientIdChangeServerRpc(bool isActive, ServerRpcParams serverRpcParams = default)
   {
    // 自分以外のPlayerが利用していたらreturn
       if (OperatingClientId.Value != serverRpcParams.Receive.SenderClientId && OperatingClientId.Value != defaultClientId)
           return;
   
   // 誰も利用していないもしくは、自分が利用している場合は権限を操作する。
       if (isActive)
           OperatingClientId.Value = serverRpcParams.Receive.SenderClientId;
       else
       {
           OperatingClientId.Value = defaultClientId;
       }
   }
   
   // RPCは外から利用できないので外出し用の関数
   public void RequestClientIdChange(bool isActive)
   {
   RequestClientIdChangeServerRpc(isActive)
   }
   
   // 操作権限が変更されたらフラグを更新する。
   private void OnOperatingClientChange(ulong pre, ulong current)
   {
       imOperating = current == NetworkManager.Singleton.LocalClientId;
       someoneOperating = current != defaultClientId;
   CanOperate = imOperating || !someoneOperating;
   }
}

本来分けるべき機能をめんどくさかったので一緒のクラスにしたのでややこしくなっていますが、

  • 操作しているプレイヤーのClientIdを全体で共有することで、現在操作する権限を持つプレイヤーを共有する。
  • 権限を持っているPlayerのtransformが動いた場合は、transformをPRCで共有し、そうでない場合はnetworkVariableの変更を受け取ってtransformに反映する。

ということをしています。

あとは、この監視中のtransformを動かす場合はRequestClientIdChange()を叩いて権限を取得できるかリクエストを行い、CanOperate == trueになっている状態の時だけtransformを操作すれば問題ないです。
例えば、下みたいなコードを操作したいtransformに張り付ければ操作できるはずです。

private Vector3 _targetPos;
[SerializeField] private TransformAsync _transformAsync;

private void SetTargetPosition(Vector3 target)
{
   _targetPos = target;
   _transformAsync.RequestClientIdChange(true)
}

void Update()
{
   if(_transformAsync.CanOperate)
   	this.transform = _targetPos;
}

ポイントとしては、値を共有してtransformに代入して動かすのではなく、transformは何も考えずに操作できるようにした点です。これによって、DOTweenやほかの関数をtransformに直接適用した場合でも共有できるようにできました。

感想

実際にモノを作ってみると、結局NetworkTransform等の簡単実装クラスよりも。ちょっと深く触ることになり、RPC周りについてとても勉強になりました。公式のnetwoerking Synchronizationの項がnetcode関係なくとてもいい資料だったのでとても助かりました。

Discussion