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

Cluster Creator Kitで利用できる機能として、新しく「テキスト入出力」「外部通信」の機能が正式化されました!
今回は新しい機能を使って、ゲームのスコアなどを表示できるランキングボードをつくってみます。外部通信機能を使うことで、異なるスペース間でも同じランキングを共有することができます。
外部通信機能の接続先として外部のサーバーなどを用意する必要がありますが、Googleアカウントがあれば無料で使える「Google Apps Script」を使うと手軽に試すことができます。

今回の作例スクリプトは$.getStateCompatを利用するため、クラフトアイテムでは使えません。Cluster Creator Kitを使ってアップロードしたワールドで試してみてください。

まずはアイテムを作成しましょう。

  • 新しいGameObjectをつくり、Scriptable Itemコンポーネントを設定してください。また、子オブジェクトとしてInteract用のCubeを追加しておきます。
  • 続いて、ランキング表示用のテキストを追加します。
    • 新しい子オブジェクトを作成し、名前をName_1とします。
      • Name_1のInspectorで「Text View」コンポーネントを追加してください。Text Viewはテキスト出力のためのコンポーネントです。ここにランキング1位のプレイヤー名を表示します。
      • Text Viewの「Text」に仮のテキストとして「Name」と入力し、表示位置を調整します。「Text Anchor」でテキストの基準位置も調整してください。例えば左揃えにしたい場合は「Middle Left」などがオススメです。
    • 同様に2位・3位のプレイヤー名を表示する「Name_2」「Name_3」という名前のオブジェクトを作成してText Viewを設定
      • また1~3位のスコアを表示する「Score_1」「Score_2」「Score_3」という名前のオブジェクトを作成しこちらもText Viewを設定してください。

これらのオブジェクトはスクリプトからsubNodeとして参照するため、必ず名前をこの通りに設定してください。

これでアイテムの準備ができました。続いてScriptable Itemコンポーネントにスクリプトを設定していきます。

上で作成したScriptable Itemに、以下のスクリプトを設定します。

// 接続先のEndpointIDを設定する(自分の設定に合わせて書き換えてください)
const endpoint = new ExternalEndpointId("CCKで登録したEndpointID");

// callExternalの呼び出し間隔(秒数)
const interval = 12;

// ランキングに表示する上位ランカーの数
const top = 3;

// 名前と点数を表示するText View
const names = [$.subNode("Name_1"), $.subNode("Name_2"), $.subNode("Name_3")];
const scores = [$.subNode("Score_1"), $.subNode("Score_2"), $.subNode("Score_3")];

$.onStart(() => {
    // 現在のランキングを取得するため、recordsが空の状態でサーバーにアクセスする
    let request = {type: "rankingSample", records: [], top: top};
    $.callExternal(endpoint, JSON.stringify(request), "syncRanking");

    // ランキングに新しく登録するレコードの配列
    $.state.records = [];

    // 前回のcallExternal呼び出しからの経過時間
    $.state.waitingTime = 0;
});

$.onUpdate(deltaTime => {
    let records = $.state.records;

    // 前回の呼び出しから一定以上の時間が経過していればcallExternalを呼び出す
    let waitingTime = $.state.waitingTime + deltaTime;
    if (waitingTime >= interval) {
        waitingTime = 0;

        // サーバーへのrequestとして「サーバーでの処理分岐用タグ」「追加するレコード」「表示する上位ランカーの数」を設定
        // requestをstringに変換したものをサーバーに送信
        let request = {type: "rankingSample", records: records, top: top};
        $.callExternal(endpoint, JSON.stringify(request), "syncRanking");

        // サーバーに送信済みのレコード情報をクリア
        $.state.records = [];
    }
    $.state.waitingTime = waitingTime;
});

$.onInteract(player => {
    // 新しいレコードを追加
    // レコードとして「ユーザー名」「スコア」を記録
    let records = $.state.records;
    let record = {name: player.userDisplayName, score: $.getStateCompat("owner", "score", "integer")};
    records.push(record);
    $.state.records = records;
});

// callExternal実行後、サーバーからの応答を受け取ったときに呼び出される処理
$.onExternalCallEnd((response, meta, errorReason) =>
{
    // サーバーとの通信でエラーが発生した場合にその理由を表示
    if (response == null) {
        $.log("callExternal ERROR: " + errorReason);
        return;
    }

    // metaを照合して処理を分岐
    // metaの値はcallExternalの第2引数に渡したもの
    if (meta === "syncRanking") {
        // サーバーからのresponseのJSONをパース
        let parsedResponse = JSON.parse(response);

        // responseの情報でtextViewを更新
        for(let i = 0; i < top; i++)
        {
            names[i].setText(parsedResponse.rankers[i].name);
            scores[i].setText(parsedResponse.rankers[i].score);
        }
    }
});

新しい機能を使っている部分を中心に、上のスクリプトを解説します。

$.onStart(() => {

$.onStartにはアイテムが生成されたときに呼び出される処理を記述します。
ここではStateの初期化に加えて、開始時点でのランキングをサーバーから取得する処理もおこなっています。

let request = {type: "rankingSample", records: records, top: top};
$.callExternal(JSON.stringify(request), "syncRanking");

$.callExternalを使うことで、登録したURLに対して通信することができます(登録方法は後述)。
引数1はサーバーに送信するテキスト、引数2は後述の$.onExternalCallendで参照できる識別用の情報です。
サーバーに送信できるのはテキストのみのため、複雑な情報を送信する際はデータをJSON形式に変換するなどの工夫が必要です。

const interval = 12;
let waitingTime = $.state.waitingTime + deltaTime;
if (waitingTime >= interval) {
    waitingTime = 0;

$.callExternalは呼び出し頻度が制限されており、それを超えて呼び出そうとするとエラーが発生します。
必要に応じて送信頻度を制御する仕組みを用意しておきましょう。

頻度制限はクラフトアイテムでは「1アイテムにつき1分間に5回」、Creator Kitでアップロードしたワールドでは「1スペースにつき1分間に100回」までとなっています(2024年2月現在)。

$.onInteract(player => {
    let records = $.state.records;
    let record = {name: player.userDisplayName, score: $.getStateCompat("owner", "score", "integer")};
    records.push(record);
    $.state.records = records;
});

Interactしたプレイヤーの名前とスコアをランキングに登録します。
既存のワールドに組み込む場合などは、プレイヤーのスコア情報を取得する$.getStateCompat(“owner”, “score”, “integer”)の部分を必要に応じて変更してください。

$.onExternalCallEnd((response, meta, errorReason) =>

$.onExternalCallEndには、$.callExternalの実行後にサーバーから応答が返ってきたときに呼び出される処理を記述します。

if (response == null) {
    $.log("callExternal ERROR: " + errorReason);
    return;
}

$.onExternalCallEndのresponseにはサーバーから返信されたテキストが入ります。何らかのエラーによってresponseが得られなかった場合は、errorReasonにその理由が格納されます。

if (meta === "updateRanking") {

metaには$.callExternalの引数2に渡したものと同じ文字列が入っています。
ひとつのスクリプト内で複数の目的の外部通信をおこなう場合などは、このmetaの内容を照合することで、どのcallExternalに対応するresponseかを識別することができます(itemHandle.sendのmessageTypeと似た用途です)。

let parsedResponse = JSON.parse(response);

responseもrequestと同様にテキストのみを扱うため、場合によってJSON形式などを利用する必要があります。

names[i].setText(parsedResponse.rankers[i].name);
scores[i].setText(parsedResponse.rankers[i].score);

TextViewコンポーネントのついたsubNodeは、setTextメソッドで表示するテキストの内容を変更することができます。

続いて、接続先となるサーバーを用意しましょう。
今回はGoogleアカウントがあれば無料で使える「Google Apps Script」を使って、ウェブアプリとして公開します。

  • Googleドライブにアクセスし、スプレッドシートを新規作成してください。
  • スプレッドシートの下にある「シート1」を右クリックして、「名前を変更」でシート名を「RankingSampleSheet」に変更し、スプレッドシートを保存します。このスプレッドシートはApps Scriptから自動で編集するため、何も書き込まないようにしてください

  • スプレッドシートの編集画面で、上部メニューの「拡張機能>Apps Script」を選択して、Google Apps Scriptのプロジェクトを開きます。
  • 左上のApps Scriptのロゴの右にあるプロジェクト名は分かりやすいものに変更しておきましょう。

スプレッドシートとGoogle Apps Scriptの準備ができたら、スクリプトとして以下を入力します。

最初からあるmyFunctionは削除して、全体を置き換えてください。

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


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


  var response = "";
 
  // requestの内容に応じて処理を分岐
  if (request.type === "rankingSample") {
    // データ保存用のスプレッドシートを取得
    const file = SpreadsheetApp.getActiveSpreadsheet();
    const sheet = file.getSheetByName("RankingSampleSheet");


    // スプレッドシートのデータがある一番下の行
    var row = sheet.getLastRow();


    // 新しいレコードをスプレッドシートの下に追加
    request.records.forEach(function(record){
      var name = record.name;
      var score = record.score;


      row += 1;
      sheet.getRange(row, 1).setValue(name);
      sheet.getRange(row, 2).setValue(score);
    });
   
    // スプレッドシートをスコア順に並べ替え
    if (row > 0) {
      var range = sheet.getRange(1, 1, row, 2);
      range.sort({column: 2, ascending: false});
    }


    // ランキングに反映する上位ランカーの数
    var top = request.top;


    // ランク圏外のデータを削除
    if (row > top) {
      var range = sheet.getRange(top + 1, 1, row - top, 2);
      range.clear();
    }


    // 上位ランカーの情報をスプレッドシートから取得し、返信用データを作成
    var topRankersInSheet = sheet.getRange(1, 1, top, 2).getValues();
    var topRankers = topRankersInSheet.map((ranker) => ({name: ranker[0], score: ranker[1]}));
    var data = {rankers: topRankers};


    response = JSON.stringify(data);
  }


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


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


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

サーバーがリクエストを受け取ったときに実行する処理を記述します。

var params = JSON.parse(e.postData.getDataAsString());

doPostの引数にはリクエストの情報がJSON形式で格納されているため、パースして情報を取り出します。

var request = JSON.parse(params.request);

アイテムのcallExternalの引数に渡した文字列は、上でパースした内容の中に「request」という名前で格納されます。
今回はrequestにJSON形式のテキストを渡しているので、パースしてデータを取り出します。

if (request.type === "rankingSample") {

callExternalの接続先に指定できるURLはアカウントごとにひとつなので、複数の目的で外部通信機能を使う場合は、サーバー側でrequestの内容によって処理を振り分ける必要があります。
requestに判別用の情報を含めておくなど、工夫してください。

const file = SpreadsheetApp.getActiveSpreadsheet();
const sheet = file.getSheetByName("RankingSampleSheet");

連携しているスプレッドシートを取得します。
getSheetByNameの引数は上で設定したシートの名前と一致させてください。

var topRankersInSheet = sheet.getRange(1, 1, top, 2).getValues();
var topRankers = topRankersInSheet.map((ranker) => ({name: ranker[0], score: ranker[1]}));
var data = {rankers: topRankers};


response = JSON.stringify(data);

スプレッドシート上で更新したランキング情報を取得し、responseを作成します。
サーバーからアイテムに返信するresponseも文字列のみなので、JSON形式の文字列に変換します。

var output = ContentService.createTextOutput();
output.setMimeType(ContentService.MimeType.JSON);


var token = PropertiesService.getScriptProperties().getProperty('token');


output.setContent(JSON.stringify({ response: response, verify: token }));


return output;

返信用のデータを作成します。
返信にはサーバーでの処理によって得られたresponseの他に、不正なアクセスでないかを確認するためのトークン(後述)を含む必要があります。

サーバー側のスクリプトが設定できたら、これをWebアプリとして公開することでcluster側のスクリプトでアクセスできるようになります。

Google Apps Script側での作業
  • Apps Scriptの編集画面で、右上にある「デプロイ」をクリックし、「新しいデプロイ」を選択してください。
  • デプロイの作成画面で、「種類の選択」の右にある歯車アイコンから「ウェブアプリ」を選択します。
  • 「次のユーザーとして実行」は「自分」「アクセスできるユーザー」は「全員」にして、「デプロイ」ボタンをクリックします。
  • アプリがスプレッドシートを編集するためのアクセス権限を求められるので、「アクセスを承認」をクリックしてください。
Google Apps Script側での作業
  • デプロイが完了すると、実行用のURLが発行されます。このURLをコピーしてください。
  • このURLを、clusterのスクリプトの接続先として設定します。
Unity側での作業
  • ワールドのUnityプロジェクトを開き、上部メニューの「Cluster」から「外部通信(callExternal)接続先URL」を選択します。
    • 設定用ウィンドウが開くので、「Endpoint」「Verify Token」をそれぞれ設定してください。
      • ※トークンは一度しか表示されないので注意してください。
Google Apps Script側での作業

発行されたトークンをサーバー側に登録します。Apps Scriptの編集画面に戻ってください。

  • 左側のメニューから、歯車アイコンの「プロジェクトの設定」を開きます。
    • 設定画面から「スクリプト プロパティ」の項目を確認し、「スクリプト プロパティを追加」ボタンをクリックしてください。
    • 「プロパティ」に「token」、「値」にコピーしたトークンを張り付けて、「スクリプト プロパティを保存」をクリックして保存します。

このプロパティはサーバー側のスクリプトの

PropertiesService.getScriptProperties().getProperty('token');

で参照しています。

ここまで設定できたら、ワールドをアップロードして試してみましょう。
こちらの記事などを参考に、プレイヤーの「score」を変更するギミックを別に用意して、スコアを増やしてアイテムをクリックしてみてください。
クリックして少し時間が経つとランキングが更新されます。

サーバー側のスクリプトを修正して更新する場合は、以下のように設定します(スクリプトを保存するだけでは変更が反映されません)。

  • Apps Scriptの編集画面で「デプロイ」をクリックし、「デプロイを管理」を選択します。
  • 「デプロイを管理」画面で、右上の「編集」ボタン(ペンのアイコン)をクリックしてください。
  • 各項目が編集できるようになるので、「バージョン」の部分で「新バージョン」を選択してから「デプロイ」をクリックします。
  • 新しいバージョンのデプロイが公開され、同じURLのまま変更が反映されます。

外部通信機能を使うと、ワールド内の情報を外部で処理したり保存したりといったことができます。
接続先サーバーの用意など必要な手順が多いですが、今までできなかったさまざまな機能を実装することができるので、試してみてください!

記事をシェアしてワールド制作を盛り上げよう!

Cluster Creators Guide|バーチャル空間での創作を学ぶならをもっと見る

今すぐ購読し、続きを読んで、すべてのアーカイブにアクセスしましょう。

続きを読む