オープンソースのゲームエンジンGodotでサクッとMQTTを使う話―コピペで動く(はず)Godotノンゲーム利用のための通信まわり―

※ この記事の内容はGodot Engine 4.2.2と4.3.RC1でテストしています。

さて、ゲームエンジンをNon Game(ゲーム開発以外、たとえば産業用とか、研究とか)用途で使いたいとき、なにかと他システムとの通信が必要になる場合が多いと思います。

色々と方法はあると思いますが、汎用性とか気軽さとか考えるとやっぱりMQTTが良さそうかな?

qiita.com

ところで、最近オープンソースのゲームエンジン、Godotエンジンに注目しています。

ゲームエンジンといえば今はUnityとUnreal Engineの2強ですが、最近いろいろあってGodotも存在感を増してきている感じがします。

godotengine.org

Godotの魅力はOSSでライセンス料がかからないことも大きいですが、2強に対して圧倒的なのがその軽さです。ストレージ的にもパフォーマンス的にもかなり身軽なので、あえてGodotを使いたくなるシーンもありそうです。

まあそういうわけで最近ちょっとGodotにかぶれてるので、今日はGodotとMQTTのお話です。

できるだけGDscriptを使いたい

UnityでMQTTNetを使ったときに、.NETのバージョンやら何やらで苦しんだ覚えがあるので、そういう外的要因の少ない(と思ってる)Godot組み込みのスクリプト言語GDScriptで実装されている方法が良い気がします。

Godotアセットストアで探してみると、なんだかぴったりのものが見つかりました!

godotengine.org

動作確認は、GitHubにアップされている立派なサンプルシーンを実行するだけでOKでした。

github.com

逆にサンプルが立派すぎて、使い方を読み解くのにちょっとつまづいたりしたので、GodotにMQTTを喋らせる最短手をまとめてておきます。

※GDScript入門レベルを予習しておけばつまづかないくらいのレベルです、僕は「Python使えればなんとかなるっしょ!」という気持ちでやって初歩レベルでつまづいてました。

MQTTアドオンを追加する

新規プロジェクトの作り方は割愛します。MQTT Clientをダウンロードして、「addons」フォルダをプロジェクトのルートに入れましょう。

ノードを作ってGDスクリプトを追加する。

まず、スクリプトをアタッチするためのノードを追加します。使い勝手的に、このスクリプトはルートに置くのが良さそうです。

ノードの種類はなんでも良いですが、何か2Dオブジェクトを動かして動作確認するつもりでNode2Dにしておきましょうか。

右クリックメニューで「スクリプトをアタッチ」、名前はMyMqttManager.gdとでもしておきます、ここにMQTTの送受信時の処理を書いていくことになります。

アドオンも必要なので、子ノードを追加して名前を「MQTT」に変更、スクリプトの追加でMQTT.gdを探してアタッチします。

                   

これで入れものの準備は完了です。

シグナルを設定してサブスクライブする

Godotでは、スクリプト間の通信にシグナルという仕組みを使います、JavaScriptのイベントリスナーみたいなやつです(っていうふんわりした理解で使ってます、動いたからヨシ)

MQTT.gdで通信した結果をMyMqttManager.gdで受けたいので

MQTTノードを選択して、右側のインスペクター(?)で使いそうなイベントを選んでNode2Dに接続していきます。

最低限接続完了とメッセージの受信があれば良いでしょう。ちなみに送信のほうはシグナル設定不要です。

接続するとMyMqttManager.gdにon〜〜が追加されていくので、カッコ内に発火したい処理を書けばOKです。

今回はとりあえず、座標を送ってオブジェクトを動かしてみるところまで。 プロジェクトにデフォルトでGodotのアイコンが入っているので、シーンに配置します。

そうしたらMyMqttManager.gdをこのように編集、positionトピック宛にXY座標をカンマ区切りで送る想定です。

extends Node2D

func _ready():
        randomize()
        $MQTT.client_id = "Godot_s%d" % randi()
        $MQTT.set_last_will("status", "disconnected")
        $MQTT.connect_to_broker("tcp://localhost:1883")

func _process(delta):
        pass

func _on_mqtt_broker_connected():
        $MQTT.publish("status", "connected")
        $MQTT.subscribe("position", 0)

func _on_mqtt_received_message(topic, message):
        var v = message.split(",")
        if v.size() == 2:
                var position = str_to_var("Vector2({0})".format([message]))
                $Icon.position = position
        else:
                print("Message format is incorrect.")

MQTTブローカーを立てて接続確認する

そうしたらMQTTブローカーを立てて接続確認してみます。 ちなみに、こういうプロトタイピング的なフェーズで使うブローカーは、インストール簡単でパッと見で接続&通信状態がわかるShiftr.ioが超おすすめです。

www.shiftr.io

つながった! 見辛いですがGodot_〜〜ってIDが付いたノードがGodotです。

HTML + MQTT.jsでサクッと動作確認する

MQTTのプロトタイピングでダミーデータを送りたいような時、クライアントアプリを使う手もありますが、動的なデータが欲しい時はHTMLとMQTT.jsで簡単な送信ツールを作ってしまうのも結構手軽でおすすめです。

今回はオブジェクトの座標をコントロールするので、たとえばこんなふうに、マウスの座標を読み取って送るのはどうでしょう。

<!DOCTYPE html>
<html>

<head>
    <script src="https://unpkg.com/mqtt/dist/mqtt.min.js"></script>
</head>

<body>
    <p id="value"></p>
    <script>
        const connection = mqtt.connect('ws://localhost:1884');
        const valueElement = document.getElementById("value");

        document.onmousemove = function (e) {
            const x = e.pageX * 2;
            const y = e.pageY * 2;
            const message = [x,y].join(",");
            
            connection.publish("position", message);
            valueElement.innerText = message;
        };

    </script>
</body>

</html>

動いた!

上のHTMLソースをmouse-mqtt-publisher.htmlとか適当な名前で保存して、Webブラウザで開けば準備完了です。 マウスを動かしてみると...

x.com

バッチリですね!

とりあえずMQTTさえ喋れれば、ROSとかOPC-UAみたいなインダストリアル系の諸々にも簡単にブリッジできるはず。

なんとあのテスラも採用しているらしいですし、Godot for Industry の可能性に期待です!

x.com

【ちょっとハマりどころ】_process()の中でPublishするときの注意事項

_process()の中でリアルタイムに情報を送り続けたいような用途の場合、通信が確立する前にPublishするとソケット通信のエラー(コンソールにE=1とか出る)を吐いて全部ダメな感じになってしまうので、ステータスが接続済みになっていたらPublishする処理が必要になります。

具体的にはこんな感じです。

if $MQTT.brokerconnectmode == $MQTT.BCM_CONNECTED:
    $MQTT.publish("/your/topic/here",var_to_str(something_value))
else:
    print("DISCONNECTED")

これでOK。

【ちょっとハマりどころ2】通信が切れたときにソケットエラーで再接続できない問題と対策

mqtt.gdの100行目あたり、receiveintobuffer()内で,ソケットの受信データが0のときassert()で止める処理になっていますが,これだとソケットを閉じる処理がないので,再接続しようとするとすると競合してエラーを吐きます。

ホントはGithubでプルリクすべきところだと思いますが,Gitよくわかってない勢なのでとりあえず手元でやっつけときます。

func receiveintobuffer():
    if sslsocket != null and (sslsocket.get_status() == StreamPeerTLS.STATUS_CONNECTED or sslsocket.get_status() == StreamPeerTLS.STATUS_HANDSHAKING):
        handle_socket(sslsocket)
    elif socket != null and socket.get_status() == StreamPeerTCP.STATUS_CONNECTED:
        handle_socket(socket)
    elif websocket != null:
        handle_web_socket()

func handle_socket(sock):
    sock.poll()
    var n = sock.get_available_bytes()
    if n != 0:
        var sv = sock.get_data(n)
        if sv.size() > 0 and sv[0] != 0:
            print("Error: Received data has non-zero error code.")
            # ソケットを閉じる
            disconnect_from_server()
            emit_signal("broker_connection_failed")
            return
        receivedbuffer.append_array(sv[1])

func handle_web_socket():
    websocket.poll()
    while websocket.get_available_packet_count() != 0:
        receivedbuffer.append_array(websocket.get_packet())