LoginSignup
20
22

More than 3 years have passed since last update.

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

Posted at

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

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

環境

  • Windows10
  • Unity 2019.2.2f1
  • Visual Studio Community 2019

おおまかな流れ

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

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

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

イメージこんな感じ。
uni1.png

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

前準備

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

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;とかを入れて再表示させることで、今回のサンプルではごまかしました)

20
22
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
20
22