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

初めまして。
初投稿です。
未熟も未熟者ですが、よろしくお願いいたします。


早速ですが掲題の通り、
WebSocketを使ったUnityのゲームの作り方、サーバの用意の仕方とかを紹介したいと思います。
初心者でも分かるように、スクショ多めにがんばります。

# 環境
- Windows10
- Unity 2019.2.2f1
- Visual Studio Community 2019

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

やり方ですが、一つのプロジェクト上に、
サーバ側でのみ使うシーン、
クライアント側でのみ使うシーンの2つを作ります。

クライアント側のシーンは、サーバへデータを送信する処理を、
サーバ側のシーンは、クライアントからのデータ送信を待ち受ける処理を実行させることで、
サーバ、クライアント間でのデータ送受信を実現したいと思います。

イメージこんな感じ。
![uni1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/410208/87192f66-d6c1-fc5d-53a1-3f15949e39a5.png)



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

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

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

![i1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/410208/2415a101-2116-c20c-dfe8-a5756b8c2ed6.png)

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

ソリューションエクスプローラーから、Exampleは消してしまいましょう。
![i2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/410208/29191362-b397-48ca-a439-7893f89147d0.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/

```cs:CookieException.cs(修正前)
  public class CookieException : FormatException
  {
```

```cs:CookieException.cs(修正後)
  public class CookieException : FormatException, ISerializable
  {
```

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

![i3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/410208/aac8fc2e-b796-8ed8-6aca-d7479ebaa10c.png)

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

![i4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/410208/e60b6402-3d72-c249-943a-9bc50d2b4248.png)

正常にビルドが終了すると、websocket-sharp\bin\Release上にwebsocket-sharp.dllができるはずです。これを、Unityのプロジェクトに適用します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/410208/95e1bf12-fddd-8b1e-2273-52645f5d8705.png)

## websocket-sharpの適用

Unityのプロジェクトを新規に作りましょう。ここでは「SampleProject」と呼称しておきます。
次に、Assetsフォルダの中に、Pluginsフォルダを作ります。
その中に、さきほど生成したwebsocket-sharp.dllを入れましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/410208/04dfb38d-a618-e90f-e20c-2ca1422b9394.png)

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

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

- ChatText・・・チャットを表示するためのText
- MessageInput・・・メッセージを送信するためのInputField
- SendButton・・・メッセージ送信処理をするためのButton
- ClientManager・・・WebSocketでサーバとの処理をするための空オブジェクト
![i8.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/410208/fce5c8ee-e74a-6616-8160-694366d8dfd1.png)



次に、プログラムを作っていきましょう。
Assetsフォルダの中に、Scriptsフォルダを作り、その中に「ClientManager.cs」というC#スクリプトを用意します。


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

```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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/410208/36ccb420-72de-85c1-1d8b-1abf3b5a39d0.png)

以上でクライアント側の実装は終わりです。
「画面を起動したらサーバへ自動的にログインをし、メッセージを書いてSendボタンを押したらメッセージが送信される」、これらすべてが用意できました。


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

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

サーバはあくまでサーバですから、画面を作るってことは特にないですね・・・
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/410208/d3af54d6-fe41-ad42-6e77-97baac090b90.png)



では、サーバ側もプログラムを作っていきましょう。
「ServerManager.cs」というC#スクリプトを用意します。

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

```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だけ使いましたが、
他にも様々なメソッドがあります。また様々なフィールドも定義されています。適宜、必要なものを使っていくといいと思います。

ちなみに、「接続者全員にメッセージを送る」という処理は、以下のように代替もできます。

```cs:
     //接続者全員にメッセージを送る
      // 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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/410208/14bc073e-dd5c-ec69-ee2d-2de4360b3d2d.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/410208/28421ffe-9728-9a50-3eb3-ed7575f0c547.png)

# 動作確認

では動かしていきましょう。
チャットしてる感を出したいので、サーバ側のSampleProject.exeを1つ、クライアント側のSampleProject.exeを3つ起動してみましょう。
(クライアント側は、起動したらすぐに接続処理を始めてしまうので、サーバ側の方を先に起動しておくようにしましょう)
![websocket1.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/410208/fdc55651-bba5-e051-74ca-ea42efef45af.gif)
3つのクライアントで、チャットができるようになりました。またクライアントが切断をすると、他のクライアントにログアウトのメッセージが表示されること、
サーバを閉じるとその旨がクライアントにも通知されること、なども確認できると思います。

# デプロイしてみよう（おまけ）

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

上記の動作確認では、ローカルのアドレスに対し接続を行っていたので、いまいちオンラインしてる感がありません。
実際にインターネット上のサーバを借りて、そこにサーバ側SampleProjectを実行してみることにしました。

VPSでもEC2でも、なんでもいいのですが、
僕はとりあえず、EC2のUbuntuで作成してみました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/410208/2ea90cce-1065-5488-2a27-ef8284fd299e.png)

Linuxサーバでサーバ側SampleProjectを動かすには、Linux用にビルドをする必要があります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/410208/e4edf36c-4a96-91e5-d062-5883da24d403.png)

上記設定でビルドすると、SampleProject.x86_64とSampleProject_Dataが作成されました。
この2つをLinuxサーバ上に持っていきます。

```sh:
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の実行権限を付与してあげましょう。

```sh:
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
```

それではサーバ起動！

```sh:
ubuntu@xxxxxxxxxx:~/unity/SampleProject$ ./SampleProject.x86_64 
～～～
（なんやかんやログが出る）
～～～
UnloadTime: 0.677017 ms
サーバ起動
```

起動したっぽいですね。それでは、ローカルのPC上から、このLinuxサーバのSampleProjectに接続をしてみます。
あ、ClientManager.cs内にある、接続先IPアドレスやポート番号の指定を、このLinuxサーバに合わせて書き換えることをお忘れずに。

![websocket2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/410208/c40d3786-a2d1-6294-9a98-92e9118c1b6c.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を変更しても、画面上に反映されない現象に見舞われています。　

```cs:
      chatText.text += (text + "\n");
```
これやっても画面上に変更が反映されません。なんでこんな単純なことができなくなったんや　:innocent::innocent::innocent::innocent::innocent:
（結局、この処理の前後に、chatText.enabled = false;とかtrue;とかを入れて再表示させることで、今回のサンプルではごまかしました）