ユカイ工学のコミュニケーションロボット「BOCCO emo」をMinecraft内からしゃべらせてみたので紹介します。
※本記事は前回の投稿からの延長の開発です。
開発環境
PC:Windows 11
※Raspberry Pi 4 Model B (Linux)
開発ツール:Node.js (v18.19.0)
Node-RED (v.3.1.3)
インターネット:社内の無線LAN
Minecraft:v1.20.51 (執筆時点)
※Node-REDをRaspberry Piで動かしています。Windowsで動かしていれば不要です。
事前準備
今回はこちらの記事を参考に開発しました。
ここで紹介されているノードの一部を引用しています。
ノード中にBOCCOのACCESS_TOKEN、REFRESH_TOKENを使用します。予め控えておきましょう。公式ドキュメントにも取得の方法が詳しく載っています。
完成像
動画のように、Minecraftから「[bocco]マイクラのチャットを読み上げるよ」とチャットで送ると、BOCCOがしゃべってくれる。完成像はこんな感じです。
フローの概要

全体像はこんな感じです。Minecraftからのチャット情報は前回の投稿のイベント名を「”PlayerMessage”」に変更して取得しました。
取得したチャットデータの処理
Websocket通信で取得したチャットの情報はデバッグ画面でこのように表示されます。

jsonノード後のSwitchノードでは、BOCCOにすべてのチャットを読み上げさせるのであれば必要ありませんが、今回の文中の「[bocco]」のように指定されたワードがあるときにだけ読み上げさせたい場合は必要となり、そのワードもお好みで変更可能です。

次のfunctionノードでは、不要な文字列を削除し、BOCCO送信用の文字列に合わせています。ここで変更を加えた文字列は、別名称のmsg.payload2に格納していることに注意してください。
const text = msg.payload.body.message;
const texts = text.replace("[bocco]","");
msg.payload2 = '{ "text": "' + texts + '" }';
return msg;
BOCCOへの送信
前述の、今回参考にさせていただいたこちらの記事からのノードの編集部分は「発話内容」部分のみです。

このchangeノードでは先ほどpayload2に格納したデータをpayloadに戻しています。
最後に、http request内のURLのパス内に、BOCCO emoに登録した部屋IDを入力します。
以上でフローは完成です。実際に動かしてみましょう!
ノードデータ
[ { "id": "4938ac7ca726c955", "type": "uuid", "z": "aa9a062666553723", "uuidVersion": "v4", "namespaceType": "", "namespace": "", "namespaceCustom": "", "name": "", "field": "payload", "fieldType": "msg", "x": 390, "y": 660, "wires": [ [ "f81dfbee325de408" ] ] }, { "id": "b01389a60d486859", "type": "inject", "z": "aa9a062666553723", "name": "", "props": [ { "p": "payload" }, { "p": "topic", "vt": "str" } ], "repeat": "", "crontab": "", "once": false, "onceDelay": 0.1, "topic": "", "payload": "", "payloadType": "date", "x": 180, "y": 660, "wires": [ [ "4938ac7ca726c955" ] ] }, { "id": "f81dfbee325de408", "type": "function", "z": "aa9a062666553723", "name": "PlayerMessageCmd", "func": "const uuid = msg.payload;\n\nconst subscribeMessageJSON = {\n \"header\": {\n \"version\": 1, // プロトコルのバージョンを指定。1.18.2の時点では1で問題ない\n \"requestId\": uuid, // UUIDv4を指定\n \"messageType\": \"commandRequest\", // \"commandRequest\" を指定\n \"messagePurpose\": \"subscribe\", // \"subscribe\" を指定\n },\n \"body\": {\n \"eventName\": \"PlayerMessage\" // イベント名を指定。イベント名は後述\n },\n};\n\n // イベント購読用のJSONをシリアライズ(文字列化)して送信\nmsg.payload= JSON.stringify(subscribeMessageJSON);\n\nreturn msg;", "outputs": 1, "timeout": 0, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 600, "y": 660, "wires": [ [ "d3da139c84e4a214" ] ] }, { "id": "d3da139c84e4a214", "type": "websocket out", "z": "aa9a062666553723", "name": "", "server": "c838b41f45982027", "client": "", "x": 880, "y": 660, "wires": [] }, { "id": "b4e7055c1c21e3d2", "type": "comment", "z": "aa9a062666553723", "name": "Minecraftからチャット情報を取得", "info": "", "x": 200, "y": 600, "wires": [] }, { "id": "753af17b26c73d20", "type": "websocket in", "z": "aa9a062666553723", "name": "", "server": "c838b41f45982027", "client": "", "x": 130, "y": 1000, "wires": [ [ "06dc5f59f065e6ef" ] ] }, { "id": "06dc5f59f065e6ef", "type": "json", "z": "aa9a062666553723", "name": "", "property": "payload", "action": "", "pretty": true, "x": 250, "y": 1000, "wires": [ [ "3610a14c11ec5100" ] ] }, { "id": "3610a14c11ec5100", "type": "switch", "z": "aa9a062666553723", "name": "", "property": "payload.body.message", "propertyType": "msg", "rules": [ { "t": "cont", "v": "[bocco]", "vt": "str" } ], "checkall": "true", "repair": false, "outputs": 1, "x": 370, "y": 1000, "wires": [ [ "e72b362ba0d11290" ] ] }, { "id": "e72b362ba0d11290", "type": "function", "z": "aa9a062666553723", "name": "読み上げ用テキストに変換", "func": "const text = msg.payload.body.message;\n\nconst texts = text.replace(\"[bocco]\",\"\");\n\nmsg.paypay = '{ \"text\": \"' + texts + '\" }';\n\nreturn msg;", "outputs": 1, "timeout": 0, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 580, "y": 1000, "wires": [ [ "88364ad3af6416bf" ] ] }, { "id": "1ca5079cd767b8fa", "type": "change", "z": "aa9a062666553723", "name": "Content-Type", "rules": [ { "t": "set", "p": "headers.Content-Type", "pt": "msg", "to": "application/json", "tot": "str" } ], "action": "", "property": "", "from": "", "to": "", "reg": false, "x": 340, "y": 1180, "wires": [ [ "b6397f8c4990e69e" ] ] }, { "id": "88364ad3af6416bf", "type": "function", "z": "aa9a062666553723", "name": "JSONのデータ作成", "func": "msg.payload = \"Bearer \" + global.get(\"access_token\");\nreturn msg;\n", "outputs": 1, "timeout": "", "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 350, "y": 1120, "wires": [ [ "d743485513320ca6" ] ] }, { "id": "b6397f8c4990e69e", "type": "change", "z": "aa9a062666553723", "name": "発話内容を引用", "rules": [ { "t": "set", "p": "payload", "pt": "msg", "to": "payload2", "tot": "msg" } ], "action": "", "property": "", "from": "", "to": "", "reg": false, "x": 540, "y": 1180, "wires": [ [ "01cc7b4bccd16ef9" ] ] }, { "id": "d743485513320ca6", "type": "change", "z": "aa9a062666553723", "name": "アクセストークンの指定", "rules": [ { "t": "set", "p": "headers.Authorization", "pt": "msg", "to": "payload", "tot": "msg" } ], "action": "", "property": "", "from": "", "to": "", "reg": false, "x": 610, "y": 1120, "wires": [ [ "1ca5079cd767b8fa" ] ] }, { "id": "01cc7b4bccd16ef9", "type": "http request", "z": "aa9a062666553723", "name": "", "method": "POST", "ret": "txt", "paytoqs": "ignore", "url": "https://platform-api.bocco.me/v1/rooms/部屋IDはここに記述/messages/text", "tls": "", "persist": false, "proxy": "", "insecureHTTPParser": false, "authType": "", "senderr": false, "headers": [], "x": 730, "y": 1180, "wires": [ [ "595c7bbdfa751292" ] ] }, { "id": "595c7bbdfa751292", "type": "debug", "z": "aa9a062666553723", "name": "", "active": true, "tosidebar": true, "console": false, "tostatus": false, "complete": "payload", "targetType": "msg", "statusVal": "", "statusType": "auto", "x": 890, "y": 1180, "wires": [] }, { "id": "6286be1b37454ac7", "type": "comment", "z": "aa9a062666553723", "name": "チャットデータの処理", "info": "", "x": 160, "y": 960, "wires": [] }, { "id": "800fb7d9dde34272", "type": "comment", "z": "aa9a062666553723", "name": "BOCCOへ送信", "info": "", "x": 140, "y": 1080, "wires": [] }, { "id": "f575c2581ab616a4", "type": "http request", "z": "aa9a062666553723", "name": "POST", "method": "POST", "ret": "obj", "paytoqs": "ignore", "url": "https://platform-api.bocco.me/oauth/token/refresh", "tls": "", "persist": false, "proxy": "", "insecureHTTPParser": false, "authType": "", "senderr": false, "headers": [], "x": 570, "y": 860, "wires": [ [ "060db2ebde4d4f7b", "835ee884a06eeae6", "1238dacee4eb47cd" ] ] }, { "id": "80297e7ca86300ee", "type": "inject", "z": "aa9a062666553723", "name": "", "props": [ { "p": "payload" }, { "p": "topic", "vt": "str" } ], "repeat": "1800", "crontab": "", "once": true, "onceDelay": 0.1, "topic": "アクセストークンを定期的に取得", "payload": "", "payloadType": "str", "x": 220, "y": 780, "wires": [ [ "d6ad18516e9a66f3" ] ] }, { "id": "060db2ebde4d4f7b", "type": "debug", "z": "aa9a062666553723", "name": "", "active": true, "tosidebar": true, "console": false, "tostatus": false, "complete": "payload.access_token", "targetType": "msg", "statusVal": "", "statusType": "auto", "x": 820, "y": 900, "wires": [] }, { "id": "8d87f25c441c3bd2", "type": "change", "z": "aa9a062666553723", "name": "リフレッシュトークン", "rules": [ { "t": "set", "p": "payload", "pt": "msg", "to": "{\"refresh_token\":\"ここにリフレッシュトークン\"}", "tot": "json" } ], "action": "", "property": "", "from": "", "to": "", "reg": false, "x": 380, "y": 860, "wires": [ [ "f575c2581ab616a4" ] ] }, { "id": "d6ad18516e9a66f3", "type": "change", "z": "aa9a062666553723", "name": "Content-Type", "rules": [ { "t": "set", "p": "headers.Content-Type", "pt": "msg", "to": "application/json", "tot": "str" } ], "action": "", "property": "", "from": "", "to": "", "reg": false, "x": 180, "y": 860, "wires": [ [ "8d87f25c441c3bd2" ] ] }, { "id": "1238dacee4eb47cd", "type": "change", "z": "aa9a062666553723", "name": "グローバル変数に格納", "rules": [ { "t": "set", "p": "access_token", "pt": "global", "to": "payload.access_token", "tot": "msg" } ], "action": "", "property": "", "from": "", "to": "", "reg": false, "x": 800, "y": 860, "wires": [ [] ] }, { "id": "835ee884a06eeae6", "type": "debug", "z": "aa9a062666553723", "name": "", "active": true, "tosidebar": true, "console": false, "tostatus": false, "complete": "payload.refresh_token", "targetType": "msg", "statusVal": "", "statusType": "auto", "x": 820, "y": 940, "wires": [] }, { "id": "626c7a18399f1a13", "type": "comment", "z": "aa9a062666553723", "name": "参考元から引用", "info": "", "x": 140, "y": 720, "wires": [] }, { "id": "c838b41f45982027", "type": "websocket-listener", "path": "ws", "wholemsg": "false" } ]