clusterでWeb申込を再現してみた

この記事は202406アドベントカレンダー17日目の記事となります。
【VR未経験エンジニアがメタバースでのWEB申込を実現するための挑戦】シリーズの第6弾として、保険のWeb申込システムをPoCしていたチームが、メタバースプラットフォームのCluster上にWeb申込画面を再現する取り組みを記事にまとめていきます。
前回の記事はこちらです。

web申込画面を再現する際の実装前にやったこと

保険のweb申込画面をメタバースプラットフォーム上に再現する上で、第一弾の記事でも取り上げた、個人情報入力画面と保険商品の選択・見積もりをする画面の2つを再現することにしました。

申込画面の一部の実装として、まずは個人情報入力画面と保険商品の選択・見積もりをする画面の2つを再現してみようという話になりました。

https://www.insurtechlab.net/metaverse-challenge-1/

上記2点の画面をメタバース上に再現するために、まずは画面ごとにどのような要素があるか抽出し、それぞれをcluster上で再現できるか確認していくことにしました。

個人情報入力画面の要素、挙動を書き出しました。

次に、抽出した要素からメタバースで再現したい要素を選びました。

画面から抽出した要素の中でメタバース上で再現したい要素を整理

要素を抽出したはいいものの、メタバース上での申込画面のイメージがうまく湧きませんでした。
そこで再現したい要素を踏まえてざっくりとしたイメージを作成し、作りながらUIイメージをブラッシュアップしていくことにしました。

このくらいの解像度が低いざっくりイメージからスタートしました

商品選択画面も同じような感じでどのような要素があるかを抽出し、それぞれをcluster上で再現できるか確認していくことにしました。

商品選択画面から要素を抽出しました

商品選択画面は以前に個人情報入力画面を作成したこともあり、書きやすかったです。

個人選択画面と同様にざっくりのイメージを書きました

それぞれの画面から抽出した要素を以下にまとめました。

  • テキストの入出力
  • エラー表示
  • 性別の入力ボタン
  • 住所検索の外部通信

それぞれどのように作成したかをご紹介します。

web申込画面をVR上で実装

テキストの入出力

個人情報入力画面を再現するのに一番重要な要素はなんでしょうか。
そう、入力です。
ということで、まずはテキスト入力についてメタバースで再現してみようと思い、入力されたテキストを表示するためのパネルを作りました。
パネル上に個人情報の各項目(氏名、生年月日など)の数だけText Legacyを用意し、固定文言を入れた状態で表示しました。
また、入力される個人情報の内容は、出力する必要があるため、Text Viewを使用しました。

Text Viewの使い方としては、空のオブジェクトを作って、Text ViewをAdd Componentを追加します。
Text Viewの内容をScriptから変えるには、Text Viewを作成したオブジェクトの親にScriptable ItemをAdd Componentする必要があります。
※TextViewはフォントなどを変えられません。

子となるオブジェクトにText ViewをAdd Componentから追加します。
作成したTextViewの親オブジェクトにScriptable ItemをAdd Componentから追加します。


個人情報を入力するための開始方法として、申込ボタンを作成して、ユーザが申込開始ボタンを押下した際に入力画面が表示するようにします。
新しくCubeを作り、申込開始ボタンとして使用しました。


onInteractを使用して、申込開始ボタンをクリックした際にrequestTextInputを呼び出すことでcluster標準入力ポップアップによりテキスト入力ができるようにしました。

$.onInteract((playerHandle) => {
    $.state.player = playerHandle;
    playerHandle.requestTextInput(currentMeta, QuestionObj[currentMeta]);
  });
申込を開始するボタンを押下すると画像のようにテキスト入力ができるポップアップが出てきます。

表示されたポップアップに対して、入力されたテキストを出力するには、subNodeでText Viewのオブジェクトを取得して、入力されたテキストをsetTextで出力します。
以下は、一例として、氏名(漢字)のテキストを入出力するためのscriptになります。

// 氏名(漢字)
const inputKanjiNameTextView = $.subNode('InputKanjiName');

// 入力処理
$.onTextInput((text, meta, status) => {
    switch (status) {
	case TextInputStatus.Success:
            inputKanjiNameTextView.setText(text);
    }
}

エラー表示

各入力項目に対してバリデーションチェックとエラー表示の実装も行なっています。
個人情報の内容を入出力する際に作ったオブジェクトと同様の作り方で、エラー表示をするオブジェクトにTextViewを追加して作ります。

エラー表示をするオブジェクトを新しく作りTextViewを追加

初期表示ではonStartでエラー表示が非表示の状態にしておきます。

$.onStart(()=>{
    // エラービュー初期化(初期:非表示)
    errTextView.setEnabled(false);
})

例えば「氏名(漢字)」の入力項目で何も入力せずポップアップの送信を押した場合には、以下のようにエラー内容を表示するようにしています。

未入力の状態でポップアップの送信を押した場合のエラー表示

性別の入力ボタン

テキストの入出力を実装することが出来たので、次はボタン形式での入力を実装します。ラジオボタンのように2つの選択肢のうちどちらかを選択できるようにするイメージです。
テキスト入力の途中でこのボタンが現れ、ボタンでの入力が完了すると再度テキスト入力のコンソールが表示されるように実装していきます。

ラジオボタンのように2択のボタン選択をしたい

まず「男性」、「女性」と書かれたオブジェクトを作成します。

男性・女性と書かれたオブジェクトを作成

前段で実装したテキスト入力はあるオブジェクト内の1つのClusterScript内で処理を行っていますが、ここで作成したオブジェクトはそのScriptの適用範囲外にあります。
そのため、Scriptから別のオブジェクトを呼び出す方法として、CreateItemGimmickでオブジェクトを生成することにしました。
ClusterScriptの制約で、Script内からGlobalにSignalを送信することはできないため、ScriptがついているオブジェクトにGimmickをつけ、thisにSignalを送信することでオブジェクトをCreateできるようになりました。

右側のScript→Gimmickが今回の説明の対象になります。
Scriptable ItemとGimmickを共存させる
GimmickはthisをTargetに設定することでScriptから呼び出すことができる

これでテキスト入力のScriptから別のオブジェクトを呼び出すことができるようになりました。
次に、作成した「男性・女性」ボタンを押すと個人情報表示パネル上の男性と女性が切り替わって表示されるようにしたいので、それを実装していこうと思います。先ほどのGimmickで作成したオブジェクトに対して、Triggerをそれぞれ設定し、変数genderに対して異なる値が送信されるようにします。

「女性」ボタンにInteractItemTriggerを設定し触れるようにした上で、Globalに”gender”、Integerの2を送信する(男性は”gender”,、Integerの1を送信)

ここで使用するTriggerはInteractItemTriggerです。アイテムに触れることでGlobalにgenderというKeyでIntegerの数値が送信されるようになります。男性:1、女性:2の数値を送るように設定したため、次は数値を受け取ってテキストを切り替える処理を作ります。

const view = $.subNode("TextObject");  
$.onUpdate( playerHandle => {
    const gender = $.getStateCompat("global","gender","integer")
    //変数genderの値が1の場合は男性、2の場合は女性を表示。それ以外の場合は初期表示
    if (gender === 1){
        view.setText("男性")
    } else if(gender === 2) {
        view.setText("女性")
    } else {
        view.setText("例)男性")
    }
})

個人情報を表示するパネル上で、性別の表示を行うオブジェクトに対して、Scriptで上記の処理を行います。
Globalから変数genderでInteger型の数値を受け取り、その値で男性・女性の表示を切り替えるようにしました。
ここまでで、男性・女性のボタンを押すことでパネル上の性別表示を切り替えることが可能になりましたが、ここから再度ユーザーをテキスト入力に戻す必要があります。
今実装しているボタンから、スムーズにテキスト入力に戻る方法がわからなかったため、新しく「確定」ボタンを用意しました。
このボタンにはcreateしたアイテムを削除するメッセージを送りつつ、再度テキスト入力を呼び出す処理をさせることにしました。

<この画像、後でソースコード貼り付けにしてください>

ここでは性別ボタンが押されている場合に確定ボタンを押下すると、Createされたアイテムに対して削除の信号を送るようにしたいです。
ただし、ClusterScriptの制約でSignalを別オブジェクト(=Global)に対して送ることはできないため、getItemsNear関数を用いて周りのアイテムを取得し、取得したアイテムに対してメッセージを送ることにしました。
メッセージを受け取る側のアイテムにはScriptをつけておき、ここでは”destroy”というメッセージを受け取ると、アイテム自身を削除する処理を記述しました。

$.onReceive((messageType, arg, sender) => {
    switch (messageType) {
      case "destroy":
        //アイテムが自身を削除する処理
        $.destroy();
        break;
    }
  });
男性・女性の各ボタンを押すことで画像左上の「性別」欄の男女が切り替わる

住所検索の外部通信

次はclusterの外部通信機能を使って住所検索機能を実装してみましょう。

以前の記事でも軽く触れましたが、clusterで外部通信するためにはclusterにAPIのエンドポイントとなるURLを登録してclusterのサーバを経由する必要があります。

このスクリプトを実行しているアイテムがクラフトアイテムであった場合、ひとつのアイテムあたり5回/分

このスクリプトを実行しているアイテムがワールドアイテムであった場合、ひとつのスペースあたり全てのワールドアイテムの合計で100回/分

瞬間的にこの制限を超えることはできますが、平均回数はこの制限を下回るようにしてください。 制限を超えている場合、ClusterScriptError (rateLimitExceeded)が発生し操作は失敗します。

https://docs.cluster.mu/script/interfaces/ClusterScript.html#callExternal

また、1つのワールドに対して1つのエンドポイントしか登録できないため複数のエンドポイントを使用したい場合には、リクエストパラメータに処理振り分け用のパラメータを含める等工夫が必要になります。

まずは、clusterに登録するためのAPIサーバを作成します。
cluster公式がGoogleAppScript(GAS)を使用した実装例を公開しているので、そちらを参考に実装していきます。

テキスト出力と外部通信を使って「ランキングボード」をつくってみよう

上記記事ではGoogleスプレッドシートからデータを取得していますが、今回の機能では郵便番号検索APIを提供しているzipcloudからデータを取得できるようにしたいと思います。

それではまずはサーバー側のコードを書いていきましょう。
Google App Scriptで新規プロジェクトを作成して以下のコードを記述します。

function doPost(e) {
  // 受け取ったデータを取得
  var params = JSON.parse(e.postData.getDataAsString());
  // 受け取ったデータからrequestの内容を取得
  var request = JSON.parse(params.request);

  let response = '';

  response = UrlFetchApp.fetch(`https://zipcloud.ibsnet.co.jp/api/search?zipcode=${request.zipcode}`).getContentText();

  Logger.log(response);

  // 返信用のデータを作成
  var output = ContentService.createTextOutput();
  output.setMimeType(ContentService.MimeType.JSON);

  // Cluster Creator Kitで発行されたトークン
  var token = PropertiesService.getScriptProperties().getProperty('token');

  // 返信の内容にstringにしたデータと認証用のトークンを設定
  output.setContent(JSON.stringify({ response, 'verify': token }));

  return output;
}

これでサーバ側の準備はできたので、作成したURLをclusterに追加します。

clusterメニューから外部通信先URLを設定
ここからURLの登録を行う

clusterScript側はonTextInputからcallExternalを呼び出し、onExternalCallEndでレスポンスを受け取るコードを追加します。

// テキスト入力された際の処理
$.onTextInput((text, meta, status) => {
    switch (status) {
        // テキスト入力ダイアログでOKが押された場合
        case TextInputStatus.Success:
            // テキストが空でない場合
            if (text) {
                // リクエストパラメータを作成する
                // 複数エンドポイントを使用したい場合はリクエストパラメータに何かしら設定して、GAS側で分岐させる
                let request = {'zipcode': text};
                // clusterに登録されているURLにリクエストを投げる
                $.callExternal(JSON.stringify(request), 'zipcode_search');
            }
            break;
        case TextInputStatus.Busy:
            break;
        case TextInputStatus.Refused:
            break;
    }
});

// callExternalの結果が返ってきた際の処理
$.onExternalCallEnd((response, meta, errorReason) => {
    // レスポンスが空の場合エラー
    if (response === null) {
        $.log("callExternal ERROR: " + errorReason);
        textView.setText(address);
        return;
    }
    // 複数エンドポイントを呼び出す場合には、metaを使って処理を振り分ける
    if (meta === 'zipcode_search') {
        const result = JSON.parse(response);
        const address = result.results[0].address1;
        textView.setText(address);
    }

});

ローカルで動作確認するために、CSEmulatorにもURLを登録しておきます。

window>かおもラボからCSEmulatorの設定を開く
callExternal用URLにGASで作成したAPIのURLを入力する

これでローカルで動作確認ができるようになりました。
試しに実行してみましょう。

郵便番号を入力
ローカルで住所検索ができた

ワールドをアップロードしてcluster上でも確認してみましょう。

clusterワールドでも試してみる
ワールドでも動いた!

これで住所検索機能が実装できました。

まとめ

今回は、clusterで個人情報の入力機能と保険商品選択機能の実装を行いました。
実際に機能の作りこみをしてみるとclusterの機能がだいぶ理解できるかと思います!

次の記事では「Unityを使ったチーム開発の進め方」についてご紹介します!