新規Unityプロジェクトを作成し(2022年12月14日Unity2022.1.20f1)、以下の手順でホストアプリとクライアントアプリの両方に対応するものにしていきます。最終的には、ホストアプリはLinux用にビルドし、マルチユーザーサーバーとして動かし、いわゆる「NAT越え」の形でインターネット経由でクライアントアプリから接続できるようにしたいところ。クライアントアプリは、さまざまなプラットフォーム向けにビルドしマルチプラットフォームなアプリに。
Unityでホストアプリとクライアントアプリを作成
Unityで新規プロジェクトを作成し、ホストアプリ、クライアントアプリを同じプロジェクトとして作っていきます。
Netcode for GameObjectsとUnity Transportをインストール
Window > Package Manager から Add package by name で「com.unity.netcode.gameobjects
」と入れてAdd(2022年12月13日現在でversion 1.2.0)。これだけでUnity Transportも入るようになったみたい?
com.unity.netcode.adapter.utp
は入れなくていいようです。
NetworkManagerを準備
- Create Emptyで空のオブジェクトを作り、NetworkManager(任意)と名づけ、Add Componentで「Network Manager」を検索して追加。
- Select Transport で「Unity Transport」を選択。Unity TransportコンポーネントのConnection DataにAddress 127.0.0.1、Port 7777とあり、ローカル環境への接続はされるようになるのですが、ここは後からスクリプトで変更するようにするので、とりあえずそのままに。
プレハブ(操作するキャラ)を準備
- キューブ(任意の3Dオブジェクト)を作成し、Projectパネル上にドラッグしてプレハブを作成し、シーン内に作成したそのキューブは削除。
- Projectパネル上に作成されたプレハブを選択し、プロパティインスペクタからAdd Componentで「Network Object」を追加。
- NetworkManagerのNetwork Managerコンポーネントにある「Player Prefab」欄に上記で作成したプレハブをドラッグ&ドロップ。これで操作する自分のキャラを紐づけたことになる。
- Projectパネルでプレハブを選択した状態でプロパティインスペクタから新しいスクリプト「CharaController(任意の名称)」を作成追加する。
- 続いて、「Network Transform」もプレハブに追加。
- 作成したCharaControllerは、NetworkBehaviourを継承するようにし(using Unity.Netcode;
)、とりあえず方向キーで移動できるようにしてみる。
using System.Collections; using System.Collections.Generic; using UnityEngine; using Unity.Netcode; public class CharaController : NetworkBehaviour { private Vector3 pos; void Start() { if (this.IsOwner) { this.pos = transform.position; }; } void Update() { if (this.IsOwner) { //自分のキャラだけ操作するため float inputX = Input.GetAxis("Horizontal"); float inputY = Input.GetAxis("Vertical"); this.setServerRpc(inputX, inputY); } if (this.IsServer) { //サーバーから受け取った情報を反映 move(); } } //キャラを動かす [ServerRpc] private void setServerRpc(float x, float z) { this.pos.x += x; this.pos.z += z; move(); } private void move() { transform.position = this.pos; } //Netcode for GameObjects のサンプルにある //ClientNetworkTransformを使う方法もある }
画面上にボタンを用意して起動の仕方をテストできるようにする
テストボタンを二つ用意し、「Host」「Client」の挙動を試せるようにします。それぞれ、クリックされると、Unity.Netcode.NetworkManager.Singleton.StartHost();
、Unity.Netcode.NetworkManager.Singleton.StartClient();
を実行するようにします。
このテストボタンは、Network Managerのインスペクタパネル上にある「Start Host」や「Start Client」と同じ挙動をランタイム時に実行できるようにするボタンです。
- Hierarchyパネル上でCreate Emptyをし、「TestUI(任意)」と名づけ、そのオブジェクトにAdd Componentし、TestUI.cs を作成しときます。コードは以下のように。
using System.Collections; using System.Collections.Generic; using UnityEngine; public class TestUI : MonoBehaviour { public void StartHost() { Unity.Netcode.NetworkManager.Singleton.StartHost(); } public void StartClient() { Unity.Netcode.NetworkManager.Singleton.StartClient(); } }
- UI > Legacy > Button で二つボタンを作成し、それぞれのラベルに「Host」「Client」と記入。
- それらのボタンのOn Clickに上記で作成したTestUIを指定し、Functionはそれぞれ対応するメソッドを指定し、テストボタンを押したときに上記のコードが実行されるようにします。
Mac用にビルドし、ローカル環境で挙動を確かめる
Player SettingsのResolution and PresentationでFullscreen ModeをWindowed の1600 x 1200 位にし、MacOS用にビルドしてみます。(開発環境がMacOSなので)
書き出したアプリを複製し(あるいは書き出したアプリとUnity Editor上の再生で)、一つはホストボタンを押し、一つはクライアントボタンを押すと、プレハブが二つ生成され、マルチユーザー状態になったことが確認できると思います。これは接続先がデフォルトの127.0.0.1となっているため、ホストアプリにうまく繋がった状態です。
次に、このホストアプリを家の外にあるインターネット上のLinuxサーバー上に配置して、接続できるようにしてみようと思います。
Linux用 Dedicated Serverの作成
Sakura VPSやXSever VPSなどでUbuntu 22.10を用意して、最終的にはそこで動かす予定ですが(おそらく両者とも2週間程度のトライアル期間があるので、その期間中にテストすべし)、DigitalOcean が割と素早くテストできたのでオススメです。こちらの動画で説明してるのでほぼこの通りでいけます。(追記:AWSでもいいと思います。AWSの場合の注意点を書いておきました →「Unity : マルチプレイ用ホストLinux用 Dedicated ServerをAWSで起動」)
Dedicated Server ビルドとNAT越えまで解説してる動画はあまりなかったのですが、ほぼ唯一この方が最初から最後まで一通り試しています。その一方で上記の最新のNetcodeに対応した解説ではないため、DigitalOcean部分だけ参考にするといいです。
NAT越えで重要なところは、サーバーのIPアドレスとポート番号です。DigitalOceanで上記の通りサーバーを立ち上げると、どんなポートでもアクセスできる状態となると思います。テスト段階では、ファイアウォールや既存のオススメセットなどでポートに制限をかけない方がいいです。どこに問題があって接続できていないのかが非常に分かりづらいので。
うまくいってからポートの制限をかけていく手順がいいと思います。(公開後までほったらかしも良くないので)
パラメータを受け取ってホストアプリを起動できるようにする
Dedicated ServerとしてLinuxサーバー上に配置するビルドでは、上記のままビルドしてもだめです。Unity TransportコンポーネントのConnection DataでIPとPORTを指定しておく必要がありますが、サーバー起動時にパラメータで明示的にどのIPアドレスとポート番号でStartHostするかを設定できるようにしておいた方が便利です。
- Hierarchyパネル上でCreate Emptyをし、そのオブジェクトにAdd Componentし、AwakeController.cs を作成しときます。コードは以下のように。
Environment.GetCommandLineArgs
で起動時のパラメータを取得して分解(ちょっと強引な書き方ですが)
using System; using System.Collections; using System.Collections.Generic; using UnityEngine; using Unity.Netcode; using Unity.Netcode.Transports.UTP; public class AwakeController : MonoBehaviour { void Awake() { string[] args = Environment.GetCommandLineArgs(); string portNum = "7777"; string serverAddress = "127.0.0.1"; bool isListenServer = false; bool isServer = false; for (int i = 0; i < args.Length; i++) { if (args[i] == "-server") { Debug.Log("found -server"); isServer = true; // -server の次がある前提 serverAddress = args[i + 1]; } if (args[i] == "-p") { Debug.Log("found -p"); // -p の次がある前提 portNum = args[i + 1]; } if (args[i] == "-listen") { Debug.Log("found -listen"); // -listen の次がある前提 isListenServer = (args[i + 1] == "1"); } } if (isServer) { // Invoke("startHostFn", 1.0f); //invoke時に引数でポートを渡すようにしてみる StartCoroutine(startHostFn(1.0f, serverAddress, Convert.ToUInt16(portNum), isListenServer)); } else { //パラメータがない場合、1秒後にクライアントとして動作させる場合 //StartCoroutine(startClientFn(1.0f)); }; } IEnumerator startHostFn(float delay, string serverAddress, ushort portNum, bool isListenServer) { yield return new WaitForSeconds(delay); if (isListenServer) { NetworkManager.Singleton.GetComponent<UnityTransport>().SetConnectionData( serverAddress, // The IP address is a string portNum, // The port number is an unsigned short "0.0.0.0" ); } else { NetworkManager.Singleton.GetComponent<UnityTransport>().SetConnectionData( serverAddress, // The IP address is a string portNum // The port number is an unsigned short ); } Debug.Log("started as Host: " + serverAddress + " Port:" + portNum + " isListenSever:" + isListenServer); NetworkManager.Singleton.StartHost(); } IEnumerator startClientFn(float delay) { yield return new WaitForSeconds(delay); NetworkManager.Singleton.StartClient(); } }
Dedicated Serverとしてビルド
まず、Linux版のDedicated Serverとしてビルドするために、あまり通常ではインストールしていないモジュールを追加しておきます。Unity HubでAdd Moduleしておきましょう。
適当な場所に書き出すと「.x86_64」とつくファイルとその他フォルダ等を書き出すので、これらをサーバーにアップロードします。
DigitalOceanにLinuxサーバーを準備
VPSを準備してそこにホストアプリを配置します。素早くサーバーを準備できるので、DigitalOcean を使ってそこで接続テストしてみます。アカウントを作り、最初に用意されているfirst-projectにDropletsを一つ用意します。最初に$300くらいの期限付きクーポンがもらえるのでその範囲で色々試してみましょう。期限切れる頃には、そのまま継続するとその後クレジットカードから引き落とされ続ける可能性もあるので要注意。不要なものは削除しておくように。
新規Dropletを作成して以下のように最安でサーバーを用意。
接続テストが目的なので、まずは手軽なパスワードでのアクセスで設定。
作成後、1分ほどでグローバルIPを確認できます。ここのipv4の値です。Consoleを開いてcurl inet-ip.info
でもグローバルIPを調べられます。
Dedicated Server ビルドをサーバーへアップロード
ターミナルを開いてDedicated Serverビルドを書き出したフォルダのある階層へcd
で移動。書き出したビルドが入っているフォルダ名(下記の例では「linux」というフォルダ名)を指定して、サーバーの /home へアップロードします。
scp -r "linux" root@上記で調べたIP:/home
その後、「Are you sure you want to continue connecting?」に対しては「yes」で、次に 上記で設定したパスワードを聞かれるので入力すると、アップロードが開始されます。
サーバー上でホストアプリを起動してみる
DigitalOceanのコンソールか、ターミナルから ssh root@IPアドレス
+パスワードでサーバーに入り、cd /home
に移動し ls
コマンドで、上記でアップロードした「linux」がアップロードされていることを確認。 cd linux
して、 ls -la
で一覧を表示してみると、「.x86_64」のついたファイルが「-rwxr-xr-x」になっていることが確認できます。もしなってなかったら、chmod +x ファイル名.x86_64
で権限を変更しておきましょう。
いよいよ、起動してみます。
上記の「パラメータを受け取ってホストアプリを起動できるようにする」で起動時に引数を渡せるようにしておいたので、以下のように「-server」の後にIPアドレスを入れ、「-p」の後にポート番号を明示的に入力して起動させます。ちなみに、なぜ明示的に指定するような仕組みにしているかというと、何度もテストして接続がうまくいかなかったりしたVPSサービスがあったりで、何度も書き出し直すのが面倒だったからです。「-listen」というパラメータも付けていますが、この環境では関係なさそうだったので使いませんでした。0を渡しておけばOKです。ポート番号はなんでもいいですが、最初にうまくいったのが「56714」だったのでその値にしています。
./ファイル名.x86_64 -server IPアドレス -p ポート番号 -listen 0
ホストアプリが起動するとエラーも含めて色々ログが流れます。アプリを終了するときは、「Ctrl + C」です。ホストアプリはターミナルを終了してもプロセスが終了します。これは後にバックグラウンドで動作するようにする予定です。
ログの中に「started as Host: IPアドレス Port:ポート番号 isListenSever:False
」という行があれば、うまく起動したと見ていいと思います。この状態で接続待機状態です。
Unity Editorのクライアントアプリでホストアプリに接続してみる
上記のターミナルはそのままでホストアプリを待機状態にさせておいて、Unityから接続してみます。NetworkManagerにアタッチされているUnity TransportコンポーネントのConnection Data欄のAddressとPortに上記でホストアプリ(.x86_64)を起動した際に明示的に指定したアドレスとポートと同じ値を入力して、Unity Editorの再生ボタンを押し、「Client」ボタンを押してクライアントとして接続してみます。
画面中央に、プレハブが二つ現れ(一つはホストアプリ自身のもの。これはそのうち除外したいところ)、方向キーを押すとプレハブが左右前後に動くようになると思います。これは接続された証拠です。
ホストアプリのバックグラウンド起動とCRONによる監視
ここまで行ければ、あとはポートやファイアウォールをどうするかとか、Unityアプリをどう作るかとかの話になると思いますが、最後にバックグラウンド起動とそのプロセスの監視、ついでに落ちた時にSlackへ通知するところまでMEMOしておきます。
バックグラウンド起動とプロセスの表示
バックグラウンド起動は、頭にnohup
、お尻に&
を付けてコマンドを叩く感じ。
nohup /home/linux/ファイル名.x86_64 -server IPアドレス -p ポート番号 -listen 0 &
こうすると、ターミナルを閉じてもサーバー上で起動したままになってくれます。では、これの終了はどうするかですが、top
コマンドでその時点で起動しているプロセスのPIDを確認して、kill
コマンドで落とせます。(ホストアプリ起動直後に表示されるのもプロセスIDのようですね)topコマンドはCtrl + Cで終了できます。
kill プロセスID
ちなみに接続がうまくいかない時、UDPの確認をしたい時がありますが、ポートの確認は、ss -anu
あるいは、ss -atnu
で出力できます。
CRONを使って監視
なんらかのエラーでホストアプリのプロセスが終了した場合に再度自動的に立ち上げ直すのとSlackへの通知をするCRONをMEMOしておきます。
まず、Slackへの通知は、Incoming WebhockというAppを使うのでWebhock URLを取得します。以下で取得方法をMEMOしてあります。
あとは、CRONファイルを書いておけば、定期的にスクリプトを実行することでホストアプリの監視が行えます。ホストアプリのプロセスがなくなっていたら、上記のバックグランド起動を再度行い、ついでにSlackへ通知する、ということが書かれています。
CRONについてはこちらのサイトを参考にさせていただきました。
#! /bin/bash #監視対象プロセス PROCESS_NAME=ファイル名.x86_64 #プロセス数をカウント count=`ps -ef | grep $PROCESS_NAME | grep -v grep | wc -l` #該当のプロセスがない場合の処理 if [ $count = 0 ]; then #サービスを起動 echo "$PROCESS_NAME Down" echo "$PROCESS_NAME Start" #起動コマンド #sudo systemctl restart httpd.service nohup /home/linux/ファイル名.x86_64 -server IPアドレス -p ポート番号 -listen 0 # Slack通知 WEBHOOK_URL="https://hooks.slack.com/services/XXXXX" MESSAGE="ホストアプリがダウンしたので再起動しました。" PAYLOAD="payload={\"attachments\": [{ \"color\": \"#D00000\", \"text\": \"$MESSAGE\"}] }" curl -XPOST --data-urlencode "${PAYLOAD}" \ "$WEBHOOK_URL" > /dev/null else echo "$PROCESS_NAME OK" fi
上記のコードを「process_check.sh(任意)」という名前で「script(任意)」というフォルダに保存し、ホストアプリをアップロードしたのと同じように /home
(任意)にアップしておきます。
scp -r "script" root@IPアドレス:/home
権限を変更してCRONに登録すれば、プロセス終了を監査し始めるはずです。SSHで接続し、chmod o+x /home/script/process_check.sh
で権限変更。crontab -e
を実行すると、エディタとしてnanoが簡単だよ、というメッセージが出て選択すると編集画面に入ります。最後の行に以下を記述して保存(Ctrl + XのあとYを押してEnter)。
# m h dom mon dow command */1 * * * * /home/script/process_check.sh
最後にプロセスをわざと殺してSlackに通知がくるか確認してみます。top
でPIDを確認して kill プロセスID
を実行。CRONが正しく動作すれば、1分以内にSlackに通知がきて、ホストアプリが再起動するはずです。
Dedicated Serverとして動いているホストアプリのプレハブも落ちてくることが気になりますが、テストとしてはうまくいったのでここまでのMEMOとしておきます。何かわかったら追記します。
東京造形大学卒業後、マクロメディア(現アドビ)に入社。QAやテクニカルサポートマネージャーとしてFlash、DreamweaverなどのWeb製品を担当。独立後、2007年に虫カゴデザインスタジオ株式会社を設立。2021年東京三鷹を拠点に。最近は、Unity, Unity Netcode for GameObjects, CakePHP, Laravel, ZBrush, Modo, Adobe Substance 3D, Adobe Firefly, Xcode, Apple Vision Pro, Firebaseにフォーカスしています。モバイルアプリ開発情報を主としたブログ「MUSHIKAGO APPS MEMO」の中の人。