※ この記事の内容はGodot Engine 4.2.2と4.3.RC1でテストしています。
さて、ゲームエンジンをNon Game(ゲーム開発以外、たとえば産業用とか、研究とか)用途で使いたいとき、なにかと他システムとの通信が必要になる場合が多いと思います。
色々と方法はあると思いますが、汎用性とか気軽さとか考えるとやっぱりMQTTが良さそうかな?
ところで、最近オープンソースのゲームエンジン、Godotエンジンに注目しています。
ゲームエンジンといえば今はUnityとUnreal Engineの2強ですが、最近いろいろあってGodotも存在感を増してきている感じがします。
Godotの魅力はOSSでライセンス料がかからないことも大きいですが、2強に対して圧倒的なのがその軽さです。ストレージ的にもパフォーマンス的にもかなり身軽なので、あえてGodotを使いたくなるシーンもありそうです。
まあそういうわけで最近ちょっとGodotにかぶれてるので、今日はGodotとMQTTのお話です。
- できるだけGDscriptを使いたい
- MQTTアドオンを追加する
- ノードを作ってGDスクリプトを追加する。
- シグナルを設定してサブスクライブする
- MQTTブローカーを立てて接続確認する
- HTML + MQTT.jsでサクッと動作確認する
- 動いた!
- 【ちょっとハマりどころ】_process()の中でPublishするときの注意事項
- 【ちょっとハマりどころ2】通信が切れたときにソケットエラーで再接続できない問題と対策
できるだけGDscriptを使いたい
UnityでMQTTNetを使ったときに、.NETのバージョンやら何やらで苦しんだ覚えがあるので、そういう外的要因の少ない(と思ってる)Godot組み込みのスクリプト言語GDScriptで実装されている方法が良い気がします。
Godotアセットストアで探してみると、なんだかぴったりのものが見つかりました!
動作確認は、GitHubにアップされている立派なサンプルシーンを実行するだけでOKでした。
逆にサンプルが立派すぎて、使い方を読み解くのにちょっとつまづいたりしたので、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が超おすすめです。
つながった! 見辛いですが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.comMQTT.js → Shiftr io → Godot Engine 動かしてみた様子
— アツユキ (@aaa_tu) July 27, 2024
ブラウザ上のマウス座標を取って(左下)ブローカーに送って(右下)Godotで受けてGodot君の座標に反映(左上)
ローカルでのレイテンシは8〜25msくらい pic.twitter.com/7WuyukP9rX
バッチリですね!
とりあえずMQTTさえ喋れれば、ROSとかOPC-UAみたいなインダストリアル系の諸々にも簡単にブリッジできるはず。
なんとあのテスラも採用しているらしいですし、Godot for Industry の可能性に期待です!
x.comテスラのアプリにGodot(とReact Native)が使われてるという今日のGodot日本ユーザーグループDiscordの話題。どうも2021年にはもう求人にGodotへの言及があったようで、こんなに早く採用された経緯が気になる https://t.co/Vn9ctytj7o
— こりん@VR (@korinVR) July 24, 2024
【ちょっとハマりどころ】_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())