WebGLでビルドされたUnityとコンソールアプリケーション間で通信したい

# WebGLのUnityとコンソールアプリを通信したい
ブラウザ上に表示されるWebGLビルドされたUnityのアプリケーションと，ローカルで起動したコンソールアプリケーションを通信できるようにしたいときってありますよね．

スタンドアロンなUnityアプリケーションとコンソールアプリケーションを通信するだけであれば，TCPソケットはもちろん，Websocketでも接続できます．それから推察するに，WebGLビルドされたUnityのアプリケーションとコンソールアプリケーションもWebsocketを使えば通信できそうですが，なぜかそうはいかないようです（[参考](http://tips.hecomi.com/entry/2014/12/28/223129)）．

当エントリーでは，WebGLビルドされたUnityのアプリケーションとコンソールアプリケーションを通信できるように頑張った結果を報告します．もしかしたらよりエレガントな解決策があるかもしれません．

以下環境設定．
- Unity: 2019.1.8f1
- chrome: 79.0
- Windows: 10 pro 1903

# 解決策
`*.jslib`にWebsocketで通信するjavascriptを書き，Unityのスクリプト(C#)ではjavascriptが受け取ったメッセージをUnityの空間に持ち上げるようにする．

## 具体的には？
### 1. ファイルの用意
まずUnityの`Assets`ディレクトリ以下に，ファイルを二つ作ります．ファイル名はなんでもいいのですが，便宜上以下のものに決めます．階層構造は一応順守で．
- `Assets/Plugins/Connector.jslib`
- `Assets/Scripts/Main.cs`

### 2. `Connector.jslib`の編集
WebGLビルドされたUnityはこのファイルを通じてjavascriptの空間にアクセスできるようです．詳細は以下をURLを．
- https://qiita.com/gtk2k/items/1c7aa7a202d5f96ebdbf
- https://docs.unity3d.com/jp/540/Manual/webgl-interactingwithbrowserscripting.html

このファイルを以下のように編集し，まずはjavascript上でWebsocketのクライアントを作成します．

```javascript:Connector.jslib
var ws;
var messages;

mergeInto(LibraryManager.library, {

  Initialize: function (server) {
    ws = new WebSocket( Pointer_stringify(server) );

 	messages = new Array();
    
	ws.onmessage = function (evt) {
	    var message = evt.data;

		messages.push( message );
	};

	ws.onopen = function () {
		console.log("system::open")
	};

	ws.onclose = function () {
		console.log("system::close")
	};
  },

  GetMessage: function () {
	var msg = "";

	if( messages.length > 0 ) {
		msg = messages.shift();
	}

	var buffer = _malloc( lengthBytesUTF8(msg) + 1 );
	writeStringToMemory(msg, buffer);
	return buffer;
  },
  
  SendMessage: function (message) {
    if (ws) {
		ws.send( Pointer_stringify(message) );
    }
  },

  Close: function () {
    if (ws) {
		ws.close();
	}
  }

});
```

### 3. `Main.cs`の編集
次に`Main.cs`を以下のように編集します．

```cs:Main.cs
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using UnityEngine;

public class Main : MonoBehaviour
{
  private GameObject logger;
  private Queue<string> ReceivedMessages = null;
  private static string server = @"ws://localhost:2333/ConnectionTest";

  [DllImport( "__Internal" )]
  private static extern void Initialize( string server );
  [DllImport( "__Internal" )]
  private static extern string GetMessage();
  [DllImport( "__Internal" )]
  private static extern void Send( string message );
  [DllImport( "__Internal" )]
  private static extern void Close();


  private void OnDestroy() {
    Close();
  }

  void Start() {
    logger = GameObject.Find( "Logger" );

    ReceivedMessages = new Queue<string>();

    Initialize( server );
  }


  private static string Formatter( string message ) => $"message from server:\n{message}";

  void Update() {

    string message = GetMessage();

    if( message != "" ) {
      ReceivedMessages.Enqueue( message );

      Debug.Log( $"Received -> {message}" );
    }

    if( ReceivedMessages.Any() ) {

      string line = ReceivedMessages.Dequeue();

      logger.GetComponent<TextMesh>().text = Formatter( line );
    }

      
  }
}

```

Unityの`Scene`上にテキストを表示できる`GameObject`を配置し，受け取ったメッセージを表示するようにしておきました．下図参照．
![unity.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/319806/c94c0249-d215-e058-1661-cc189ff93e57.png)

ちなみにこのスクリプトは，javascript上に定義された関数をインポートして呼び出しているので，WebGLビルド以外ではそれら関数が呼び出せずエラーを吐くはずです．つまり，WebGLビルドしないと動作確認できません．

変数`server`には，Websocketサーバーが起動しているPCのIPアドレスを入力します．今は`localhost`としていますが，LAN内の別PC (`192.168.11.*`)を指定しても接続できていました．

### 4. UnityをWebGLビルドし実行，接続確認
ソースコードの編集が終わったのち，UnityをWebGLビルドします．方法は適当に調べてください．

作成された`index.html`をchromeで開くと，下図のようにUnityのプレーヤーの初期化時にエラーが生じて実行できない（Firefoxでは実行できるとかなんとか）ようです．エラーメッセージ見ると，ウェブサーバーにアップロードしろ的な感じなので，ビルドにより生成されたすべてのファイルをウェブサーバーにアップロードします．
![error.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/319806/3acc3bf8-5e32-7733-4794-f4c65daa76c9.png)

そして，Websocketサーバーとしての機能を持つコンソールアプリケーションをあらかじめ作成しておき，起動します．Websocketサーバーはnode.jsで作る人が多いようですが，筆者はC#信者なのでC#で作りました．ライブラリとして[websocket-sharp](https://github.com/sta/websocket-sharp)にお世話になりました．

ローカルでWebsocketサーバーを立ち上げたのち，ブラウザから先ほどの`index.html`にアクセスします．その後ローカルのサーバーからメッセージを送信すると，ブラウザ上のUnityにメッセージが表示されました（下GIF参照）．

![setuzoku_dekita.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/319806/1d2f5e98-5970-32dd-f3b0-f48ea55c5b9e.gif)

このGIFの上半分は`index.html`にアクセスしているブラウザの様子で，下半分はローカルに起動しているWebsocketサーバーの様子です．Websocketサーバーのコンソールに文字を入力し，エンターキーを入力すると，それまでの文字列がブラウザ上のUnityに送信されていることが確認できます．ブラウザ上のUnity上に表示されている文字が多少見切れていますが，全画面表示にするといい感じに表示されています．

# おわりに
エレガントな方法かどうかはわかりませんが，ブラウザのUnityアプリケーションとローカルのコンソールアプリケーションが通信できるようになりました．

そういえば，[ここ](https://qiita.com/gtk2k/items/1c7aa7a202d5f96ebdbf)によればjavascriptの空間からC#のスクリプト上に定義されたメソッドを呼ぶこともできるようなので，うまくやればメッセージがあるかどうかを無意味にビジーウェイトしなくてもよくなりそうです．

後日暇を見つけて，作成したUnityのスクリプトなどをGithub上にアップロードしてようと思います．Unityのエディター上でも動作確認できるような工夫もしていたり，Websocketのサーバーアプリケーションもアップロードしておこうかなあとも思っていたり．

これでなんか面白いアプリケーション作れないかな...