note_記事見出し_googlehome_1280_670___9

Google Home, Nest Hub向けにアクションを作ろう - 第9話: ユーザーインタラクション

ユーザーインタラクションの設計

第8話までで簡易版動物しりとり(動物ゲーム)を動物の写真を見ながら楽しむことができるようになりました。
Interactive CanvasはグラフィカルユーザーインターフェースをGoogle Homeにもたらすものと紹介しましたが、まだユーザーからのインタラクションの要素はこのゲームには入っていません。この回では、ユーザーが画面上のボタンをタップすると、それにActionが反応するように簡易版動物しりとりを改良していきたいと思います。

パスボタン

今回追加するボタンは”パス”ボタンです。動物ゲームをしていて、動物の名前が思いつかなかったとき、”パス”ボタンを押して自分の番をスキップできるようなボタンをつけたいと思います。

HTMLの修正

HTMLにbuttonタグを下のように追加します。

<body>
<div class="container">
 <div id="animal">
   <div id="image_block">
     <img id="animalimage"  />
   </div>

   <div id="name">
   </div>

   <button id="pass_button">PASS</button>
 </div>
</div>
<script src="js/index.js"></script>
</body>

このbuttonタグのidを"pass_button"、タイトルをPASSにします。

public/js/index.jsの修正

public/js/index.jsを修正します。(Firebase Functionsのほうではないので注意してください。

'use strict';

window.onload = () => {
 interactiveCanvas.ready({
   onUpdate(data) {
     if (data.scene === 'animal') {
       if(data.picture){
         document.querySelector('#animalimage').src = `images/${data.picture}`;
       }else{
         document.querySelector('#animalimage').src = `images/no_image.jpg`;
       }
       document.querySelector('#name').innerText = `${data.name}`;
     }
   }
 })

 interactiveCanvas.getHeaderHeightPx().then((height) => {
   document.body.style.paddingTop = `${height}px`;
 })

 document.querySelector('#pass_button').addEventListener('click', elem => {
   interactiveCanvas.sendTextQuery("パス");
 })
}

上の一番下の部分を追加します。
これは先程ボタンに、clickイベントのリスナーを追加し、ボタンクリック時に下のメソッドを読んでいます。

interactiveCanvas.sendTextQuery("パス");

このメソッドの名前sendTextQueryが示すようにこれは”パス”という文字列のクエリを送っているということになりますが、もっと砕いた言い方で言うとこのメソッドは

”ユーザーが「パス」と喋った時と同じリクエストをDialogFlowに送る”

ということをしています。

Firebase Functionsでは、この「パス」という言葉に対する会話の受け皿を作ってあげればボタンタップに応じた処理が行えるということになります。

それでは次のステップから、「パス」という言葉を受け取って処理を行う部分をDialog FlowとFirebase Functionsに追加して行きましょう。

Entityの追加

まず

「パス」という言葉を受け取れる = 知りたいキーワード「パス」を抽出できる

ということなので「パス」という言葉用のEntityを作ります。

DialogFlowのEntitiesからCREATE ENTITYをクリックし、新しいEntityを作ります。ここで、パスという名前で、Synonym(類似語)が「パス」、「ぱす」、「pass」、「Pass」という下のようなEntityを作ります。

スクリーンショット 2020-02-04 12.23.24

Entityを作ったらTraining phrasesにこのEntityを使って受け皿を作っていきましょう。

Training phrasesの追加

Intentsのwhat_is_thisを選択し、Training phrasesに、下のように「パス」を追加します。ここをマウスで選択し、出てくるEntityのリストの中から、先程作った@passエンティティを選択します。

スクリーンショット 2020-02-04 12.24.33

これで、what_is_thisは、「パス」という発話の受け皿となり、「パス」と言われたときは、@passエンティティに”パス”という文字列を格納してくれるようになりました。この値をFirebase Functionsのwhat_is_thisインテントのハンドラーで受け取りたいと思います。

Intentハンドラーの修正

/functions/index.jswhat_is_thisインテントのハンドラーの最初の行に、passパラメーターを新たに受け取るように修正をいれます。

// 'what_is_this'というIntentのハンドラーの実装
app.intent('what_is_this', (conv, {animal, pass, any}) => {

これで、このハンドラーはanimal, pass, any3種類のパラメーターを受け取れるようになりました。

ハンドラーのなかで今までは下のように、animalパラメーターがあるかどうかで処理を分岐していたところに

if (animal) {

}else {

}

次のように、passパラメーターが入っていた場合の分岐を新たに加えます。

if (animal) {

}else if(pass){

}else {

}

passパラメーターが入っていたら、ユーザーが「パス」を選択したことになりますので、新たにランダムに動物を選んで発話する返答するコードを書きます。

else if(pass){

       //Assistantの読み上げる動物をランダムに選ぶ
       let newAnimal = getRandomMember(animals);

       //過去に読み上げたアニマルを調べる
       result = consumedAnimals.indexOf(newAnimal);

       if(result !== -1){
           //Assistantが動物を答える
           conv.ask(newAnimal);

           //すでに応えた動物なので、あれ?、ととぼけた後負けを認める
           conv.close('あれ' + newAnimal + 'は2回目ですね。私の負けです');
           consumedAnimals.length = 0;
           return
       }

       //Assistantの選んだ動物が正しいものだったので、動物を登録
       consumedAnimals.push(newAnimal);

       //Interactive Canvasに対応していればHtmlResponseを返す
       if(hasInteractiveCanvas(conv)){
           conv.ask(new HtmlResponse({
               url: `https://${firebaseConfig.projectId}.firebaseapp.com/`,
               data: {
                   scene: 'animal',
                   name: newAnimal,
                   picture: animalsPics[newAnimal]
               }
           }));
       }

       //Assistantが動物を答える
       conv.ask(newAnimal);
               
   }

これはanimalパラメーターがあった場合のハンドラーの後半の部分のコードと全く同じですので、関数を作って共通化してもいいかもしれません。

これでフロントエンド側(public以下のコード)とバックエンド側(functions以下のコード)が整いましたので、

firebase deploy

でディプロイしましょう。

ユーザーインタラクションの動作

ゲームを始めると下のように動物の写真、名前がでて、その下に「PASS」ボタンが表示されるようになりました。

スクリーンショット 2020-02-04 12.41.12

このPASSボタンをクリックすると、シミューレーターの右に「パス」と書かれたことがわかるのではないでしょうか?

スクリーンショット 2020-02-04 12.41.35

何が起きているかというと、PASSボタンを押すと、クライアント側のコードはユーザーが「パス」というセリフを発した時とおなじリクエストをFirebasee Functionsに投げているのです。

先程説明したpublic/js/index.js

interactiveCanvas.sendTextQuery("パス");

が発動している証拠です。

Firebase Functionsは、ユーザーが実際に「パス」と話したのか、PASSボタンを押したのかはわかりません。

とにかくIntentハンドラーのpassパラメーターに値が入っていたので、Intentハンドラーで処理して次の動物を読み上げたというわけです。

もちろん、口頭で「パス」とシミューレーターに呼びかけても同じようにIntentが受け取ってくれるので次の動物を読み上げてくれます。

このようにして、グラフィカルユーザーインターフェースからのインタラクションとヴォイスユーザーインターフェースからのインタラクションを一貫性を持って処理できます。(私は初めてInteractive Canvasを試したときにこのシンプルな仕組みに感動しました。)

本日のまとめ

HTMLにPASSボタンを追加

public/js/index.jsに、ボタンタップ時のリスナーを登録し、その中で、sentTextQueryに"パス"という文字列を送った。

DialogFlowに、「パス」という言葉を抽出するための@passエンティティを作成。

what_is_thisインテントに「パス」というTraining phrasesを追加し、@passエンティティでこの言葉をpassパラメーターに抽出するようにした。

Firebase Functionsのwhat_is_thisインテントハンドラーのパラメーターに新たにpassパラメーターを追加した。

Passパラメーターが送られて来たときは、動物のリストからランダムに動物を選んで返答するようにした。

おまけ

さきほど

Firebase Functionsは、ユーザーが実際に「パス」と話したのか、PASSボタンを押したのかはわかりません。

と説明しましたが、

あえてグラフィカルユーザーインターフェースからのインタラクションだけに反応したいハンドラを作るには、

interactiveCanvas.sendTextQuery();

で送るテキストに、人間が喋らないような言葉を設定し、それ専用にEntityを作ることで、間違ってユーザーの声でトリガーがかからないようなインタラクションを作ることができます。


このブログに関する質問やActions on Googleの開発の相談はこちらから↓↓↓

@mizutory
mizutori@goldrushcomputing.com

次回最終回はデバッグです!↓↓↓




この記事が気に入ったらサポートをしてみませんか?