※このブログはFour Strategies for Debugging your Alexa Web API for Games Skillを翻訳したものです。
Alexa Web API for Gamesを利用すると、任意のウェブテクノロジーやツールを使って、リッチで没入感のある音声対応ゲームを作成できます。このAPIは柔軟性が高いため、さまざまなアプローチで開発やデバッグを進めることができます。このブログでは、Alexaゲームスキルのデバッグとモニタリングに役立つ4つの方法について説明します。
Alexa Web API for Gamesを使用するメリットの1つとして、一般的なウェブ開発ワークフローを利用できることが挙げられます。ブラウザでAlexaクライアントサイドライブラリを使用することはできませんが、コードを少し分離して、ウェブアプリケーションの体裁に関わるAlexa以外の要素を開発またはデバッグすることは可能です。Alexaの制御部を切り離すと、WebGLキャンバスのレンダリング、HTML要素のスタイル設定、外観の制御ロジックなどの体裁に関わる要素を、ローカルウェブサーバーですばやく繰り返し調整できます。HTML側で動作するゲームであれば、ゲームのコアロジックをデバッグできる場合もあります。ここでは、ロジックを分離し、有用なデータを使ってゲームを起動する手法をご紹介します。まず、Alexaライブラリを初期化する方法を見てみましょう。
const mockData = require('./mockStartupData.json');
...
// 1.ゲームループを開始し、Alexa以外のローカルエクスペリエンスに関するセットアップを行う
initgame();
// 2.Alexaクライアントを初期化する
Alexa.create({version: '1.0'})
.then((args) => {
const {
alexa,
message
} = args;
alexaClient = alexa;
alexaLoaded = true;
...
//3.Alexa起動データペイロードの必要なデータを使用して、ゲームを初期化する
setupGame(message);
//4.Alexaコールバック(speech.onStartedやskill.onMessageなど)を初期化する
...
})
.catch(error => {
console.log(JSON.stringify(error));
...
alexaClient = null;
//5.上記と同様(ただし、ここではモックデータを渡す)
setupGame(mockData);
});
このコードは、以下を実行します。
これでAlexaロジックを切り離すことができたので、モックデータファイルのパラメーターを調整するだけで、さまざまな開始シナリオをテストできます。音声入力を必要としないほとんどのシナリオは、ローカルブラウザだけでテストできます。デバッグを開始するには、ローカルウェブアプリケーションを起動する必要があります。NPMパッケージのhttp-serverの使用方法については詳細な記事がありますが、任意のローカルサーバーを利用できます。
http-server . -p 8080 -o /dist/ -c-1
次に、お好きなブラウザを開いて、ローカルホストに移動します。私の環境では、次のようになります。
http://127.0.0.1:8080/dist/
このセットアップの動作する完全なサンプルについては、サンプルリポジトリのwebappディレクトリにあるREADMEを確認してください。
この方法では、Alexa搭載デバイスで実行されるエンドツーエンドのエクスペリエンスはデバッグできませんが、Alexa以外の部分のエクスペリエンスをすばやく何度も確認できるという利点があります。音声コマンドの使用やスキルのバックエンドとの通信はできませんが、このアプローチによってゲーム開発を大きく進めることができます。また、次に取り上げる2つの戦略と組み合わせても便利です。
ウェブアプリケーションのデバッグオーバーレイを作成すると、実行デバイスの画面でJavaScriptコンソールログが確認できるようになります。ウェブアプリケーションを起動して、ローカルのJavaScriptコードからHTMLドキュメントへの書き込みを行うことができます。まず、ログを書き込み、最上部に表示するdivを作成します。メインのHTMLページを開き、本文に次のコードを追加します。
<div class="hud scrollable" id="debugInfo">Debug</div>
ここにはCSSを必要とするクラスが2つあります。ページのスタイル部分に以下を追加します。
.scrollable {
overflow-y:auto;
}
.hud {
position: absolute;
z-index: 100;
display:block;
}
ここでhudクラスは、タグ付けされた要素をゲーム内のほかのHTML要素(canvasなど)の上にオーバーレイするためのスタイルを定義しています(覆われる要素のz-indexが100未満と仮定)。scrollableクラスは、div要素を一度に表示しきれない場合にスクロールできるようにするためのものです。次のCSSは、この要素自体のスタイルを設定します。
#debugInfo {
width: 100%;
background-color: rgba(0,0,0,0.05);
top: 0%;
text-align: left;
white-space: pre-wrap;
}
次に、ログを画面に出力する必要があります。以下のJavaScriptコードで、参照を取得できます。
var debugElement = document.getElementById("debugInfo");
要素に書き込むには、テキストノードを作成し、divに追加します。たとえば次の例では、起動エラーが画面に表示されます。
debugElement.appendChild(document.createTextNode("\n" + JSON.stringify(errMessage)));
white-spaceプロパティでpre-wrapを指定して改行を挿入すると、ログが読みやすくなります。これで、このdivを使って画面にログを表示することができます。このdivはほかの要素よりも上部に配置してください。
この方法は、特別な設定をしなくてもAlexa Web APIに対応しているデバイスに適用できるため、デバッグに便利です。ただし、この方法では画面にログが出力され、コンソールに不要な書き込みが行われるので、最終段階のアプリケーションでは使用しないでください。公開中のスキルを更新する前に、忘れずにこのコードをオフにする必要があります。また、付属のロガーほどの詳しい情報は取得できません。
この方法を実行するには、次のものが必要です。
Chromeブラウザを使用すると、ブラウザのローカルインスタンスをアタッチできるので、Chromeのデバッグツール一式を利用できるようになります。これにより、次の操作を実行できます。
1)ネットワークタブを使用して、リアルタイムでウェブリクエストをモニタリングします。
2)コンソールタブを使用して、リアルタイムでエラー、警告、全般的なログを探します。
3)コードをデバッグするためのブレークポイントを設定します。シミュレーションやプロキシではなく、実際のデバイスを使用するので、ローカルブラウザツールを介して、実機環境でのエクスペリエンスをデバッグできます。
これには、Android Debug Bridge(ADB)をインストールする必要があります。ADBは、ローカルネットワークやUSB経由でAndroidデバイスと通信するためのコマンドラインツールです。ローカルChromeブラウザのデバッグツールを活用するには、このツールが必要です。
1)ADBを介してFire TVをコンピューターに接続します。詳しい手順はこちらを参照してください。
2)Fire TVをモニターやテレビに接続します。
3)Alexaスキルを起動します。
4)Chromeブラウザを開き、chrome://inspectに移動します。デバイスでWebViewが起動したら、接続しているデバイス(WebView in com.amazon.csm.htmlruntime ...)の下にあるinspectがクリック可能になります。
WebViewが初期化されたら、chrome://inspectページのinspectボタンをクリックします。
ポップアップされるビューでは、デバッグに必要なローカルブラウザツールを使用して、実際のデバイスでの実行画面とほぼ同じ結果が表示されます。WebGLキャンバスなど、一部の要素はローカルのデバッガービューでレンダリングされないので、この方法で得られる結果は近似的なものに過ぎません。
このアプローチでは、実際のハードウェアでローカルブラウザツールを最大限に活用して検証を行い、結果をスキルのバックエンドコードに反映させることができます。Fire TVではこの方法がうまく機能しますが、Echo Showデバイスでコードをデバッグする場合は、このブログで説明しているほかの方法に従ってください。
この戦略をAlexaのライブスキルに適用すると、ウェブアプリケーションで発生し得るクライアントサイドの問題を、すべてのデバイスでモニタリングおよびデバッグできます。alexa.skill.sendMessage関数を使用して、任意のログをバックエンドに出力し、Amazon CloudWatchなどのクラウドサイドロガーに保存することができます。この例では、AWS Lambda関数とAmazon CloudWatch、Node.jsを使用していますが、ほかのサービスとログ作成方法を使用することもできます。
基本的なクラウドサイドロガー
基本的には、クライアントサイドのJavaScriptで関数をラップし、その関数を使用してスキルのバックエンドにメッセージを送信します。
まずはコンソールログラッパーを記述します。
/**
* ローカルにメッセージを出力し、Alexaが初期化されていれば、Lambdaにメッセージをプッシュしてログを残す
* @param {*} messageStr
*/
function cloudLog(payload) {
console.log(payload);
if(alexaLoaded) {
alexaClient.skill.sendMessage({
intent: "log",
log: payload
});
}
}
ここでは、intentとlogという2つのプロパティが定義されています。ログメッセージはpayloadとして渡す必要があります。これでconsole.log()のすべてのインスタンスを、この新しいcloudLog関数で置き換えることができます。元のコードは次のとおりです。
console.log("これはログです");
これを次のコードに置き換えます。
cloudLog("メッセージ");
次に、スキルコードでこの処理を行う必要があります。sendMessage APIは、Alexa.Presentation.HTML.Message形式のリクエストをバックエンドに送信します。これに応答するために、新しいハンドラーを追加する必要があります。
/**
* ウェブアプリから送信されたメッセージをログ出力するシンプルなハンドラー
*/
const WebAppCloudLogger = {
canHandle(handlerInput) {
return Alexa.getRequestType(handlerInput.requestEnvelope) === "Alexa.Presentation.HTML.Message"
&& getMessageIntent(handlerInput.requestEnvelope) === 'log';
},
handle(handlerInput) {
const messageToLog = handlerInput.requestEnvelope.request.message.log;
console.log(messageToLog);
return handlerInput.responseBuilder
.getResponse();
}
}
この中のgetMessageIntent()ヘルパー関数は、次のとおりです。
function getMessageIntent(requestEnvelope) {
const requestMessage = requestEnvelope.request.message;
if(requestMessage) {
if(requestMessage.intent) {
return requestMessage.intent;
}
}
return null; // それ以外の場合は、メッセージ本文にインテントが存在しない
}
このコードにより、メッセージリクエストペイロードのインテントプロパティがログ出力されている場合にのみ、ハンドラーが実行されます。このハンドラー自体は、単にペイロードを取り出して、Amazon CloudWatchに出力するだけです。
この方法には若干の問題点があるので、慎重に使用する必要があります。この方法では、sendMessage JavaScript APIのレート制限は考慮されず、処理も行われません。このアプローチを実装し、ロガーを頻繁に使用してデバッグしていると、すぐにレートが制限されます。これにより、同時に複数のLambdaランタイムが実行され、ログが複数のCloudWatch Logstreamに分割されて、デバッグが困難になることがあります。 この問題は、ログのバッチ処理で改善することができます。
バッチ処理されたクラウドサイドロガー
この問題を回避するには、メッセージを0.5秒間隔で一括送信するメッセージ送信ラッパーを別途実装する必要があります。こうすることで、1秒あたりに送信されるメッセージは2つまでに抑えられます。
まず、messageSenderというクラスを作成します。具体的な手順は、ウェブアプリケーションのJavaScriptコードの管理方法によって異なります。ここでは、いくつかのメソッドを作成します。まずはinitです。
init(alexa) {
alexaClient = alexa;
}
これは、Alexaクライアントオブジェクトの参照を格納するだけのシンプルなメソッドです。Alexaの起動成功ブロックから呼び出します。
Alexa.create({version: '1.0'})
.then((args) => {
const {
alexa,
message
} = args;
alexaClient = alexa;
...
//messageSenderクラスを初期化する
messageSender.init(alexaClient);
...
});
次に、ローカルゲームループから呼び出されるupdate関数を追加します。
const MESSAGE_CADENCE_MS= 500; //メッセージを送信する間隔
update(deltaTime) {
if(currentTime >= MESSAGE_CADENCE_MS) {
//タイムトラッカーをリセットする
currentTime = currentTime - MESSAGE_CADENCE_MS;
//メッセージキューをクリアする
this.flushMessageQueue(messageQueue);
messageQueue=[]; //メッセージキューをクリアする
} else {
currentTime += deltaTime;
}
},
ここでは、直前のフレームから経過した時間を表すパラメーター、deltaTimeを使用します。ゲームが特定のフレームレートで実行されていても、このようなゲームロジックに合わせて等間隔に区切られているフレームには依存しないでください。上記の関数では、内部に保持されているメッセージキュー(上の例ではmessageQueue)を送信するflushMessageQueue関数を利用しています。
flushMessageQueue(queue) {
if(queue.length <= 0) {
return Promise.resolve("キューにメッセージがありません。");
}
const messagePromise = new Promise((resolve, reject) => {
alexaClient.skill.sendMessage({
intent:"log",
messageQueue:queue
},
function(messageSendResponse) {
console.log(messageSendResponse.statusCode);
switch(messageSendResponse.statusCode) {
case 500:
case 429:
//TODO:messageSendResponse.rateLimit.timeUntilResetMsとtimeUntilNextRequestMsを確認する
//このエラーが発生した場合、500とは切り分けて、これらのフィールドを使用してすみやかに再試行する
console.error(messageSendResponse.reason);
reject(messageSendResponse.reason);
break;
case 200:
default:
resolve("Alexaスキルが正常に呼び出されました。");
}
});
});
return messagePromise;
}
このコードは前の例とよく似ていますが、スタブコードが追加されています。これは、非同期実行用のPromiseにラップされた各種のステータスコードを処理するためのものです。この方法では、レンダリングループがブロックされず、エクスペリエンスが途切れることはありません。キューが空の場合は、リクエストは送信されません。
この段階で、messageQueueの内容を出力し、コードに取り込む方法はいくつか存在します。その出力を利用して、すべてのAlexaメッセージ送信リクエストをルーティングすることも、ログ出力だけを実行することもできます。今回はサンプルでログに使用するだけにして、error、warn、infoの各メソッドを出力しました。以下はその一例です。
warn(payload) {
//ローカルに出力する
console.log(payload);
//キューの最後に新しいログをプッシュする
this.pushMessage(payload, "warn");
},
pushMessage(payload, level) {
messageQueue.push({
level: level,
log: payload
});
}
上記のcloudLog関数に代えて、この方法を使用することができます。これで上限を気にせず、自由にログを出力できるようになりました。新しいmessageSend APIに合わせて、スキル側のメッセージハンドラーを調整します。
handle(handlerInput) {
const {
messageQueue
} = handlerInput.requestEnvelope.request.message;
messageQueue.forEach(message => {
const {
level,
log
} = message;
switch (level) {
case "error":
console.error(log);
break;
case "warn":
console.warn(log);
break;
case "info":
console.log(log);
break;
}
});
return handlerInput.responseBuilder
.getResponse();
}
メッセージリストの送信というタスクの性質上、繰り返し実行して、適度にログを出力する必要があります。そうすることで、大規模なアプリケーションの稼働を続けながら、クラウドでこれらのログを見て、モニタリングを実施できます。クラウドにログを出力する方法を使用すれば、エラーや警告をクラウドに送信し、それらのモニタリングを(Amazon CloudWatchモニタリングツールを活用して)セットアップして、問題を検出することができます。また、デバッグに役立つメッセージを追加して、デバッガーを介さずにデバイスでデバッグを進めることも可能です。これらの実例については、GitHubのMy Cactus(サボテン栽培)シミュレーションゲームをご覧ください。messageSenderクラスのコードを確認するだけでもかまいません。
注: Amazon CloudWatchを使用する場合は、CloudWatch埋め込みメトリクスフォーマットを用いてゲームデータからCloudWatchメトリクスを生成することを検討してください。
この記事を読んで、Alexa Web API for Gamesでのゲーム開発に使えるアイデアをつかんでいただけることを願っています。最初にローカルデバッグを行い、エクスペリエンスをすばやく何度も確認したら、オンスクリーンのデバッガーで、すべてのAlexa Web API for Gamesデバイスから出力されたログを確認します。また、Fire TVデバイスで動作するローカルブラウザツールで詳しく検証し、さらにクラウドサイドロガーを使用して、本番環境に置かれた複数のデバイスで発生する問題をモニタリングします。Alexa Web APIを使用したゲームをこの記事以外の方法でデバッグしている方は、Twitterで@JoeMoCodeまでお知らせください。