初めまして。
初投稿です。
未熟も未熟者ですが、よろしくお願いいたします。
早速ですが掲題の通り、
WebSocketを使ったUnityのゲームの作り方、サーバの用意の仕方とかを紹介したいと思います。
初心者でも分かるように、スクショ多めにがんばります。
環境
- Windows10
- Unity 2019.2.2f1
- Visual Studio Community 2019
おおまかな流れ
とりあえず簡単なチャットアプリを作りたいと思います。
やり方ですが、一つのプロジェクト上に、
サーバ側でのみ使うシーン、
クライアント側でのみ使うシーンの2つを作ります。
クライアント側のシーンは、サーバへデータを送信する処理を、
サーバ側のシーンは、クライアントからのデータ送信を待ち受ける処理を実行させることで、
サーバ、クライアント間でのデータ送受信を実現したいと思います。
プロジェクトにwebsocket-sharpを適用しよう
前準備
websocket-sharpのgithubのページに行き、ソースをzipでダウンロードしましょう。
ダウンロードしたらファイルを解凍し、中のwebsocket-sharp.slnをダブルクリックしてVisualStudioを起動させましょう。
ソリューションエクスプローラーから、Exampleは消してしまいましょう。
このあと、ビルドをしたいのですが、websocket-sharp\Net\CookieException.csでエラーが起きてしまいますので、
下記のように書き換えます。
(下記記事より引用)
https://techblog.primestructure.co.jp/2019/06/28/unity-%E3%81%A7-websocket-%E3%83%A9%E3%82%A4%E3%83%96%E3%83%A9%E3%83%AA%E3%82%92%E7%94%A8%E3%81%84%E3%81%A6%E3%83%AA%E3%82%A2%E3%83%AB%E3%82%BF%E3%82%A4%E3%83%A0%E9%80%9A%E4%BF%A1%E3%81%99%E3%82%8B/
public class CookieException : FormatException
{
public class CookieException : FormatException, ISerializable
{
ソリューション構成を、「Release」にします。
以上の手順が済んだら、ビルドをしましょう。
正常にビルドが終了すると、websocket-sharp\bin\Release上にwebsocket-sharp.dllができるはずです。これを、Unityのプロジェクトに適用します。
websocket-sharpの適用
Unityのプロジェクトを新規に作りましょう。ここでは「SampleProject」と呼称しておきます。
次に、Assetsフォルダの中に、Pluginsフォルダを作ります。
その中に、さきほど生成したwebsocket-sharp.dllを入れましょう。
これで、SampleProjectはWebSocketを使うための準備が整いました。
クライアント側の実装
Sceneフォルダに、「ClientScene」というシーンを作り、その配下に以下のオブジェクトを用意しておきましょう。
- ChatText・・・チャットを表示するためのText
- MessageInput・・・メッセージを送信するためのInputField
- SendButton・・・メッセージ送信処理をするためのButton
- ClientManager・・・WebSocketでサーバとの処理をするための空オブジェクト
次に、プログラムを作っていきましょう。
Assetsフォルダの中に、Scriptsフォルダを作り、その中に「ClientManager.cs」というC#スクリプトを用意します。
ClientManager.csの中身には、
サーバへの接続処理、メッセージの送信処理、メッセージ表示処理を用意しておきます。
using UnityEngine;
using WebSocketSharp;
using WebSocketSharp.Net;
using UnityEngine.UI;
public class ClientManager : MonoBehaviour
{
public WebSocket ws;
public Text chatText;
public Button sendButton;
public InputField messageInput;
//サーバへ、メッセージを送信する
public void SendText()
{
ws.Send(messageInput.text);
}
//サーバから受け取ったメッセージを、ChatTextに表示する
public void RecvText(string text)
{
chatText.text += (text + "\n");
}
//サーバの接続が切れたときのメッセージを、ChatTextに表示する
public void RecvClose()
{
chatText.text = ("Close.");
}
void Start()
{
//接続処理。接続先サーバと、ポート番号を指定する
ws = new WebSocket("ws://localhost:12345/");
ws.Connect();
//送信ボタンが押されたときに実行する処理「SendText」を登録する
sendButton.onClick.AddListener(SendText);
//サーバからメッセージを受信したときに実行する処理「RecvText」を登録する
ws.OnMessage += (sender, e) => RecvText(e.Data);
//サーバとの接続が切れたときに実行する処理「RecvClose」を登録する
ws.OnClose += (sender, e) => RecvClose();
}
}
ではこのClientManager.csを、空オブジェクトとして用意しておいたClientManagerオブジェクトにアタッチします。
ついでに他の各オブジェクトを、ClientManager.csのpublic フィールドとして定義していた変数たちにドラッグ&ドロップで適用させておきます。
以上でクライアント側の実装は終わりです。
「画面を起動したらサーバへ自動的にログインをし、メッセージを書いてSendボタンを押したらメッセージが送信される」、これらすべてが用意できました。
サーバ側の実装
「ServerScene」というシーンを作り、その配下に以下のオブジェクトを用意しておきましょう。
- ServerManager・・・WebSocketでクライアントとの処理をするための空オブジェクト
サーバはあくまでサーバですから、画面を作るってことは特にないですね・・・
では、サーバ側もプログラムを作っていきましょう。
「ServerManager.cs」というC#スクリプトを用意します。
ServerManager.csの中身には、
サーバ起動処理、クライアントのメッセージ送信待ち受け処理、接続クライアント全員へのメッセージ送信処理を用意しておきます。
using System.Collections.Generic;
using UnityEngine;
using WebSocketSharp;
using WebSocketSharp.Net;
using WebSocketSharp.Server;
public class ServerManager : MonoBehaviour
{
WebSocketServer ws;
void Start()
{
//ポート番号を指定
ws = new WebSocketServer(12345);
//クライアントからの通信時の挙動を定義したクラス、「ExWebSocketBehavior」を登録
ws.AddWebSocketService<ExWebSocketBehavior>("/");
//サーバ起動
ws.Start();
Debug.Log("サーバ起動");
}
private void OnApplicationQuit()
{
Debug.Log("サーバ停止");
ws.Stop();
}
public class ExWebSocketBehavior : WebSocketBehavior
{
//誰が現在接続しているのか管理するリスト。
public static List<ExWebSocketBehavior> clientList = new List<ExWebSocketBehavior>();
//接続者に番号を振るための変数。
static int globalSeq = 0;
//自身の番号
int seq;
//誰かがログインしてきたときに呼ばれるメソッド
protected override void OnOpen()
{
//ログインしてきた人には、番号をつけて、リストに登録。
globalSeq++;
this.seq = globalSeq;
clientList.Add(this);
Debug.Log("Seq" + this.seq + " Login. (" + this.ID + ")");
//接続者全員にメッセージを送る
foreach (var client in clientList)
{
client.Send("Seq:" + seq + " Login.");
}
}
//誰かがメッセージを送信してきたときに呼ばれるメソッド
protected override void OnMessage(MessageEventArgs e)
{
Debug.Log("Seq:" + seq + "..." + e.Data);
//接続者全員にメッセージを送る
foreach (var client in clientList)
{
client.Send("Seq:" + seq + "..." + e.Data);
}
}
//誰かがログアウトしてきたときに呼ばれるメソッド
protected override void OnClose(CloseEventArgs e)
{
Debug.Log("Seq" + this.seq + " Logout. (" + this.ID + ")");
//ログアウトした人を、リストから削除。
clientList.Remove(this);
//接続者全員にメッセージを送る
foreach (var client in clientList)
{
client.Send("Seq:" + seq + " Logout.");
}
}
}
}
ExWebSocketBehaviorはWebSocketBehaviorを継承しています。
WebSocketBehaviorは抽象クラスで、このクラスの持つ様々なメソッドをオーバーライドすることで、
サーバ側の挙動をカスタマイズすることができます。今回のサンプルではOnOpenとOnMessageとOnCloseだけ使いましたが、
他にも様々なメソッドがあります。また様々なフィールドも定義されています。適宜、必要なものを使っていくといいと思います。
ちなみに、「接続者全員にメッセージを送る」という処理は、以下のように代替もできます。
//接続者全員にメッセージを送る
// foreach (var client in clientList)
// {
// client.Send("Seq:" + seq + "..." + e.Data);
// }
this.Sessions.Broadcast("Seq:" + seq + "..." + e.Data);
「とにかく全員に送ればよい」ということであれば、上記コードでいいと思います。
しかし、「特定の誰かだけに送る」とか、「条件によって、特定の人には送らない」みたいなことをやろうと思ったら、
上記のようなWebSocketBehaviorのリストを作って、その中から送る相手を選択するような実装になるんじゃないかなーと思っています。
それでは、このServerManager.csを、空オブジェクトとして用意しておいたServerManagerオブジェクトにアタッチします。
これでサーバ側の実装は終わりです。
「接続しているクライアントを管理し、クライアントからメッセージを受信したら、全員にそのメッセージを送信する」、これらすべての用意ができました。
ビルド
実際に動かすために、ビルドしていきましょう。
手順ですが、クライアント側、サーバ側、それぞれを分けてビルドしていく必要があります。
サーバ側は、ServerSceneだけを対象にビルド、
クライアント側は、ClientSceneだけを対象にビルド、という感じになります。
それぞれフォルダを分けて、別々のところに保存しましょう。
動作確認
では動かしていきましょう。
チャットしてる感を出したいので、サーバ側のSampleProject.exeを1つ、クライアント側のSampleProject.exeを3つ起動してみましょう。
(クライアント側は、起動したらすぐに接続処理を始めてしまうので、サーバ側の方を先に起動しておくようにしましょう)
3つのクライアントで、チャットができるようになりました。またクライアントが切断をすると、他のクライアントにログアウトのメッセージが表示されること、
サーバを閉じるとその旨がクライアントにも通知されること、なども確認できると思います。
デプロイしてみよう(おまけ)
※ここからの作業は、お金がかかってきます。
上記の動作確認では、ローカルのアドレスに対し接続を行っていたので、いまいちオンラインしてる感がありません。
実際にインターネット上のサーバを借りて、そこにサーバ側SampleProjectを実行してみることにしました。
VPSでもEC2でも、なんでもいいのですが、
僕はとりあえず、EC2のUbuntuで作成してみました。
Linuxサーバでサーバ側SampleProjectを動かすには、Linux用にビルドをする必要があります。
上記設定でビルドすると、SampleProject.x86_64とSampleProject_Dataが作成されました。
この2つをLinuxサーバ上に持っていきます。
ubuntu@xxxxxxxxxx:~/unity/SampleProject$ ls -la
total 27648
drwxrwxr-x 3 ubuntu ubuntu 4096 Aug 31 22:22 .
drwxrwxr-x 4 ubuntu ubuntu 4096 Aug 31 22:22 ..
drwxrwxr-x 5 ubuntu ubuntu 4096 Aug 31 22:22 SampleProject_Data
-rw-rw-r-- 1 ubuntu ubuntu 28295568 Aug 31 22:22 SampleProject.x86_64
さて、実際に動かすわけですが、そのまえにSampleProject.x86_64の実行権限を付与してあげましょう。
ubuntu@xxxxxxxxxx:~/unity/SampleProject$ chmod 755 SampleProject.x86_64
ubuntu@xxxxxxxxxx:~/unity/SampleProject$ ls -la
total 27648
drwxrwxr-x 3 ubuntu ubuntu 4096 Aug 31 22:22 .
drwxrwxr-x 4 ubuntu ubuntu 4096 Aug 31 22:22 ..
drwxrwxr-x 5 ubuntu ubuntu 4096 Aug 31 22:22 SampleProject_Data
-rwxr-xr-x 1 ubuntu ubuntu 28295568 Aug 31 22:22 SampleProject.x86_64
それではサーバ起動!
ubuntu@xxxxxxxxxx:~/unity/SampleProject$ ./SampleProject.x86_64
~~~
(なんやかんやログが出る)
~~~
UnloadTime: 0.677017 ms
サーバ起動
起動したっぽいですね。それでは、ローカルのPC上から、このLinuxサーバのSampleProjectに接続をしてみます。
あ、ClientManager.cs内にある、接続先IPアドレスやポート番号の指定を、このLinuxサーバに合わせて書き換えることをお忘れずに。
問題なく動作していますね。サーバ側は、画面を起動していません。こういった起動モードを、Headlessモードとか言ったりします。
Headlessモードで起動した場合、Debug.Log()とかで書いたログ出力処理は上記のようにコンソール上に出るようになります。
ログ解析とかに役立ちそうですね。
ちなみに、「自分がHeadlessモードで起動しているか」を判断する方法があったりします。
https://qiita.com/su10/items/a56762ce3fe1b529e0bd
ですので、「Headlessモードであれば、ServerManagerを実行し、そうでなければClientManagerを実行する」みたいな分岐処理を入れておけば、
いちいちビルドを2つに分ける必要もなくなるので、かなり楽になりそうですね。
終わりに
特に作り込むこともなく、オンライン処理ができるようになりました。
また、サーバ側、クライアント側両方をUnity上で実装できるっていうのは、かなりありがたい(?)ことに感じます。
(昔オンラインゲームをちょっと作ってみた時、クライアント側はUnity、サーバ側はPythonとかいう謎構成でやっていました。
なのでC#のコード、Pythonのコード、お互いで使い回せる部分が少なすぎて手間2倍だった思い出があります)
このサーバが、どのぐらいの負荷に耐えられるかは未検証ですが、ガリガリ作り込めば、カードゲームだろうとFPSだろうと、
なんでも作れそうな感じがして、夢が膨らみますね。
独り言
- Unityエディタ上でシーンをダブルクリックすると、VSCodeが起動してしまうのは何故なんでしょう・・・? 確かにC#コーディングはVSCodeでやってますが、シーンは別にVSCode使わないっつーの
- Textオブジェクトのtextを変更しても、画面上に反映されない現象に見舞われています。
chatText.text += (text + "\n");
これやっても画面上に変更が反映されません。なんでこんな単純なことができなくなったんや
(結局、この処理の前後に、chatText.enabled = false;とかtrue;とかを入れて再表示させることで、今回のサンプルではごまかしました)