e.blog

主にUnity/UE周りのことについてまとめていきます

UnityでWebSocketを使ってブラウザと通信する

概要

WebブラウザからUnityへ(そしてビルドしたアプリにも)データを送信したいことがあったのでWebSocketをUnityで利用する方法およびWebブラウザからデータを送信する方法を試してみたのでそのメモです。

Unity側のWebSocketの利用は以下のライブラリを利用させていただきました。

github.com

ちなみにサーバ側はNode.jsとwsモジュールを利用しています。

今回のサンプルはGitHubにアップしてあるので実際の動作を確認した人はこちらを参照ください。

github.com



ライブラリをビルドする

冒頭のプロジェクトをgit cloneするかzipファイルをダウンロードしプロジェクトをビルドします。
手順は以下の通りです。

プロジェクト内にあるwebsocket-sharp.slnVisual Studioで開きます。

不要なプロジェクトの削除

最初に含まれているいくつかのサンプルがあるとビルドに失敗するようなので該当のプロジェクトを削除します。
具体的にはExampleとついているプロジェクトすべてです。

フォルダごと削除するか、Visual Studioの「ソリューションエクスプローラ」から右クリック→削除を実行することで削除することができます。

ソリューションをビルド

削除後、websocket-sharpソリューションをビルドします。ビルドターゲットはReleaseを選びます。
以下の図のように、ソリューション構成をReleaseに変更します。

f:id:edo_m18:20210124163142p:plain

その後、ソリューションエクスプローラからwebsocket-sharpを選択、右クリックからビルドを選択することでビルドできます。(もしうまく行かない場合はリビルドを選ぶとうまく行くかもしれません)

f:id:edo_m18:20210124162350p:plain

不要プロジェクトの削除などについては以下の記事を参考にさせていただきました。

qiita.com

DLLをプロジェクトに配置

ビルドが正常にできたら以下のパスにDLLファイルが生成されます。それをUnityプロジェクトのPluginsフォルダにコピーします。

DLLの場所

/path/to/project/websocket-sharp/bin/Release/websocket-sharp.dll

C#でWebSocketを扱う

これで無事、C#でWebSocketを扱う準備が整ったのでC#のコードを書いていきます。

WebSocketサーバに接続する

まずはC#側のコードから見ていきましょう。
今回は冒頭で紹介したライブラリを使って実装しています。

まずは簡単に、WebSocketサーバに接続するコードです。

using System.Threading;
using UnityEngine;
using UnityEngine.UI;
using WebSocketSharp;

public class MessageTest : MonoBehaviour
{
    [SerializeField] private string _serverAddress = "localhost";
    [SerializeField] private int _serverPort = 3000;

    private WebSocket _webSocket = null;

    private void Start()
    {
        _webSocket = new WebSocket($"ws://{_serverAddress}:{_serverPort}/");

        // Event handling.
        _webSocket.OnOpen += (sender, args) => { Debug.Log("WebSocket opened."); };
        _webSocket.OnMessage += (sender, args) => { Debug.Log("OnMessage"); };
        _webSocket.OnError += (sender, args) => { Debug.Log($"WebScoket Error Message: {args.Message}"); };
        _webSocket.OnClose += (sender, args) => { Debug.Log("WebScoket closed"); };

        _webSocket.Connect();
    }
}

WebSocketクラスを、URLを引数にインスタンス化してConnectメソッドを呼ぶだけです。シンプルですね。
いくつかコールバックがあるので必要に応じて設定します。

特にOnMessageイベントは必須になるでしょう。引数に渡ってくるMessageEventArgsに送信されてきたデータへの参照があるので適宜これを利用します。

サーバを立てる

今回はNode.jsを利用してWebSocketサーバを立ててテストしました。
サーバ側のコードは以下の通りです。今回はテストなので受信したメッセージを接続しているクライアントにそのまま流しているだけです。

WebSocketのサーバにはwsモジュールを利用しました。

var WebSocket = require('ws');
var ws = WebSocket.Server;
var wss = new ws({port: 3000});

console.log(wss.clients);

wss.brodcast = function (data) {
    this.clients.forEach(function each(client) {
        if (client.readyState === WebSocket.OPEN) {
            client.send(data);
        }
    });
};

wss.on('connection', function (ws) {
    ws.on('message', function (message) {
        var now = new Date();
        console.log(now.toLocaleString() + ' Received.');
        wss.brodcast(message);
    });
});

wsモジュールを読み込んでポート3000で待ち受けているだけですね。
そして来たメッセージをそのままブロードキャストしているだけです。

Webブラウザからデータを送信する

次に、今回の主目的であるWebブラウザからUnityに向かってデータを送信する部分を見てみます。

まずは簡単にHTMLを組みます。(あくまでデータ送信のためのUIを作るため)

<!DOCTYPE html>
<html>
<head><meta charset="utf-8"></head>
<body>
    <h1>WebSocket x Unityサンプル(Message Sample)</h1>

    <p>
        <input id="serverAddress" value="localhost" />
        <input id="serverPort" value="3000" />
        <input type="button" id="Connect" value="Connect" />
        <input type="button" id="Disconnect" value="Disconnect" />
    </p>

    <p>
        <span id="eventType"></span>
        <span id="dispMsg"></span>
    </p>

    <p>
        <input id="inputField" />
        <input type="button" id="Send" value="Send Message" />
    </p>
</body>
<script type="text/javascript" src="main.js"></script>
</html>

上記サンプルではテキストメッセージを送るだけのシンプルなものです。
実際にデータの送信を行っているWebブラウザ側で実行されているJSは以下です。

class WebSocketController {
    connection = null;
    eventTypeField = null;
    messageDisplayField = null;

    constructor(eventTypeField, messageDisplayField) {
        this.eventTypeField = eventTypeField;
        this.messageDisplayField = messageDisplayField;
    }

    connect(url, port) {
        const URL = "ws://" + url + ":" + port + "/";
        console.log("Will connect to " + URL);

        // Connect to the server.
        this.connection = new WebSocket(URL);

        // 接続通知
        this.connection.onopen = (event) => {
            this.eventTypeField.innerHTML = "通信接続イベント受信";
            this.messageDisplayField.innerHTML = event.data === undefined ? "--" : event.data;
        };

        //エラー発生
        this.connection.onerror = (error) => {
            this.eventTypeField.innerHTML = "エラー発生イベント受信";
            this.messageDisplayField.innerHTML = error.data;
        };

        //メッセージ受信
        this.connection.onmessage = (event) => {
            console.log("Received a message [" + event.data + "]");

            this.eventTypeField.innerHTML = "メッセージ受信";
            this.messageDisplayField.innerHTML = event.data;
        };

        //切断
        this.connection.onclose = () => {
            this.eventTypeField.innerHTML = "通信切断イベント受信";
            this.messageDisplayField.innerHTML = "";
        };
    }

    disconnect() {
        console.log("Will disconnect from " + URL);
        this.connection.close();
    }

    sendMessage(message) {
        console.log("Will send message.");
        this.connection.send(message);
    }
}

let eventTypeField = document.getElementById("eventType");
let messageDisplayField = document.getElementById("dispMsg");

const ws = new WebSocketController(eventTypeField, messageDisplayField);

document.getElementById("Connect").addEventListener('click', () => {
    let url = document.getElementById("serverAddress").value;
    let port = document.getElementById("serverPort").value;
    ws.connect(url, port);
});

document.getElementById("Disconnect").addEventListener('click', ws.disconnect);

document.getElementById("Send").addEventListener('click', () => {
    var message = document.getElementById("inputField").value;
    ws.sendMessage(message);
});

JS書くの久しぶりなので全然モダンな感じじゃないですが目をつぶってくださいw

やっていることはWebSocketサーバに接続して、Inputに入力されているテキストを送るだけのシンプルなものです。

テキストの送信は非常にシンプルに、WebSocketクラスのsendMessageメソッドに文字列を渡すだけです。
これでUnity側にテキストがしっかりと送信されます。

受信したテキストをログ出力するには以下のようにします。

_webSocket.OnMessage += (sender, args) => { Debug.Log(args.Data); };

受信した文字列はMessageEventArgsクラスのDataプロパティに格納されているので、それを参照することで文字列を簡単に取り出すことができます。

WebSocketを利用してバイナリデータを送る

ここからは応用編です。文字列は非常に簡単に送ることができましたが、バイナリデータ、例えば画像などを送る場合は少し工夫をしないとなりません。

特に今回は、様々なデータを送って処理できるようにするために、バイナリデータのフォーマット決めて色々なデータを送れるようにすることを意識して作ったものをメモとして残しておきます。

まずはJS側から(つまり送信側から)のコードを見てみます。
WebSocketControllerクラスに関しては上のコードと同様なので割愛します。sendMessageはbyte配列をそのまま受け付けるので変更せずに利用することができます)

まずは簡単に仕様から。

データ・フォーマットとしては以下を想定しています。
(カッコ内の数字はバイト数です)

+------------------+-----------------
| データタイプ(1) |  データ本体
+------------------+-------------

byte配列の先頭に、データ・タイプを示す1byteのデータを入れます。
0ならカラー情報(文字列)、1なら画像ファイルという具合です。

WebSocketControllerクラスにデータ・タイプを示す値を追加しています。(ここだけ追加された部分です)

class WebSocketController {
    static MessageType = {
        Color: 0,
        Image: 1,
    };
    // 後略
}

実際のデータの組み立てと送信部分は以下のようになります。

カラー情報の送信

最初はカラー情報の送信です。ブラウザのColor Pickerから色を選択したらそれを文字列としてUnity側に送るものです。

function changedColor(evt) {
    var colorData = getColorData(evt.target.value);
    var array = new Uint8Array(colorData);
    var buffer = new ArrayBuffer(array.byteLength + 1);
    var data = new Uint8Array(buffer);
    data.set([WebSocketController.MessageType.Color], 0);
    data.set(array, 1);
    ws.sendMessage(data.buffer);
}

function getColorData(colorStr) {
    return new TextEncoder().encode(colorStr).buffer;
}

changedColor関数はPickerのchangeイベントのコールバックです。
文字列のbyte配列データはTextEncoderクラスを使って行いました。

そして取得したバイト列をUint8Array型配列として参照するようにしつつ、データ・タイプ用に1byteプラスしたArrayBufferインスタンスを生成します。

生成したArrayBufferも同様にUint8Array型配列として参照できるようにしておきます。
この時点で生成されたbuffer変数はまだ空の状態なので、1バイト目にデータ・タイプを設定します。

具体的には以下の部分ですね。

data.set([WebSocketController.MessageType.Color], 0);

Uint8Arrayなどの型配列にはsetメソッドがあるのでそれでデータを設定します。
そしてその後ろに続くように文字列をバイト配列化したデータを追加します。

data.set(array, 1);

第二引数に指定しているのは書き込むデータのオフセットです。
すでに1バイトのデータ・タイプが設定されているのでオフセットは1ということですね。

これで無事、データの準備ができたのでWebSocket経由で送ることができます。

画像データの送信

C#側の(つまり受信側の)コードを見る前に画像データの送信部分も見ておきましょう。

function changedFile(evt) {
    var files = evt.target.files;

    if (files.length <= 0) {
        return;
    }

    var file = files.item(0);
    var reader = new FileReader();

    reader.onload = function () {
        var array = new Uint8Array(reader.result);

        var buffer = new ArrayBuffer(array.byteLength + 1);
        var data = new Uint8Array(buffer);
        data.set([WebSocketController.MessageType.Image], 0);
        data.set(array, 1);

        console.log(data.buffer.byteLength);

        ws.sendMessage(data.buffer);
    }

    reader.readAsArrayBuffer(file);
}

changedFileInputのファイルが変更された際に呼び出されるコールバックです。
選択されたファイルをFileReaderを使って読み出し、それをカラー情報のときと同様にデータ・タイプを先頭にセットして送信しています。

バイト配列の取り出し方が少し異なるだけで基本的にはやっていることは同じです。

C#側で受信する

最後に、ブラウザから送信されてきたデータの受信部分を書いて終わりにしたいと思います。

といっても、バイト配列を適宜操作するだけなのでバイトデータの取り扱いに慣れていればそんなにむずかしいことはしていません。

private void Connect()
{
    // 前略

    SynchronizationContext context = SynchronizationContext.Current;

    // 中略

    _webSocket.OnMessage += (sender, args) =>
    {
        Debug.Log("OnMessage");

        context.Post(_ => { HandleMessage(args.RawData); }, null);
    };
    // 後略
}

// ----------------------------

private void HandleMessage(byte[] data)
{
    DataType type = GetDataType(data);

    switch (type)
    {
        case DataType.Color:
            HandleAsColor(data);
            break;

        case DataType.Image:
            HandleAsImage(data);
            break;
    }
}

private void HandleAsColor(byte[] data)
{
    string colorStr = Encoding.UTF8.GetString(data, 1, 7);

    if (ColorUtility.TryParseHtmlString(colorStr, out Color color))
    {
        Debug.Log(color);
        _material.color = color;
    }
}

private void HandleAsImage(byte[] data)
{
    byte[] texData = new byte[data.Length - 1];
    Array.Copy(data, 1, texData, 0, texData.Length);

    Texture2D tex = new Texture2D(1, 1);
    tex.LoadImage(texData);
    tex.Apply();
    
    _material.mainTexture = tex;

    Debug.Log(tex.width);
}

private DataType GetDataType(byte[] data)
{
    if (data == null || data.Length == 0)
    {
        return DataType.None;
    }

    return (DataType)data[0];
}

実際に操作している部分だけを抜き出しました。
まず冒頭の、メッセージ受信時のコールバック登録です。

private void Connect()
{
    // 前略

    SynchronizationContext context = SynchronizationContext.Current;

    // 中略

    _webSocket.OnMessage += (sender, args) =>
    {
        Debug.Log("OnMessage");

        context.Post(_ => { HandleMessage(args.RawData); }, null);
    };
    // 後略
}

WebSocketの受信イベントは非同期で呼び出されるのでメインスレッド以外で実行されます。
そのため、メインスレッドで実行できるように準備しておきます。(SynchronizationContextの部分です)

そして受信した際には、イベント引数のRawDataにバイト配列が設定されているのでそれを利用して処理しています。

処理に関してはバイト配列から必要なバイト数分取り出して処理をしているだけなので特に解説はいらないでしょう。

ひとつだけ補足しておくと、今回のサンプルコードを書いていて知ったのがColorUtility.TryParseHtmlStringです。16進数などで表された色情報をColor構造体に変換してくれるユーティリティです。
ブラウザのColor Pickerはまさに16進数でカラー情報を表現しているので、受け取った文字列をそのままユーティリティに投げて色情報として受け取っているわけです。

あとはこれをマテリアルなどに設定してやればOKというわけですね。

まとめ

WebSocketを使うとシンプルにデータの送受信が行えることが分かりました。
特にモックなどで「外からこういうデータをさっと渡したいな」みたいなことをやる場合には非常に重宝しそうです。

一方で、Photonなどが提供してくれているような仕組みを作ろうとするとがっつりとフレームワークとして開発することになるので、結局Photonとか使ったほうがいいよね、とはなりそうです。

ちなみに余談ですが、Photonには、今回利用したWebSocketのDLLが梱包されているらしく、後追いでPhotonをインポートしたら定義がバッティングしてエラーが出てしまいました。
なのでPhotonを利用している人は特になにもしなくても今回のコードがそのまま動きます。