Posted at

【Unity】クライアントもサーバもwebsocket-sharpで、オンラインゲームを作ってみよう【WebSocket】

初めまして。

初投稿です。

未熟も未熟者ですが、よろしくお願いいたします。

早速ですが掲題の通り、

WebSocketを使ったUnityのゲームの作り方、サーバの用意の仕方とかを紹介したいと思います。

初心者でも分かるように、スクショ多めにがんばります。


環境


  • Windows10

  • Unity 2019.2.2f1

  • Visual Studio Community 2019


おおまかな流れ

とりあえず簡単なチャットアプリを作りたいと思います。

やり方ですが、一つのプロジェクト上に、

サーバ側でのみ使うシーン、

クライアント側でのみ使うシーンの2つを作ります。

クライアント側のシーンは、サーバへデータを送信する処理を、

サーバ側のシーンは、クライアントからのデータ送信を待ち受ける処理を実行させることで、

サーバ、クライアント間でのデータ送受信を実現したいと思います。

イメージこんな感じ。

uni1.png


プロジェクトにwebsocket-sharpを適用しよう


前準備

websocket-sharpのgithubのページに行き、ソースをzipでダウンロードしましょう。

https://github.com/sta/websocket-sharp

i1.png

ダウンロードしたらファイルを解凍し、中のwebsocket-sharp.slnをダブルクリックしてVisualStudioを起動させましょう。

ソリューションエクスプローラーから、Exampleは消してしまいましょう。

i2.png

このあと、ビルドをしたいのですが、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/


CookieException.cs(修正前)

  public class CookieException : FormatException

{


CookieException.cs(修正後)

  public class CookieException : FormatException, ISerializable

{

ソリューション構成を、「Release」にします。

i3.png

以上の手順が済んだら、ビルドをしましょう。

i4.png

正常にビルドが終了すると、websocket-sharp\bin\Release上にwebsocket-sharp.dllができるはずです。これを、Unityのプロジェクトに適用します。

image.png


websocket-sharpの適用

Unityのプロジェクトを新規に作りましょう。ここでは「SampleProject」と呼称しておきます。

次に、Assetsフォルダの中に、Pluginsフォルダを作ります。

その中に、さきほど生成したwebsocket-sharp.dllを入れましょう。

image.png

これで、SampleProjectはWebSocketを使うための準備が整いました。


クライアント側の実装

Sceneフォルダに、「ClientScene」というシーンを作り、その配下に以下のオブジェクトを用意しておきましょう。


  • ChatText・・・チャットを表示するためのText

  • MessageInput・・・メッセージを送信するためのInputField

  • SendButton・・・メッセージ送信処理をするためのButton

  • ClientManager・・・WebSocketでサーバとの処理をするための空オブジェクト
    i8.png

次に、プログラムを作っていきましょう。

Assetsフォルダの中に、Scriptsフォルダを作り、その中に「ClientManager.cs」というC#スクリプトを用意します。

ClientManager.csの中身には、

サーバへの接続処理、メッセージの送信処理、メッセージ表示処理を用意しておきます。


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 フィールドとして定義していた変数たちにドラッグ&ドロップで適用させておきます。

i7.png

以上でクライアント側の実装は終わりです。

「画面を起動したらサーバへ自動的にログインをし、メッセージを書いてSendボタンを押したらメッセージが送信される」、これらすべてが用意できました。


サーバ側の実装

「ServerScene」というシーンを作り、その配下に以下のオブジェクトを用意しておきましょう。


  • ServerManager・・・WebSocketでクライアントとの処理をするための空オブジェクト

サーバはあくまでサーバですから、画面を作るってことは特にないですね・・・

image.png

では、サーバ側もプログラムを作っていきましょう。

「ServerManager.cs」というC#スクリプトを用意します。

ServerManager.csの中身には、

サーバ起動処理、クライアントのメッセージ送信待ち受け処理、接続クライアント全員へのメッセージ送信処理を用意しておきます。


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だけを対象にビルド、という感じになります。

それぞれフォルダを分けて、別々のところに保存しましょう。

image.png

image.png


動作確認

では動かしていきましょう。

チャットしてる感を出したいので、サーバ側のSampleProject.exeを1つ、クライアント側のSampleProject.exeを3つ起動してみましょう。

(クライアント側は、起動したらすぐに接続処理を始めてしまうので、サーバ側の方を先に起動しておくようにしましょう)

websocket1.gif

3つのクライアントで、チャットができるようになりました。またクライアントが切断をすると、他のクライアントにログアウトのメッセージが表示されること、

サーバを閉じるとその旨がクライアントにも通知されること、なども確認できると思います。


デプロイしてみよう(おまけ)

※ここからの作業は、お金がかかってきます。

上記の動作確認では、ローカルのアドレスに対し接続を行っていたので、いまいちオンラインしてる感がありません。

実際にインターネット上のサーバを借りて、そこにサーバ側SampleProjectを実行してみることにしました。

VPSでもEC2でも、なんでもいいのですが、

僕はとりあえず、EC2のUbuntuで作成してみました。

image.png

Linuxサーバでサーバ側SampleProjectを動かすには、Linux用にビルドをする必要があります。

image.png

上記設定でビルドすると、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サーバに合わせて書き換えることをお忘れずに。

websocket2.gif

問題なく動作していますね。サーバ側は、画面を起動していません。こういった起動モードを、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使わないっつーの :rage::rage::rage:

  • Textオブジェクトのtextを変更しても、画面上に反映されない現象に見舞われています。 

      chatText.text += (text + "\n");

これやっても画面上に変更が反映されません。なんでこんな単純なことができなくなったんや :innocent::innocent::innocent::innocent::innocent:

(結局、この処理の前後に、chatText.enabled = false;とかtrue;とかを入れて再表示させることで、今回のサンプルではごまかしました)