できたもの
Fighting game command input skill for Alexa.#alexa #GameControllerizer pic.twitter.com/M5NmlDFtsU
— nobu_e753 (@nobu_e753) September 16, 2019
音声で「波動拳!」「昇竜拳!」というだけで,コマンドを自動入力してくれます.
音声認識に1秒程度かかるので,実用性はいまいちですが.
はじめに
2年ほど前からディジタルゲームハック用ミドルウェアGameControllerizerをチームで開発しています.これは,平たく言えば**「プログラムを通じて電子的にボタンが押せるゲームパッド」**のようなもので,各種IoT機器,マイコン,APIとゲームを結びつけるHubとして動作します.
GameControllerizerを作成する過程で**「Alexaを使って格闘ゲームのコマンドを音声で入力する装置」**をデモ向けに作ったのですが.最近になって何件か製作方法を聞かれることがありましたので,ここにまとめます.ほぼ "ハイスコアガール" まんまな子供時代を過ごした人たち(自分を含む)が夢見たであろうそれです.
必要なもの
- Amazone Echoデバイス
- Raspberry Pi Zero W
- GameControllerizer H/W Gamepad emulator
- Amazone Alexa開発環境
- ゲームコンソール(今回はノートPC)
EchoデバイスはEcho dot(第2世代)を使いました.Alexaが使えさえすればよいです.次に,RaspberryPiですが,今回はZero Wを使いました.「インターネットに接続できる」「Node-REDが動く」「GameControllerizerがマウントできる」Linuxボードであれば,RasPi 3A+/3B+/4に限らず,例えばNVIDIA JetsonNanoでもよいです.
キーパーツであるGameControllerizerはRasPiにマウントして使用し,ネットワーク経由で送られてきたメッセージに応じて,必要なコマンドを生成・ゲーム機に流し込みます.これ以外の選択肢としては
などがあります.ネットワーク接続部やコマンド入力シークエンス(外部トリガに応じで適切なタイミングで"236P"を入力)を自前で組む必要がありますが,使い慣れたマイコン等がある方はこちらもありだと思います.GameControllerizerではこのあたりをGUI(Node-RED)で構築することができます.
全体構成
主機能はAWS Lambda & IoT Coreで構成しています.各々がやっている処理はごく単純ですが,段数は多く複雑になってしまっています.ラッパーや便利プラグインを使えばもう少し簡単にできる余地はあるように思います(Node-RED Alexa Home Skill Bridgeで行けたのかもしれないです...いまさら).
以降,入力側から順に説明してゆきます.
各部の実装と設定
1. Alexa skill
何はともあれまずは音声入力部から.一般的なAlexa Skill Kit(ASK)の開発手順に習って進めます.
以降の手順1~3までは公式チュートリアルにとても丁寧に説明されているのでお勧めです.
- スキル名:GameControll
- 呼び出し名:ゲーム制御
としてカスタムスキルを作成した後,ゲームコマンドを処理するインテント(ActionIntent)を追加.
ActionIntentはスロット"Command"を取り扱うものとします.
"Command"で取り扱うラベルは
- 波動拳(hado)
- 昇竜拳(shoryu)
2つとしました.竜巻旋風脚(tatsumaki)を追加してもよいかもしれません.
各ラベルごとに取りうるフレーズは適当に4つほど指定しました.完了後にモデルをビルドしておきます.
2. AWS Lambda
並行してAlexaから上がってくるデータを処理するコードをAWS Lambda側に実装します.言語は特にこだわりはありませんがnode.jsを使いました.コードをブラウザ上ではなく手元のマシンで編集したい場合はこちらが参考になります.
処理の主要部分,ActionIntentの処理は次としました.この時点では,まだ音声を認識しオウム返しにするだけです.
const CARD_TITLE = 'GameControl';
const ACTION_NAME = {
"hado": "波動拳",
"shoryu" : "昇竜拳"
};
...
const ActionIntentHandler = {
canHandle(handlerInput) {
return handlerInput.requestEnvelope.request.type === 'IntentRequest'
&& handlerInput.requestEnvelope.request.intent.name === 'ActionIntent';
},
handle(handlerInput) {
const tResponseObj = Alexa.getSlot(handlerInput.requestEnvelope, "Command");
// tComand = {'hado','shoryu'} という結果を得るようにする
const tCommand = tResponseObj.resolutions.resolutionsPerAuthority[0].values[0].value.name;
// AWS Lambda → AWS IoTへの送出部分.あとで実装する.
// submit(tCommand);
return handlerInput.responseBuilder
.speak(ACTION_NAME[tCommand])
.withSimpleCard(CARD_TITLE, tCommand)
.reprompt() // 繰り返し入力を可能にする
.getResponse();
}
};
const HelpIntentHandler = {
canHandle(handlerInput) {
...
3. Alexa skill と AWS Lambdaの連携とテスト
Alexa skill と AWS Lamndaを連携させるため,エンドポイントの設定を行います(公式チュートリアル).
設定後はブラウザ上でAlexa skillのテストが可能になるので,連携が取れているか確認しておきます.
4. AWS Lambda から AWS IoT へのメッセージ送出
1~3まではごく簡単なAlexa skillの制作方法と同じですが,ここから独自の工夫が入ってきます.
発話をうけてAlexa側でオウム返しに返答すると同時に,イベント内容を出力側(=今回はRasPi)側へも送信します.
すると何らかの形でAWS Cloudと手元のRasPiを連携する必要があるわけですが,この部分を取り扱うに適したサービスとしてAWS IoT Coreが利用可能です.
まず,先ほどのindex.js
にAWS Lambda から AWS IoT へメッセージを送信する部分を追加実装します.
送信先(AWS_IOT_ENDPOINT)は今の時点では適当に埋めておきます.
....
const AWS = require('aws-sdk');
// 今は仮設定.AWS IoT側の設定が済んだらその値を設定する.
const AWS_IOT_ENDPOINT = "xxxxxxxxxxxxxx-ats.iot.ap-northeast-1.amazonaws.com";
function submit(label)
{
var data = {
topic: 'alexa', // トピック名は仮に'alexa'としておく.一意に識別できればなんでもよい.
payload: JSON.stringify({
"action": label,
}),
qos: 0
};
var tIotData = new AWS.IotData( { endpoint: AWS_IOT_ENDPOINT } )
tIotData.publish(data, function(err, data){
if(err){
console.log("AWS IoT Failure: ",err);
}
else{
console.log("AWS IoT Success");
}
});
}
これで「波動拳!」と発話した場合には {"action": "hado"}
,「昇竜拳!」と発話した場合には {"action": "shoryu"}
というメッセージがAWS LambdaからAWS IoTへ送られるはずです.
もうひとつ大事なこととして権限の追加を行います.初期の状態ではAWS LambdaからAWS IoTへのメッセージ送信は許可されておらず,このままだと「Lambda関数は実行されているはずなのに,なぜかLambda→IoTの連携ができない!」ということになります.
AWS Lambdaの設定の下のほうに[実行ロール]という欄から現在設定中のロールを表示し,AWSIoTDataAccess
という権限を追加しておきます.
権限設定後,Lambdaの最上部のグラフは以下のようになっているはずです.
5. AWS IoT
公式ページのドキュメントに沿って設定を進めます.AWS Cloudと自身の手元にあるRasPiを連携させるための手順です.
ポイントは3つ,
証明書
ステップの途中で証明書を作成,Downloadします(あとでRasPi上へコピーして使う).
**失うと再発行できないので気を付けましょう.**私は何度も無くして再設定をするはめになりました.
ポリシーの設定
チュートリアルの通りに設定しましょう(AWS Lambdaの時にあつかった権限の設定と似たような感じです).設定が不十分だと「AWS IoT側でメッセージを発行しているはずなのに,なぜか手元のRasPiへメッセージが届かない!」ということになります.
エンドポイントの確認
AWS IoTの設定が済むと,エンドポイントが確定します.このアドレスで,先ほどindex.js
内で仮設定した個所を置き換えておきます.
ここまで正しく接続ができていれば**「Alexaで受けた発話の結果を,AWS IoT側で受け取れる」**はずです.確かめるには,AWS IoTの「テスト」からトピックとして"alexa"を指定し,サブスクライブ(読み取り)してみます.
6. RasPi & Node-REDの設定(GameControllerizerを使う場合)
そろそろ挫けそうな感じですが,ようやく端末側の設定に入れます.エンド側でのメッセージ処理およびゲーム機へのコマンド流し込みはNode-REDとそのカスタムノードを使って行います.最初にNode-REDの設定ですが,こちらの記事が参考になります.
設定が正常に行えていれば Alexa → AWS Lambda → AWS IoT → RasPi まで導通が確認できます.
この時に接続に失敗する(MQTT Inノードの下に緑色のマークがつかない)場合は,
- AWS IoTのポリシー設定
- 証明書が確実に読み込まれているか
- エンドポイントが間違って設定されいないか
GameControllerizerを利用する場合はカスタムノードとしてnode-red-contrib-game_controllerizer
を導入しておきます.これはゲームパッドの操作をNode-RED上で定義できるようにしたもので,導入はこちらを参照してください.
7. メッセージとコマンドのマッピング(GameControllerizerを使う場合)
やっとこさ最後のステップです.ここではAWS IoTからやってくる
{"action": "hado"}
{"action": "shoryu"}
の2メッセージに応じて,それぞれコマンド入力を定義します.最初のswitch構文内は以下としています.
送出するのは空配列になっていますが単にトリガとしての役目をさせたいがためにこうなっています.
let tLabel = msg.payload.action;
msg.payload = [];
switch (tLabel){
case "hado":
return [msg, null];
case "shoryu":
return [null, msg];
default:
return [null, null];
}
ノード構造
[{"id":"7e5dccf8.befbcc","type":"tab","label":"フロー 1","disabled":false,"info":""},{"id":"186720d7.19d037","type":"mqtt in","z":"7e5dccf8.befbcc","name":"","topic":"alexa","qos":"0","datatype":"json","broker":"12283103.70d39f","x":110,"y":160,"wires":[["5175a221.e2a3e4"]]},{"id":"5175a221.e2a3e4","type":"function","z":"7e5dccf8.befbcc","name":"switch","func":"let tLabel = msg.payload.action;\nmsg.payload = [];\nswitch (tLabel){\n case \"hado\":\n return [msg, null];\n case \"shoryu\":\n return [null, msg];\n default:\n return [null, null];\n}","outputs":2,"noerr":0,"x":230,"y":160,"wires":[["27575299.a735fe"],["baff2dc8.1f488"]]},{"id":"27575299.a735fe","type":"dpad","z":"7e5dccf8.befbcc","name":"2","dpad":"2","mode":"3","x":230,"y":240,"wires":[["b7085a43.bc106"]]},{"id":"b7085a43.bc106","type":"dpad","z":"7e5dccf8.befbcc","name":"3","dpad":"3","mode":"3","x":350,"y":240,"wires":[["285ec363.26fcac"]]},{"id":"285ec363.26fcac","type":"dpad","z":"7e5dccf8.befbcc","name":"6","dpad":"6","mode":"3","x":470,"y":240,"wires":[["8269c9b4.70ade"]]},{"id":"8269c9b4.70ade","type":"button","z":"7e5dccf8.befbcc","name":"Punch","button":"3","mode":"push","x":590,"y":240,"wires":[["bb86d01f.0445","ca799f39.3575a"]]},{"id":"bb86d01f.0445","type":"binary-serializer-g","z":"7e5dccf8.befbcc","name":"","x":780,"y":280,"wires":[["fc5c5d6f.d86"]]},{"id":"fc5c5d6f.d86","type":"serial out","z":"7e5dccf8.befbcc","name":"","serial":"b2f34f1d.0a41d","x":830,"y":340,"wires":[]},{"id":"baff2dc8.1f488","type":"dpad","z":"7e5dccf8.befbcc","name":"6","dpad":"6","mode":"3","x":230,"y":300,"wires":[["cbb81d8b.6b3a08"]]},{"id":"cbb81d8b.6b3a08","type":"dpad","z":"7e5dccf8.befbcc","name":"2","dpad":"2","mode":"3","x":350,"y":300,"wires":[["74fb32e5.debc84"]]},{"id":"74fb32e5.debc84","type":"dpad","z":"7e5dccf8.befbcc","name":"3","dpad":"3","mode":"3","x":470,"y":300,"wires":[["e848c5c8.d1ea"]]},{"id":"e848c5c8.d1ea","type":"button","z":"7e5dccf8.befbcc","name":"Punch","button":"3","mode":"push","x":590,"y":300,"wires":[["bb86d01f.0445","ca799f39.3575a"]]},{"id":"ca799f39.3575a","type":"debug","z":"7e5dccf8.befbcc","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","x":710,"y":420,"wires":[]},{"id":"12283103.70d39f","type":"mqtt-broker","z":"","name":"AWS IoT","broker":"a347lz4fqkno5q-ats.iot.ap-northeast-1.amazonaws.com","port":"8883","tls":"79a06793.3e8228","clientid":"","usetls":true,"compatmode":true,"keepalive":"60","cleansession":true,"birthTopic":"","birthQos":"0","birthPayload":"","closeTopic":"","closeQos":"0","closePayload":"","willTopic":"","willQos":"0","willPayload":""},{"id":"b2f34f1d.0a41d","type":"serial-port","z":"","serialport":"/dev/ttyAMA0","serialbaud":"115200","databits":"8","parity":"none","stopbits":"1","waitfor":"","newline":"\\n","bin":"bin","out":"char","addchar":"","responsetimeout":"10000"},{"id":"79a06793.3e8228","type":"tls-config","z":"","name":"AWS IoT","cert":"","key":"","ca":"","certname":"a2d25016d2-certificate.pem.crt","keyname":"a2d25016d2-private.pem.key","caname":"AmazonRootCA1.pem","servername":"","verifyservercert":true}]
※黄色のノードはGameControllerizerのカスタムノード
組み上げ
接続状況は以下のようになっています.GameControllerizer, Alexaともに自室のWifiに接続されています.
デバッグ
なるべく個々のパスで確認しながら開発を進めるようにしました.処理段数が多いので"とりあえず全部組んでしまってからデバッグするか!"だと,原因追及が面倒になるからです.
今回は,各段ごとで導通を確認したことでそれなりに手間はかかりましたが,大きな手戻りはほとんどありませんでした.
まとめと課題
Alexaを使い,音声で格闘ゲームのコマンドを入力できる装置を試作できました.子供のころに夢見た装置も,手の届く機材/APIだけで何とか構成できてしまうあたり,技術の進歩を感じます.
一方で(予想していたとは言え)サーバー側で認識する機構ゆえの遅延がネックとなり実用的とはとても言えないことがわかりました(レイジングストームくらい長いコマンドならあるいは).最近は末端側で深層学習ベースの音声認識モデルを動かすものも出てきているので,これらを利用すれば改善できるかもしれません.
あるいは視点を変えて
- とにかく攻めてね,よろしく
- ひたすらガードで,おねがい
など,抽象的な指示だけを音声で行い,具体的な制御はAIにやってもらうとかでしょうか.
エンジニア的には夢が膨らみます.