STYLYでシーン間の連続性を実現するために利用できる簡易的なAPIおよびその制作過程の紹介

:::note info
これは[STYLY Advent Calendar 2023](https://qiita.com/advent-calendar/2023/styly)の7日目＝2023年12月7日の記事です。なお、私はSTYLYの関係者ではありません。
:::

# はじめに

2023年の9月から12月にかけ、台湾の国立成功大學（National Cheng Kung University）の未來智慧工場（Atelier Future）でXRをテーマに開催したワークショップが開催されました。ここでは、そのために講師陣で開発した簡易的なREST APIおよびその制作過程について簡単に紹介します。

https://github.com/kotobuki/Toolkit-for-Collaborative-XR

このAPIでできることは極めてシンプルで、マルチプレーヤー対応型のXRコンテンツを対象とし、STYLYのシーン間に連続性を持たせるためにデータベース（Cloud Firestore）へのアクセス機能を提供することです。ここで、連続性とは例えば次のようなことです。

- あるシーンでアイテムを追加／削除／属性の更新をすると、その後で同じシーンにアクセスしたときにもその状態が継続している（例：あるシーンの体験中にあるアイテムを追加すると、次にそのシーンにアクセスしたときにもそのアイテムがそこにある）。
- あるシーンに複数のプレーヤーが参加している途中で、あるプレーヤーが追加／削除／属性の更新をしたアイテムは、同じシーンに参加している他のプレーヤーの画面にも反映される（例：プレーヤーAがシーンからアイテムを削除すると、プレーヤーBとCのシーンからもそのアイテムが消える）。

STYLYは、Unity＋PlayMaker＋STYLY Plugin for Unityを用いて制作したシーンを、簡単に複数のプラットフォームに展開できるのが特長の1つです。その特長と引き換えに、STYLYで利用できる機能はPlayMakerの標準パッケージ＋STYLY Plugin for Unityの範囲に限定されます。これは、限られた時間で取り組むワークショップにおいて適度な制約になります。何も制約がないと、学ばなくてはならないことが無限にあるように思え、全てを満たす「正解」を追い求めてしまいがちです。これに対して制約があると、短時間で全体像を把握しつつ、その中で何とか実現しようという割り切りが生じるからです。

しかしながら、外部のデータベースにアクセスしようとすると、ちょっとしたハードルが生じることがあります。というのは、STYLY Plugin for Unityでサポートされている外部ネットワークへのアクセスが、当初はHTTPリクエストのうちGETに限定されていたのです[^getonly]。一般的なREST APIでは、データベースに書き込むためにはPOSTやPUTでのアクセスが必要となるため、せっかくHTTPリクエストでアクセスできるようになっていても利用できないことが大半となってしまいます。また、最近のREST APIの多くでは結果がJSONで返ってくるものがほとんどであり、PlayMakerの標準機能（および現在公開されている拡張機能）で扱うのは難しいというより大きな問題もあります（参照：せぎゅさんによる[13日目の記事](https://qiita.com/segur/items/c27aa11112cab075b2ab)）。

[^getonly]: 2021年8月のアップデートによりGET以外も使えるようになっています（ご指摘ありがとうございました）。以前調査したあと、結果を更新し損ねておりました。

そこで、HTTPのGETリクエストだけで利用でき、結果をPlayMakerの標準機能で扱いやすいCSV形式で取得できる簡易的なREST APIを作成しました[^future]。ただし、後述するようにパフォーマンスには問題があり、セキュリティは必要最小限しか実装されていません。それでも、プロトタイピングなど、このAPIでも有効な場面はそれなりにあるでしょう。以下、このAPIと、その設計および実装のプロセスについて順に説明していきます。

[^future]: もちろん、GETでPOSTやPUTに相当するアクセスまでできるようにしてしまうのは、お行儀がよいとはいえません。STYLY Plugin for Unityでは既にPOSTやPUTがサポートされているため、近日中にアップデートしたいと考えております。なお、この件に限らず、往々にしてプラットフォームでどこまでサポートされるかには鶏と卵の関係があります。十分なアプリケーションが提示されない限り、プラットフォームは実装のための優先度を上げないものです。このAPIを用いた実例は近日中にワークショップのレポートとして公開される見込みです。

# APIの紹介

## 考え方

- **ブラックボックスをなくす**：全てのソースをオープンにする。
- **最小限の機能だけを提供する**：実装をできるだけコンパクトに留めるため、ユーザー登録などの機能は実装しない（ワークショップ参加者による改変にも期待する）。

## 主要な概念

### 役割（role）

このAPIでは、以下の4つの役割を設定します。各役割には固有のAPIキーがあり、必要なキーだけをデベロッパーに開示することにより、最小限の安全性だけ確保できるようになっています。

- **デザイナー**：場所、アイテム、タグの作成と管理を担当する。
- **プレイヤー**：アイテムとの対話、取得、メッセージボードを使ったコミュニケーションを行う。
- **センサー**：物理的な世界の変化に基づいてアイテムの状態を更新する。
- **アクチュエーター**：アイテムの状態変化に応答する。

### 場所（location）

すべてのアイテムは特定の場所と関連付けられます。場所には`INDOOR`と`OUTDOOR`の2種類があります。

- `INDOOR`：建物内や限定された空間内のアイテム。x、y、zの三つの座標を使って位置を指定する。
- `OUTDOOR`：開放エリアや自然環境内のアイテム。緯度と経度の地理座標を使って位置を指定する。

### アイテム（item）

アイテムはユーザーが対話できるオブジェクトまたはエンティティであり、次のような種類があります。

- **名前（name）**：アイテムの識別子またはラベル。
- **所有者（owner）**：現在アイテムを所有している人を示す。デフォルトでは、アイテムは`PUBLIC_DOMAIN`に属し、すべての人が利用できる。プレイヤーがアイテムを取得すると、所有権は`A_PLAYER`に変更される。
- **タイプ（type）**：アイテムのカテゴリーを記述する。
- **座標（coordinates）**：その場所におけるアイテムの位置を指定する。
- **タグ（tags）**（オプション）：アイテムのカテゴライズやフィルタリングに役立つキーワード。
- **属性（attributes）**（オプション）：アイテムに関する追加のプロパティや詳細。

### タグ（tag）

タグはラベルのようなものです。特定のカテゴリーやプロパティに基づいてアイテムを整理し、見つけるのに役立ちます。例えば、特定のタイプのアイテムを探している場合、タグを使用することにより素早くフィルタリングし、見つけることができます。

## 主要なコンポーネント

このAPIの開発と展開には、堅牢性、スケーラビリティ、使いやすさを保証するため、次のコンポーネントを利用しました。

- [Flask](https://flask.palletsprojects.com/)：Pythonで書かれた軽量なWebフレームワーク。
- [Cloud Firestore](https://firebase.google.com/products/firestore)：Google Firebaseが提供する、柔軟でスケーラブルなNoSQLクラウドデータベース。
- [Render](https://render.com/)：アプリケーションやデータベースのデプロイ、スケーリング、管理のプロセスを簡単に扱えるPlatform as a Service（PaaS）。

## エンドポイント

エンドポイントの一覧は[こちらのページ](https://github.com/kotobuki/Toolkit-for-Collaborative-XR/blob/main/api_documentation.md)を参照してください。

例として、新規のlocationを作成するため最初に使用することになる`/create_location`の場合、次のようなパラメーターがあります。

- `name` (string)：新しいlocationの名前 (一意でなければならず、カンマは使用できない)。
- `type` (string)：新しい場所のタイプ（`OUTDOOR`または`INDOOR`）。
- `api_key` (string)：ユーザーのAPIキー（`API_KEY_DESIGNER`と一致している必要がある）。

作成に成功すると、メッセージとして`location_id`が返ってきますので、以降はその値を用いてその場所を指定します。

新規のlocationを作成し、そこに新規のitemを作成するには、次の順でエンドポイントをコールします。

1. `/create_location`
2. `/create_item`

役割（role）とエンドポイントの関係は次のようになっています。

| Endpoint | Designer | Player | Sensor | Actuator |
| --- | --- | --- | --- | --- |
| `/create_location` | ✔ |  |  |  |
| `/delete_location` | ✔ |  |  |  |
| `/list_locations` | ✔ |  |  |  |
| `/create_item` | ✔ |  |  |  |
| `/delete_item` | ✔ |  |  |  |
| `/update_item` | ✔ |  |  |  |
| `/get_item` | ✔ | ✔ |  |  |
| `/list_items` | ✔ | ✔ |  |  |
| `/acquire_item` |  | ✔ |  |  |
| `/delete_items` | ✔ |  |  |  |
| `/create_tag` | ✔ |  |  |  |
| `/list_tags` | ✔ |  |  |  |
| `/delete_tag` | ✔ |  |  |  |
| `/update_attribute` | ✔ | ✔ | ✔ |  |
| `/get_attribute` | ✔ | ✔ |  | ✔ |

## テスト

リポジトリに登録している`test_client.html`は、APIの開発段階においてAPIが正しく動作しているかどうかを確認することが当初の目的でした。しかしながら、問題の切り分けに便利なため、ワークショップの中で参加者にも提供して操作してもらうようにしました。これにより、サーバー側の動作に問題があるのか、Unityのコード側に問題があるのか、迅速に切り分けることができます。特に、センサーやアクチュエーターが参加するプロジェクトでは複雑になりがちなので、うまく使うと効果的でしょう（見た目が地味なので最初に興味を惹かれにくいという課題はあるのですが…）。

## 限界

最初に示したように、HTTPリクエストでのアクセスが前提になっているのと、「最小限の機能だけを提供する」という考え方で設計・実装しているため、以下のような限界があります。

- 遅い：アクセスごとにセッションを張ることになるため、セッションを維持したまま通信する方式と比較すると効率が悪い。
- セキュリティが最小限：APIキーを知っていればほぼ何でもできてしまう。
- プレイヤーを区別できない：プレイヤーは全て匿名として扱われ、IDなどによる区別は行えない。

ARの場合、長時間連続して同じシーンに留まることは少ないと考えられるため、今のままでも十分に使える場合があるかと思います。実際に試した範囲では、数名が同時にアクセスし、毎秒1回程度更新するのであれば十分に対応できます。ですので、シーンを開いた時点で最新の状態を取得し、それを反映するだけであれば問題なく動作します。しかしながら、フレームごとに最新の状態に更新し続ける使い方には向いていません。この辺りは、STYLY Plugin for Unityが将来的なアップデートにより、WebSocketなどリアルタイム性の高いプロトコルに対応することを期待したいところです。

# 制作過程

ここからは余談といえばそうなのですが、もしかすると参考になる人がいるかもしれないということで書き残しておきます。

このAPIを設計する段階において、私自身にバックエンドの設計および実装の経験はほとんどありませんでした。他に担当できるメンバーがいなかったのと、ちょうど大規模言語モデル（LLMs）が急速に実用化されてきていた時期だったため、大規模言語モデルの活用を試みたいと考えました。設計と実装は大規模言語モデルベースのサービス「ChatGPT」と「GitHub Copilot」（一部に「GitHub Copilot Chat」）を用いて進めていきました。

## ChatGPTとの基本設計

基本設計は次のように進めていきました。これ以外に、PythonでAPI用のサーバーを構築するためのフレームワークなどの選択もChatGPTとの対話を通じて決めていきました。

1. ワークショップを担当するメンバーで想定されるユースケースに関して議論する。
2. 議論を基に必要とされる要件を定義する。
3. プロンプトにより指示を与えて、APIのエンドポイントおよびパラメーターを抽出する。
4. 手作業により修正する。

1に関して、UMLのシーケンス図により記述したユースケース群から直接エンドポイントを抽出することを試してみたのですが、残念ながら当時のGPT-4でそこまでの作業はできませんでした。そのため、私が手作業で要件を定義していくことになりました。それでも、全て手作業で行うのと比較すれば、かなり短い時間ですませることができたと思います。

詳細は煩雑になるので省略しますが、想定している内容に関する情報は一通り提供しつつ、最初に文脈を理解するための質問をするようChatGPTにお願いするのが効果的でした。暗黙の前提になっていることについて質問されそれに丁寧に答えていくことは、ChatGPTが生成するテキストの質を上げるだけでなく、自分たちの中で曖昧な点を明確にする上でも大きく貢献しました。

## GitHub Copilotとの実装

ChatGPTと一緒に行った基本設計を基に、次のように実装を進めていきました。

1. ChatGPTで生成したエンドポイントのドキュメントを基に、各エンドポイントのコメントを書く。
2. GitHub Copilotにより実装する。
3. 実際の動作を確認しながら手作業で修正する。

3の段階において、体感的には8割までは実装できましたが、残りの2割に関しては手作業が必要になりました。厄介だったのは、いっけんきれいなコードが書かれているように見えつつ、実際には間違った実装がなされていることがちょくちょくあったことです。テストしてみれば分かることではあるのですが、見た目が整っていると結構だまされます。これは、大規模言語モデルとの協働を実践する上で、なかなか味わい深い経験でした。

# おわりに

この記事では、STYLYでシーン間の連続性を実現するために利用できる簡易的なAPI、およびその制作過程を紹介してきました。将来的にSTYLYがアップデートすると不要になるものかもしれませんが、プロトタイピングには便利でしたので用途が合えば試してみていただけたらと思います。GitGubでのイシューやプルリクエストもお待ちしております。:pray: