Unity でローカルネットワークマルチプレイを行うフレームワーク。通称NGO。
今回はハタ揚げを2人プレイで行うために利用した。基本的な使い方からVRでの実装にあたっての工夫までを簡単にまとめる。
なお、NGOは活発にアップデートされている最中のフレームワークなので、意外にすぐ情報が古くなるので注意されたい。 今回の制作ではバージョン 1.9.1 を用いており、以下の説明ではなるべく最新 (執筆時点での最新バージョン 2.1.1) の情報を扱うよう心がけるが、一部古い情報が含まれることがあるかもしれない。
また、これを書いている本人もマルチプレイ実装はほとんど初めての試みだったので間違いや非効率な考え方が含まれうる可能性は十分にあることを了承いただきたい。
(ちょっと前のバージョンではあるが、Unity Japan が公開しているチュートリアル「Netcode for GameObjectsを使ってみよう - Unityステーション」をやった際のドキュメントはこちら。)
コードや実装方法の前に、NGOでのマルチプレイがどのような考え方のもとでつくられるかを述べたい。
ここをちゃんと理解できていれば、制作に行き詰まったときの道筋が見える……かもしれない。
NGOでのマルチプレイは基本的に “サーバクライアント型” のトポロジーを想定しており、これには “専用サーバ方式 (Dedicated server)” や “クライアントホスト型待機方式 (Client-hosted listen server)” などがある。
前者は中央集権的な構造で、専用のマシンで動かす「サーバ」という立場があり、そこに「クライアント」としてすべてのプレイヤーは接続してプレイする。このようなサーバは Dedicated server とも呼ばれる。
一般に流通している多くのネットワークゲーム (Apex Ledgends や原神など) で想像するのはこの方式だとおもう。
この方式のメリットは
などがある。
一方でデメリットは
などがある。
ハタ揚げVRで採用したのは後者の「クライアントホスト型待機方式」のマルチプレイである。
これは、プレイヤーのひとりが「ホスト」としての役割を担い、自身もプレイヤーのひとりでありながら、他のプレイヤーからの接続を待機する。このとき接続してくるプレイヤーを「クライアント」と呼ぶ。
この方式は、
というメリットがある一方で、
などのデメリットもある。
以上のようなネットワークのトポロジーについて、詳しくは公式ドキュメントを参照すると良い。
今回NGOで構築したネットワークマルチプレイは、LAN内でのマルチプレイを行っている。
インターネットを介したマルチプレイ (いわゆるNAT越え) も実装できるらしいが、今回はここには触れない。また外部のサービスである Photon Engine などをつかうほうが実装しやすい可能性も十分にある。
NGOでのローカルネット通信はUDPによる通信を行う。そのため、端末のファイアウォール設定でゲームプログラムや Unity Editor のUDP通信 (ポート番号は任意に変えられるがデフォルトは7777) を通すようにしておく必要がある。
大抵は初回実行時に通信の許可を求めるプロンプトが出てくるとおもうが、見逃した場合などはこの点を手動で設定する必要があることに注意しよう。
また、ローカル通信には通信先のデバイスのローカルIPアドレスを利用する。Windows であればコマンドプロンプトに ipconfig
と打てばすぐに確認できるが、モバイル端末や Quest などのVR機器では確認するのに手間を要すことがある。また、そもそもルータのDHCPによる動的割り当てのせいでIPアドレスが変動することもあるため、いちいち実行時にアドレスを確認するのが面倒かもしれない。
その場合は、Network Discovery という仕組みが用意されているので、こちらを用いるとよいかもしれない。
NGOでのマルチプレイでは、ローカル・グローバルのふたつの空間が厳密に管理されている。ここは特にしっかりと理解しておきたいポイントだとおもう。
ローカルな空間とは、普段 Unity でゲームをつくる際にオブジェクトを配置したりスクリプトを組んだりしてつくられた「シーン」のことだと思ってよい。Unity で一般的に使われる Monobehaviour
という名前空間で扱われるスクリプトやオブジェクトは、ローカル空間のみに作用するものである。
グローバルな空間とは、後述する NetworkObject
コンポーネントが付加されたオブジェクトによって構成される、マルチプレイ中に同期される空間のことである。
そしてNGOでは原則としてグローバル空間のすべてのオブジェクトはホスト/サーバが所有するという特徴がある。
したがって、例えば以下のようなことに注意しておく必要がある。
AddForce()
で動かしたり、SetActive()
で表示/非表示にしたり……) できるのはホスト/サーバだけであるInstantiate()
したり、ローカル空間のみで扱われる変数を変更したりしても、その内容は他のクライアントやホスト/サーバには反映されない
NetworkVariable
と呼ばれるグローバル空間で扱う専用の変数があるためこのような特徴から、マルチプレイを構築する際には誰が何の値/オブジェクトを持っており、どのように変更したいのかなどの仕組みををしっかりと管理する必要がある。なお、グローバル空間に作用するスクリプトを実行したい場合は、RPC (Remote Procedure Call) という仕組みを使うことができる。
NGOでマルチプレイを実装するために必要となるコンポーネントやスクリプトの書き方などがあるので簡単に紹介する。
NetworkBehaviour
クラス
MonoBehaviour
というクラスを用いているが、NBはこれを継承したものである
IsOwner
などの頻出するプロパティを扱ったり、OnNetworkSpawn
などのメソッドを用いたりできるNetworkObject
コンポーネントが付加されている必要があるNetworkObject
コンポーネント
NGOのインストールなどはこちらを参照して行う。 (要件などもここにある)
このセクションでは公式ドキュメントのクイックスタートの内容を主に説明する。
NGOマルチプレイでは、まず NetworkManager
と呼ばれる、マルチプレイ全体を管理するモノを作成する。
ヒエラルキーで 右クリック > Create Empty
で空オブジェクトをつくり、NetworkManager
と名付ける。
NetworkManager
を選択し、NetworkManager
コンポーネントを追加する。
追加したコンポーネントで黄色の⚠️マークが付いている Select Transport を選択し、Unity Transport
を選択する。
Unity Transport のコンポーネントが追加されるので、ここで接続情報の設定などを行うことができる。基本的にそのままで良いが、Address
と Port
はホスト/サーバーとして動いている端末を指定する。(デフォルトの 127.0.0.1
は localhost
つまり自分自身の端末を示す。デバッグ用に自分のPCでマルチプレイするのに便利。)
また、Allow Remote Connections?
にチェックを入れておかないと他端末からの接続ができないので注意しよう。
ちなみに、Debug Simulator
の項目では通信時に遅延やジッター (Ping値の揺らぎ) が生じた場合を想定したシミュレーションが行える。高遅延の環境でどのような動作をするのかを試したいときに使える。
マルチプレイに参加したときにプレイヤーのアバターとして登場させるオブジェクトを登録することができる。
Unity で Prefab をつくるだけで特に難しいことをする必要はなく、単なるカプセルモデルなどでも問題ない。ただし、登録するオブジェクトは子も含めてすべて NetworkObject
コンポーネントを付ける必要がある。
作成した Prefab を NetworkManager
にセットすれば良い。クライアントが接続したとき、ここにセットした Prefab がホスト/サーバー側のシーンに複製されて出現するようになる。
VRゲームの場合は例えば「頭」と「両手」「体」などを表すオブジェクトをひとつの親にまとめてしまえばいい。
※VRの場合は OVRCameraRig
などを追加すればよいのでは?と思うかもしれないが、それはできない。その場合の実装方法は後述する。
現在のままでもマルチプレイを実行してログインすることはできる。 (NetworkManager
の下方にある StartHost
などを押せば良い)
しかし、エディタ側でしか操作できないのは不便なので、Unity スクリプトをつかってログインやホストの開始などを行うようにする。
NGOでは「ホストの開始」や「クライアントの開始 (=ホスト/サーバーへの接続を試行)」などを行う関数が用意されている。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Unity.Netcode;
public class NetworkConnect : NetworkBehaviour
{
public void StartHost()
{
NetworkManager.Singleton.StartHost();
}
public void StartClient()
{
NetworkManager.Singleton.StartClient();
}
}
適当な Empty Object や Network Manager をつけたオブジェクトなどにこのスクリプトをアタッチし、どのような方法でもいいのでこれらの関数が実行されればよい。(例えば今作ではテスト用にキーボードのHキーを StartHost()
、Cキーを StartClient()
の呼び出しに振っていた)
関数を実行すると、ホストとしてログインして接続を待機したり、Unity Transport
コンポーネントで指定したIPアドレス/ポート番号へしてログインしたりできる。
※マルチプレイを実行中は NetworkManager
のオブジェクトがシーンから見えなくなるが、一番下の DontDestroyOnLoad
の中に移動しているだけ。
以上でマルチプレイゲームの骨組みはできあがった。試しにビルドするなどして複数のインスタンスでゲームを実行し、同じ空間にログインできるか試してみよう。
試しに実行した様子は下の画像をクリックすることで確認できる。
Unity のスクリプトでは、例えばプレイヤーの入力に対する動作を記載したり、オブジェクトを操作したりすることがあるだろう。
マルチプレイにおいては、「誰が」その操作をしたのかを明記する必要がある。
例えばプレイヤーがそれぞれのアバターを操作する場合を考えてみると、自身以外の操作によって動かされることは避けなければならない。しかし、ホスト側から見ればすべてのプレイヤーが同一のシーンに存在しているため、単にアバターに付けたスクリプトで入力に対する移動操作を記載するだけでは、すべてのプレイヤーに対して同じ動きを命令することになってしまう。
そこで使うのが Netcode.Singleton.IsOwner
という bool 型のプロパティだ。
(スクリプトの始めのインクルード部分に using Unity.Netcode
と記載しておけば、単に IsOwner
と書ける)
このプロパティは、自分がそのオブジェクトの所有権を持っているかを返してくれる。したがって、
if(IsOwner)
{
// 入力に対する操作内容など
}
と囲んであげることで、自身に所有権があるオブジェクトのみに対して操作を行うようにできる。
(あるいはコードの序盤に if(!IsOwner) return;
としてスルーさせてもいいかも?)
同様のプロパティとして IsClient
(クライアントであるか)、IsServer
(サーバーであるか)、IsHost
(ホストであるか) がある。
注意したいのは「ホスト」となったプレイヤーは、ホストでもありクライアントとしても扱われる点である。したがって、「ホストであるか」は IsHost
の True/False で判断できるが、「クライアントであるか」は !IsHost && IsClient
というようにAND条件をつけて指定しなければならない。
(もちろんホストも含めた「クライアントであるか」を判断したいなら IsClient
だけで良い)
Meta Quest のプラグインをつかって実装する場合、ちょっと工夫がいる。
Meta SDK (旧 Oculus Integration) に含まれる OVRCameraRig
はVRヘッドセットの視点映像を映すカメラやコントローラなどをまとめたオブジェクトとして使われているが、これは原則としてシーンにひとつしか存在できない (Unity のカメラの仕組みからも、ひとつのディスプレイにはひとつのカメラしか設定できないはず)。
つまり、マルチプレイに参加したときのアバターとしてこれを設定してしまうと、ホスト/サーバー側のシーンに OVRCameraRig
が複数存在してしまうことになり、正常に動作しない。
そこでVRゲームのマルチプレイを行う際には、ホスト/サーバー側のシーンに存在する OVRCameraRig
を各プレイヤーそれぞれの動きに追随させるという手法をとった。(このアイデアはKuMAのメンバーによるものである。)
具体的なスクリプトはこちらに記載した。
※プレイヤーのログイン時に OVRCameraRig
を Player Prefab の子として設定する方法も考えたが、うまくいかなかったので断念した。
余談だが、Unity 標準のXR開発用プラグイン “XR Interaction Toolkit (XRIT)” をつかうのも手かもしれない。こちらは Quest 独自の機能 (オクルージョンやマルチモーダル機能など) は使えないものの、Pico や Vive などのマルチプラットフォームの開発が容易であったり、内部まで柔軟な開発ができる可能性があったりする。
(今回の制作の序盤ではXRITに少しだけ触れたのだが、結局 Meta Quest SDK を用いた。)
NGOでは非常に多くの仕組みや機能が提供されているが、その中でも今回の制作で活用したものについて紹介・簡単な解説をする。
シーンの何らかの状態を示す変数やゲーム全体で共有されるべき数値など、すべてのプレイヤー間で共有したい変数があるとおもう。そこで活躍するのが NetworkVariable
と呼ばれる特殊な型の変数である。(公式ドキュメント)
NetworkVariable (以後、縮めてNVと書く) はRPCのようにクライアント→サーバーのように一方向的なものではなく、値の変更が即座にすべてのプレイヤー/サーバーの間で反映されるようになっている。
NVとして扱える値の種類には限りがある。具体的には bool や int、float、Vector3 などである。文字列は事前に 32/64/128/512/4096 のバイト数を指定することで扱える。
逆に扱えない種類としては GameObject などがある。したがって、ネットワーク越しに GameObject が null であるかを確認するようなことはできない。 (サポートされている値の種類はこちらから)
NVとして変数を宣言したい場合は、名前空間を Network Behaviour
に変更し、public NetworkVariable<bool> myNetworkVariable;
のように宣言することで使えるようになる。しかし、宣言するだけでは使えず、読み取り/書き込みの権限を設定する必要がある。
設定は Awake()
などの関数の中、もしくは宣言時に行うことができ、
myNetworkVariable = new NetworkVariable<bool>( // <>の中で値の種類を設定する
true, // デフォルトの値
NetworkVariableReadPermission.Everyone, // 読み取り権限の対象
NetworkVariableWritePermission.Owner // 書き込み権限の対象
);
のように初期化を行うことができる。
デフォルトでは、ホスト/サーバーは読み取りと書き込みの両方の権限を持っており、クライアントは読み取りの権限のみを持っている。
Everyone
(全員) か Owner
(そのNetworkObjectの所有権がある人のみ) を設定できるServer
(ホストやサーバー) か Owner
を設定できるまた、そもそもNVの宣言/初期化にはオブジェクトの所有権が必要であることに注意。(シーンに存在するオブジェクトは原則としてホスト/サーバーのみが所有権を持っている)
NVに設定した値を扱いたい場合は、myNetworkVariable.Value
のように .Value
属性をつけることで値を読み取ったり書き込んだりすることができる。その際、.Value
をつけたNVは、初期化時に設定した型 (int や bool など) として扱われるので、普通の変数と変わらず使用できる。
また、今回は使用しなかったが、NVの変更を検知することもできる。NetworkVariable.OnValueChanged
というイベントを登録しておくことで、NVの変更があった場合に変更前後の2つの値を取得することができる。
NGOの機能とは関係が無いが、マルチプレイ開発をスムーズに行うためのツールとして ParrelSync を活用した。
このツールを使えば全く同じプロジェクトを同一のPCの中で開くことができるようになる。
メニューから Clone Manager を開くことでクローンを作成できる。
開かれたプロジェクトはオリジナルのものと全く同一のものであるが、クローン側での編集は不可能という制限がある。 (編集して保存しようとするとエラーが出る)
オリジナルの方での編集が即座に反映されるため、同じ環境を手間なく用意することができる。
これを用いることでひとつのPCだけでマルチプレイのテストを行うことができるため、開発の効率化につながった。