Netcode for GameObjectsの機能紹介 NetworkTransformコンポーネントについて

f:id:fb8r5jymw6fd:20211202232056p:plain

 

はじめに

エンジニアの松原です。この記事はSynamon Advent Calendar 2021の4日目の記事になります。

前回の記事では簡単にNetcode for GameObjectsの内容について触れましたが、これら以外にもUNetやMLAPIで用意されていた機能を引き継いで、Netcode for GameObjectsの機能として利用できるものがあります。

この記事ではその機能の一つである、NetworkTransformコンポーネントについて紹介したいと思います。

今回も前回に引き続き、今回の内容について個人のGitHubリポジトリにアップロードしておりますので、もしよろしければ触ってみてください!

 

今回の機能を反映したサンプルシーン名はSampleScene_NetworkTransform.unityになります

f:id:fb8r5jymw6fd:20211202232951p:plain

 

NetworkTransformについて

f:id:fb8r5jymw6fd:20211202084250p:plain

NetworkTransformは前回の記事中に登場したNetworkBehaviourを継承したクラスで、Player(Player Prefab)の位置情報に関する更新を処理してくれる補助系のコンポーネントに位置づけされています。補助系とはいえ、マルチプレイを実現するためには必須としての機能が詰まっています。以下にその機能について解説します。

 

Transform(位置、回転、スケール)の同期処理

前回の記事では、公式のチュートリアル Binding on "Hello World" のコードを改造し、直接NetworkBehaviour継承クラスにPlayerの位置更新処理を行うコードを実装していました。通常のMonobehaviourではそれで問題ないのですが、NetworkBehaviourではサーバー側に更新処理を通知するRPCを送信する仕組みが必要なため、少し勝手が変わってきます。

 

前回の記事ではPlayerの位置情報を更新するためのRPCを呼び出す処理が更新がクライアント側の端末でUpdate()が呼び出されるたび発生しており、結果としてネットワークの送受信の処理が負荷がかかる実装になっていました。

NetworkTransformでは、処理負荷を減らすための設計が含まれています。以前に送信したTransform情報と現在のTransform情報の差分を取得、その差分がしきい値を超えている状態(ダーティな状態)であれば現在のTransform情報をRPCを通してサーバー側に送る仕組みになっています。

このしきい値はNetworkTransformのインスペクタから変更が可能になっています。位置情報、回転情報、スケール情報の各しきい値を変更できるようになっており、このしきい値を大きくすれば同期処理の間隔は短くなります。全くキャラクターを操作しない場合は位置情報の更新についての処理そのものが送信されなくなります。

f:id:fb8r5jymw6fd:20211202093612p:plain

 

また、必要ないと思われるパラメータを同期処理の対象から外すことができます。以下のように、通常のオンラインゲームではTransformのスケール情報はあまり扱わないと思うので、チェックを外しておくのが良さそうです。

f:id:fb8r5jymw6fd:20211202090152p:plain

 

Interpolate(位置補完)処理

Transform情報の更新間隔が荒いほど、動きのカクつきが目立つようになります。それを解決するための機能として、NetworkTranformではInterpolate(位置補完)機能が利用できます。これは同期毎のTransform情報間の差分情報を利用して、新しいデータを受け取るまでに位置の補完処理を自動で行うようになります。

f:id:fb8r5jymw6fd:20211202200703p:plain

 

Interpolateが動いている状態と動いていない状態の違いは想定しづらい思いますので、それぞれ以下の条件でInterpolateありなしの状態を比較しました。しきい値の設定は以下のようになっています。

f:id:fb8r5jymw6fd:20211202211054p:plain

 

実際にInterpolateありとなしでそれぞれ動いている動画をgifにしました。

Interpolateありでのアニメーションです。

f:id:fb8r5jymw6fd:20211202213530g:plain

Interpolateなしでのアニメーションです。

f:id:fb8r5jymw6fd:20211202213608g:plain

このようにInterpolateのありではしきい値が高めでもがっつり補完処理を掛けてくれます、その上通信負荷も減るので使用するメリットは大きいと思います。

ただ、補完処理そのものはCPU上で計算されているようなので、たくさんしないといけない場合はInterpolateの使い分けは必要になってくるかもしれません。

 

更新対象をローカル座標に変更する(In Local Space有効)

NetworkTransformはデフォルトではグローバル座標を対象とした更新処理がかかります。(コードとしては transform.position や transform.rotation など)

In Local Spaceのオプションを有効にすることで、更新対象をローカル座標に変更することができます。(コードとしては transform.localPosition や transform.localRotationなど)

f:id:fb8r5jymw6fd:20211202214808p:plain

 

以上でNetworkTransformについての機能紹介になります。便利な機能を持っていますが、実はNetcode for GameObjectsのバージョン1.0.0では、更新権限についてはNetworkBehaviourの特徴を引き継いでおり、ransformの書き換えはサーバーまたはホスト側の端末のみしか行えません。

クライアント側に相当する端末ではtransformを書き換えられないので、前回紹介したようなダミー表示を利用するなどの方法を間に挟む必要があります。

ただし、Unityの公式側でこの問題を解消するために、クライアント側端末でもtransformを更新できるようになるコンポーネントがあります。それが以下に紹介するClientNetworkTransformになります。

 

ClientNetworkTransform

ClientNetworkTransformはクライアント側端末でもtransformの更新を取り扱えるようになるコンポーネントです。このコンポーネントはNetworkTransformのクラスを継承して作られています。

厳密には他の端末への同期処理はサーバーへRPCを送信することで実現しており、実質的にはサーバー側で同期処理が行われ、サーバーから各クライアントへ位置情報の更新情報が送られているのには変わりません。

それであっても、書かないといけないコード量がだいぶ減るので、Netcode for GameObjectsを初めて触る人にはお勧めしたいコンポーネントです。

f:id:fb8r5jymw6fd:20211202220154p:plain

 

インストール方法

このコンポーネントは通常のNetcode for GameObjectsには含まれていません。サンプルとして含まれているので、サンプルをインポートすることでAssetに追加することができます。

f:id:fb8r5jymw6fd:20211202221320p:plain

 

前回の内容からClientNetworkTranformを利用できるように変更する

前回取り上げたNetcode for GameObjectsのサンプルリポジトリをClientNetworkTransformに対応したものに置き換えてみました。今回はPlayer_NetworkTransformというPrefabを用意しました。

PlayerSyncTransformBehaviourというコンポーネントは前回記事のPlayerSyncBehaviourをClientNetworkTransformに対応させたものになります。

f:id:fb8r5jymw6fd:20211202222203p:plain

 

以下はPlayerSyncTransformBehaviourのクラスのコードになります。前回紹介したPlayerSyncBehaviourのクラスより、大幅にコード量が減りました。表示用ダミーを用意しなくて済むようになったのは大きなメリットです。

#nullable enable

using Unity.Netcode;
using UnityEngine;
using UnityEngine.Events;

public class PlayerSyncTransformBehaviour : NetworkBehaviour
{
[SerializeField]
private float speed = 5f;

private PlayerInputHelper? playerInputHelper = null;

[SerializeField]
private UnityEvent<GameObject>? onTrackingObjectPresented;

public override void OnNetworkSpawn()
{
if (IsOwner)
{
playerInputHelper = FindObjectOfType<PlayerInputHelper>();

onTrackingObjectPresented?.Invoke(gameObject);

SetRandomPosition();
}
}

private void SetRandomPosition()
{
var position = new Vector3(Random.Range(-3f, 3f), 0f, Random.Range(-3f, 3f));
transform.position = position;
}

private void UpdatePlayerInputs(float delta)
{
if (playerInputHelper == null) return;

if (playerInputHelper!.HasMoveInput)
{
var move = playerInputHelper!.Move;
var direction = new Vector3(move.x, 0f, move.y);

var position = transform.position;
transform.LookAt(direction + position, Vector3.up);
transform.position += direction * speed * delta;
}
}

private void Update()
{
var delta = Time.deltaTime;

if (IsOwner)
{
UpdatePlayerInputs(delta);
}
}
}

 

ClientNetworkTransform(NetworkTransform)の課題

ClientNetworkTransformは便利な機能ですが、更新間隔がTransform情報の更新差分に対するしきい値で送信頻度を判断するため、高速で動く物体や高速で回転する物体等、Transformの数値が大きく変更されるユースケースにはあまりマッチしていないと思います。

また、Interpolationの実態はBufferedLinearInterpolatorというクラスで補完処理が行われています。このクラスの補完処理の想定外の動きをするTransformは挙動が安定しない可能性があります。競技性の高いゲームでは補完処理の実装に気を使う必要が出てくるため、あえて自前で実装していく必要があるかもしれません。

 

最後に

Unity公式のネットワークシステムは以前のUNetやMLAPIの機能を色濃く継承していますが、以前からの仕様の変更点も多いので、しばらくは自らソースコードを読むなどの努力が必要になるかと思います。引き続きNetcode for GameObjectsについて追っていきたいと思います!

 

謝辞

今回ClientNetworkTransformの情報を調べるにあたり、Denikさんの下記のブログ記事を参考にさせていただきました。ありがとうございました!

 

宣伝

今年のSynamonはアドベントカレンダーを実施しております、よろしければ他の記事もお読みいただけると嬉しいです!

qiita.com

 

また、弊社ではUnityエンジニアを募集しています。興味がある方は是非以下のページを覗いてみてください!

twitter.com

meety.net

herp.careers