LoginSignup
5

More than 1 year has passed since last update.

posted at

[Unity] [Qt] WebSocketを使ってPCツールからUnityアプリを操作する

概要

グレンジ Advent Calendar 2021 11日目の記事を担当しているMAA_と申します。
クライアントサイドのエンジニアをやっています。

この記事では、Unityアプリの開発用機能を以下のようにGUI部分とその他の部分で分離して実装する方法を紹介します。

具体的には
・GUI部分はQtを使って実装
・その他の処理はUnity側で実装
という感じになります。

QtアプリとUnityの間はタイトルにもある通りWebSocketを使ってワイヤレスで連携します。

ちなみにQtとは、クロスプラットフォームの統合開発環境で、個人的には現時点でツール類などの開発に最も適した開発環境だと思っています。
(Qt詳細はこちら)

下記画像は、実際にQtで作成したアプリをMacで実行し、Unityアプリをワイヤレス接続して操作している様子です。
同時に3つのデバイス(WindowsPC/Mac/iPad)と接続して操作しています。
174d8994-4361-0440-448c-adad99db7046.jpg

割と長い記事になってしまったので、例えばUnityのWebSocketSharpの使い方だけ見たいとか、QtのWebSocket実装方法が知りたいとか、ピンポイントで知りたいことがある場合は、下記目次から飛んでください。

なお、記事で参照するソースは下記で公開しています。
https://github.com/prog-k-dev/KTWebSocketApp

◆◆目次◆◆
・Unityにおけるデバッグ機能の実装
・準備
・サンプルを開いてみる
・サンプルで出来ること
・Webソケット通信
・通信データ
・メッセージクラス
・共通クラス一覧
・Unity側クラス等一覧
・Qt側クラス等一覧
・Unity側追加パッケージについて
・Qtアプリ側外部ライブラリ
・Qtについて
・クロスプラットフォームに関する注意点
・まとめ

Unityにおけるデバッグ機能の実装

早速ですが、皆さんはUnityを使う場合、デバッグ機能などは何を使って実装していますか?

一般的には
・UnityEditor拡張
SRDebugger
などがよく使われているかと思います。いずれもUnityEditorもしくはUnityアプリの画面にGUIを表示するものですね。

昔に比べるとUnityのGUI実装も随分と改善されてきてはいますが、そもそもUnityはツールを作成するのに向いている開発環境ではありませんので複雑で大きなリストやダイアログのポップアップなどツール系アプリでよくある表現などを細かく作り込もうとするとなかなか苦労するしメンテナンスも大変だと思います。

この記事では、その改善方法として
・GUI部分はQtを使って実装
・その他の処理はUnity側で実装
・両者の間はWebSocketを使ってワイヤレスで連携
という実装方法を提案しています。

この方法でデバッグ機能を実装するメリットは、
GUIコードをUnityから分離できる(Unityアプリ本体にデバッグ用GUIに関する無駄なコードが入らない)
・Qtで高度なGUIが比較的楽に実装できる
・PCと実機でファイルのアップロード、ダウンロードなど出来るので手軽にデータファイルの差し替えなど試すことができる
・1台のPCで複数の実機を同時に監視、操作できるのでデバッグが捗る
スマホの小さい画面でいちいちデバッグメニューを操作しなくてもいいのでデバッグが捗る
・操作にマウス、キーボード(コピペ含む)が使えるのでデバッグが捗る
スクショ共有が高速
など色々とあります。

実装次第でかなり高度なデバッグ機能が実装できるのでこの記事をきっかけにしてこのやり方が広がってくれたら個人的にとても嬉しいです。

準備

まず初めに必要なものです。

・Unity
・QtCreator(詳細はこちら

UnityにしてもQtCreatorにしてもインストール手順など全てここで書こうとするとそれだけで一つの記事になってしまうので、ここでは私が開発に使用したバージョンのみお伝えします。
・Unity: 2019.4.31.f1
・QtCreator: 5.0.0 (Based on Qt 5.15.2)

その他、UnityやQtCreatorをインストールする際にVisualStudio、XCodeなどのインストールも環境に応じて必要になると思います。

Unity側サンプルでは追加のパッケージを二つインポートしています。(詳細はこちら
サンプルでは、どちらのパッケージもインポート済みの状態なのでサンプルを実行する際には改めてインポートする必要はりません。

サンプルを開いてみる

サンプル「KTWebSocketApp」の構成は以下のようになっています。

KTWebSocketApp:ルート
 +- Qt:Qt版アプリ
 +- Unity:Unity版アプリ

それぞれ以下のファイルを開いてください。

・Qt:KTWebSocketApp/Qt/WebSocketApp/WebSocketApp.pro
・Unity:KTWebSocketApp/Unity/WebSocketApp/Assets/Scenes/WebSocketApp.unity

とりあえず両方同じPCで開きます。

なお、Qtプロジェクトを開いた際に以下のような画面が表示された場合は、Desktop Qt 5.15.1 clang 64bit(またはそれに類似するもの)を選択して「ConfigureProject」ボタンを押下してください。
image.png

両方開いたらそれぞれ実行します。
・Unity側
43b89913-c352-5dac-f68e-b19fa645c11a.png

・Qt側
7c67fe5a-8d2c-b1dc-386b-62530cb0698d.png

次にQt側のウィンドウ右上にある「開く」ボタンを押下します。
Addressポートで接続先を指定しますが、Unityアプリも同じPCで動作している場合はとりあえずはこのままで問題ないはずです。

するとこのようなウィンドウが開くのでウィンドウ右下の「接続」ボタンを押下します。
83b7f5e4-99cc-ccb5-c76e-f8d7a530b9f0.png

「Qt側ウィンドウのタイトルバー」と「Unity側のGameビュー【接続情報】」の表示がどちらも
デバイス[xxxxxx]で動作中のアプリケーション(WebSocketApp)に接続中
となれば接続成功です。
9de07ee9-97da-90ba-ff16-11572ba1d180.png

ここでは、とりあえず同一PC内でQtアプリ、Unityアプリが動作している状態での接続を試しましたが、以下のような様々な組み合わせでの接続が可能です。
・Unityアプリ:WindowsPC、Qtアプリ:Mac
・Unityアプリ:iPhone、Qtアプリ:Windows
・Unityアプリ:Android、Qtアプリ:Mac

一つのQtアプリに対して、複数のUnityアプリから接続することも可能なので、一つのPCから複数のアプリを監視して操作するような使い方もできます。

なお、UnityアプリとQtアプリをワイヤレスで接続するためには
・両方のデバイスが同じLANに接続していること
という条件があるのでこの点だけ注意してください。

サンプルで出来ること

このサンプルでは以下のようなことができます。

・ファイル操作(ファイルリスト取得、アップロード、ダウンロード)
・スクショ共有
・テキスト共有
・ツール側からUnity側のGameObject操作

◆ファイル操作(ファイルリスト取得、アップロード、ダウンロード)

今回は接続先アプリがUnityという前提があるのでファイルの操作対象となるディレクトリとして、Application.dataPath、Application.temporaryCachePathなど4種類のいずれかを選択するようになっています。

・ファイルリスト取得
ファイルリストを取得する対象のディレクトリを「UnityPathタイプ」の4つの中から一つ選択して「ファイルリスト取得」ボタンを押下します。取得に成功すると「ログ」ビューに結果が表示されます。

また「ファイル名」に対象ディレクトリ以下のサブディレクトリを指定することで深い階層にあるディレクトリのファイルリストも取得することができます。
例えば、「Temporary Cache」を選択して、ファイル名に「data/1」と入力した場合は、ディレクトリ「(Application.temporaryCachePath)/data/1」内のファイルリストが取得できます。
1c449781-f029-a9c7-2425-c5e4b9848fa9.png

・ダウンロード
ファイルリスト取得と同様にUnityPathタイプファイル名で特定のファイルのパスを指定して「ダウロード」ボタンを押下すると、Unity側アプリから指定のファイルをダウンロードしてPCのダウンロードフォルダに保存します。

・アップロード
PCからUnityアプリ側にアップロードしたいファイルのフルパスを「ファイル名」に入力。
アップロード先のディレクトリを「UnityPathタイプ」で指定したら「アップロード」ボタンを押下します。(アップロード先は「UnityPathタイプ」で指定したディレクトリの直下に限定されます)

アップロードに成功するとUnityアプリ側の 【受信ファイル】にアプロードされたファイル名が表示されます。
アップロードしたファイルが画像ファイルの場合をそれに加えて【受信画像】に受信した画像が表示されます。
3e025729-3ae1-0c80-f506-f6dc0bc6172a.jpg

◆スクショ共有

「スクリーンショットを要求」ボタンを押下するとその時点のUnityアプリ側のスクリーンショットを受信して表示します。
また「自動更新」にチェックを入れた状態で「スクリーンショットを要求」を押下すると指定した秒数ごとにスクリーンショットの送信を続けます。
自動更新を停止したい場合は、「更新停止」ボタンを押下します。
5d68dccf-b82c-b4f4-ac1b-24c0943d6822.png

現時点では、例えば0.1秒とか更新頻度が多すぎると途中でクラッシュするかもしれないのでご注意ください。(恐らく送信データのサイズと送信頻度の問題)

◆テキスト共有

「テキスト」に任意のテキストを入力して「テキスト送信」ボタンを押下するとUnity側の「受信テキスト」の内容が更新されます。
9cc53aee-078d-c0c0-c8ce-8936a76aa157.png

◆ツール側からUnity側のGameObject操作

Unity側に存在するGameObjectの名前を入力して「GameObjectと接続」ボタンを押下します。
ad9d6b93-8481-3592-6cea-c5f2ad805f75.png
その名前のGameObjectが存在する場合は、GameObjectの色が赤に変わります。

上下左右の各ボタンを押下すると接続済みのGameObjectの位置を移動することができます。
5b47ce0a-acca-d3e9-f9d4-fe9153d33dcf.png

その他

最後に滑り込みで入れた機能なので必ずしも正確な情報を表示できるかちょっとわからないけどUnity側アプリの画面右上あたりにそのデバイスのWiFiのIPアドレスを表示するようにしてみました。(Qt側からUnity側に接続する際にIPアドレスが必要だけどいちいち端末設定を開いてIPアドレスメモるのが面倒だと思うので)
image.png
あんまりいい方法見つからずまあまあ力業なんですが、一応MacとiPhoneとiPadでは正常にWiFiのアドレスが取れているようです。

WebSocketApp.cs
private string GetIPAddress(string idFilter = "")
{
    foreach (var networkInterface in NetworkInterface.GetAllNetworkInterfaces())
    {
        if (networkInterface.NetworkInterfaceType != NetworkInterfaceType.Wireless80211 &&
            networkInterface.NetworkInterfaceType != NetworkInterfaceType.Ethernet)
        {
            continue;
        }

        if (!string.IsNullOrEmpty(idFilter) && networkInterface.Id != idFilter)
        {
            continue;
        }

        foreach (var ip in networkInterface.GetIPProperties().UnicastAddresses)
        {
            if (ip.Address.AddressFamily != System.Net.Sockets.AddressFamily.InterNetwork)
            {
                continue;
            }
            return ip.Address.ToString();
        }
    }
    return "";
}

private bool Setup()
{
    Application.logMessageReceivedThreaded += ReceiveLogMessage;

    // 接続中のWiFiのIPアドレスを取得
    // NOTE: 多分あんまりいい方法ではないのだけど、、、とりあえず正しい値を取れてはいる
    string address = GetIPAddress("en1");
    if (string.IsNullOrEmpty(address))
    {
        address = GetIPAddress("en0");
    }
    if (string.IsNullOrEmpty(address))
    {
        address = GetIPAddress();
    }
    _ipAddressText.text = "【WiFiのIPアドレス】" + (string.IsNullOrEmpty(address) ? "" : address);

Webソケット通信

サンプルでは、UnityアプリとQtアプリ間でWebソケットを使って通信しています。

以下では、それぞれのアプリでどのようにWebソケット通信を実装しているか見ていきます。

Webソケット通信(Unity側)

「WebSocketSharp」という追加のパッケージを使用します。(詳細はこちら
サンプルでは、Webソケット通信に関連する処理はWebSocketConnection.csに全て実装されています。

基本的にはUnity側が特定のポートを開けてQtアプリ側からの接続を待機する実装になっています。

◆Webソケット通信開始
WebSocketSharpパッケージのクラス「WebSocketServer」を生成し初期化します。

WebSocketConnection.cs
public void Connect() {
    if (_webSocket != null)
    {
        Disconnect();
    }
/*①*/ _webSocket = new WebSocketServer(Port);
/*②*/ _webSocket.AddWebSocketService<MyWebSocketBehavior>(_path, InitWebSocketBehavior);
/*③*/ _webSocket.Start();
}

WebSocketServer生成
WebSocketServerWebSocketSharpパッケージに含まれるWebソケット処理の中心となるクラスです。
生成時にはポート番号を指定します。

②WebSocketServer初期化
AddWebSocketService()では
・Webソケット通信時の挙動を実装しているクラス型(ここではMyWebSocketBehavior)
・ソケットのパス
・初期化用のアクション
を指定します。

③Webソケット通信を開始
WebSocketServerStart()を呼び出してソケット通信を開始します。

◆Webソケット通信停止

WebSocketConnection.cs
public void Disconnect()
{
     _webSocket?.Stop();
     _webSocket = null;
}

WebSocketServerStop()を呼び出してソケット通信を停止します。

◆Webソケット通信時の挙動を実装
WebSocketBehaviorの派生クラス(サンプルではMyWebSocketBehavior)に
・接続が開いた時:void OnOpen()
・接続が閉じた時:void OnClose(CloseEventArgs)
・データを受信した時:void OnMessage(MessageEventArgs)
などのイベントが発生した際の挙動をoverrideします。

WebSocketConnection.cs
private class MyWebSocketBehavior : WebSocketBehavior
{
    public WebSocketConnection Connection {
        get;
        set;
    } = null;

    public bool IsOpen {
        get {
            return State == WebSocketState.Open;
        }
    }

    /// テキスト送信
    public void SendString(string val) {
        if (!IsOpen) {
            Debug.LogWarningFormat("ソケットが開いてないので送信がキャンセルされた:{0} バイト", val.Length);
            return;
        }

        Send(val);
    }

    /// テキスト送信(非同期)
    public void SendStringAsync(string val)
    {
        if (!IsOpen)
        {
            Debug.LogWarningFormat("ソケットが開いてないので送信がキャンセルされた:{0} バイト", val.Length);
            return;
        }

        SendAsync(val, comp =>
        {
        });
    }

    /// 接続オープン時
    protected override void OnOpen() {
        Connection?.OnOpen();
    }

    /// 接続クローズ時
    protected override void OnClose(CloseEventArgs e) {
        Connection?.OnClose();
    }

    /// メッセージを受信
    protected override void OnMessage(MessageEventArgs args) {
        if (args.Data != null) {
            /// テキストデータを受信
            Connection?.OnReceiveMessage(args.Data);
        } else if (args.RawData != null) {
            /// バイナリデータを受信
            Connection?.OnReceiveMessage(args.RawData);
        }
    }
} // class MyWebSocketBehavior

このサンプルではMyWebSocketBehaviorクラスは、WebSocketConnectionクラスのプライベートクラスとして定義しており、それぞれのイベントが発生した際にはほぼそのままWebSocketConnectionに処理を渡しています。

受信したテキストデータはWebSocketConnection側でデコードされます。
デコード結果はWebSocketAppに渡されて、そのデコード内容に応じた処理が実行されます。

データ送信用のメソッドは
・Send:即時送信
・SendAsync:非同期送信
の2種類あります。

Webソケット通信(Qt側)

Webソケット通信にwebsocketsという追加のモジュールを使います。

Qtのプロジェクト定義ファイル(WebSocketApp.pro)で以下のように追加モジュールの指定をします。

WebSocketApp.pro
greaterThan(QT_MAJOR_VERSION, 4): QT += widgets websockets

Qtではプロジェクトへの追加モジュールの定義などは基本的にはこのようにプロジェクト定義ファイルに手作業で追しなければなりません。ここは今後もうちょっと改善されて欲しい点。

サンプルでは、Webソケット通信に関連する処理はクラスConnectionDialogに全て実装されています。

Unity側アプリ側の
・IPアドレス
・ポート番号
を指定することで相互の接続を確立します

◆接続開始

ConnectionDialog.cpp
bool ConnectionDialog::Open() {
        Close();

/*①*/ _socket = new QWebSocket();
/*②*/ connect(_socket, &QWebSocket::connected, this, &ConnectionDialog::onConnected);
        connect(_socket, &QWebSocket::disconnected, this, &ConnectionDialog::onClosed);
        connect(_socket, &QWebSocket::aboutToClose, this, &ConnectionDialog::onAboutToClose);
        connect(_socket, &QWebSocket::stateChanged, this, &ConnectionDialog::onStateChanged);
        connect(_socket, QOverload<QAbstractSocket::SocketError>::of(&QWebSocket::error), [=](QAbstractSocket::SocketError error) {
            onError(error);
        });
/*③*/ _socket->open(QUrl(_path.c_str()));
        return true;
}

QWebSocket生成
QWebSocketはWebソケット処理の中心となるクラスです。

②Webソケット通信イベントとコールバックを結びつける
Webソケット通信の各種イベントに対応するコールバック関数を結びつけます。

以下のイベントとコールバックを結びつけています
・接続が開いた時:onConnected
・接続が閉じた時:onClosed
・接続が閉じる前:onAboutToClose
・Webソケットのステータスが変化した時:onStateChanged

また、接続が開いた際にはonConnectedで以下のコールバックも追加で結びつけています。

ConnectionDialog.cpp
void ConnectionDialog::onConnected() {
    SetConnectFlag(true);

    connect(_socket, &QWebSocket::textMessageReceived, this, &ConnectionDialog::onTextMessageReceived);
    connect(_socket, &QWebSocket::binaryMessageReceived, this, &ConnectionDialog::onBinaryMessageReceived);

・テキストデータを受信:onTextMessageReceived
・バイナリデータを受信:onBinaryMessageReceived
なお、このサンプルでは、データは全てbase64でエンコード済みのテキストにして送受信しているのでバイナリデータを受信することはありません。

③Webソケット通信を開始
ソケットのパスを指定します。

このパスの内容は、 ws://(IPアドレス):(ポート番号)(接続パス) となっています。
このサンプルでは、例えばローカルPCに接続する場合のパスは、"ws://127.0.0.1:5637/WebSocketApp/takahashi_kenji"となります。

◆接続停止

ConnectionDialog
void ConnectionDialog::Close() {
        if (_socket != nullptr) {
/*①*/     _socket->close();

/*②*/     disconnect(_socket, &QWebSocket::textMessageReceived, this, &ConnectionDialog::onTextMessageReceived);
            disconnect(_socket, &QWebSocket::binaryMessageReceived, this, &ConnectionDialog::onBinaryMessageReceived);

            delete _socket;
            _socket = nullptr;
        }
}

①Webソケットを閉じる
②データ受信のコールバッグとの結びつきを解除する

通信データ

本サンプルでは、通信データは全て以下の形式になっています。
(データタイプ),(圧縮タイプ),データ本体(base64エンコード済みテキスト)

例)SocketLogMessage,c,H4sIAAAAAAAEE6tWis9NLS5OTE8NqSxIVbJSCs5Pzk4tCUotLE0tLvGFSCnpKMUXQUTgKpzz8/JSk0sy8/M889Lyi3ITQUxM9Z4pSlYGtQBfz1+wZgAAAA==

ちなみにデータタイプとデータ本体を分けている理由はデータ本体をbase64デコード+解凍せずにデータタイプを判別できるようにするため。
こうすることで処理不要なデータタイプの場合は、デコードと解凍の処理をスキップできるようにしています。(主に負荷軽減のため)

◆データタイプ
ASCIIテキスト。基本的にはデータのクラス名。

◆圧縮タイプ
データ本体がGZipにより圧縮されている場合は「C」。
圧縮されていない場合は「-」が入る。

◆データ本体
base64でエンコードされたテキストデータ。
圧縮されている場合は、圧縮した後にbase64でエンコードされている。

エンコードの処理手順を簡単にまとめると以下の通り。

SocketMessage.cpp
bool SocketMessageBase::ExportMessage(QString& message) const {
        QJsonObject obj;
/*①*/ if (!ToJson(obj)) {
            return false;
        }

/*②*/ message = QFORMAT_STR("%s,", _messageType.c_str());

        if (!obj.empty()) {
            QJsonDocument document(obj);

            QByteArray array = document.toJson(QJsonDocument::Compact);
            {
                std::vector<char> compressed;
/*③*/         if (WebSocketApp::CompressGZip(array.data(), array.length(), compressed) <= 0) {
                    message += "-,";
                } else if ((int)compressed.size() >= array.length()) {
                    message += "-,";
                } else {
                    message += "c,";
                    array = QByteArray(&(compressed[0]), compressed.size());
                }
            }
/*④*/     message += array.toBase64();
        }
        return true;
}

①オブジェクトをJSON化
送信されるメッセージは全てSocketMessageBaseからの派生クラスで、それぞれが自身をJSON化するためのメソッドToJson()をオーバーライドしている。
例えば、ログデータを送受信するためのメッセージ(SocketLogMessage)の場合は以下のようになっている。

SocketMessage.cpp
bool SocketLogMessage::ToJson(QJsonObject& obj) const {
    if (!SocketMessageBase::ToJson(obj)) {
        return false;;
    }

    SET_JSON_INT_VALUE(_logType, obj);
    SET_JSON_VALUE(_log, obj);
    SET_JSON_VALUE(_stackTrace, obj);
    return true;
}

②データタイプをメッセージ先頭に付与

③データの圧縮
JSON化したデータをGZipで圧縮する。

④base64エンコード
最後に③で生成したGZipで圧縮したJSONデータをbase64でエンコードする。
ただしGZip圧縮した結果のデータサイズが圧縮前のサイズ以上だった場合は圧縮前のデータがエンコード対象となる。

メッセージクラス

Unity側、Qt側どちらも通信データはメッセージクラス「SocketMessageBase」の派生クラスとして実装されています。

メッセージクラスは、Unity側で実行したいデバッグ機能ごとに定義するので、おそらくこのサンプルをカスタマイズしようと考えた場合、最もコード量が大きいのはこの部分になると思います。

◆Unity側
SocketMessageBaseの派生クラスにSerializable属性をつけて定義。
JSON化はJsonUtility.ToJson()にSocketMessageBaseの派生クラスのインスタンスを渡すだけ。

例えばSocketLogMessage(ログメッセージ)は以下のように定義されている。

SocketMessage.cs
[Serializable]
public class SocketLogMessage : SocketMessageBase
{
    public static readonly string MESSAGE_TYPE = typeof(SocketLogMessage).Name;

    public SocketLogMessage(LogType logType, string log, string stackTrace)
    {
        var now = DateTime.Now;
        _logType = logType;
        _time = string.Format("{0:00}:{1:00}:{2:00}", now.Hour, now.Minute, now.Second);
        _log = log;
        _stackTrace = stackTrace;
    }

    public LogType _logType;
    public string _time;
    public string _log;
    public string _stackTrace;
}; // class SocketLogMessage

◆Qt側
通信データのクラスを全てSocketMessageBaseの派生クラスにするのはUnity側と同様。

ここで注意が必要なのは、クラス名、メンバ変数名をUnity側、Qt側で一致させること。
ここに違いがあるとSocketMessageBase派生クラスのJSONからのインスタンス化やメンバ変数の取り込みが失敗します。

例えばSocketLogMessage(ログメッセージ)は以下のように定義されています。(メンバ変数のGet、Setメソッドは省略)

SocketMessage.h
class SocketLogMessage : public SocketMessageBase {
public:
    static const char* MESSAGE_TYPE;

    enum class LogType : int
    {
        Invalid = -1,

        Error,
        Assert,
        Warning,
        Log,
        Exception
    };

    SocketLogMessage()
        : SocketMessageBase(MESSAGE_TYPE)
        , _logType(LogType::Invalid)
    {
    }

//  (・・・中略・・・)

private:
    LogType _logType;
    std::string _time;
    std::string _log;
    std::string _stackTrace;

    bool FromJson(QJsonObject& obj) override;
    bool ToJson(QJsonObject& obj) const override;
}; // class SocketLogMessage

オーバーライドしたFromJson()でJSONからのデシリアライズ、ToJson()でJSONへのシリアライズを実装している。

JSON化のために使用するQtクラスは主に以下の4つ。
・QJsonDocument
・QJsonObject
・QJsonValue
・QJsonArray

実際にどのようにJSON化するかは実際のコードを見て欲しいが、基本的にはメンバ変数名をキーにしてメンバ変数の値をJSON要素にSetまたはGetしているだけである。

共通クラス一覧

メッセージデータはUnity、Qt同じクラス名で実装されています。
ただし、当然ですがソースコードはそれぞれ別になります。

・SocketMessageBase:メッセージデータ基底クラス
・SocketRequestMessage:リクエストデータ
・SocketScreenShotRequestMessage:スクリーンショットリクエスト
・SocketFileListRequestMessage:ファイルリストリクエスト
・SocketFileUploadRequestMessage:ファイルアップロードリクエスト
・SocketConnectGameObjectRequestMessage:ゲームオブジェクト接続リクエスト
・SocketConnectionInformationMessage:Webソケット接続先情報
・SocketLogMessage:ログデータ
・SockeTextMessage:テキストデータ
・SocketFileListMessage:ファイルリスト
・SocketFileMessage:ファイルデータ
・SocketImageDataMessage:画像データ
・SocketScreenShotMessage:スクリーンショットデータ
・SocketMoveGameObjectMessage:ゲームオブジェクト移動データ

Unity側クラス等一覧

◆クラス
・ISocketMessageAccepter:メッセージ受信用インターフェイス
・WebSocketApp:アプリメイン(ISocketMessageAccepterを実装)
・WebSocketConnection:Webソケット接続
・MyWebSocketBehavior:Webソケット通信時挙動(WebSocketConnectionのprivateクラス)

◆ファイル
・WebSocketApp.unity: メインシーン

Qt側クラス等一覧

◆クラス
・MainWindow:メインウィンドウ
・ConnectionDialog:Webソケット接続ダイアログ(通信関係の処理も大体ここ)
・ImageWidget:画像描画用カスタムウィジット

◆ファイル
WebSocketApp.h, WebSocketApp.cpp
アプリ全体の共通処理をまとめたもの。

主な公開関数
・CompressGZip:GZip圧縮
・DecompressGZip:GZip解凍

Unity側追加パッケージについて

以下の2つのパッケージを追加でインポートしています。
・NuGet
・WebSocketSharp

NuGet

WebSocketSharpを取ってくるのに必要になります。

下記から最新のunitypackageをダウンロードしてインポートするだけです。
https://github.com/GlitchEnzo/NuGetForUnity/releases

UnityEditorメニューに「NuGet」が追加されていればインポートは成功です。
7b17773b-658e-9e1f-2c08-db6af778bf9d.png

WebSocketSharp

Webソケット通信処理を実装するための必須パッケージです。

NuGetメニューの「Manage NuGet Packages」でパッケージマネージャを開きます。

パッケージマネージャの「Search」ボックスに「WebSocketSharp」と入れてSearchボタンを押下すると「WebSocketSharp-netstandard」がヒットすると思いますので、「Install」ボタンを押下します。
002ac40f-1709-40cb-09e6-37c7b736aff6.png

Qtアプリ側外部ライブラリ

バイナリデータの圧縮解凍に外部ライブラリである zlib を使用しています。

Qtについて

Qtは、クロスプラットフォームの統合開発環境で開発用のメイン言語はC++になります。

・公式ホームページ
https://www.qt.io/ja-jp/

現時点では、ツール類などの開発に最も適した開発環境だと思います。

クロスプラットフォームなので作成したプロジェクトは、Windows、MacOSX、Linuxなど様々なプラットフォームで動作するバイナリをビルドすることができます。
この記事で公開しているソースもWindows、MacOSX両方での動作を確認済みです。

私は、現在スマートフォン向けアプリの開発を主にやっているので開発のメインマシンはMacになります。
普段の業務でもツールを作成することがありますが、この場合も主にMacを使って開発します。

ただ、私が作成したツールは、エンジニアだけではなく他の業種の方(テスターさんとかディレクターさん)も使います。

現在の職場では、エンジニアは主にMacを使っていて、その他の業種の方は主にWindowsを使っていることが多いです。従ってツールはWindows/Macの両方で動かすことができるのが理想ですが、それを実現しようとした場合Qt以外の良い選択肢は正直見当たりません。

なおQtにはオープンソースを使って実装された無料で使えるオープンソース版Qtもあるのでこちらを使えば社内ツールの作成なんかは無料でできます。

ちなみにAutodesk社のMayaもQtを使って実装されています。他にも割とメジャーなソフトでQtを使っているものも結構ありますが、商用の開発となると開発者一人当たり100万円/年くらいのライセンス料がかかるのでこれがもうちょい安くなってくれたらもっと広まる気がするんだけどなーーと残念に思います。

クロスプラットフォームに関する注意点

Qtはクロスプラットフォームですが、プラットフォーム間の違いで発生する問題も全くないわけではありません。
以下は、その問題の一部と解決方法になります。

◆文字化け
これまで複数のプラットフォームで動作するコードを何度も書いてきましたが、経験上はまるのは大体いつでも文字コード関係です。

Qtも例に漏れずWindows版アプルで日本語文字列が文字化けする問題があります。(これはまあQtが悪いわけじゃなくMicrosoftのVisualStudioの問題といった方が正しいかもしれませんが)

例えば単純にボタンの文字を設定する以下のコード。

以下は、ボタンを押下するたびにボタンのテキストを切り替えるだけの単純なコードです。

void MainWindow::on_pushButton_clicked()
{
    static bool toggle = true;
    if (toggle) {
        ui->pushButton->setText("QTワールド");
    } else {
        ui->pushButton->setText("ハロー");
    }
    toggle = !toggle;
}

Macでは正常にボタンのテキストが切り替わります。
6406971d-d88a-b405-7c53-cfd48229ee94.png

ところがこのコードをWindowsで実行した場合は、こうなります。
e6af7f9f-20a3-1a26-7c8b-49e3ec07ea7c.png

理由ですが、どうやらWindows版ではコンパイル時にVisualStudioが文字列上定数の文字コードをデフォルトでShift-JISに変換してしまうためにこのような文字化けが起きるようです。

文字列定数をUTF-8としてコンパイルする方法は3つあります。
上から順番にお勧めの方法になりますが、一長一短あるので状況によって使い分けてください。

1)コンパイルオプションを追加する

今回のサンプルではこの方法を採用してます。
Windows版をビルドする際のコンパイルオプションとして文字列定数のエンコードを指定してます。

WebSocketApp.pro
win32 {
    QMAKE_CXXFLAGS += -execution-charset:utf-8
}

この記述をプロジェクトファイル(拡張子.pro)に追加します。Windowsでのみ必要なオプションなので win32で囲んでいます。

2)pragma で文字列定数のエンコードを指定する

以下をプロジェクトの共通ヘッダの先頭にでも入れておきます。

#pragma execution_character_set("utf-8")

1と同じような方法ですが、「Visual Studio 2015 Update 2 以降では廃止された」との情報もあるのでこっちを使うなら1の方法が確実かも。

3)u8プレフィクスをつける

文字列定数の先頭には必ず「u8」を付けて確実にUTF-8としてコンパイルされるようにします。

ui->pushButton->setText(u8"QTワールド");

1と2に比べて面倒といえば面倒ですが文字列定数に明示的にプレフィクスを書くことでより意図がわかりやすいというメリットがあります。

◆関数の有無
一部の関数は、プラットフォームによって微妙に名前が違ったり、もしくは存在自体がなかったりすることがあります。
このサンプルでは、「vasprintf」を使っていますが、Windowsでは存在しないのでコンパイルエラーになります。

以下のように自前でvasprintfを定義することでこの問題を回避しています。

#ifdef Q_OS_WIN
inline int vasprintf(char** buffer, const char* format, va_list& vaList) {
    int length = _vscprintf(format, vaList);
    *buffer = (char*)malloc(length + 1);
    return  vsprintf(*buffer, format, vaList);
}
#endif

Q_OS_WINはQt組み込みのマクロ定義でWindowsの場合のみ有効になります。

その他の組み込みマクロ定義は下記リンク先に記載されているので状況に応じたコードを書く必要がある場合に参照ください。
https://doc.qt.io/qt-5/qtglobal.html

まとめ

今回使用したサンプルは全般的にそこまで厳しく動作検証してないです。
サンプルを実際に利用する場合、不具合などに関しては自己責任で。できたら追加の検証などしてから利用してください。
でも何か改善点など見つけたら是非ご報告いただけたら嬉しいですw

ちなみにUnityとQtの間の接続や、やりとりしているデータは特にプラットフォームには依存しないのでUnityの部分もQtの部分もどちらもそれ以外の開発環境でも成立します。実際似たようなツールを以前Qt+XcodeとかXCode+XCodeで作成しています。(XCodeでの画面処理は癖が強くで面倒だったな、、、)

今回使用したQtに関しては、多少癖があるので最初はとっつきづらいかもしれませんが、そもそも全ての開発環境には何らかの癖があり、そういった意味ではQtの癖はそこまで強くはありません。画面の構築もボタンとかリストビューとかをドラッグ&ドロップで配置するだけで概ね完成するのでXCodeなんかの大癖開発環境と比べると習得は容易だと思います。

また今回紹介した方法は、普段Unityメインで開発している方にとっては、C++とかQtの学習コストが必要になるので結構面倒かもしれませんが、デバッグ機能の実装に必要な機能は割とサンプルで網羅したつもりなので皆さんの開発環境向けにカスタマイズして使う場合も大体のことはできると思っています。

是非このサンプルを活用して皆様の開発ライフが少しでも快適になるようであれば幸いです。

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
What you can do with signing up
5