見出し画像

【Unity】Websocket-sharpで通信する

Websocket-sharpを使ってUnityのサーバとクライアントでWebsocket通信を行ってみます。


環境

Unity 2022.3.34f1、Windows11

1. Websocket-sharpライブラリの作成

Websocket-sharpをUnityで使用するためには、ビルドして生成されたDLLファイルをUnityに読み込ませる必要があります。まずは下記のWebsocket-sharpのリポジトリからプロジェクトファイルをダウンロードします。

ダウンロードが完了したら、websocket-sharp.slnファイルをダブルクリックします。Visual Studioがインストールされていない場合は、インストールを行ってください。今回はVisual Studio 2019でビルドを行います。

スクリーンショット 2021-06-23 153107_New

プロジェクトファイルを開いたときに、アップグレードのダイアログが表示された場合は[OK]ボタンを押して先に進みます。

無題

Visual Studioのソリューションエクスプローラーウィンドウで「Example」、「Example~Example3」の項目を削除します。

スクリーンショット 2021-06-25 111115_New

次にメニューからビルド形式を「Release」に設定します。

スクリーンショット 2021-06-25 111918_New

最後にメニューの[ビルド] > [ソリューションのビルド]を選択してビルドを実行します。

スクリーンショット 2021-06-25 111542_New

ビルドが成功すると、プロジェクトファイルが存在するフォルダの\websocket-sharp\bin\Releaseに「websocket-sharp.dll」が生成されます。

スクリーンショット 2021-06-25 112344

2. UnityへのWebsocket-sharpライブラリ読み込み

生成された「websocket-sharp.dll」をUnityエディタ上のProjectパネルにドラッグアンドドロップすると読み込ませることができます。今回はREADMEにならってAssets/Pluginsフォルダを作成してそこに保存します。

3. コード

websocket-sharpはクライアントとサーバの両方の機能に対応しています。両者をwebsocket-sharpで実装する場合は、Unityのプロジェクトを分けると良いでしょう。クライアント側のコードは以下のようになります。

using UnityEngine;
using System.Collections;
using WebSocketSharp;

public class ClientExample : MonoBehaviour {

   private WebSocket ws;

   void Start()
   {
       ws = new WebSocket("ws://localhost:3000/");

       ws.OnOpen += (sender, e) =>
       {
           Debug.Log("WebSocket Open");
       };

       ws.OnMessage += (sender, e) =>
       {
           Debug.Log("WebSocket Data: " + e.Data);
       };

       ws.OnError += (sender, e) =>
       {
           Debug.Log("WebSocket Error Message: " + e.Message);
       };

       ws.OnClose += (sender, e) =>
       {
           Debug.Log("WebSocket Close");
       };

       ws.Connect();

   }

   void Update()
   {

       if (Input.GetKeyUp("s"))
       {
           ws.Send("Test Message");
       }

   }

   void OnDestroy()
   {
       ws.Close();
       ws = null;
   }
}

実行中に「s」キーを押すとサーバに「Test message」という文字列を送信します。次にサーバ側のコードは以下のようになります。

using UnityEngine;
using System.Collections;
using WebSocketSharp;
using WebSocketSharp.Server;

public class ServerExample : MonoBehaviour {

   private WebSocketServer server;

   void Start ()
   {
       server = new WebSocketServer(3000);

       server.AddWebSocketService<Echo>("/");
       server.Start();

   }

   void OnDestroy()
   {
       server.Stop();
       server = null;
   }

}

public class Echo : WebSocketBehavior
{
   protected override void OnMessage (MessageEventArgs e)
   {
       Debug.Log(e.data);
       Sessions.Broadcast(e.Data);
   }
}

サーバはクライアントからメッセージを受け取ると同じ内容をブロードキャストします。

4. 実行

サーバとクライアントをUnityエディタ上で実行し、クライアントから「s」キーを押してメッセージを送ってみます。両者のConsoleパネル上にメッセージが表示されれば成功です。

5. 応用編

クライアントからデータを受け取ってサーバ側のシーン上で何か処理を行いたい場合は少し工夫が必要になります。

というのも、コードから見て分かるように、サーバ側の受信処理はWebSocketBehaviorを継承したクラスをWebSocketServerにサービスとして追加することで実現しています。このとき、WebSocketBehaviorの初期化やメッセージの受信時に実行されるOnMessageはメインスレッドとは異なるスレッドで非同期に処理されます。

言い換えると、受信のタイミングでシーン上のText UIにメッセージを表示させたり、オブジェクトを動かすといった操作はメインスレッドでしか実行できないため、このままのコードではデータをシーンに反映させることができません。(Debug.Logはメインスレッド以外でも処理可能です)

最も簡単に受信データをメインスレッドで利用する手段としてはWebSocketBehaviorクラスにstatic変数を追加する方法があります。コードとしては以下のようになります。

Static版

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using WebSocketSharp;
using WebSocketSharp.Server;
using UnityEngine.UI;

public class Controller : MonoBehaviour
{
   private WebSocketServer server;
   [SerializeField] Text messageText;

   void Start() {
       server = new WebSocketServer(3000);
       server.AddWebSocketService<Echo>("/");
       server.Start();       
   }

   void Update()
   {
      messageText.text = Echo.message;
   }

   void OnDestroy() {
       server.Stop();
       server = null;
   }
}

public class Echo : WebSocketBehavior
{

   public static string message;

   protected override void OnOpen() {
       Debug.Log("OnOpen");
   }

   protected override void OnMessage (MessageEventArgs e) {
       message = e.Data;
   }
}

R3(旧UniRx)版

static変数やUpdateを使用したくない場合は、R3を使用する方法があります(旧式のUniRxでも可)。R3の使用はライブラリのインストールが必要になります。

using UnityEngine;
using R3;
using TMPro;
using WebSocketSharp;
using WebSocketSharp.Server;

public class Controller : MonoBehaviour
{
    private WebSocketServer m_Server;
    [SerializeField] private TextMeshProUGUI messageText;
    private CompositeDisposable m_CompositeDisposable = new();
    
    private void Start() {
        m_Server = new WebSocketServer(3000);
        m_Server.AddWebSocketService<Echo>("/", service => {
            service.OnReceiveMessage
                .ObserveOnMainThread()
                .Subscribe(OnMessage)
                .AddTo(m_CompositeDisposable);
        });
        
        m_Server.Start();
    }

    private void OnMessage(string msg) {
        // Debug.Log($"OnMessage: {msg}");
        messageText.text = msg;
    }

    private void OnDestroy() {
        m_CompositeDisposable.Dispose();
        m_Server.Stop();
        m_Server = null;
    }
}

public class Echo : WebSocketBehavior
{
    private Subject<string> m_Subject = new();
    public Observable<string> OnReceiveMessage => m_Subject;

    protected override void OnOpen() {
        Debug.Log("OnOpen");
    }

    protected override void OnMessage (MessageEventArgs e) {
        m_Subject.OnNext(e.Data);
    }
}

AddWebSocketServiceの第二引数でWebSocketBehaviorのインスタンスを受け取れるため、ここでフィールドで公開しているOnReceiveMessageをSubscribeしてメインスレッドで処理すればデータをUIに表示させることができます。

Action版

static変数やUpdateを使用しない方法として、もう一つActionを使用した方法を紹介します。こちらはR3とは異なり、C#の標準機能のためライブラリのインストールは不要です。

using System;
using System.Threading;
using TMPro;
using UnityEngine;
using WebSocketSharp;
using WebSocketSharp.Server;

public class Controller : MonoBehaviour
{
    private WebSocketServer m_Server;
    [SerializeField] private TextMeshProUGUI messageText;
    private SynchronizationContext m_Context;
    
    private void Start() {
        m_Server = new WebSocketServer(3000);
        m_Server.AddWebSocketService<Echo>("/", service => {
            service.OnReceiveMessage = OnMessage; 
        });
        
        m_Server.Start();
        m_Context = SynchronizationContext.Current;
    }

    private void OnMessage(string msg) {
        // メインスレッドでテキストの更新を実行
        m_Context.Post(_ => {
            messageText.text = msg;
        }, null);
    }

    private void OnDestroy() {
        m_Server.Stop();
        m_Server = null;
    }
    
    private class Echo : WebSocketBehavior {
        
        public Action<string> OnReceiveMessage;
        
        protected override void OnOpen() {
            Debug.Log("OnOpen");
        }

        protected override void OnMessage (MessageEventArgs e) {
            OnReceiveMessage?.Invoke(e.Data);
        }
    }
}

UIへ反映させる部分はSynchronizationContextを使用しています。SynchronizationContextは指定した処理を指定したスレッドで実行することができます。(スレッドの指定はStart関数で行ってます)

6. 全般のハマりポイント

Windowsマシンで実行する場合、サーバとクライアントで異なる端末で通信するときは、Windowsのファイヤーウォールで使用するポート(例:3000番)を開放する必要があります。

ポートの解放は実行するアプリケーション単位で設定可能なため、node.jsでは問題なくてもUnityでは通信できないといった症状が発生します。この場合はUnityからでもポートの送受信が可能なように、セキュリティに留意した上でファイヤーウォールの設定を行ってください。

7. おわりに

websocket-sharpを解説したサイトは他にもたくさんありますが、エコーサーバやチャットサーバの内容が多いため、この記事では受け取ったデータをシーンで活用する方法に重点を置いて解説しました。

最後にwebsocket-sharpを使ってスマホの傾きをサーバへ送り、シーンに配置してあるバーチャルスマホとシンクロさせてみました。リアルタイムに処理する手段としてぜひ活用してみてください。🌱

8. 参考


この記事が気に入ったらサポートをしてみませんか?