この記事は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はフォントなどを変えられません。


個人情報を入力するための開始方法として、申込ボタンを作成して、ユーザが申込開始ボタンを押下した際に入力画面が表示するようにします。
新しく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を追加して作ります。

初期表示ではonStartでエラー表示が非表示の状態にしておきます。
$.onStart(()=>{
// エラービュー初期化(初期:非表示)
errTextView.setEnabled(false);
})
例えば「氏名(漢字)」の入力項目で何も入力せずポップアップの送信を押した場合には、以下のようにエラー内容を表示するようにしています。

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

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

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


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

ここで使用する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に追加します。


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を登録しておきます。


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


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


これで住所検索機能が実装できました。
まとめ
今回は、clusterで個人情報の入力機能と保険商品選択機能の実装を行いました。
実際に機能の作りこみをしてみるとclusterの機能がだいぶ理解できるかと思います!
次の記事では「Unityを使ったチーム開発の進め方」についてご紹介します!